Artykuł techniczny

Deklaratywny układ PDF w Delphi ze znakowanym wyjściem

HotPDF potrafi zbudować stronicowany dokument z drzewa deklaratywnego zamiast ze współrzędnych. Składasz THPDFDOMDocument z sekcji, stosów, tekstu, list i tabel, przekazujesz go do THPDFDOMRenderer, a renderer mierzy, dzieli na strony, rysuje elementy strony i na żądanie generuje drzewo struktury PDF/UA, które czyni wynik dostępnym. Kod układu nigdy nie oblicza współrzędnej y

Każdy, kto utrzymywał generator raportów sterowany współrzędnymi, wie, dlaczego to ma znaczenie. Pierwsza wersja działa. Potem adres klienta rozrasta się do trzech wierszy, tabela zyskuje wiersze, zlokalizowany nagłówek się zawija i każda kolejna pozycja y jest błędna. Poprawki gromadzą się jako ręczne sprawdzenia podziału stron rozproszone po logice biznesowej, a wymóg znakowanego PDF, który pojawia się dwa lata później, nie da się dobudować do kodu, który nie ma pojęcia, czym jest akapit

Co należy do drzewa i dlaczego własność jest ścisła

DOM wymusza pojedynczą własność na każdym poziomie: dokument jest właścicielem swoich sekcji, sekcja jest właścicielem swojego ciała, nagłówka i stopki, a stosy, kontenery i tabele są właścicielami swoich elementów podrzędnych. Ponowne użycie odbywa się przez Clone albo przez zarejestrowaną fabrykę, nigdy przez dołączenie tego samego obiektu do dwóch rodziców. Ta reguła nie jest formalnością. Komponent pojawiający się w drzewie dwukrotnie zostałby zmierzony dwa razy z różnymi ograniczeniami i zwolniony dwa razy przy niszczeniu

Praktyczną konsekwencją dla kodu wywołującego jest to, że funkcje pomocnicze zwracają nowe instancje. Zarejestrowanie fabryki za pomocą RegisterComponent i wywołanie CreateComponent daje nazwany przepis, który za każdym razem tworzy nowy komponent, i tak właśnie do drzewa trafiają powtarzalne elementy, takie jak blok podpisu czy stopka prawna

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // generuj drzewo struktury PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 w punktach
  Section.PageHeight := 842;
  Section.MarginLeft := 56;
  Section.MarginTop := 56;
  Section.MarginRight := 56;
  Section.MarginBottom := 56;
  Section.Style.FontName := 'Helvetica';
  Section.Style.FontSize := 10;

  Section.Body.AddHeading('Annual maintenance report', 1);
  Section.Body.AddText('Every asset inspected during the reporting ' +
    'period is listed below, grouped by site.');
  Section.Body.AddSpacer(12);

  Table := THPDFDOMTable.Create('assets');
  Table.AddColumn(3);                 // wagi, nie bezwzględne szerokości
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // wiersz nagłówka
  Row[0].Text := 'Asset';
  Row[1].Text := 'Last service';
  Row[2].Text := 'Status';
  for I := 0 to High(Assets) do
  begin
    Row := Table.AddRow(16);
    Row[0].Text := Assets[I].Name;
    Row[1].Text := Assets[I].ServiceDate;
    Row[2].Text := Assets[I].Status;
  end;
  Section.Body.Add(Table);
end;

Jak podział na strony unika kosztu kwadratowego?

Naiwny sposób dzielenia drzewa na strony polega na klonowaniu tego, co się nie zmieściło, i przenoszeniu tego na kolejną stronę. Przy tabeli z dziesięcioma tysiącami wierszy klonuje to pozostałe wiersze raz na stronę i zamienia dokument liniowy w kwadratowy

HotPDF zamiast tego dzieli wąsko. Renderer najwyższego poziomu przechodzi po elementach podrzędnych ciała po indeksie i nigdy nie klonuje całej sekcji ani całego ciała. Klonowany jest tylko dotknięty poddrzewo zagnieżdżonych stosów i kontenerów, które faktycznie rozciągają się przez granicę strony, a dwa ciężkie typy liści niosą kursor zamiast kopii: kontynuacja tekstu przechowuje zakres znaków źródłowych, które wciąż jest winna, a kontynuacja tabeli przechowuje wycinek wierszy, których jeszcze nie umieściła. Długie dokumenty pozostają liniowe, a długie akapity kosztują tyle samo, czy łamią się raz, czy pięć razy

Pomiar pozostaje uczciwy co do efektów ubocznych. THPDFLayoutElement.Measure musi być wolne od efektów ubocznych rysowania, a faktyczne umieszczenie zawsze przechodzi przez THotPDF.PlaceLayoutElement, tę samą centralną procedurę, która ponownie mierzy umieszczony fragment, ustanawia własność nadmiaru i zapisuje diagnostykę. Renderer DOM decyduje wyłącznie o zasadach nowej strony, elementach strony, odstępach i cyklu życia kontynuacji

Reguły nagłówka tabeli zapobiegające nieskończonemu dokumentowi

Powtarzanie nagłówków tabeli na kolejnych stronach brzmi prosto, a kryje dwa tryby awarii. HotPDF wymaga, aby wiersze nagłówka pojawiały się tylko w pierwszej serii kolejnych wierszy oraz aby pierwszy podział mieścił wszystkie wiersze nagłówka plus co najmniej jeden wiersz treści. Bez tej drugiej reguły nagłówek wyższy niż pozostała przestrzeń dawałby stronę zawierającą tylko nagłówek, po której następowałaby kolejna identyczna strona, w nieskończoność

Strony kontynuacji ponownie rysują nagłówek, a ta odtworzona kopia jest oznaczana jako artefakt, a nie jako treść, co jest poprawną odpowiedzią zarówno dla dostępności, jak i dla ekstrakcji tekstu. Oryginalny wiersz nagłówka pozostaje w logicznej strukturze tabeli dokładnie raz. Pomiń to, a czytnik ekranu ponownie ogłosi tytuły kolumn w środku danych, a ekstraktor tekstu wstawi zduplikowany wiersz nagłówka między wierszami treści

Istnieje też obronny pułap głębokości kontynuacji, ponieważ niestandardowy komponent może dowolnie zaimplementować Split tak, by zawsze zwracał równoważny ogon. Renderer sprawdza ten limit po odłączeniu ogona i przed rozpoczęciem kolejnej strony, a bieżąca iteracja zwalnia ogon we własnym bloku finally, dzięki czemu źle zachowujący się komponent zewnętrzny zawodzi z błędem możliwym do zdiagnozowania, zamiast zapełniać dysk

Jeden element logiczny, wiele fragmentów strony

Automatyczne znakowanie to miejsce, w którym model podziału na strony i model struktury muszą się zgadzać. Akapit podzielony między dwie strony to jeden logiczny akapit, więc musi pozostać jednym elementem struktury. Ale identyfikatory treści znakowanej są przypisane do strony, więc każdy widoczny fragment potrzebuje własnego MCID na stronie, na której się pojawia

HotPDF rozwiązuje to, utrzymując pojedynczy element struktury i dołączając dla każdego fragmentu odwołanie do treści znakowanej w jego tablicy /K, gdzie para /Pg i /MCID identyfikuje stronę i identyfikator. Miejsce w ParentTree dla tego MCID wskazuje z powrotem na ten sam element. To dokładnie to, czego wymaga ISO 14289, i to właśnie dlatego klony kontynuacji różnią się od zwykłych klonów: zwykły Clone oznacza nową treść logiczną i otrzymuje nową tożsamość semantyczną, podczas gdy wewnętrzny klon kontynuacji dziedziczy tożsamość komponentu, który kontynuuje

Ponowne użycie elementu jest wyszukiwane za pomocą indeksu tożsamości semantycznych posortowanego według wskaźnika komponentu i przeszukiwanego przez porównanie binarne, co utrzymuje wyszukiwanie na poziomie logarytmicznym nawet dla dużych drzew. Indeks przechowuje wyłącznie referencje nieposiadające, a cykl życia samych obiektów struktury pozostaje związany z grafem obiektów PDF

Reguły struktury, które renderer wymusza z góry

Przy włączonym GenerateStructure kilka reguł PDF/UA jest sprawdzanych w trakcie renderowania drzewa, a nie po tym, jak plik już istnieje. Nagłówki zaczynają się od poziomu 1 i nie mogą pomijać poziomów. LI może pojawić się tylko wewnątrz L, a Lbl i LBody tylko wewnątrz LI. TR należy do tabeli, a TH i TD do wiersza. Rysunek bez tekstu alternatywnego jest odrzucany w trybie PDF/UA

Wczesne odrzucenie to tutaj świadomy wybór. Walidator, który zgłasza brak tekstu alternatywnego po zapisaniu dokumentu, mówi ci, że partia dziesięciu tysięcy wyciągów wymaga ponownego wygenerowania; renderer, który odrzuca komponent, mówi ci, który to komponent, podczas gdy dane, które go wyprodukowały, są wciąż w zasięgu. Weryfikacja zgodności nadal należy do potoku jako osobny krok, a jej mechanika jest opisana w walidacji PDF/A, PDF/X i PDF/UA

var
  Pdf: THotPDF;
  Renderer: THPDFDOMRenderer;
  Stats: THPDFDOMRenderStatistics;
begin
  Pdf := THotPDF.Create(nil);
  Renderer := THPDFDOMRenderer.Create;
  try
    Pdf.FileName := 'maintenance-report.pdf';
    Pdf.BeginDoc;
    Stats := Renderer.Render(Doc, Pdf);
    Pdf.EndDoc;

    Writeln(Format('%d page(s), %d placement(s), %d split(s)',
      [Stats.PageCount, Stats.PlacementCount, Stats.SplitCount]));
    Writeln(Format('structure elements=%d marked content=%d artifacts=%d',
      [Stats.StructureElementCount, Stats.MarkedContentCount,
       Stats.ArtifactCount]));
    Writeln(Format('deepest continuation chain: %d',
      [Stats.MaximumContinuationDepth]));
  finally
    Renderer.Free;
    Doc.Free;
    Pdf.Free;
  end;
end;

Rekord statystyk jest bardziej użyteczny, niż wygląda na pierwszy rzut oka. Gwałtowny wzrost SplitCount po zmianie szablonu zwykle oznacza, że jakiś komponent zaczął mierzyć się jako wyższy niż jego kontener. Pełzający wzrost MaximumContinuationDepth to wczesne ostrzeżenie o komponencie, którego Split robi zbyt mały postęp na stronę. A porównanie ArtifactCount z liczbą stron kontynuacji potwierdza, że powtarzane nagłówki rzeczywiście zostały oznaczone jako artefakty

Gdzie DOM pasuje obok bezpośredniego API

DOM nie zastępuje bezpośredniego rysowania; siedzi na tych samych obiektach strony. Wszystko, co umieszcza renderer, można przeplatać z bezpośrednimi wywołaniami na THotPDF, co ma znaczenie, gdy raport potrzebuje jednego ręcznie pozycjonowanego elementu, na przykład obrazu podpisu w dokładnie określonym miejscu. Zamykanie stron pozostaje pod kontrolą AddPage i EndDoc, więc tryb natychmiastowego zrzutu nie przechowuje w pamięci żadnych ukończonych stron, a pamięć rezydentna pozostaje kontrolowana przez bieżące kontynuacje, zasoby fontów i zwykły graf obiektów dokumentu

Wybieraj DOM, gdy treść jest sterowana danymi, a układ regułami, a bezpośrednie rysowanie zostaw dla stałej grafiki. Jeśli twoim obecnym problemem jest konkretnie podział tabel na strony, warto najpierw przeczytać węższe podejście w generowaniu tabel w PDF, a zachowanie na poziomie tekstu, takie jak justowanie, jest opisane w justowaniu tekstu

Deklaratywny układ, automatyczne znakowanie i bezpośrednie API rysowania są dostarczane w tym samym komponencie dla Delphi i C++Builder; pełna lista funkcji znajduje się na stronie komponentu PDF HotPDF dla Delphi