Technický článek

Deklarativní rozvržení PDF v Delphi se značkovaným výstupem

HotPDF dokáže sestavit stránkovaný dokument z deklarativního stromu namísto ze souřadnic. Sestavíte THPDFDOMDocument ze sekcí, zásobníků, textu, seznamů a tabulek, předáte jej THPDFDOMRenderer a vykreslovač změří, rozstránkuje, nakreslí vybavení stránky a na požádání vytvoří strukturální strom PDF/UA, který výsledek zpřístupní. Kód rozvržení nikdy nepočítá souřadnici y

Každý, kdo udržoval generátor sestav řízený souřadnicemi, ví, proč na tom záleží. První verze funguje. Pak adresa zákazníka naroste na tři řádky, tabulka přibere řádky, lokalizovaný nadpis se zalomí a každá následující souřadnice y je špatně. Opravy se hromadí jako ruční kontroly zalomení stránky roztroušené po obchodní logice, a požadavek na značkované PDF, který přijde o dva roky později, nelze dodatečně zabudovat do kódu, který nemá ponětí, co je odstavec

Co strom vlastní a proč je vlastnictví přísné

DOM na každé úrovni vynucuje jediné vlastnictví: dokument vlastní své sekce, sekce vlastní své tělo, hlavičku a patičku a zásobníky, kontejnery a tabulky vlastní své potomky. Opakované použití probíhá přes Clone nebo přes registrovanou továrnu, nikdy připojením stejného objektu ke dvěma rodičům. Toto pravidlo není formalita. Komponenta, která se ve stromu objeví dvakrát, by byla dvakrát změřena s odlišnými omezeními a při úklidu dvakrát uvolněna

Praktickým důsledkem pro volající kód je, že pomocné funkce vrací nové instance. Registrace továrny přes RegisterComponent a volání CreateComponent vám dá pojmenovaný recept, který pokaždé vyprodukuje čerstvou komponentu, a tak do stromu patří opakované vybavení, jako je podpisový blok nebo právní patička

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // vydat strukturální strom PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 v bodech
  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, ne absolutní šířky
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // hlavičkový řádek
  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 se stránkování vyhýbá kvadratickým nákladům?

Naivní způsob stránkování stromu je naklonovat vše, co se nevešlo, a přenést to na další stránku. U tabulky s deseti tisíci řádky to naklonuje zbývající řádky pokaždé znovu na každé stránce a promění lineární dokument v kvadratický

HotPDF místo toho dělí úzce. Vykreslovač nejvyšší úrovně prochází potomky těla podle indexu a nikdy neklonuje celou sekci nebo tělo. Naklonuje se pouze zasažený podstrom vnořených zásobníků a kontejnerů, které skutečně přesahují hranici stránky, a dva náročné listové typy nesou kurzor místo kopie: pokračování textu ukládá zdrojový rozsah znaků, který ještě dluží, a pokračování tabulky ukládá výřez řádků, které ještě čekají na umístění. Dlouhé dokumenty zůstávají lineární a dlouhé odstavce stojí stejně, ať se zalomí jednou nebo pětkrát

Měření zůstává upřímné ohledně vedlejších účinků. THPDFLayoutElement.Measure musí být bez vedlejších účinků kreslení a skutečné umístění vždy prochází přes THotPDF.PlaceLayoutElement, stejnou centrální rutinu, která umístěný fragment znovu změří, nastaví vlastnictví přetečení a zaznamená diagnostiku. Vykreslovač DOM rozhoduje pouze o politice nové stránky, vybavení stránky, rozestupech a životnosti pokračování

Pravidla hlaviček tabulek, která zabraňují nekonečnému dokumentu

Opakování hlaviček tabulky napříč stránkami zní jednoduše, ale skrývá dva režimy selhání. HotPDF vyžaduje, aby se hlavičkové řádky objevovaly pouze v prvním souvislém běhu řádků a aby se do prvního rozdělení vešly všechny hlavičkové řádky plus alespoň jeden řádek těla. Bez druhého pravidla vytvoří hlavička vyšší, než je zbývající prostor, stránku obsahující jen hlavičku, po níž následuje další identická stránka, donekonečna

Navazující stránky hlavičku překreslí a tato překreslená kopie je označena jako artefakt, nikoli jako obsah, což je správná odpověď jak pro přístupnost, tak pro extrakci textu. Původní hlavičkový řádek zůstává v logické struktuře tabulky přesně jednou. Vynechte toto a čtečka obrazovky ohlásí názvy sloupců znovu uprostřed dat a extraktor textu vloží duplicitní hlavičkový řádek mezi řádky těla

Existuje také ochranný strop na hloubku pokračování, protože vlastní komponenta může implementovat Split způsobem, který vždy vrátí ekvivalentní zbytek. Vykreslovač kontroluje limit po odpojení zbytku a před zahájením další stránky a aktuální iterace uvolní zbytek ve vlastním bloku finally, takže nesprávně se chovající komponenta třetí strany selže s diagnostikovatelnou chybou místo zaplnění disku

Jeden logický prvek, mnoho fragmentů stránky

Automatické značkování je místem, kde se model stránkování a model struktury musí shodnout. Odstavec rozdělený na dvě stránky je jeden logický odstavec, takže musí zůstat jedním strukturálním prvkem. Ale identifikátory značkovaného obsahu jsou vázané na stránku, takže každý viditelný fragment potřebuje vlastní MCID na stránce, na které se objevuje

HotPDF to řeší tak, že udržuje jediný strukturální prvek a připojuje odkaz na značkovaný obsah do jeho pole /K pro každý fragment, přičemž dvojice /Pg a /MCID identifikuje stránku a identifikátor. Slot ParentTree pro daný MCID ukazuje zpět na stejný prvek. Přesně toto očekává ISO 14289, a proto se klony pokračování liší od běžných klonů: běžný Clone znamená nový logický obsah a dostane novou sémantickou identitu, zatímco interní klon pokračování dědí identitu komponenty, kterou pokračuje

Opětovné použití prvků se vyhledává přes index sémantických identit seřazený podle ukazatele komponenty a prohledávaný binárním porovnáním, což udržuje vyhledávání logaritmické i u rozsáhlých stromů. Index obsahuje pouze nevlastnící reference; životnost samotných strukturálních objektů zůstává vázaná na graf objektů PDF

Pravidla struktury, která vykreslovač vynucuje předem

Se zapnutým GenerateStructure se několik pravidel PDF/UA kontroluje během vykreslování stromu, ne až po vzniku souboru. Nadpisy začínají na úrovni 1 a nesmí přeskakovat úrovně. LI se smí objevit pouze uvnitř L a Lbl a LBody pouze uvnitř LI. TR patří tabulce a TH a TD patří řádku. Obrázek bez alternativního textu je v režimu PDF/UA odmítnut

Odmítnutí co nejdříve je zde záměrná volba. Validátor, který nahlásí chybějící alternativní text až po zapsání dokumentu, vám řekne, že je potřeba přegenerovat dávku deseti tisíc výpisů; vykreslovač, který komponentu odmítne, vám řekne kterou, zatímco data, z nichž vznikla, jsou stále v dosahu. Ověřování shody stále patří do pipeline jako samostatný krok a jeho mechanika je popsána v článku ověřování 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;

Statistický záznam je užitečnější, než se na první pohled zdá. Prudký nárůst SplitCount po změně šablony obvykle znamená, že komponenta se začala měřit vyšší, než je její kontejner. Postupně rostoucí MaximumContinuationDepth je včasné varování před komponentou, jejíž Split dělá na stránku příliš malý pokrok. A porovnání ArtifactCount s počtem navazujících stránek potvrzuje, že opakované hlavičky byly skutečně označeny jako artefakty

Kam DOM zapadá vedle přímého API

DOM nenahrazuje přímé kreslení; sedí na stejných objektech stránky. Cokoli, co vykreslovač umístí, lze prokládat přímými voláními na THotPDF, což je důležité, když sestava potřebuje jeden ručně umístěný prvek, například obrázek podpisu na přesné poloze. Uzavírání stránek zůstává pod kontrolou AddPage a EndDoc, takže režim okamžitého vyprázdnění nedrží v paměti žádné dokončené stránky a rezidentní paměť zůstává řízena aktuálními pokračováními, prostředky fontů a běžným grafem objektů dokumentu

Zvolte DOM, když je obsah řízen daty a rozvržení pravidly, a ponechte přímé kreslení pro pevnou grafiku. Pokud je vaší aktuální bolestí konkrétně stránkování tabulek, stojí za přečtení nejprve užší přístup v článku generování tabulek v PDF, a chování na úrovni textu, jako je zarovnání do bloku, popisuje článek zarovnání textu do bloku

Deklarativní rozvržení, automatické značkování a přímé kreslicí API jsou součástí stejné komponenty pro Delphi a C++Builder; kompletní seznam funkcí je na stránce komponenty HotPDF PDF pro Delphi