Odborný článok

Deklaratívne rozloženie PDF v Delphi so značkovaným výstupom

HotPDF dokáže zostaviť stránkovaný dokument z deklaratívneho stromu namiesto zo súradníc. Zostavíte THPDFDOMDocument zo sekcií, zásobníkov, textu, zoznamov a tabuliek, odovzdáte ho THPDFDOMRenderer a renderer zmeria, rozdelí na stránky, vykreslí prvky stránky a na požiadanie vygeneruje aj štruktúrny strom PDF/UA, ktorý výsledok sprístupní. Kód rozloženia nikdy nepočíta súradnicu y

Každý, kto niekedy udržiaval generátor zostáv riadený súradnicami, vie, prečo na tomto záleží. Prvá verzia funguje. Potom adresa zákazníka narastie na tri riadky, tabuľka získa ďalšie riadky, lokalizovaný nadpis sa zalomí a každá nasledujúca pozícia y je nesprávna. Opravy sa hromadia ako ručné kontroly zalomenia strán rozptýlené po celej obchodnej logike, a požiadavku na značkované PDF, ktorá príde o dva roky neskôr, sa nedá dodatočne naimplementovať do kódu, ktorý netuší, čo je odsek

Čo strom vlastní a prečo je vlastníctvo prísne

DOM na každej úrovni vynucuje jediné vlastníctvo: dokument vlastní svoje sekcie, sekcia vlastní svoje telo, hlavičku a pätu a zásobníky, kontajnery a tabuľky vlastnia svoje deti. Opätovné použitie prebieha cez Clone alebo cez zaregistrovanú továrenskú metódu, nikdy pripojením toho istého objektu k dvom rodičom naraz. Toto pravidlo nie je formalita. Komponent, ktorý by sa v strome objavil dvakrát, by sa meral dvakrát s odlišnými obmedzeniami a pri rušení by sa uvoľnil dvakrát

Praktickým dôsledkom pre volajúci kód je, že pomocné funkcie vracajú nové inštancie. Zaregistrovanie továrenskej metódy pomocou RegisterComponent a volanie CreateComponent vám dá pomenovaný recept, ktorý pri každom volaní vytvorí čerstvý komponent, čo je spôsob, akým do stromu patria opakujúce sa prvky, ako je blok podpisu alebo právna päta

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // generovať štruktúrny strom PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 v bodoch
  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);                 // váhy, nie absolútne šírky
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // hlavičkový riadok
  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;

Ako sa stránkovanie vyhýba kvadratickej cene?

Naivný spôsob stránkovania stromu je klonovať to, čo sa nezmestilo, a preniesť to na ďalšiu stránku. Pri tabuľke s desaťtisíc riadkami to klonuje zostávajúce riadky raz na každú stránku a mení lineárny dokument na kvadratický

HotPDF namiesto toho delí úzko. Renderer najvyššej úrovne prechádza deti tela podľa indexu a nikdy neklonuje celú sekciu alebo telo. Klonovaný je iba dotknutý podstrom vnorených zásobníkov a kontajnerov, ktoré skutočne prekračujú hranicu stránky, a oba náročné listové typy nesú kurzor namiesto kópie: textová kontinuácia ukladá rozsah znakov zdroja, ktorý ešte dlhuje, a kontinuácia tabuľky ukladá výrez riadkov, ktoré ešte treba umiestniť. Dlhé dokumenty zostávajú lineárne a dlhé odseky stoja rovnako bez ohľadu na to, či sa zalomia raz alebo päťkrát

Meranie zostáva úprimné, čo sa týka vedľajších účinkov. THPDFLayoutElement.Measure musí byť bez akýchkoľvek vedľajších účinkov kreslenia a skutočné umiestnenie vždy prebieha cez THotPDF.PlaceLayoutElement, tú istú centrálnu rutinu, ktorá umiestnený fragment znovu zmeria, nastaví vlastníctvo pretečenia a zaznamená diagnostiku. Renderer DOM rozhoduje iba o politike novej stránky, prvkoch stránky, rozostupoch a životnosti kontinuácií

Pravidlá hlavičky tabuľky, ktoré zabraňujú nekonečnému dokumentu

Opakovanie hlavičiek tabuliek naprieč stránkami znie jednoducho a skrýva dva režimy zlyhania. HotPDF vyžaduje, aby sa hlavičkové riadky objavili iba v prvom súvislom bloku riadkov a aby sa do prvého rozdelenia zmestili všetky hlavičkové riadky plus aspoň jeden riadok tela. Bez druhého pravidla hlavička vyššia než zostávajúci priestor vytvorí stránku obsahujúcu iba hlavičku, po ktorej nasleduje ďalšia identická stránka, donekonečna

Pokračujúce stránky prekresľujú hlavičku a táto prekreslená kópia je označená ako artefakt, nie ako obsah, čo je správna odpoveď pre prístupnosť aj pre extrakciu textu. Pôvodný hlavičkový riadok zostáva v logickej štruktúre tabuľky presne raz. Ak toto vynecháte, čítačka obrazovky oznámi názvy stĺpcov znovu uprostred dát a extraktor textu vloží duplicitný hlavičkový riadok medzi riadky tela

Existuje aj ochranný strop na hĺbku kontinuácie, pretože vlastný komponent môže implementovať Split spôsobom, ktorý vždy vráti ekvivalentný zvyšok. Renderer kontroluje limit po odpojení zvyšku a pred začatím ďalšej stránky, a aktuálna iterácia uvoľní zvyšok vo vlastnom bloku finally, takže nesprávne sa správajúci komponent tretej strany zlyhá s diagnostikovateľnou chybou namiesto zapĺňania disku

Jeden logický element, mnoho fragmentov na stránkach

Automatické značkovanie je miesto, kde sa musí model stránkovania zhodnúť s modelom štruktúry. Odsek rozdelený na dve stránky je jeden logický odsek, takže musí zostať jedným štruktúrnym elementom. No identifikátory značkovaného obsahu sú viazané na stránku, takže každý viditeľný fragment potrebuje vlastný MCID na stránke, na ktorej sa objavuje

HotPDF to rieši tak, že ponechá jediný štruktúrny element a pre každý fragment pripojí do jeho poľa /K odkaz na značkovaný obsah, pričom dvojica /Pg a /MCID identifikuje stránku a identifikátor. Slot ParentTree pre daný MCID ukazuje späť na ten istý element. Presne toto očakáva norma ISO 14289 a je to aj dôvod, prečo sa klony kontinuácie líšia od bežných klonov: bežný Clone znamená nový logický obsah a dostane novú sémantickú identitu, zatiaľ čo interný klon kontinuácie dedí identitu komponentu, ktorý pokračuje

Opätovné použitie elementov sa vyhľadáva cez index sémantických identít zoradený podľa ukazovateľa komponentu a prehľadávaný binárnym porovnávaním, čo udržuje vyhľadávanie logaritmické aj na veľkých stromoch. Index obsahuje iba nevlastniace referencie; životnosť samotných štruktúrnych objektov zostáva viazaná na graf objektov PDF

Pravidlá štruktúry, ktoré renderer vynucuje vopred

Pri zapnutom GenerateStructure sa niekoľko pravidiel PDF/UA kontroluje počas vykresľovania stromu, nie až po tom, čo súbor existuje. Nadpisy začínajú na úrovni 1 a nesmú preskočiť úroveň. LI sa môže objaviť iba vnútri L a Lbl a LBody iba vnútri LI. TR patrí tabuľke a TH a TD patria riadku. Obrázok bez alternatívneho textu je v režime PDF/UA odmietnutý

Skoré odmietnutie je tu zámerná voľba. Validátor, ktorý nahlási chýbajúci alternatívny text až po zapísaní dokumentu, vám povie, že treba znovu vygenerovať dávku desaťtisíc výpisov; renderer, ktorý odmietne komponent, vám povie ktorý komponent, kým dáta, ktoré ho vytvorili, sú ešte v dosahu. Overenie zhody stále patrí do pipeline ako samostatný krok a jeho mechanika je opísaná v časti validácia PDF/A, PDF/X a 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;

Záznam štatistík je užitočnejší, než sa na prvý pohľad zdá. Prudký nárast SplitCount po zmene šablóny zvyčajne znamená, že komponent sa začal merať vyšší než jeho kontajner. Postupné narastanie MaximumContinuationDepth je včasné varovanie pre komponent, ktorého Split robí príliš malý pokrok na stránku. A porovnanie ArtifactCount s počtom pokračujúcich stránok potvrdzuje, že opakované hlavičky boli skutočne označené ako artefakty

Kde DOM zapadá popri priamom API

DOM nenahrádza priame kreslenie; stavia sa nad rovnaké objekty stránky. Čokoľvek, čo renderer umiestni, sa dá prekladať s priamymi volaniami na THotPDF, čo je dôležité, keď zostava potrebuje jeden ručne umiestnený prvok, napríklad obrázok podpisu na presnom mieste. Uzatváranie stránok zostáva pod kontrolou AddPage a EndDoc, takže režim okamžitého vyprázdňovania nedrží v pamäti žiadne dokončené stránky a rezidentná pamäť zostáva riadená aktuálnymi kontinuáciami, zdrojmi písiem a bežným grafom objektov dokumentu

Zvoľte DOM, keď je obsah riadený dátami a rozloženie riadené pravidlami, a ponechajte priame kreslenie pre pevnú grafiku. Ak je vašim aktuálnym problémom konkrétne stránkovanie tabuliek, oplatí sa najprv prečítať užší prístup v časti generovanie tabuliek v PDF, a správanie na úrovni textu, ako je zarovnávanie do bloku, opisuje časť zarovnávanie textu do bloku

Deklaratívne rozloženie, automatické značkovanie a API na priame kreslenie sú súčasťou toho istého komponentu pre Delphi a C++Builder; kompletný zoznam funkcií nájdete na stránke HotPDF Delphi PDF component