Techninis straipsnis

PDF turinio pertvarkymas į adaptyvų HTML Delphi aplinkoje

PDFium Component paverčia fiksuoto maketo PDF failą semantiniu modeliu, kurį galima pertvarkyti (reflow), naudodama BuildReflowDocument, ir eksportuoja tą modelį kaip savarankišką HTML per ToHtml. Antraštės lieka antraštėmis, sąrašo elementai lieka sąrašo elementais, o puslapyje aptiktos lentelės virsta tikru lentelės žymėjimu, išsaugant antraštės langelius ir apjungimus (span). Nė vienoje rezultato vietoje nėra nuorodos į išorinį scenarijų ar stilių lapą

Priežastis, kodėl to reikia, yra ta, kad PDF puslapis yra pozicionuotų glifų rinkinys, o tai visiškai netinka telefono ekranui, ekrano skaitytuvui ar paieškos indeksui. Kiekvienas bandymas tai išspręsti ištraukiant grynąjį tekstą prarandą struktūrą, dėl kurios dokumentas buvo skaitomas, o kiekvienas bandymas tai išspręsti konvertuojant puslapius į vaizdus visiškai praranda tekstą. Pertvarkymo modelis išsaugo abu dalykus: žodžius ir ryšius tarp jų

Iš kur ateina semantinė informacija?

Viskas prasideda nuo GetStructuredText – vienintelio teksto ir semantikos šaltinio komponente. Kai PDF failas neša struktūros medį – žymėtą PDF, kaip apibrėžta ISO 32000-1 14.7 punkte – modelis seka loginę hierarchiją, kurią įrašė kūrėjas. Kai jo nėra, o dauguma realaus pasaulio PDF failų jo neturi, modelis persijungia į fizinę maketo tvarką, jau apskaičiuotą skaitymo tvarkos tikslams

Šis pasirinkimas išlaiko griežtą ribą: nediegiamas antras PDF analizatorius ir antras atvaizdavimo variklis, kad atsakytų į klausimus, į kuriuos jau gali atsakyti esamas. Skaitymo tvarkos mechanika, esanti apačioje, aprašyta straipsnyje struktūrizuoti teksto blokai ir skaitymo tvarka, o pertvarkymo modelis yra semantinis sluoksnis virš jos, o ne jos pakaitalas

Kiekvienas mazgas įrašo, iš kur atsirado jo informacija, todėl naudotojas gali atskirti antraštę, kurią deklaravo dokumentas, nuo antraštės, kurią išvedė maketo euristika. Konvejeriai, jautrūs pasitikėjimo lygiui, turėtų skaityti šį lauką, o ne traktuoti visus mazgus kaip vienodai patikimus

Plokščias medis, ir kodėl tai ne objektų medis

Modelis yra plokščias, prieš-tvarkos (pre-order) būdu suplotas medis: mazgų masyvas, kur kiekvienas mazgas neša ParentIndex ir Depth, o ne rekursyvus įrašas ar objektų grafas su nuosavybe. Puslapiai, antraštės, pastraipos, sąrašai, sąrašo elementai, iliustracijos, antraštės po iliustracijomis, lentelės, eilutės ir langeliai – visi jie gyvena tame viename linijiniame masyve

Iš to seka du privalumai. Naudotojai gali srautu apeiti masyvą tvarka be rekursijos, todėl HTML, Markdown ar medžio vaizdo generavimas tampa paprastu ciklu. Ir maketas išlieka perkeliamas tarp Delphi, C++Builder ir Free Pascal, kurie skiriasi tuo, kaip apdoroja rekursyvius valdomus tipus per ABI ribą. Rekursyvus dinaminių masyvų įrašas yra būtent tokia konstrukcija, kuri kompiliuojasi visur, bet kiekvienoje aplinkoje elgiasi subtiliai kitaip

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;          // vidinis stiliaus blokas, be išorinio failo
    Options.MaxNodes := 200000;          // biudžetas, kuris uždaro vykdymą pasiekus ribą
    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;

Kaip lentelės apsaugomos nuo dvigubo pasirodymo?

Lentelių aptikimas vyksta po to, kai puslapiui jau surinktas struktūrizuotas tekstas, o tai sukuria akivaizdų pavojų: tas pats langelio turinys egzistuoja ir teksto blokuose, ir aptiktoje lentelėje. Išvedus abu, gaunamas HTML, kuriame po kiekvienos lentelės dar kartą seka jos pačios turinys kaip laisvos pastraipos

Tai išsprendžianti taisyklė yra geometrinė. Kai aptikta lentelė užima daugiau nei pusę teksto bloko ploto, lentelės mazgas pakeičia tą bloką, o ne prisijungia prie jo. Langelių indeksavimas eilutės viduje sudaromas skaičiuojant į kibirus (buckets), todėl modelio kūrimas išlieka linijinis langelių ir eilučių atžvilgiu, o ne peržiūri kiekvieną langelį iš naujo kiekvienai eilutei – tai svarbu finansiniuose dokumentuose, kur vienas puslapis gali nešti šimtus langelių

Aptikta struktūra sąžiningai pripažįsta esanti aptikimo rezultatas. Lentelė su linijomis atpažįstama patikimiau nei lentelė, sulygiuota vien tarpais, ir mazgo pasitikėjimo reikšmė tai atspindi. Turiniui, kur neteisinga lentelė geriau nei jos nebuvimas, palikite aptikimą įjungtą; archyvavimo konvertavimui, kur neteisinga lentelė yra blogiau, filtruokite pagal pasitikėjimą

HTML eksportavimas, kuris išlieka savarankiškas

ToHtml apeina jau sukurtą modelį ir niekada nesikreipia atgal į PDFium, todėl eksportavimas du kartus nekainuoja nieko papildomo ir negali duoti kitokio rezultato iš to paties modelio. Teksto ir atributų reikšmės nuosekliai apsaugomos (escaped), antraščių lygiai apribojami iki h1h6 diapazono, kurį iš tikrųjų apibrėžia HTML, o antraštės langeliai, RowSpan ir ColumnSpan perkeliami taip, kaip parašyti

Nebūtinas CSS yra paprastas vidinis stiliaus blokas. Nėra jokio scenarijaus, jokio žiniatinklio šrifto ir jokio išorinio resurso – būtent tai daro rezultatą saugų įterpti į el. laišką, pagalbos peržiūros priemonę ar izoliuotą naršyklės valdiklį:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // išlaikyti matomas puslapių ribas
  Options.PreserveLineBreaks := False;   // leisti naršyklei pačiai laužyti pastraipas

  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 yra parinktis, apie kurią labiausiai verta pagalvoti. PDF eilutės lūžis yra spausdinimo sprendimas, priimtas fiksuotam puslapio pločiui, todėl jo išsaugojimas siaurame ekrane atkuria būtent tą problemą, kurią pertvarkymas ir turi išspręsti. Išsaugokite lūžius poezijai, kodo sąrašams ir adresams; atmeskite juos prozai

Biudžetai, atšaukimas ir puslapio būsena

Simboliai, mazgai, lentelės ir langeliai kiekvienas turi savo lubą, ir kiekviena patikrinama prieš paskirstant, o ne po to, todėl sugadintas ar priešiškas dokumentas sužlunga tvarkingai, o ne naudoja atmintį tol, kol tai padaro kas nors kitas. Atšaukimo žymuo tikrinamas puslapio, bloko, lentelės, eilutės ir langelio ribose, todėl atšauktas tūkstančio puslapių dokumento skenavimas išlieka reaguojantis

Viena elgsena konkrečiai svarbi GUI programoms: visas dokumento skenavimas vyksta apimtyje, kuri atkuria aktyvų puslapį, todėl sėkmė, biudžeto sužlugimas ir atšaukimas visi palieka kviečiančiojo dabartinį puslapį nepaliestą. Peržiūros priemonė, leidžianti vartotojui eksportuoti žiūrint į 340 puslapį, po to vis dar randasi 340 puslapyje

Kam pertvarkymas tinka, o kam ne

Pertvarkymo rezultatas yra puikus įėjimas paieškos indeksavimui, prieinamiems skaitymo vaizdams, mobiliam rodymui ir turinio migracijai. Tai ne tikslumą išsaugantis konverteris: absoliučios pozicijos, tikslūs šriftai, vektorinė grafika ir tiksli puslapio geometrija sąmoningai nepatenka į jo paskirtį. Kai darbui reikia, kad puslapis atrodytų taip pat, atvaizduokite jį; kai reikia, kad puslapis būtų skaitomas kitur, pertvarkykite jį

Konkrečiai pagalbinėms technologijoms pertvarkymo modelis dera su skaitymo funkcijomis, aprašytomis straipsnyje prieinamo skaitytuvo kūrimas, o dokumentai, nešantys tikrą struktūros medį, duoda pastebimai geresnius modelius – tai geras argumentas patikrinti žymėjimą anksčiau, kaip aprašyta straipsnyje PDF/UA struktūros medžio validavimas

Pertvarkymas, struktūrizuotas tekstas, žymėjimo validavimas ir atvaizdavimas dalijasi vienu dokumento objektu Delphi, C++Builder ir Lazarus aplinkose; pilnas API aprašytas PDFium Component Delphi puslapyje