Tehnički članak

Deklarativni raspored PDF-a u Delphiju sa označenim izlazom

HotPDF može da izgradi paginirani dokument iz deklarativnog stabla umesto iz koordinata. Sastavljate THPDFDOMDocument od sekcija, stekova, teksta, listi i tabela, predajete ga THPDFDOMRenderer-u, a renderer meri, paginira, crta nameštaj stranice i, kada se to traži, emituje PDF/UA strukturno stablo koje čini rezultat pristupačnim. Kod rasporeda nikada ne izračunava y koordinatu

Svako ko je održavao generator izveštaja vođen koordinatama zna zašto je ovo bitno. Prva verzija radi. Zatim adresa kupca naraste na tri reda, tabela dobije redove, lokalizovan naslov se prelama, i svaka nizvodna y pozicija je pogrešna. Popravke se gomilaju kao ručne provere preloma stranica raspršene po poslovnoj logici, a zahtev za označeni PDF koji stigne dve godine kasnije ne može se naknadno ugraditi u kod koji nema pojma šta je pasus

Šta stablo poseduje, i zašto je vlasništvo strogo?

DOM primorava jedinstveno vlasništvo na svakom nivou: dokument poseduje svoje sekcije, sekcija poseduje svoje telo, zaglavlje i podnožje, a stekovi, kontejneri i tabele poseduju svoju decu. Ponovna upotreba se dešava kroz Clone ili kroz registrovanu fabriku, nikada tako što se isti objekat prikači na dva roditelja. To pravilo nije ceremonija. Komponenta koja se pojavljuje dvaput u stablu bila bi izmerena dvaput sa različitim ograničenjima i oslobođena dvaput pri rušenju

Praktična posledica za pozivajući kod je da pomoćnici vraćaju nove instance. Registrovanje fabrike pomoću RegisterComponent i pozivanje CreateComponent daje vam imenovan recept koji proizvodi svežu komponentu svaki put, što je način na koji ponovljeni nameštaj poput bloka za potpis ili pravnog podnožja pripada stablu

uses
  HPDFDoc, HPDFLayoutDOM;

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

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 u tač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);      // red 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 izbegava kvadratnu cenu?

Naivan način paginacije stabla je kloniranje svega što nije stalo i prenošenje na sledeću stranicu. Na tabeli sa deset hiljada redova, to klonira preostale redove jednom po stranici i pretvara linearan dokument u kvadratan

HotPDF umesto toga deli usko. Renderer najvišeg nivoa obilazi decu tela po indeksu i nikada ne klonira celu sekciju ili telo. Samo ugnježdeni stekovi i kontejneri koji zaista prelaze preko granice stranice dobijaju kloniranje pogođenog podstabla, a dva teška lista tipa nose kursor umesto kopije: nastavak teksta čuva izvorni opseg karaktera koji još duguje, a nastavak tabele čuva odsečak redova koji tek treba da postavi. Dugi dokumenti ostaju linearni, a dugi pasusi koštaju isto bez obzira da li se prelome jednom ili pet puta

Merenje ostaje pošteno u pogledu sporednih efekata. THPDFLayoutElement.Measure je obavezan da bude bez sporednih efekata crtanja, a stvarno postavljanje se uvek izvodi kroz THotPDF.PlaceLayoutElement, istu centralnu rutinu koja ponovo meri postavljen fragment, postavlja vlasništvo prekoračenja i beleži dijagnostiku. DOM renderer odlučuje samo politiku sveže stranice, nameštaj stranice, razmak i vek trajanja nastavaka

Pravila zaglavlja tabele koja sprečavaju beskonačan dokument

Ponavljanje zaglavlja tabele preko stranica zvuči jednostavno i krije dva otkaza. HotPDF zahteva da se redovi zaglavlja pojavljuju samo u prvom nizu uzastopnih redova, i da prvo deljenje smesti sve redove zaglavlja plus barem jedan red tela. Bez drugog pravila, zaglavlje više od preostalog prostora proizvodi stranicu koja ne sadrži ništa osim zaglavlja, praćenu drugom identičnom stranicom, zauvek

Stranice nastavka ponovo crtaju zaglavlje, a ta ponovo nacrtana kopija je označena kao artefakt, a ne kao sadržaj, što je ispravan odgovor i za pristupačnost i za izvlačenje teksta. Originalni red zaglavlja ostaje u logičkoj strukturi tabele tačno jednom. Preskočite ovo i čitač ekrana najavljuje naslove kolona ponovo usred podataka, a izvlakač teksta umeće duplirani red zaglavlja između redova tela

Postoji i odbrambeni plafon na dubinu nastavka, jer je prilagođena komponenta slobodna da implementira Split na način koji uvek vraća ekvivalentan rep. Renderer proverava ograničenje nakon odvajanja repa i pre početka sledeće stranice, a trenutna iteracija oslobađa rep u sopstvenom bloku finally, tako da komponenta treće strane koja se loše ponaša ne uspe sa dijagnostikovanom greškom umesto da puni disk

Jedan logički element, mnogo fragmenata stranice

Automatsko označavanje je mesto gde model paginacije i model strukture moraju da se slože. Pasus podeljen na dve stranice je jedan logički pasus, pa mora ostati jedan element strukture. Ali identifikatori obeleženog sadržaja su po stranici, pa svaki vidljivi fragment treba sopstveni MCID na stranici na kojoj se pojavljuje

HotPDF ovo rešava tako što drži jedan element strukture i dodaje referencu obeleženog sadržaja u njegov niz /K za svaki fragment, sa parom /Pg i /MCID koji identifikuje stranicu i identifikator. Slot ParentTree za taj MCID pokazuje nazad na isti element. Ovo je tačno ono što ISO 14289 očekuje, 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 dobija novi semantički identitet, dok interni klon nastavka nasleđuje identitet komponente koju nastavlja

Ponovna upotreba elementa se traži preko indeksa semantičkih identiteta sortiranih po pokazivaču komponente i pretraženih binarnim poređenjem, što drži pretragu logaritamskom na velikim stablima. Indeks čuva samo reference bez vlasništva; vek trajanja samih objekata strukture ostaje uz graf objekata PDF-a

Pravila strukture koja renderer sprovodi unapred

Sa uključenim GenerateStructure, nekoliko pravila PDF/UA se proverava dok se stablo renderuje, a ne nakon što fajl već postoji. Naslovi počinju od nivoa 1 i ne smeju preskočiti nivoe. LI može da se pojavi samo unutar L, a Lbl i LBody samo unutar LI. TR pripada tabeli, a TH i TD redu. Figura bez alternativnog teksta se odbacuje u PDF/UA režimu

Odbacivanje rano je namerni izbor ovde. Validator koji prijavljuje nedostajući alternativni tekst nakon što je dokument napisan govori vam da je serija od deset hiljada izvoda potrebno ponovo generisati; renderer koji odbija komponentu govori vam koja komponenta, dok su podaci koji su je proizveli još uvek u opsegu. Provera usklađenosti i dalje pripada pipeline-u kao poseban korak, a mehanika toga je obrađena u validaciji 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 je korisniji nego što na prvi pogled izgleda. Naglo rastuće SplitCount nakon izmene šablona obično znači da je komponenta počela da se meri viša od svog kontejnera. Sporo rastuće MaximumContinuationDepth je rani znak upozorenja za komponentu čiji Split pravi premalo napretka po stranici. A poređenje ArtifactCount sa brojem stranica nastavka potvrđuje da su ponovljena zaglavlja zaista označena kao artefakti

Gde se DOM uklapa uz direktan API

DOM ne zamenjuje direktno crtanje; sedi na vrhu istih objekata stranice. Sve što renderer postavi može se isprepletati sa direktnim pozivima na THotPDF, što je bitno kada izveštaj treba jedan ručno pozicioniran element poput slike potpisa na tačnoj lokaciji. Zatvaranje stranice ostaje pod kontrolom AddPage i EndDoc, tako da režim trenutnog ispiranja ne drži nijednu završenu stranicu u memoriji, a rezidentna memorija ostaje upravljana trenutnim nastavcima, resursima fonta i običnim grafom objekata dokumenta

Izaberite DOM kada su podaci vođeni podacima, a raspored pravilima, i zadržite direktno crtanje za fiksnu grafiku. Ako je vaš trenutni problem specifično paginacija tabele, uži pristup u generisanju tabela u PDF-u vredi pročitati prvo, a ponašanje na nivou teksta poput poravnanja je opisano u poravnanju teksta

Deklarativni raspored, automatsko označavanje i API direktnog crtanja isporučuju se u istoj komponenti za Delphi i C++Builder; kompletna lista funkcija je na stranici HotPDF Delphi PDF komponente