Teknisk artikel

Omflöda PDF-innehåll till responsiv HTML i Delphi

PDFium Component omvandlar en fastlayoutad PDF till en semantisk modell som kan omflödas, med hjälp av BuildReflowDocument, och exporterar den modellen som självständig HTML via ToHtml. Rubriker förblir rubriker, listobjekt förblir listobjekt, och tabeller som upptäcks på sidan kommer ut som riktig tabellmarkup med rubrikceller och spann bevarade. Inget i utdatan refererar till ett externt skript eller en extern stilmall

Skälet att vilja ha detta är att en PDF-sida är en uppsättning positionerade glyfer, vilket är precis fel för en telefonskärm, en skärmläsare eller ett sökindex. Varje försök att lösa det genom att extrahera ren text förlorar den struktur som gjorde dokumentet läsbart, och varje försök att lösa det genom att konvertera sidor till bilder förlorar texten helt. En omflödesmodell behåller båda: orden och relationerna mellan dem

Var kommer den semantiska informationen ifrån?

Allt börjar från GetStructuredText, den enda källan till text och semantik i komponenten. När PDF:en bär ett strukturträd, taggad PDF enligt definitionen i ISO 32000-1 klausul 14.7, följer modellen den logiska hierarki producenten registrerade. När den inte gör det, och de flesta PDF-filer i det vilda gör inte det, faller modellen tillbaka på den fysiska layoutordning som redan beräknats för läsordningsändamål

Det valet håller en hård gräns: ingen andra PDF-tolk och ingen andra renderingsmotor introduceras för att besvara frågor den befintliga redan kan besvara. Läsordningsmaskineriet under beskrivs i strukturerade textblock och läsordning, och omflödesmodellen är ett semantiskt lager ovanpå det snarare än en ersättning

Varje nod registrerar varifrån dess information kom, så en konsument kan skilja en rubrik dokumentet deklarerade från en rubrik layoutheuristiken härledde. Konfidenskänsliga pipelines bör läsa det fältet snarare än att behandla alla noder som lika auktoritativa

Ett platt träd, och varför det inte är ett träd av objekt

Modellen är ett förordningsplattat träd: en array av noder där varje nod bär ett ParentIndex och ett Depth, snarare än en rekursiv post eller en objektgraf med ägande. Sidor, rubriker, stycken, listor, listobjekt, figurer, bildtexter, tabeller, rader och celler bor alla i den enda linjära arrayen

Två fördelar följer. Konsumenter kan strömma arrayen i ordning utan rekursion, vilket gör att skriva ut HTML, Markdown eller en trädvy blir en enkel loop. Och layouten förblir portabel över Delphi, C++Builder och Free Pascal, som skiljer sig åt i hur de hanterar rekursiva hanterade typer över en ABI-gräns. En rekursiv post av dynamiska arrayer är precis den typ av konstruktion som kompilerar överallt och beter sig subtilt olika i vardera

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;          // inbäddat stilblock, ingen extern 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;

Hur hindras tabeller från att förekomma dubbelt?

Tabelldetektering körs efter att strukturerad text har samlats in för en sida, vilket skapar en uppenbar risk: samma cellinnehåll finns både i textblocken och i den detekterade tabellen. Att skriva ut båda ger HTML där varje tabell följs av sitt eget innehåll igen som lösa stycken

Regeln som löser det är geometrisk. När en detekterad tabell täcker mer än hälften av arean hos ett textblock ersätter tabellnoden det blocket snarare än att gå med i det. Cellindexering inuti en rad byggs genom att räkna in i hinkar, så att bygga modellen förblir linjärt i celler plus rader istället för att skanna om varje cell för varje rad, vilket spelar roll på finansiella dokument där en enda sida kan bära hundratals celler

Detekterad struktur är ärlig om att vara detektering. En tabell med linjeindelningar känns igen mer tillförlitligt än en som bara är justerad genom mellanslag, och nodens konfidens speglar det. För innehåll där en felaktig tabell är bättre än ingen tabell, håll detekteringen påslagen; för arkivkonvertering där en felaktig tabell är värre, spärra på konfidens

Att exportera HTML som förblir självständig

ToHtml går igenom modellen som redan byggts och återbesöker aldrig PDFium, så att exportera två gånger kostar inget extra och kan inte producera ett annat resultat från samma modell. Text- och attributvärden eskaperas enhetligt, rubriknivåer klipps till intervallet h1 till h6 som HTML faktiskt definierar, och rubrikceller, RowSpan och ColumnSpan förs igenom som skrivna

Den valfria CSS:en är ett enkelt inbäddat stilblock. Det finns inget skript, inget webbtypsnitt och ingen extern resurs av något slag, vilket är det som gör utdatan säker att bädda in i ett e-postmeddelande, en hjälpvisare eller en sandlådad webbläsarkontroll:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // håll sidgränser synliga
  Options.PreserveLineBreaks := False;   // låt webbläsaren radbryta stycken

  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 är alternativet mest värt att fundera över. En PDF-radbrytning är ett typografiskt beslut fattat för en fast sidbredd, så att bevara den på en smal skärm återskapar just det problem omflöde finns för att lösa. Bevara brytningar för poesi, kodlistningar och adresser; släpp dem för löptext

Budgetar, avbrytning och sidtillstånd

Tecken, noder, tabeller och celler har vardera ett tak, och vart och ett kontrolleras före allokering snarare än efteråt, så ett missbildat eller fientligt dokument misslyckas rent istället för att förbruka minne tills något annat gör det. Avbrytningstoken kontrolleras vid sid-, block-, tabell-, rad- och cellgränser, vilket håller en avbruten genomsökning av ett tusensidigt dokument responsiv

Ett beteende spelar roll specifikt för GUI-applikationer: hela dokumentgenomsökningen körs inuti ett scope som återställer den aktiva sidan, så framgång, budgetfel och avbrytning lämnar alla anroparens aktuella sida orörd. En visare som låter användaren exportera medan denne tittar på sida 340 befinner sig fortfarande på sida 340 efteråt

Vad omflöde är bra för, och vad det inte är

Omflödesutdata är utmärkt indata för sökindexering, tillgängliga läsvyer, mobilvisning och innehållsmigrering. Det är inte en trohetsbevarande konverterare: absoluta positioner, exakta typsnitt, vektorgrafik och exakt sidgeometri ligger utanför dess syfte med avsikt. När ett jobb behöver att sidan ser likadan ut, rendera den; när det behöver att sidan är läsbar någon annanstans, omflöda den

Specifikt för hjälpmedelsteknik samspelar omflödesmodellen med de läsfunktioner som beskrivs i att bygga en tillgänglig läsare, och dokument som bär ett äkta strukturträd producerar märkbart bättre modeller, vilket är ett gott argument för att validera taggning uppströms enligt beskrivningen i validering av PDF/UA-strukturträd

Omflöde, strukturerad text, taggningsvalidering och rendering delar ett dokumentobjekt över Delphi, C++Builder och Lazarus; hela API:et beskrivs på sidan för PDFium Component för Delphi