Tehnički članak

Preoblikovanje PDF sadržaja u responzivni HTML u Delphiju

PDFium Component pretvara PDF fiksnog rasporeda u semantički model koji se može preoblikovati, koristeći BuildReflowDocument, i taj model izvozi kao samostalni HTML putem ToHtml. Naslovi ostaju naslovi, stavke popisa ostaju stavke popisa, a tablice otkrivene na stranici izlaze kao prave oznake tablice s očuvanim ćelijama zaglavlja i rasponima. Ništa u izlazu ne upućuje na vanjsku skriptu ili stylesheet

Razlog za želju za ovime jest da je PDF stranica skup pozicioniranih glifova, što je potpuno pogrešno za zaslon telefona, čitač zaslona ili indeks pretrage. Svaki pokušaj rješavanja izdvajanjem čistog teksta gubi strukturu koja je dokument činila čitljivim, a svaki pokušaj rješavanja pretvaranjem stranica u slike u potpunosti gubi tekst. Model preoblikovanja zadržava oboje: riječi i odnose među njima

Odakle dolazi semantička informacija?

Sve počinje od GetStructuredText, jedinog izvora teksta i semantike u komponenti. Kada PDF nosi strukturno stablo, označen (tagged) PDF kako je definiran u ISO 32000-1 klauzuli 14.7, model prati logičku hijerarhiju koju je proizvođač zapisao. Kada je ne nosi, a većina PDF-ova u stvarnom svijetu je ne nosi, model pada natrag na fizički redoslijed rasporeda već izračunat za potrebe redoslijeda čitanja

Taj izbor čuva strogu granicu: ne uvodi se drugi PDF parser ni drugi mehanizam renderiranja za odgovaranje na pitanja na koja postojeći već može odgovoriti. Mehanika redoslijeda čitanja ispod opisana je u strukturiranim tekstualnim blokovima i redoslijedu čitanja, a model preoblikovanja semantički je sloj na vrhu toga, a ne zamjena

Svaki čvor bilježi odakle je njegova informacija došla, tako da potrošač može razlikovati naslov koji je dokument deklarirao od naslova koji su heuristike rasporeda zaključile. Pipelineovi osjetljivi na pouzdanost trebaju čitati to polje umjesto da tretiraju sve čvorove kao jednako mjerodavne

Ravno stablo i zašto to nije stablo objekata

Model je poravnato stablo u pre-order redoslijedu: polje čvorova gdje svaki čvor nosi ParentIndex i Depth, umjesto rekurzivnog zapisa ili grafa objekata s vlasništvom. Stranice, naslovi, odlomci, popisi, stavke popisa, slike, natpisi, tablice, retci i ćelije svi žive u tom jednom linearnom polju

Slijede dvije prednosti. Potrošači mogu strujati polje po redoslijedu bez rekurzije, čime generiranje HTML-a, Markdowna ili prikaza stabla postaje jednostavna petlja. A raspored ostaje prenosiv između Delphija, C++Buildera i Free Pascala, koji se razlikuju u načinu na koji obrađuju rekurzivne upravljane tipove preko ABI granice. Rekurzivan zapis dinamičkih polja upravo je vrsta konstrukcije koja se svugdje kompilira, a u svakom se okruženju suptilno drugačije ponaša

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // ugrađeni blok stila, bez vanjske datoteke
    Options.MaxNodes := 200000;          // proračun koji zatvara pri prekoračenju
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

Kako se sprječava da se tablice pojave dvaput?

Otkrivanje tablica pokreće se nakon što je strukturirani tekst prikupljen za stranicu, što stvara očitu opasnost: isti sadržaj ćelije postoji i u tekstualnim blokovima i u otkrivenoj tablici. Generiranje oboje proizvodi HTML u kojem nakon svake tablice ponovno slijedi njezin vlastiti sadržaj kao slobodni odlomci

Pravilo koje to rješava geometrijsko je. Kada otkrivena tablica pokriva više od pola površine tekstualnog bloka, čvor tablice zamjenjuje taj blok umjesto da mu se pridružuje. Indeksiranje ćelija unutar retka gradi se brojanjem u spremnike (buckets), pa izgradnja modela ostaje linearna u ćelijama plus retcima umjesto ponovnog pregledavanja svake ćelije za svaki redak, što je važno na financijskim dokumentima gdje jedna stranica može nositi stotine ćelija

Otkrivena struktura iskrena je o tome da je otkrivena. Tablica s linijama razgraničenja prepoznaje se pouzdanije od one poravnate isključivo bjelinom, a pouzdanost čvora to odražava. Za sadržaj gdje je pogrešna tablica bolja od nikakve tablice, ostavite otkrivanje uključenim; za arhivsku pretvorbu gdje je pogrešna tablica gora, ograničite prema pouzdanosti

Izvoz HTML-a koji ostaje samostalan

ToHtml prolazi kroz model koji je već izgrađen i nikada se ne vraća PDFium-u, pa dvostruki izvoz ne košta ništa dodatno i ne može proizvesti drugačiji rezultat iz istog modela. Tekst i vrijednosti atributa jednoobrazno se escapiraju, razine naslova ograničene su na raspon h1 do h6 koji HTML doista definira, a ćelije zaglavlja, RowSpan i ColumnSpan prolaze onako kako su zapisani

Neobavezan CSS obični je ugrađeni blok stila. Nema skripte, nema web fonta i nema vanjskog resursa bilo koje vrste, a upravo to čini izlaz sigurnim za ugradnju u e-poštu, preglednik pomoći ili izoliranu (sandboxed) kontrolu preglednika:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // zadrži granice stranica vidljivima
  Options.PreserveLineBreaks := False;   // pusti preglednik da prelama odlomke

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

PreserveLineBreaks opcija je o kojoj se najviše isplati razmisliti. Prijelom retka u PDF-u odluka je slaganja teksta donesena za fiksnu širinu stranice, pa njezino očuvanje na uskom zaslonu reproducira upravo problem koji preoblikovanje treba riješiti. Očuvajte prijelome za poeziju, ispise koda i adrese; odbacite ih za prozu

Proračuni, otkazivanje i stanje stranice

Znakovi, čvorovi, tablice i ćelije svaki imaju gornju granicu, a svaka se provjerava prije alokacije, ne poslije, tako da deformiran ili zlonamjeran dokument čisto zakaže umjesto da troši memoriju dok to nešto drugo ne učini. Token za otkazivanje provjerava se na granicama stranice, bloka, tablice, retka i ćelije, čime otkazano skeniranje dokumenta od tisuću stranica ostaje odzivno

Jedno ponašanje posebno je važno za GUI aplikacije: cijelo skeniranje dokumenta odvija se unutar dosega koji obnavlja aktivnu stranicu, pa uspjeh, neuspjeh proračuna i otkazivanje jednako ostavljaju trenutnu stranicu pozivatelja netaknutom. Preglednik koji korisniku dopušta izvoz dok gleda stranicu 340 nakon toga se i dalje nalazi na stranici 340

Za što je preoblikovanje dobro, a za što nije

Izlaz preoblikovanja izvrstan je ulaz za indeksiranje pretrage, pristupačne prikaze čitanja, mobilni prikaz i migraciju sadržaja. To nije pretvarač koji čuva vjernost: apsolutne pozicije, točni fontovi, vektorska grafika i precizna geometrija stranice po dizajnu su izvan njegove svrhe. Kada posao treba da stranica izgleda isto, renderirajte je; kada treba da stranica bude čitljiva negdje drugdje, preoblikujte je

Posebno za asistivnu tehnologiju, model preoblikovanja nadopunjuje se sa značajkama čitanja opisanima u izgradnji pristupačnog čitača, a dokumenti koji nose istinsko strukturno stablo proizvode primjetno bolje modele, što je dobar argument za provjeru označavanja uzvodno, kako je opisano u provjeri PDF/UA strukturnog stabla

Preoblikovanje, strukturirani tekst, provjera označavanja i renderiranje dijele jedan objekt dokumenta u Delphiju, C++Builderu i Lazarusu; puni API opisan je na stranici PDFium Component za Delphi