HotPDF può costruire un documento paginato a partire da un albero dichiarativo invece che da coordinate. Si assembla un THPDFDOMDocument composto da sezioni, stack, testo, elenchi e tabelle, lo si passa a THPDFDOMRenderer, e il renderer misura, pagina, disegna gli elementi di cornice della pagina e, su richiesta, emette l'albero di struttura PDF/UA che rende accessibile il risultato. Il codice di layout non calcola mai una coordinata y
Chiunque abbia mantenuto un generatore di report guidato da coordinate sa perché questo conta. La prima versione funziona. Poi l'indirizzo di un cliente cresce a tre righe, una tabella guadagna righe, un'intestazione localizzata va a capo, e ogni posizione y a valle è sbagliata. Le correzioni si accumulano come controlli manuali di interruzione pagina sparsi nella logica di business, e il requisito di PDF taggato che arriva due anni dopo non può essere applicato retroattivamente a codice che non ha idea di cosa sia un paragrafo
Cosa possiede l'albero, e perché la proprietà è rigida
Il DOM impone la proprietà singola a ogni livello: il documento possiede le proprie sezioni, una sezione possiede il proprio corpo, intestazione e piè di pagina, e stack, contenitori e tabelle possiedono i propri figli. Il riutilizzo avviene tramite Clone o tramite una factory registrata, mai collegando lo stesso oggetto a due genitori. Questa regola non è una formalità. Un componente che compare due volte nell'albero verrebbe misurato due volte con vincoli diversi e liberato due volte alla distruzione
La conseguenza pratica per il codice chiamante è che gli helper restituiscono nuove istanze. Registrare una factory con RegisterComponent e chiamare CreateComponent fornisce una ricetta denominata che produce un componente nuovo ogni volta, ed è così che elementi ripetuti come un blocco firma o un piè di pagina legale trovano posto nell'albero
uses
HPDFDoc, HPDFLayoutDOM;
var
Doc: THPDFDOMDocument;
Section: THPDFDOMSection;
Table: THPDFDOMTable;
Row: THPDFDOMTableRow;
I: Integer;
begin
Doc := THPDFDOMDocument.Create;
Doc.GenerateStructure := True; // emette l'albero di struttura PDF/UA
Doc.Language := 'en-US';
Section := Doc.AddSection;
Section.PageWidth := 595; // A4 in punti
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); // pesi, non larghezze assolute
Table.AddColumn(1);
Table.AddColumn(1);
Table.RepeatHeaders := True;
Row := Table.AddRow(18, True); // riga di intestazione
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;
Come evita la paginazione un costo quadratico?
Il modo ingenuo di paginare un albero è clonare ciò che non è entrato e portarlo alla pagina successiva. Su una tabella con diecimila righe, questo clona le righe rimanenti una volta per pagina e trasforma un documento lineare in uno quadratico
HotPDF invece divide in modo mirato. Il renderer di livello superiore percorre i figli del corpo per indice e non clona mai un'intera sezione o corpo. Solo gli stack e i contenitori annidati che effettivamente attraversano un confine di pagina fanno clonare il proprio sottoalbero interessato, e i due tipi di foglia più pesanti portano un cursore anziché una copia: una continuazione di testo memorizza l'intervallo di caratteri sorgente che deve ancora produrre, e una continuazione di tabella memorizza la porzione di righe che deve ancora collocare. I documenti lunghi restano lineari, e i paragrafi lunghi costano lo stesso sia che si interrompano una volta sia cinque
La misurazione resta onesta riguardo agli effetti collaterali. THPDFLayoutElement.Measure deve essere privo di effetti collaterali di disegno, e il posizionamento effettivo passa sempre attraverso THotPDF.PlaceLayoutElement, la stessa routine centrale che rimisura il frammento posizionato, imposta la proprietà dell'overflow e registra la diagnostica. Il renderer DOM decide solo la politica di nuova pagina, gli elementi di cornice della pagina, la spaziatura e il ciclo di vita delle continuazioni
Le regole sulle intestazioni di tabella che impediscono un documento infinito
Ripetere le intestazioni di tabella su più pagine sembra semplice ma nasconde due modalità di fallimento. HotPDF richiede che le righe di intestazione compaiano solo nella prima sequenza di righe consecutive, e che il primo split contenga tutte le righe di intestazione più almeno una riga di corpo. Senza la seconda regola, un'intestazione più alta dello spazio rimanente produce una pagina che non contiene altro che l'intestazione, seguita da un'altra pagina identica, all'infinito
Le pagine di continuazione ridisegnano l'intestazione, e quella copia ridisegnata viene marcata come artefatto anziché come contenuto, il che è la risposta corretta sia per l'accessibilità sia per l'estrazione del testo. La riga di intestazione originale rimane nella struttura logica della tabella esattamente una volta. Se si salta questo passaggio, uno screen reader annuncia di nuovo i titoli delle colonne nel mezzo dei dati, e un estrattore di testo inserisce una riga di intestazione duplicata tra le righe di corpo
Esiste anche un limite massimo difensivo sulla profondità di continuazione, perché un componente personalizzato è libero di implementare Split in un modo che restituisce sempre una coda equivalente. Il renderer controlla il limite dopo aver staccato la coda e prima di iniziare la pagina successiva, e l'iterazione corrente libera la coda nel proprio blocco finally, così un componente di terze parti che si comporta male fallisce con un errore diagnosticabile invece di riempire un disco
Un elemento logico, molti frammenti di pagina
Il tagging automatico è il punto in cui il modello di paginazione e il modello di struttura devono concordare. Un paragrafo diviso su due pagine è un unico paragrafo logico, quindi deve rimanere un unico elemento di struttura. Ma gli identificatori di contenuto marcato sono per pagina, quindi ogni frammento visibile ha bisogno del proprio MCID sulla pagina in cui compare
HotPDF risolve questo problema mantenendo un unico elemento di struttura e aggiungendo un riferimento a contenuto marcato al suo array /K per ogni frammento, con la coppia /Pg e /MCID che identifica la pagina e l'identificatore. Lo slot ParentTree per quell'MCID punta di nuovo allo stesso elemento. Questo è esattamente ciò che richiede la ISO 14289, ed è il motivo per cui i cloni di continuazione sono distinti dai cloni ordinari: un Clone ordinario significa nuovo contenuto logico e riceve una nuova identità semantica, mentre il clone di continuazione interno eredita l'identità del componente che continua
Il riutilizzo degli elementi viene cercato tramite un indice di identità semantiche ordinato per puntatore al componente e cercato con confronto binario, il che mantiene la ricerca logaritmica su alberi di grandi dimensioni. L'indice contiene solo riferimenti non proprietari; il ciclo di vita degli oggetti di struttura stessi resta legato al grafo degli oggetti PDF
Le regole di struttura che il renderer applica in anticipo
Con GenerateStructure abilitato, diverse regole PDF/UA vengono verificate mentre l'albero viene renderizzato, anziché dopo che il file esiste già. I titoli iniziano al livello 1 e non possono saltare livelli. LI può comparire solo dentro L, e Lbl e LBody solo dentro LI. TR appartiene a una tabella, e TH e TD a una riga. Una figura senza testo alternativo viene rifiutata in modalità PDF/UA
Rifiutare in anticipo è la scelta deliberata qui. Un validatore che segnala un testo alternativo mancante dopo che il documento è stato scritto ti dice che un lotto di diecimila estratti conto deve essere rigenerato; un renderer che rifiuta il componente ti dice quale componente, mentre i dati che lo hanno prodotto sono ancora nell'ambito. La verifica di conformità resta comunque parte della pipeline come passaggio separato, e le sue meccaniche sono trattate in validazione PDF/A, PDF/X e 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;
Il record delle statistiche è più utile di quanto sembri a prima vista. Un forte aumento di SplitCount dopo una modifica al template di solito significa che un componente ha iniziato a misurarsi più alto del proprio contenitore. Un aumento graduale di MaximumContinuationDepth è l'avviso precoce di un componente il cui Split fa troppo poco progresso per pagina. E confrontare ArtifactCount con il numero di pagine di continuazione conferma che le intestazioni ripetute sono state davvero taggate come artefatti
Dove si colloca il DOM accanto all'API diretta
Il DOM non sostituisce il disegno diretto; si colloca sopra gli stessi oggetti pagina. Qualsiasi cosa posizionata dal renderer può essere intervallata con chiamate dirette su THotPDF, il che conta quando un report ha bisogno di un elemento posizionato manualmente, come un'immagine di firma in una posizione esatta. La chiusura della pagina resta sotto il controllo di AddPage e EndDoc, quindi la modalità di flush immediato non mantiene in memoria pagine completate e la memoria residente resta governata dalle continuazioni correnti, dalle risorse dei font e dal normale grafo degli oggetti del documento
Scegli il DOM quando il contenuto è guidato dai dati e il layout è guidato dalle regole, e mantieni il disegno diretto per la grafica fissa. Se il problema attuale riguarda specificamente la paginazione delle tabelle, vale la pena leggere prima l'approccio più mirato in generare tabelle in PDF, e il comportamento a livello di testo come la giustificazione è descritto in giustificazione del testo
Layout dichiarativo, tagging automatico e API di disegno diretto sono distribuiti nello stesso componente per Delphi e C++Builder; l'elenco completo delle funzionalità si trova nella pagina del componente PDF Delphi HotPDF