Teknisk artikel

Omflyd PDF-indhold til responsivt HTML i Delphi

PDFium Component omdanner en fastlayoutet PDF til en semantisk model, der kan omflydes, ved hjælp af BuildReflowDocument, og eksporterer denne model som selvstændig HTML via ToHtml. Overskrifter forbliver overskrifter, listeelementer forbliver listeelementer, og tabeller detekteret på siden kommer ud som ægte tabelmarkup med headerceller og spænd bevaret. Intet i outputtet refererer til et eksternt script eller stylesheet

Grunden til at ønske dette er, at en PDF-side er et sæt positionerede glyffer, hvilket er præcis forkert til en telefonskærm, en skærmlæser eller et søgeindeks. Ethvert forsøg på at løse det ved at udtrække ren tekst mister den struktur, der gjorde dokumentet læsbart, og ethvert forsøg på at løse det ved at konvertere sider til billeder mister teksten helt. En omflydningsmodel beholder begge dele: ordene og relationerne mellem dem

Hvor kommer den semantiske information fra?

Alt starter fra GetStructuredText, den eneste kilde til tekst og semantik i komponenten. Når PDF'en bærer et strukturtræ, tagget PDF som defineret i ISO 32000-1 afsnit 14.7, følger modellen det logiske hierarki, producenten registrerede. Når den ikke gør, og de fleste PDF'er i naturen gør ikke, falder modellen tilbage til den fysiske layoutrækkefølge, allerede beregnet til læserækkefølgeformål

Det valg holder en hård grænse: ingen anden PDF-parser og ingen anden rendermotor indføres for at besvare spørgsmål, den eksisterende allerede kan besvare. Læserækkefølge-mekanikken bagved er beskrevet i strukturerede tekstblokke og læserækkefølge, og omflydningsmodellen er et semantisk lag oven på den frem for en erstatning

Hver knude registrerer, hvor dens information kom fra, så en forbruger kan skelne en overskrift, dokumentet erklærede, fra en overskrift, layoutheuristikkerne udledte. Konfidensfølsomme pipelines bør læse det felt frem for at behandle alle knuder som lige autoritative

Et fladt træ, og hvorfor det ikke er et træ af objekter

Modellen er et pre-order-fladet træ: et array af knuder, hvor hver knude bærer et ParentIndex og en Depth, frem for en rekursiv post eller en objektgraf med ejerskab. Sider, overskrifter, afsnit, lister, listeelementer, figurer, billedtekster, tabeller, rækker og celler bor alle i det ene lineære array

To fordele følger. Forbrugere kan streame arrayet i rækkefølge uden rekursion, hvilket gør det at udsende HTML, Markdown eller en trævisning til en simpel løkke. Og layoutet forbliver portabelt på tværs af Delphi, C++Builder og Free Pascal, som adskiller sig i, hvordan de håndterer rekursive administrerede typer på tværs af en ABI-grænse. En rekursiv post af dynamiske arrays er præcis den slags konstruktion, der kompilerer overalt og opfører sig subtilt forskelligt i hver

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;          // inline stilblok, ingen ekstern fil
    Options.MaxNodes := 200000;          // fail-closed budget
    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;

Hvordan holdes tabeller fra at optræde to gange?

Tabeldetektion kører, efter at struktureret tekst er blevet indsamlet for en side, hvilket skaber en åbenlys risiko: det samme celleindhold findes både i tekstblokkene og i den detekterede tabel. At udsende begge dele producerer HTML, hvor hver tabel efterfølges af sit eget indhold igen som løse afsnit

Reglen, der løser det, er geometrisk. Når en detekteret tabel dækker mere end halvdelen af arealet af en tekstblok, erstatter tabelknuden den blok frem for at slutte sig til den. Celleindeksering inde i en række bygges ved at tælle ind i spande, så det at bygge modellen forbliver lineær i celler plus rækker i stedet for at genscanne hver celle for hver række, hvilket betyder noget på finansielle dokumenter, hvor en enkelt side kan bære hundredvis af celler

Detekteret struktur er ærlig omkring at være detektion. En tabel med linjeføring genkendes mere pålideligt end en, der kun er justeret via mellemrum, og knudens konfidens afspejler det. Til indhold, hvor en forkert tabel er bedre end ingen tabel, hold detektion slået til; til arkivkonvertering, hvor en forkert tabel er værre, spær på konfidens

Eksport af HTML, der forbliver selvstændig

ToHtml gennemløber modellen, der allerede er bygget, og genbesøger aldrig PDFium, så at eksportere to gange koster intet ekstra og kan ikke producere et andet resultat ud fra samme model. Tekst- og attributværdier escapes ensartet, overskriftsniveauer klemmes fast til intervallet h1 til h6, som HTML rent faktisk definerer, og headerceller, RowSpan og ColumnSpan går igennem som skrevet

Den valgfrie CSS er en almindelig inline stilblok. Der er intet script, ingen webskrifttype og ingen ekstern ressource af nogen art, hvilket er, hvad der gør outputtet sikkert at indlejre i en e-mail, en hjælpeviewer eller en sandkasse-browserkontrol:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // hold sidegrænser synlige
  Options.PreserveLineBreaks := False;   // lad browseren ombryde afsnit

  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 er den indstilling, der er mest værd at overveje. Et PDF-linjeskift er en sætningsbeslutning truffet for en fast sidebredde, så at bevare det på en smal skærm genskaber netop det problem, omflydning findes for at løse. Bevar skift til poesi, kodelister og adresser; drop dem til løbende tekst

Budgetter, annullering og sidetilstand

Tegn, knuder, tabeller og celler har hver et loft, og hver tjekkes før allokering frem for bagefter, så et misdannet eller fjendtligt dokument fejler rent i stedet for at forbruge hukommelse, indtil noget andet gør. Annulleringstokenet tjekkes ved side-, blok-, tabel-, række- og cellegrænser, hvilket holder en annulleret scanning af et tusind-siders dokument responsivt

Én adfærd betyder noget specifikt for GUI-applikationer: hele dokumentscanningen kører inde i et scope, der genopretter den aktive side, så succes, budgetfejl og annullering alle efterlader den kaldendes nuværende side urørt. En viewer, der lader brugeren eksportere, mens de kigger på side 340, befinder sig stadig på side 340 bagefter

Hvad omflydning er godt til, og hvad det ikke er

Omflydningsoutput er fremragende input til søgeindeksering, tilgængelige læsevisninger, mobilvisning og indholdsmigration. Det er ikke en troskabsbevarende konverter: absolutte positioner, eksakte skrifttyper, vektorgrafik og præcis sidegeometri ligger uden for dets formål ved design. Når et job har brug for, at siden ser ens ud, rendér den; når det har brug for, at siden er læsbar et andet sted, omflyd den

For hjælpeteknologi specifikt parrer omflydningsmodellen sig med læsefunktionerne beskrevet i opbygning af en tilgængelig læser, og dokumenter, der bærer et ægte strukturtræ, producerer mærkbart bedre modeller, hvilket er et godt argument for at validere tagging opstrøms, som beskrevet i PDF/UA-strukturtræ-validering

Omflydning, struktureret tekst, taggingvalidering og rendering deler ét dokumentobjekt på tværs af Delphi, C++Builder og Lazarus; det fulde API er beskrevet på siden PDFium Component til Delphi