Tehnički članak

Deklarativni PDF raspored u Delphiju s označenim (tagged) izlazom

HotPDF može izgraditi paginirani dokument iz deklarativnog stabla umjesto iz koordinata. Sastavljate THPDFDOMDocument od odjeljaka, hrpa (stack), teksta, popisa i tablica, predajete ga THPDFDOMRenderer-u, a renderer mjeri, paginira, crta elemente stranice i, na zahtjev, generira PDF/UA strukturno stablo koje rezultat čini pristupačnim. Kod rasporeda nikada ne izračunava y koordinatu

Svatko tko je održavao generator izvještaja utemeljen na koordinatama zna zašto je ovo važno. Prva verzija radi. Zatim adresa kupca naraste na tri retka, tablica dobije nove retke, lokalizirani naslov se prelama u novi red i svaka daljnja y pozicija postane pogrešna. Popravci se gomilaju kao ručne provjere prijeloma stranice razbacane po poslovnoj logici, a zahtjev za označenim (tagged) PDF-om koji stigne dvije godine kasnije ne može se naknadno ugraditi u kod koji nema pojma što je odlomak

Što stablo posjeduje i zašto je vlasništvo strogo

DOM na svakoj razini provodi jedinstveno vlasništvo: dokument posjeduje svoje odjeljke, odjeljak posjeduje svoje tijelo, zaglavlje i podnožje, a hrpe, kontejneri i tablice posjeduju svoju djecu. Ponovna uporaba odvija se putem Clone ili putem registrirane tvornice, nikada pripajanjem istog objekta dvama roditeljima. To pravilo nije formalnost. Komponenta koja se u stablu pojavi dvaput bila bi izmjerena dvaput s različitim ograničenjima i oslobođena dvaput pri rušenju

Praktična posljedica za pozivajući kod jest da pomoćne funkcije vraćaju nove instance. Registriranje tvornice pomoću RegisterComponent i pozivanje CreateComponent daje vam imenovani recept koji svaki put proizvodi novu komponentu, a upravo tako ponovljeni elementi poput bloka za potpis ili pravnog podnožja pripadaju u stablo

uses
  HPDFDoc, HPDFLayoutDOM;

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

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 u točkama
  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);                 // težine, ne apsolutne širine
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // redak zaglavlja
  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;

Kako paginacija izbjegava kvadratni trošak?

Naivan način paginacije stabla jest kloniranje svega što nije stalo i prenošenje toga na sljedeću stranicu. Na tablici s deset tisuća redaka to klonira preostale retke jednom po stranici i pretvara linearan dokument u kvadratni

HotPDF umjesto toga dijeli usko. Renderer najviše razine prolazi kroz djecu tijela po indeksu i nikada ne klonira cijeli odjeljak ili tijelo. Samo se ugniježđenim hrpama i kontejnerima koji doista sežu preko granice stranice klonira pogođeno podstablo, a dva teška lista tipa nose kursor umjesto kopije: nastavak teksta pohranjuje raspon izvornih znakova koje još duguje, a nastavak tablice pohranjuje isječak redaka koje tek treba postaviti. Dugi dokumenti ostaju linearni, a dugi odlomci koštaju jednako bez obzira lome li se jednom ili pet puta

Mjerenje ostaje pošteno prema nuspojavama. THPDFLayoutElement.Measure mora biti bez nuspojava crtanja, a stvarno postavljanje uvijek prolazi kroz THotPDF.PlaceLayoutElement, istu središnju rutinu koja ponovno mjeri postavljeni fragment, uspostavlja vlasništvo nad prelijevanjem i bilježi dijagnostiku. DOM renderer odlučuje samo o politici nove stranice, elementima stranice, razmacima i životnom vijeku nastavaka

Pravila zaglavlja tablice koja sprječavaju beskonačan dokument

Ponavljanje zaglavlja tablice preko stranica zvuči jednostavno, a skriva dva načina otkazivanja. HotPDF zahtijeva da se redovi zaglavlja pojavljuju samo u prvom nizu uzastopnih redaka i da prvo dijeljenje stane sve retke zaglavlja plus barem jedan redak tijela. Bez ovog drugog pravila, zaglavlje više od preostalog prostora proizvodi stranicu koja ne sadrži ništa osim zaglavlja, praćenu drugom identičnom stranicom, zauvijek

Stranice nastavka ponovno crtaju zaglavlje, a ta ponovno nacrtana kopija označena je kao artefakt, a ne kao sadržaj, što je ispravan odgovor i za pristupačnost i za izdvajanje teksta. Izvorni redak zaglavlja ostaje u logičnoj strukturi tablice točno jednom. Preskočite ovo i čitač zaslona ponovno najavljuje nazive stupaca usred podataka, a alat za izdvajanje teksta umeće dvostruki redak zaglavlja između redaka tijela

Postoji i obrambeni gornji limit na dubinu nastavaka, jer prilagođena komponenta može implementirati Split na način koji uvijek vraća ekvivalentni rep. Renderer provjerava ograničenje nakon odvajanja repa, a prije početka sljedeće stranice, a trenutna iteracija oslobađa rep u vlastitom bloku finally, tako da komponenta treće strane koja se loše ponaša zakaže s dijagnosticiranom greškom umjesto da napuni disk

Jedan logički element, mnogo fragmenata stranica

Automatsko označavanje mjesto je gdje se model paginacije i model strukture moraju usuglasiti. Odlomak razdijeljen preko dvije stranice jedan je logički odlomak, pa mora ostati jedan strukturni element. No identifikatori označenog sadržaja vrijede po stranici, pa svaki vidljivi fragment treba vlastiti MCID na stranici na kojoj se pojavljuje

HotPDF ovo rješava zadržavanjem jedinstvenog strukturnog elementa i dodavanjem reference na označeni sadržaj u njegovo polje /K za svaki fragment, pri čemu par /Pg i /MCID identificira stranicu i identifikator. Mjesto ParentTree za taj MCID pokazuje natrag na isti element. Upravo to zahtijeva ISO 14289, i to je razlog zašto se klonovi nastavka razlikuju od običnih klonova: obični Clone znači novi logički sadržaj i dobiva novi semantički identitet, dok interni klon nastavka nasljeđuje identitet komponente koju nastavlja

Ponovna uporaba elementa traži se kroz indeks semantičkih identiteta poredanih prema pokazivaču komponente i pretražuje binarnom usporedbom, čime pretraga ostaje logaritamska na velikim stablima. Indeks sadrži samo reference bez vlasništva; životni vijek samih strukturnih objekata ostaje vezan uz graf PDF objekata

Pravila strukture koja renderer provodi unaprijed

Kada je GenerateStructure uključen, nekoliko pravila PDF/UA provjerava se dok se stablo renderira, a ne nakon što datoteka već postoji. Naslovi počinju na razini 1 i ne smiju preskakati razine. LI se smije pojaviti samo unutar L, a Lbl i LBody samo unutar LI. TR pripada tablici, a TH i TD retku. Slika bez alternativnog teksta odbacuje se u PDF/UA načinu rada

Rano odbacivanje ovdje je namjerni izbor. Validator koji nakon što je dokument napisan prijavi nedostajući alternativni tekst govori vam da je paket od deset tisuća izvoda potrebno ponovno generirati; renderer koji odbije komponentu govori vam koju komponentu, dok su podaci koji su je proizveli još u dosegu. Provjera usklađenosti i dalje pripada pipelineu kao zaseban korak, a mehanika toga opisana je u provjeri usklađenosti 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;

Zapis statistike korisniji je nego što se čini na prvi pogled. Naglo povećanje SplitCount nakon promjene predloška obično znači da je komponenta počela mjeriti veću visinu od svog spremnika. Postupno rastuća vrijednost MaximumContinuationDepth rano je upozorenje za komponentu čiji Split ostvaruje premalo napretka po stranici. A usporedba ArtifactCount s brojem stranica nastavka potvrđuje da su ponovljena zaglavlja doista označena kao artefakti

Kako se DOM uklapa uz izravni API

DOM ne zamjenjuje izravno crtanje; sjedi na vrhu istih objekata stranice. Sve što renderer postavi može se isprepletati s izravnim pozivima na THotPDF, što je važno kada izvještaj treba jedan ručno pozicioniran element, poput slike potpisa na točnoj lokaciji. Zatvaranje stranice ostaje pod kontrolom AddPage i EndDoc, tako da način neposrednog pražnjenja ne drži dovršene stranice u memoriji, a rezidentna memorija ostaje pod kontrolom trenutnih nastavaka, resursa fontova i uobičajenog grafa objekata dokumenta

Odaberite DOM kada je sadržaj vođen podacima, a raspored pravilima, a izravno crtanje zadržite za fiksnu grafiku. Ako je vaš trenutni problem konkretno paginacija tablica, isplati se prvo pročitati uži pristup u generiranju tablica u PDF-u, a ponašanje na razini teksta poput poravnanja opisano je u poravnanju teksta

Deklarativni raspored, automatsko označavanje i izravni API za crtanje isporučuju se u istoj komponenti za Delphi i C++Builder; cjelokupni popis značajki nalazi se na stranici HotPDF Delphi PDF komponente