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