Tehnični članak

Preoblikovanje vsebine PDF v odzivni HTML v Delphiju

PDFium Component z uporabo BuildReflowDocument PDF s fiksno postavitvijo spremeni v semantični model, ki ga je mogoče preoblikovati, ta model pa prek ToHtml izvozi kot samostojen HTML. Naslovi ostanejo naslovi, elementi seznama ostanejo elementi seznama, tabele, zaznane na strani, pa pridejo ven kot prava tabelarna oznaka z ohranjenimi celicami glave in razponi. Nič v izhodu se ne sklicuje na zunanjo skripto ali slogovno datoteko

Razlog za to željo je, da je stran PDF nabor postavljenih znakov, kar je natanko narobe za zaslon telefona, bralnik zaslona ali iskalni indeks. Vsak poskus rešitve z izvlečenjem golega besedila izgubi strukturo, ki je dokument naredila berljiv, vsak poskus rešitve s pretvorbo strani v slike pa besedilo v celoti izgubi. Model za preoblikovanje ohrani oboje: besede in odnose med njimi

Od kod prihajajo semantične informacije?

Vse se začne pri GetStructuredText, edinem viru besedila in semantike v komponenti. Kadar PDF nosi strukturno drevo, torej označen PDF, kot je definiran v členu 14.7 ISO 32000-1, model sledi logični hierarhiji, ki jo je zabeležil izdelovalec. Kadar je ne nosi, in večina PDF-jev v naravi je ne, se model prevesi na fizični vrstni red postavitve, ki je že izračunan za namene vrstnega reda branja

Ta izbira ohranja trdno mejo: za odgovarjanje na vprašanja, na katera lahko odgovori že obstoječi razčlenjevalnik PDF in upodabljalni mehanizem, se ne uvaja noben drugi. Spodaj ležeča mehanika vrstnega reda branja je opisana v strukturiranih besedilnih blokih in vrstnem redu branja, model za preoblikovanje pa je semantična plast na njej in ne njena zamenjava

Vsako vozlišče zabeleži, od kod prihajajo njegove informacije, tako da lahko porabnik razlikuje naslov, ki ga je deklariral dokument, od naslova, ki so ga sklepale hevristike postavitve. Cevovodi, občutljivi na zaupanje, naj berejo to polje, namesto da bi vsa vozlišča obravnavali kot enako verodostojna

Ploščato drevo, in zakaj ni drevo objektov

Model je sploščeno drevo v vrstnem redu pred-obiska: polje vozlišč, kjer vsako vozlišče nosi ParentIndex in Depth, namesto rekurzivnega zapisa ali grafa objektov z lastništvom. Strani, naslovi, odstavki, seznami, elementi seznama, slike, napisi, tabele, vrstice in celice — vsi živijo v tem enem samem linearnem polju

Iz tega izhajata dve prednosti. Porabniki lahko polje pretakajo po vrstnem redu brez rekurzije, kar izpis HTML, Markdown ali pogleda drevesa naredi preprosto zanko. Postavitev pa ostaja prenosljiva med Delphijem, C++Builderjem in Free Pascalom, ki se razlikujejo v tem, kako obravnavajo rekurzivne upravljane tipe čez mejo ABI. Rekurziven zapis dinamičnih polj je natanko vrsta konstrukta, ki se povsod prevede, a se v vsakem okolju obnaša rahlo drugače

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;          // vgrajen blok sloga, brez zunanje datoteke
    Options.MaxNodes := 200000;          // proračun, ki ob preseganju zapre izvajanje
    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 prepreči, da bi se tabele pojavile dvakrat?

Zaznavanje tabel se izvede po tem, ko je strukturirano besedilo za stran že zbrano, kar ustvari očitno nevarnost: ista vsebina celice obstaja tako v besedilnih blokih kot v zaznani tabeli. Izpis obeh ustvari HTML, kjer vsaki tabeli sledi njena lastna vsebina še enkrat kot ohlapni odstavki

Pravilo, ki to razreši, je geometrijsko. Kadar zaznana tabela pokrije več kot polovico površine besedilnega bloka, vozlišče tabele nadomesti ta blok, namesto da bi se mu pridružilo. Indeksiranje celic znotraj vrstice je zgrajeno s štetjem v predale, tako da gradnja modela ostane linearna glede na celice plus vrstice, namesto da bi za vsako vrstico ponovno preiskala vsako celico, kar je pomembno pri finančnih dokumentih, kjer lahko ena sama stran nosi na stotine celic

Zaznana struktura je odkrita glede tega, da je zaznava. Tabela z ločilnimi črtami je prepoznana bolj zanesljivo kot taka, poravnana zgolj z belim prostorom, in zaupanje vozlišča to odraža. Za vsebino, kjer je napačna tabela boljša od nobene tabele, pustite zaznavanje vklopljeno; za arhivsko pretvorbo, kjer je napačna tabela slabša, zaporite na podlagi zaupanja

Izvoz HTML, ki ostane samostojen

ToHtml prehodi že zgrajen model in se nikoli ne vrne k PDFium, zato dvojni izvoz ne stane nič dodatnega in iz istega modela ne more dati drugačnega rezultata. Vrednosti besedila in atributov so pobegnjene enotno, ravni naslovov so omejene na razpon h1 do h6, ki ga HTML dejansko definira, celice glave, RowSpan in ColumnSpan pa gredo skozi tako, kot so zapisane

Izbirni CSS je preprost vgrajen blok sloga. Ni nobene skripte, spletne pisave ali zunanjega vira kakršne koli vrste, kar naredi izhod varen za vdelavo v e-pošto, pregledovalnik pomoči ali peskovniško nadzorno komponento brskalnika:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // ohrani meje strani vidne
  Options.PreserveLineBreaks := False;   // prepusti brskalniku prelom odstavkov

  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 je možnost, o kateri se najbolj splača premisliti. Prelom vrstice v PDF je stavčna odločitev, sprejeta za fiksno širino strani, zato njena ohranitev na ozkem zaslonu ponovno ustvari prav tisto težavo, zaradi katere preoblikovanje sploh obstaja. Prelome ohranite za poezijo, izpise kode in naslove; za prozo jih opustite

Proračuni, preklic in stanje strani

Znaki, vozlišča, tabele in celice imajo vsak svojo zgornjo mejo, vsaka pa je preverjena pred dodelitvijo in ne po njej, tako da popačen ali sovražen dokument čisto spodleti, namesto da bi porabljal pomnilnik, dokler ne odpove nekaj drugega. Žeton za preklic se preveri na mejah strani, bloka, tabele, vrstice in celice, kar prekinjen pregled dokumenta s tisoč stranmi ohranja odzivnega

Eno vedenje je pomembno posebej za aplikacije z grafičnim vmesnikom: celoten pregled dokumenta teče znotraj obsega, ki obnovi aktivno stran, tako da uspeh, neuspeh proračuna in preklic vsi pustijo trenutno stran klicatelja nedotaknjeno. Pregledovalnik, ki uporabniku dovoli izvoz, medtem ko gleda stran 340, se nato znajde še vedno na strani 340

Za kaj je preoblikovanje dobro, in za kaj ni

Izhod preoblikovanja je odličen vhod za indeksiranje iskanja, dostopne bralne poglede, prikaz na mobilnih napravah in migracijo vsebine. Ni pretvornik, ki bi ohranjal zvestobo: absolutni položaji, natančne pisave, vektorska grafika in natančna geometrija strani so po zasnovi zunaj njegovega namena. Kadar opravilo zahteva, da stran izgleda enako, jo upodobite; kadar zahteva, da je stran berljiva nekje drugje, jo preoblikujte

Posebej za podporno tehnologijo se model za preoblikovanje poveže z bralnimi funkcijami, opisanimi v gradnji dostopnega bralnika, dokumenti, ki nosijo pravo strukturno drevo, pa dajo občutno boljše modele, kar je dober argument za predhodno preverjanje označevanja, kot je opisano v preverjanju strukturnega drevesa PDF/UA

Preoblikovanje, strukturirano besedilo, preverjanje označevanja in upodabljanje si delijo en objekt dokumenta v Delphiju, C++Builderju in Lazarusu; celoten API je opisan na strani PDFium Component za Delphi