Articol tehnic

Layout PDF declarativ în Delphi cu rezultat marcat (tagged)

HotPDF poate construi un document paginat dintr-un arbore declarativ în loc de coordonate. Asamblezi un THPDFDOMDocument din secțiuni, stive, text, liste și tabele, îl predai lui THPDFDOMRenderer, iar randorul măsoară, paginează, desenează elementele de decor ale paginii și, la cerere, emite arborele de structură PDF/UA care face rezultatul accesibil. Codul de layout nu calculează niciodată o coordonată y

Oricine a întreținut un generator de rapoarte bazat pe coordonate știe de ce contează asta. Prima versiune funcționează. Apoi o adresă de client crește la trei rânduri, un tabel câștigă rânduri, un titlu localizat se rupe pe mai multe linii, iar fiecare poziție y din aval devine greșită. Corecțiile se acumulează ca verificări manuale de întrerupere de pagină răspândite prin logica de business, iar cerința de PDF marcat (tagged) care apare doi ani mai târziu nu poate fi adăugată retroactiv pe un cod care nu are nicio idee ce este un paragraf

Ce deține arborele și de ce proprietatea este strictă

DOM-ul impune proprietate unică la fiecare nivel: documentul își deține secțiunile, o secțiune își deține corpul, antetul și subsolul, iar stivele, containerele și tabelele își dețin copiii. Reutilizarea se face prin Clone sau printr-o fabrică înregistrată, niciodată prin atașarea aceluiași obiect la doi părinți. Această regulă nu e formalism. O componentă care apare de două ori în arbore ar fi măsurată de două ori cu constrângeri diferite și eliberată de două ori la demontare

Consecința practică pentru codul apelant este că funcțiile ajutătoare returnează instanțe noi. Înregistrarea unei fabrici cu RegisterComponent și apelarea CreateComponent îți dă o rețetă denumită care produce o componentă proaspătă de fiecare dată, ceea ce e modul în care elemente de decor repetate, cum ar fi un bloc de semnătură sau un subsol legal, aparțin arborelui

uses
  HPDFDoc, HPDFLayoutDOM;

var
  Doc: THPDFDOMDocument;
  Section: THPDFDOMSection;
  Table: THPDFDOMTable;
  Row: THPDFDOMTableRow;
  I: Integer;
begin
  Doc := THPDFDOMDocument.Create;
  Doc.GenerateStructure := True;        // emite arborele de structură PDF/UA
  Doc.Language := 'en-US';

  Section := Doc.AddSection;
  Section.PageWidth := 595;           // A4 în puncte
  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);                 // ponderi, nu lățimi absolute
  Table.AddColumn(1);
  Table.AddColumn(1);
  Table.RepeatHeaders := True;
  Row := Table.AddRow(18, True);      // rând de antet
  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;

Cum evită paginarea costul pătratic?

Modul naiv de a pagina un arbore este să clonezi tot ce nu a încăput și să duci la pagina următoare. Pe un tabel cu zece mii de rânduri, asta clonează rândurile rămase o dată pentru fiecare pagină și transformă un document liniar într-unul pătratic

HotPDF împarte strâmt, în schimb. Randorul de nivel superior parcurge copiii corpului după index și nu clonează niciodată o secțiune sau un corp întreg. Doar stivele și containerele imbricate care chiar traversează o limită de pagină primesc subarborele afectat clonat, iar cele două tipuri de frunze grele poartă un cursor în loc de o copie: o continuare de text stochează intervalul de caractere sursă pe care încă îl datorează, iar o continuare de tabel stochează felia de rânduri pe care încă trebuie să o plaseze. Documentele lungi rămân liniare, iar paragrafele lungi costă la fel indiferent dacă se rup o dată sau de cinci ori

Măsurarea rămâne onestă cu privire la efectele secundare. THPDFLayoutElement.Measure trebuie să fie lipsită de efecte secundare de desenare, iar plasarea efectivă rulează întotdeauna prin THotPDF.PlaceLayoutElement, aceeași rutină centrală care re-măsoară fragmentul plasat, configurează proprietatea de overflow și înregistrează diagnosticele. Randorul DOM decide doar politica de pagină nouă, elementele de decor ale paginii, spațierea și durata de viață a continuărilor

Regulile de antet de tabel care previn un document infinit

Repetarea anteturilor de tabel pe mai multe pagini pare simplă și ascunde două moduri de eșec. HotPDF impune ca rândurile de antet să apară doar în prima serie de rânduri consecutive și ca prima despărțire să încapă toate rândurile de antet plus cel puțin un rând de corp. Fără a doua regulă, un antet mai înalt decât spațiul rămas produce o pagină care nu conține nimic altceva decât antetul, urmată de o altă pagină identică, la nesfârșit

Paginile de continuare redesenează antetul, iar acea copie redesenată este marcată ca artefact, nu ca și conținut, ceea ce este răspunsul corect atât pentru accesibilitate, cât și pentru extragerea de text. Rândul original de antet rămâne exact o singură dată în structura logică a tabelului. Sari peste asta și un cititor de ecran anunță din nou titlurile coloanelor în mijlocul datelor, iar un extractor de text inserează un rând de antet duplicat între rândurile de corp

Există și un plafon defensiv pentru adâncimea de continuare, deoarece o componentă personalizată este liberă să implementeze Split într-un mod care returnează mereu o coadă echivalentă. Randorul verifică limita după detașarea cozii și înainte de a începe pagina următoare, iar iterația curentă eliberează coada în propriul bloc finally, astfel încât o componentă terță care se comportă greșit eșuează cu o eroare diagnosticabilă, nu prin umplerea unui disc

Un element logic, mai multe fragmente de pagină

Marcarea automată este locul unde modelul de paginare și modelul de structură trebuie să fie de acord. Un paragraf despărțit pe două pagini este un singur paragraf logic, deci trebuie să rămână un singur element de structură. Dar identificatorii de conținut marcat sunt per pagină, așa că fiecare fragment vizibil are nevoie de propriul MCID pe pagina pe care apare

HotPDF rezolvă asta păstrând un singur element de structură și adăugând o referință de conținut marcat la array-ul său /K pentru fiecare fragment, cu perechea /Pg și /MCID identificând pagina și identificatorul. Slotul ParentTree pentru acel MCID indică înapoi la același element. Exact asta prevede ISO 14289, și tocmai de aceea clonele de continuare sunt distincte de clonele obișnuite: un Clone obișnuit înseamnă conținut logic nou și primește o identitate semantică nouă, în timp ce clona internă de continuare moștenește identitatea componentei pe care o continuă

Reutilizarea elementelor este căutată printr-un index de identități semantice sortat după pointer de componentă și căutat prin comparație binară, ceea ce menține căutarea logaritmică pe arbori mari. Indexul păstrează doar referințe fără proprietate; durata de viață a obiectelor de structură rămâne cu graful de obiecte PDF

Reguli de structură pe care randorul le impune din start

Cu GenerateStructure activat, mai multe reguli PDF/UA sunt verificate în timp ce arborele este randat, nu după ce fișierul există deja. Titlurile încep la nivelul 1 și nu pot sări niveluri. LI poate apărea doar în interiorul unui L, iar Lbl și LBody doar în interiorul unui LI. TR aparține unui tabel, iar TH și TD unui rând. O figură fără text alternativ este respinsă în modul PDF/UA

Respingerea timpurie este alegerea deliberată aici. Un validator care raportează un text alternativ lipsă după ce documentul a fost deja scris îți spune că un lot de zece mii de extrase trebuie regenerat; un randor care refuză componenta îți spune exact care componentă, cât timp datele care au produs-o încă sunt în domeniu. Verificarea conformității rămâne totuși un pas separat în pipeline, iar mecanica acesteia este acoperită în validarea 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;

Înregistrarea statistică este mai utilă decât pare la prima vedere. O creștere bruscă a SplitCount după o schimbare de șablon înseamnă de obicei că o componentă a început să se măsoare mai înaltă decât containerul ei. MaximumContinuationDepth care urcă treptat este avertismentul timpuriu pentru o componentă al cărei Split face prea puțin progres per pagină. Iar compararea ArtifactCount cu numărul de pagini de continuare confirmă că anteturile repetate au fost chiar marcate ca artefacte

Unde se potrivește DOM-ul alături de API-ul direct

DOM-ul nu înlocuiește desenarea directă; se așază peste aceleași obiecte de pagină. Tot ce plasează randorul poate fi intercalat cu apeluri directe pe THotPDF, ceea ce contează atunci când un raport are nevoie de un singur element poziționat manual, cum ar fi o imagine de semnătură la o locație exactă. Închiderea paginilor rămâne sub controlul AddPage și EndDoc, așa că modul de golire imediată nu păstrează pagini finalizate în memorie, iar memoria rezidentă rămâne guvernată de continuările curente, resursele de fonturi și graful obișnuit de obiecte al documentului

Alege DOM-ul când conținutul este condus de date și layout-ul este condus de reguli, și păstrează desenarea directă pentru grafica fixă. Dacă problema ta curentă ține în mod specific de paginarea tabelelor, abordarea mai restrânsă din generarea tabelelor în PDF merită citită mai întâi, iar comportamentul la nivel de text, cum ar fi justificarea, este descris în justificarea textului

Layout-ul declarativ, marcarea automată și API-ul de desenare directă se livrează în aceeași componentă pentru Delphi și C++Builder; lista completă de funcții este pe pagina componentei HotPDF PDF pentru Delphi