Műszaki cikk

PDF-tartalom átfolyatása reszponzív HTML-be Delphiben

A PDFium Component a BuildReflowDocument segítségével egy rögzített elrendezésű PDF-et olyan szemantikus modellé alakít, amely átfolyatható, és ezt a modellt a ToHtml-en keresztül önálló HTML-ként exportálja. A címsorok címsorok maradnak, a listaelemek listaelemek maradnak, az oldalon felismert táblázatok pedig valódi táblázatjelölésként jelennek meg, megőrzött fejléccellákkal és span-ekkel. A kimenetben semmi nem hivatkozik külső szkriptre vagy stíluslapra

Az efféle igény oka az, hogy egy PDF-oldal pozicionált glifák halmaza, ami pontosan rossz egy telefon képernyőjéhez, egy képernyőolvasóhoz vagy egy keresőindexhez. Minden olyan próbálkozás, amely egyszerű szöveg kinyerésével oldja meg a problémát, elveszíti azt a struktúrát, amely olvashatóvá tette a dokumentumot, minden olyan próbálkozás pedig, amely az oldalak képpé alakításával oldja meg, teljesen elveszíti a szöveget. Egy átfolyatási modell mindkettőt megtartja: a szavakat és a köztük lévő kapcsolatokat

Honnan származik a szemantikai információ?

Minden a GetStructuredText-ből indul ki, amely a komponens egyetlen szöveg- és szemantikaforrása. Amikor a PDF struktúrafát hordoz, azaz jelölt PDF az ISO 32000-1 14.7. szakaszának definíciója szerint, a modell a producer által rögzített logikai hierarchiát követi. Amikor nem, és a valóságban a legtöbb PDF nem, a modell visszaesik az olvasási sorrend céljára már kiszámított fizikai elrendezési sorrendre

Ez a választás egy szigorú határt tart fenn: nem vezet be sem második PDF-elemzőt, sem második renderelőmotort olyan kérdések megválaszolására, amelyeket a meglévő is meg tud válaszolni. A mögöttes olvasási-sorrend gépezetét a strukturált szövegblokkok és olvasási sorrend ismerteti, az átfolyatási modell pedig egy szemantikus réteg efölött, nem annak helyettesítője

Minden csomópont rögzíti, honnan származik az információja, így egy fogyasztó meg tudja különböztetni a dokumentum által deklarált címsort az elrendezési heurisztika által kikövetkeztetett címsortól. A megbízhatóságra érzékeny folyamatoknak ezt a mezőt kell olvasniuk ahelyett, hogy minden csomópontot egyformán mérvadónak kezelnének

Egy lapos fa, és miért nem objektumfa

A modell egy előrendben ellaposított fa: csomópontok tömbje, ahol minden csomópont egy ParentIndex-et és egy Depth-et hordoz, nem pedig egy rekurzív rekord vagy egy tulajdonlással rendelkező objektumgráf. Az oldalak, címsorok, bekezdések, listák, listaelemek, ábrák, feliratok, táblázatok, sorok és cellák mind ebben az egyetlen lineáris tömbben élnek

Ebből két előny következik. A fogyasztók rekurzió nélkül, sorrendben streamelhetik a tömböt, ami egyszerű ciklussá teszi a HTML, Markdown vagy fanézet előállítását. Az elrendezés emellett hordozható marad Delphi, C++Builder és Free Pascal között, amelyek eltérően kezelik a rekurzív felügyelt típusokat egy ABI-határon át. Egy dinamikus tömbökből álló rekurzív rekord pontosan az a fajta konstrukció, amely mindenhol lefordul, és mindegyikben finoman másképp viselkedik

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;          // soron belüli stílusblokk, nincs külső fájl
    Options.MaxNodes := 200000;          // biztonságosan záró költségkeret
    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;

Hogyan kerülik el a táblázatok, hogy kétszer jelenjenek meg?

A táblázatfelismerés azután fut le, hogy a strukturált szöveget összegyűjtötték egy oldalhoz, ami egy nyilvánvaló veszélyt teremt: ugyanaz a cellatartalom létezik a szövegblokkokban is és a felismert táblázatban is. Mindkettő kiadása olyan HTML-t eredményez, amelyben minden táblázatot újra követ a saját tartalma, laza bekezdésekként

A megoldó szabály geometriai. Amikor egy felismert táblázat egy szövegblokk területének felénél többet fed le, a táblázatcsomópont lecseréli azt a blokkot, ahelyett hogy csatlakozna hozzá. A soron belüli cellaindexelés vödrökbe számolással épül fel, így a modell felépítése lineáris marad a cellák plusz sorok függvényében, ahelyett hogy minden sorhoz újra végigpásztázna minden cellát, ami pénzügyi dokumentumoknál számít, ahol egyetlen oldal több száz cellát is hordozhat

A felismert struktúra őszinte afelől, hogy felismerés. Egy vonalakkal rendelkező táblázatot megbízhatóbban ismer fel a rendszer, mint egyet, amely kizárólag üres helyekkel van igazítva, és a csomópont megbízhatósága ezt tükrözi. Olyan tartalomnál, ahol egy rossz táblázat jobb, mint egyáltalán semmilyen, tartsd bekapcsolva a felismerést; archív konverziónál, ahol egy rossz táblázat rosszabb, kapuzz a megbízhatóság alapján

Önmagában megálló HTML exportálása

A ToHtml végigjárja a már felépített modellt, és soha nem tér vissza a PDFiumhoz, így a kétszeri exportálás semmi extrát nem kerül, és nem is adhat eltérő eredményt ugyanabból a modellből. A szöveg- és attribútumértékek egységesen escape-elve vannak, a címsorszintek a HTML által ténylegesen definiált h1-től h6-ig terjedő tartományra vannak korlátozva, a fejléccellák, a RowSpan és a ColumnSpan pedig változatlanul kerülnek át

Az opcionális CSS egy egyszerű soron belüli stílusblokk. Nincs szkript, nincs webes betűtípus és nincs semmilyen külső erőforrás, és pontosan ez teszi biztonságossá a kimenet beágyazását egy e-mailbe, egy súgónézőbe vagy egy homokozózott böngészővezérlőbe:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // az oldalhatárok láthatók maradnak
  Options.PreserveLineBreaks := False;   // a böngésző tördeli a bekezdéseket

  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;

A PreserveLineBreaks az az opció, amelyen a leginkább érdemes elgondolkodni. Egy PDF-beli sortörés egy fix oldalszélességre hozott tördelési döntés, így egy keskeny képernyőn való megőrzése pontosan azt a problémát reprodukálja, amelynek megoldására az átfolyatás létezik. Őrizd meg a töréseket versnél, kódlistáknál és címeknél; hagyd el őket prózánál

Költségkeretek, megszakítás és oldalállapot

A karaktereknek, csomópontoknak, táblázatoknak és celláknak mindegyiknek van felső határa, mindegyiket a lefoglalás előtt ellenőrzi, nem utána, így egy rosszul formázott vagy rosszindulatú dokumentum tisztán bukik el, ahelyett hogy addig fogyasztana memóriát, amíg valami más nem áll le. A megszakítási tokent oldal-, blokk-, táblázat-, sor- és cellahatároknál ellenőrzi, ami reszponzívan tartja egy ezeroldalas dokumentum megszakított pásztázását

Egy viselkedés kifejezetten a GUI-alkalmazásoknál számít: a teljes dokumentumpásztázás egy olyan hatókörön belül fut, amely visszaállítja az aktív oldalt, így a siker, a költségkeret-hiba és a megszakítás egyaránt érintetlenül hagyja a hívó aktuális oldalát. Egy megjelenítő, amely lehetővé teszi a felhasználónak az exportálást a 340. oldal nézése közben, utána is a 340. oldalon találja magát

Mire jó az átfolyatás, és mire nem?

Az átfolyatás kimenete kiváló bemenet a keresőindexeléshez, a hozzáférhető olvasónézetekhez, a mobilmegjelenítéshez és a tartalommigrációhoz. Nem hűségmegőrző konverter: az abszolút pozíciók, a pontos betűtípusok, a vektorgrafika és a pontos oldalgeometria tervezésénél fogva kívül esik a céljain. Ha egy feladatnak arra van szüksége, hogy az oldal ugyanúgy nézzen ki, rendereld; ha arra van szüksége, hogy az oldal máshol is olvasható legyen, folyasd át

Kifejezetten az akadálymentesítési technológiákhoz az átfolyatási modell párosul a akadálymentes olvasó építése cikkben leírt olvasási funkciókkal, a valódi struktúrafát hordozó dokumentumok pedig érezhetően jobb modelleket adnak, ami jó érv amellett, hogy a jelölést a folyamat elején validáld, ahogy azt a PDF/UA struktúrafa-validáció ismerteti

Az átfolyatás, a strukturált szöveg, a jelölésvalidáció és a renderelés egyetlen dokumentumobjektumon osztozik Delphiben, C++Builderben és Lazarusban; a teljes API-t a PDFium Component for Delphi oldala ismerteti