Genera un report, incorpora un font TrueType e l'output si aprirà correttamente in qualsiasi visualizzatore. I glifi sono corretti, il testo è selezionabile, il file è valido. L'unica cosa errata è la dimensione. Un documento che utilizza poche decine di caratteri latini porta con sé l'intero font da 350 KB. Un documento che stampa un paragrafo in cinese include un font CJK da 14 MB anziché la porzione da mezzo megabyte effettivamente necessaria. Nessuna eccezione sollevata, nessun avviso registrato e il file supera la convalida. Questo è l'aspetto esterno di un passaggio di finalizzazione non ordinato correttamente: nulla fallisce e l'unica prova è un numero troppo grande
Vale la pena scriverla come lezione, perché la forma dell'errore è generale. Qualsiasi motore documentale ha una fase di finalizzazione che modifica gli oggetti poco prima di scriverli, e la correttezza di quella fase dipende interamente dall'ordine dei suoi passi rispetto alla serializzazione. Metti un passo sul lato sbagliato della scrittura e non fa più nulla, in silenzio
Cosa dovrebbe fare il subsetting dei font
Un font sottoinsieme (subset) è la parte di un file TrueType effettivamente utilizzata da un documento. La norma ISO 32000-1 §9.9 descrive come un programma di font incorporato risieda in uno stream a cui fa riferimento il descrittore del font, e per un programma TrueType tale stream è /FontFile2 con un valore /Length1 che indica il conteggio dei byte non compressi. Il subsetting riscrive le tabelle glyf e loca in modo che contengano solo i glifi a cui fa riferimento il documento, rinumera gli identificatori dei glifi e antepone al nome /BaseFont un tag di sei lettere come ABCDEF+ per contrassegnare il font come sottoinsieme, esattamente come richiesto dalle specifiche. Un font latino che viene ridotto a dieci o quindici kilobyte fa la differenza tra un PDF leggero e uno che include un intero carattere per un solo titolo
Il momento in cui questo avviene è fondamentale. Il subsetting non è una trasformazione applicata ai byte già su disco. Modifica il grafo degli oggetti in memoria: riduce il contenuto dello stream /FontFile2, sistema /Length1 e riscrive la stringa /BaseFont. Tutto questo deve essere pronto quando il serializzatore scorre il grafo ed emette i byte. Se le modifiche avvengono dopo la scrittura dei byte, aggiorneranno oggetti che nessuno leggerà mai
Il sintomo e perché non ci sono stati errori
Quando il subsetting atterra sul lato sbagliato della scrittura, il sintomo è font completi nell'output senza alcun diagnostico. Registra un font TrueType Unicode, produci un documento normale, e l'oggetto font incorporato esce con la stessa lunghezza del file .ttf sorgente, senza il prefisso di subset di sei lettere sul nome /BaseFont. L'output non si contrae mai tra esecuzioni che usano dieci glifi ed esecuzioni che ne usano diecimila
L'assenza di qualsiasi errore è la parte che rende questa classe di errori costosa. Una routine di subsetting che gira al momento sbagliato gira comunque. Percorre l'uso accumulato dei punti di codice, costruisce un subset perfettamente corretto e lo applica al grafo di oggetti in memoria. Internamente il lavoro è fatto e la chiamata ritorna pulita. L'unica cosa sbagliata è che il grafo di oggetti che ha modificato non è più la cosa che viene scritta, perché il writer ha già finito. Dal punto di vista del chiamante il documento è stato prodotto e salvato senza incidenti, che è esattamente l'impressione che dà un guasto silenzioso
Dove il subsetting siede nell'ordine di finalizzazione
In HotPDF il lavoro di chiusura avviene all'interno di EndDoc. Il passaggio di subsetting è una routine interna denominata BuildAndApplyUnicodeFontSubset. Legge l'insieme dei punti di codice utilizzati per documento, memorizzato in una bitmap popolata dal percorso di emissione del testo man mano che i glifi vengono mostrati, mappa ogni punto di codice utilizzato tramite la tabella dei punti di codice in glifi memorizzata nella cache in un identificatore di glifo reale e riscrive il programma del font attorno a tale chiusura. Quando viene registrato un font TrueType Unicode, il percorso di emissione imposta un bit nell'insieme dei punti di codice utilizzati per ogni carattere disegnato, in modo che alla chiusura del documento il motore sappia esattamente quali glifi conservare nel subset
La trappola è la posizione di quella chiamata. Se BuildAndApplyUnicodeFontSubset viene invocato dopo che SaveToStream o SaveToFile hanno già serializzato il documento, le modifiche del subsetter a /FontFile2, il suo /Length1 corretto e il prefisso di sei lettere di /BaseFont vengono tutti calcolati contro un grafo di oggetti già trasformato in byte. La sequenza corretta esegue prima il subsetter e serializza dopo, così il writer emette il font ridotto a subset invece dell'originale. Farlo giusto è questione della posizione di una chiamata, nient'altro
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // il subsetting gira qui, prima della scrittura
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Tutto ciò non è visibile nel codice chiamante. Il subsetting è attivo per default una volta registrato un font TrueType Unicode. Registri il font, apri il documento, disegni e lo chiudi, e il subset viene costruito dai glifi che hai usato prima che i byte lascino la memoria
Perché un singolo passaggio fuori posto rappresenta un'intera categoria
Il motivo per cui questa è una lezione e non una semplice nota a piè di pagina è che EndDoc esegue una serie di passaggi di chiusura, e ognuno di essi è sensibile alla sua posizione rispetto alla scrittura. Il subsetting dei font è uno di questi. L'output PDF/A richiede uno stream /CIDSet che elenchi esattamente gli identificatori dei glifi presenti nel subset, un vincolo imposto da ISO 19005 affinché un validatore possa confermare che il programma incorporato corrisponda a quanto dichiarato dal descrittore del font; tale stream viene emesso nella stessa finestra di finalizzazione e dipende dal fatto che il subset sia stato creato prima. La specifica PDF/UA-1 richiede, secondo ISO 14289-1 §7.18.3, che ogni pagina contenente un'annotazione dichiari /Tabs con il valore /S, e una routine interna chiamata EnsurePDFUATabsOnAnnotatedPages scrive tale chiave durante la stessa fase. Anche i controlli sull'intento di output vengono eseguiti qui
Lo stesso difetto di ordinamento che disattiva il subsetting farebbe cadere anche la chiave di ordine di tabulazione PDF/UA sulle pagine con annotazioni, perché quel passo siede nella stessa finestra. veraPDF e PAC segnalano un /Tabs /S mancante come violazione del checkpoint 21-001 del protocollo Matterhorn. Così una singola chiamata fuori posto non si limita a gonfiare la dimensione del file; rompe silenziosamente anche un requisito di conformità di accessibilità, con la stessa totale assenza di errori. Questo è il pericolo di una fase di finalizzazione: i suoi passi condividono una precondizione, e un singolo errore di ordinamento può eliminarne diversi in una volta mentre ogni chiamata continua a riportare successo
Come viene effettivamente rilevato un errore di emissione silenzioso
Un guasto che non solleva eccezioni non si becca eseguendo il programma. Si becca ispezionando l'output e confrontandolo con ciò che l'input avrebbe dovuto produrre. Per il subsetting dei font i controlli sono concreti. Confronta la dimensione del file di output con un'aspettativa approssimativa: un documento che ha toccato una manciata di glifi non dovrebbe essere grande come un font completo. Apri l'oggetto font incorporato e leggi la sua lunghezza in byte; un /FontFile2 ridotto per un font latino è una piccola frazione del file sorgente. Leggi il nome /BaseFont e conferma che il prefisso di sei lettere sia presente, perché la sua assenza è un segnale diretto che nessun subset è stato applicato
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// Pochi glifi presi da un font di ~700 KB non devono produrre uno stream da centinaia di KB
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Per l'output PDF/A il controllo è ancora più preciso, poiché un validatore svolge il lavoro al posto tuo. Imposta il livello di conformità ed esegui il risultato tramite veraPDF: un /CIDSet mancante, o un subset che non corrisponde al descrittore, viene segnalato come clausola non superata anziché essere lasciato alla tua osservazione visiva. I selettori di conformità che guidano questo lavoro di finalizzazione sono proprietà del documento. PDFACompliance accetta una stringa come '2B' per PDF/A-2 Livello B, e PDFUACompliance è un booleano che attiva i requisiti per il PDF taggato e l'ordine di tabulazione
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Livello B, guida l'emissione di /CIDSet
Pdf.PDFUACompliance := True; // timbra /Tabs /S sulle pagine con annotazioni
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
La lezione di ingegneria
Da qui scendono due regole. La prima: qualunque passo di finalizzazione che modifica oggetti deve girare prima che quegli oggetti vengano serializzati, e la fase di chiusura di un motore documentale va letta come una pipeline ordinata in cui la serializzazione è l'ultima azione, non una fra le diverse. La seconda è quella più facile da dimenticare: per un passo di emissione, l'assenza di un errore non è prova di successo. Una routine che costruisce il subset giusto e lo applica al grafo sbagliato, già scritto, non segnala nulla di anormale, perché dal suo punto di vista non c'era nulla di anormale. La verifica deve guardare l'artefatto, non il codice di ritorno. Controlla la dimensione dell'output, leggi la lunghezza in byte del font incorporato e il suo prefisso /BaseFont, e lascia che veraPDF giudichi l'output PDF/A, dove un /CIDSet mancante trasforma una carenza silenziosa in un fallimento nominato
La gestione dei font lato produttore, inclusa la registrazione e l'incorporamento dei caratteri per l'output dei report, è trattata nel nostro articolo sui font e le immagini nell'output dei report. La convalida, in cui questi passaggi di finalizzazione vengono verificati rispetto agli standard, è descritta nella guida alla convalida PDF/A e PDF/UA. Entrambi si affiancano al lavoro su subsetting e conformità descritto qui, fornito come parte di HotPDF Delphi Component per Delphi e C++Builder, insieme alle API di caricamento, modifica, crittografia e firma trattate altrove in questo blog