PDFium Component превръща PDF с фиксирано оформление в семантичен модел, който може да бъде пренареден, използвайки BuildReflowDocument, и експортира този модел като самостоятелен HTML чрез ToHtml. Заглавията остават заглавия, елементите от списъци остават елементи от списъци, а таблиците, открити на страницата, излизат като истинска табличка разметка със запазени заглавни клетки и обхвати. Нищо в изхода не препраща към външен скрипт или стилов лист
Причината да искате това е, че PDF страница е набор от позиционирани глифи, което е точно грешното нещо за телефонен екран, екранен четец или индекс за търсене. Всеки опит да се реши това чрез извличане на обикновен текст губи структурата, която е правила документа четим, а всеки опит да се реши чрез конвертиране на страници в изображения губи текста напълно. Модел за пренареждане запазва и двете: думите и връзките между тях
Откъде идва семантичната информация?
Всичко започва от GetStructuredText, единственият източник на текст и семантика в компонента. Когато PDF файлът носи структурно дърво — маркиран (tagged) PDF, както е дефиниран в ISO 32000-1, клауза 14.7 — моделът следва логическата йерархия, записана от производителя. Когато не носи, а повечето реални PDF файлове не носят, моделът се връща към физическия ред на оформлението, вече изчислен за целите на реда на четене
Този избор поддържа твърда граница: не се въвежда втори PDF анализатор и не се въвежда втори двигател за рендиране, за да се отговори на въпроси, на които съществуващият вече може да отговори. Механиката за ред на четене отдолу е описана в структурирани текстови блокове и ред на четене, а моделът за пренареждане е семантичен слой върху нея, а не негова замяна
Всеки възел записва откъде е дошла неговата информация, така че потребител може да различи заглавие, което документът е декларирал, от заглавие, изведено от евристиките за оформление. Конвейери, чувствителни към увереност, трябва да четат това поле, вместо да третират всички възли като еднакво авторитетни
Плоско дърво и защо не е дърво от обекти
Моделът е сплескано дърво в pre-order: масив от възли, където всеки възел носи ParentIndex и Depth, вместо рекурсивен запис или обектен граф със собственост. Страници, заглавия, параграфи, списъци, елементи от списъци, фигури, надписи, таблици, редове и клетки всички живеят в този единствен линеен масив
Следват две ползи. Потребителите могат да поточно обработват масива по ред без рекурсия, което прави извеждането на HTML, Markdown или дървовиден изглед прост цикъл. А оформлението остава преносимо между Delphi, C++Builder и Free Pascal, които се различават по това как обработват рекурсивни управлявани типове през ABI граница. Рекурсивен запис от динамични масиви е точно от вида конструкция, която се компилира навсякъде и се държи фино различно във всяка среда
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; // вграден блок стилове, без външен файл
Options.MaxNodes := 200000; // бюджет с провал по подразбиране (fail-closed)
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;
Как таблиците се пазят да не се появят два пъти?
Откриването на таблици се изпълнява, след като структурираният текст вече е събран за страница, което създава очевидна опасност: съдържанието на една и съща клетка съществува както в текстовите блокове, така и в откритата таблица. Извеждането и на двете произвежда HTML, в който всяка таблица е последвана от собственото си съдържание отново като свободни параграфи
Правилото, което решава това, е геометрично. Когато открита таблица покрива повече от половината площ на текстов блок, възелът на таблицата заменя този блок, вместо да се присъединява към него. Индексирането на клетки вътре в ред се изгражда чрез броене в кофи (buckets), така че изграждането на модела остава линейно спрямо клетки плюс редове, вместо да прескенира всяка клетка за всеки ред, което има значение при финансови документи, където една единствена страница може да носи стотици клетки
Откритата структура е честна за това, че е откриване. Таблица с разделителни линии се разпознава по-надеждно от такава, подравнена единствено чрез бели интервали, и увереността на възела отразява това. За съдържание, където грешна таблица е по-добра от липсваща таблица, оставете откриването включено; за архивно конвертиране, където грешна таблица е по-лоша, филтрирайте по увереност
Експортиране на HTML, който остава самостоятелен
ToHtml обхожда вече изградения модел и никога не се връща обратно към PDFium, така че двойно експортиране не струва нищо допълнително и не може да произведе различен резултат от същия модел. Текстовите и атрибутните стойности се екранират еднакво, нивата на заглавия се ограничават до диапазона от h1 до h6, който HTML действително дефинира, а заглавните клетки, RowSpan и ColumnSpan преминават както са записани
Незадължителният CSS е обикновен вграден блок стилове. Няма скрипт, няма уеб шрифт и никакъв външен ресурс от какъвто и да е вид, което е това, което прави изхода безопасен за вграждане в имейл, помощен визуализатор или контрола за браузър в пясъчник:
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // пазете границите на страници видими
Options.PreserveLineBreaks := False; // оставете браузъра да пренася параграфите
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 е опцията, за която най-много си струва да се замислите. Прекъсване на ред в PDF е решение за набор, взето за фиксирана широчина на страница, така че запазването му на тесен екран възпроизвежда точно проблема, за чието решаване съществува пренареждането. Запазвайте прекъсванията за поезия, кодови листинги и адреси; изхвърляйте ги за проза
Бюджети, отказ и състояние на страницата
Знаци, възли, таблици и клетки всеки имат таван, и всеки се проверява преди разпределяне, а не след това, така че деформиран или враждебен документ се проваля чисто, вместо да консумира памет, докато нещо друго не го направи. Токенът за отказ се проверява на границите на страница, блок, таблица, ред и клетка, което поддържа отменено сканиране на документ от хиляда страници отзивчиво
Едно поведение има значение конкретно за GUI приложения: цялото сканиране на документа се изпълнява вътре в обхват, който възстановява активната страница, така че успех, провал по бюджет и отказ всички оставят текущата страница на извикващия недокосната. Визуализатор, който позволява на потребителя да експортира, докато гледа страница 340, се оказва все още на страница 340 след това
За какво е добро пренареждането и за какво не е
Изходът от пренареждане е отличен вход за индексиране за търсене, достъпни изгледи за четене, мобилен дисплей и миграция на съдържание. Той не е конвертор, запазващ вярност: абсолютни позиции, точни шрифтове, векторна графика и прецизна геометрия на страницата са извън предназначението му по замисъл. Когато задача се нуждае страницата да изглежда по същия начин, рендирайте я; когато се нуждае страницата да бъде четима някъде другаде, пренаредете я
Конкретно за помощни технологии, моделът за пренареждане се съчетава с функциите за четене, описани в изграждане на достъпен четец, а документи, носещи истинско структурно дърво, произвеждат забележимо по-добри модели, което е добър аргумент за валидиране на маркирането по-нагоре по веригата, както е описано в валидиране на структурното дърво на PDF/UA
Пренареждането, структурираният текст, валидирането на маркирането и рендирането споделят един обект на документ в Delphi, C++Builder и Lazarus; пълното API е описано на страницата на PDFium Component за Delphi