Articolo tecnico

Subsetting dei font e ordine di finalizzazione in EndDoc

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

HotPDF: il subsetting dei font passa un volto TrueType completo attraverso una chiusura dei glifi usati e riscrive glyf e loca prima di aggiornare in memoria gli oggetti FontFile2, Length1 e BaseFont
Il subsetting ricostruisce glyf e loca attorno ai glifi che un documento usa davvero, poi aggiorna /FontFile2, /Length1 e il prefisso /BaseFont in memoria. Tutto deve essere al proprio posto prima che venga scritto un byte qualsiasi

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

Cronologia HotPDF che confronta l'ordine difettoso di EndDoc, in cui BuildAndApplyUnicodeFontSubset girava dopo SaveToStream, con l'ordine corretto in cui il subsetting precede la serializzazione
Spostare una chiamata avanti rispetto alla scrittura non cambia nulla nel codice chiamante eppure decide se i byte salvati custodiscono un font completo o un vero subset. Un passo sul lato sbagliato della serializzazione ritorna pulito e non fa nulla
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

I quattro passi di finalizzazione in coda dentro EndDoc di HotPDF, tra cui subsetting dei font, emissione di CIDSet e stamping dei Tabs PDF/UA, tutti a monte del serializzatore prima che esista un qualsiasi byte
Subsetting, emissione di /CIDSet e timbro /Tabs /S condividono un unico slot di finalizzazione prima della serializzazione. Una sola chiamata fuori posto può perdere sia il subset del font sia un requisito di accessibilità mentre ogni routine 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