Articolo tecnico

Creare subset dei font già incorporati in un PDF da Delphi

PDFium Component crea subset dei font già incorporati in un PDF esistente attraverso SubsetPdfDocumentFonts nell'unità FPdfDocumentFontSubset. La funzione sostituisce un programma TrueType /FontFile2 incorporato con uno più piccolo solo quando le risorse del font stesso dimostrano ogni glifo che il documento può referenziare; un font il cui dominio di glifi non può essere dimostrato resta esattamente com'era

Il motivo usuale per volerlo è un archivio che non avete prodotto voi. Fatture, estratti conto e report di generatori più vecchi portano spesso una Arial completa o un font CJK completo in ogni file, e qualche centinaio di kilobyte di font per documento di due pagine fanno pila in fretta su qualche milione di documenti. Rifare il render dalla sorgente raramente è un'opzione. Ciò che avete è il PDF, e l'unica domanda sicura è quanta parte di ogni font incorporato può andare via senza che una sola pagina perda un carattere

Perché creare subset di un PDF esistente rischia di perdere glifi?

Creare subset di un PDF esistente rischia di perdere glifi perché il post-processore non sa quali glifi usava lo scrittore originale. Il produttore che ha incorporato il font conosceva il proprio testo; uno strumento che gira dopo deve ricostruire quella conoscenza, e ogni glifo che gli sfugge diventa una casella vuota o una forma .notdef su una pagina che nessuno guarda finché non la guarda un cliente

L'approccio ovvio è analizzare ogni content stream, ogni Form XObject, ogni aspetto di annotazione e ogni aspetto predefinito di campo, raccogliere i codici mostrati con ciascun font, e conservare quei glifi. Funziona finché non incontra una risorsa font condivisa tra pagine e form, un campo modulo il cui aspetto un viewer rigenera da /DA, o un content stream con una sequenza di operatori che lo scanner non modella. SubsetPdfDocumentFonts deliberatamente non scannerizza i content stream. Deriva il dominio dei glifi dai metadati che il PDF deve comunque portare su ogni font: la /CIDToGIDMap, gli array di larghezze /W e /W2, la bitmap /CIDSet, la CMap /ToUnicode, l'array /Widths dei font semplici e il /Encoding. Quando quei metadati non delimitano il dominio, il font resta intatto

Come PDFium Component dimostra il dominio dei glifi quando crea subset di font TrueType incorporati da Delphi: /CIDToGIDMap, larghezze /W e /W2, /CIDSet, /ToUnicode, /Widths e /Encoding delimitano il dominio senza scannerizzare i content stream, e un font non dimostrabile viene preservato intatto
SubsetPdfDocumentFonts trasforma i metadati del font in un dominio di glifi dimostrato prima di tagliare qualsiasi cosa, e un font le cui risorse non delimitano il dominio viene riportato e lasciato intatto

Conservare i glyph ID originali così i content stream restano intatti

Il backend di subset dietro SubsetPdfDocumentFonts, TPdfTrueTypeSubsetBackend, conserva ogni slot di glyph ID originale, così nessun content stream, CID o /CIDToGIDMap deve mai essere riscritto. Ricostruisce le tabelle glyf e loca, dà a ogni glifo inutilizzato un intervallo loca di lunghezza zero e lascia maxp.numGlyphs invariato. Il glifo 4711 è ancora il glifo 4711 dopo il subset; ha semplicemente contorni solo se il documento può raggiungerlo

Quella scelta elimina la parte più rischiosa del creare subset di un documento finito. Rinumerare i glifi significherebbe riscrivere ogni stringa in ogni content stream che usa il font, più lo stream di mappatura, più tutto ciò che mette in cache glyph ID. Gli ID conservati costano un po' di dimensione, perché loca tiene una voce per glifo originale, ma il grosso di un programma TrueType sono i dati di contorno, ed è quello che va via. I glifi compositi vengono seguiti fino alle loro componenti, così una lettera accentata conserva la lettera base e l'accento da cui è costruita; i riferimenti compositi ciclici o troppo profondi fanno fallire il font invece di produrre una chiusura rotta

Glyph ID conservati dopo la creazione di subset di font TrueType incorporati con PDFium Component in Delphi: il glifo 4711 conserva numero e contorni, i glifi inutilizzati ricevono intervalli loca di lunghezza zero mentre maxp.numGlyphs resta invariato, così content stream, CID e /CIDToGIDMap non vengono mai riscritti
Poiché i glyph ID sopravvivono al subset, content stream, CID e /CIDToGIDMap non richiedono mai riscritture - il guadagno di dimensione viene dai contorni eliminati, non dalla rinumerazione

Il font rinominato segue anche ISO 32000-1 §9.6.4, che richiede che il nome di un font subset cominci con sei lettere maiuscole e un segno più. Il componente deriva le sei lettere da un digest SHA-256 del nuovo programma del font, toglie prima qualunque prefisso ABCDEF+ esistente, risolve le collisioni tra font nello stesso documento, e scrive il nuovo nome sia nel /FontName del descrittore sia nel /BaseFont del dizionario del font, compreso il genitore Type0 di un CIDFont

uses
  System.SysUtils, System.Classes, FPdfDocumentFontSubset;

procedure SubsetEmbeddedFonts(const SourceName, TargetName: string);
var
  Source, Target: TFileStream;
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Source := TFileStream.Create(SourceName, fmOpenRead or fmShareDenyWrite);
  try
    if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
      raise Exception.Create('Font subsetting failed: ' + Report.ErrorMessage);
  finally
    Source.Free;
  end;
  // L'output è sempre un PDF completo: il file ricostruito quando Report.Applied
  // è True, altrimenti una copia byte per byte della sorgente
  Target := TFileStream.Create(TargetName, fmCreate);
  try
    if Length(Output) > 0 then
      Target.WriteBuffer(Output[0], Length(Output));
  finally
    Target.Free;
  end;
end;

Come dimostra SubsetPdfDocumentFonts il dominio dei glifi di un CIDFont?

SubsetPdfDocumentFonts dimostra il dominio dei glifi di un font CIDFontType2 dalla sua mappatura CID-a-glifo, e la mappatura decide quanta prova extra serve. Uno stream /CIDToGIDMap esplicito (ISO 32000-1 §9.7.4.2) è già un elenco di ogni glifo che il font può indirizzare, quindi ogni GID nello stream entra nel dominio. Uno stream di mappatura più grande di 65536 voci, o con un numero di byte dispari, fallisce il profilo delimitato e il font viene preservato

/CIDToGIDMap /Identity è il caso difficile, ed è anche il più comune nei file moderni. Identity dice che CID è uguale a GID, il che non vi dice nulla su quali CID usa il documento. Il componente richiede quindi una prova primaria: un array di larghezze /W o /W2 esplicito (§9.7.4.3), le cui voci CID marchiano i glifi per cui il produttore ha dichiarato le metriche, o uno stream /CIDSet nel descrittore del font (§9.8.3), una bitmap con un bit per CID, bit più significativo per primo. Un font Identity nudo senza nessuna delle due viene preservato con il motivo Identity CIDToGIDMap requires an explicit W array or CIDSet stream

La trappola nell'usare /W come prova è /DW. Al produttore è consentito omettere ogni CID la cui larghezza è pari alla larghezza predefinita, e uno scrittore ben ottimizzato fa esattamente questo per le larghezze di glifo più comuni. Se il dominio venisse solo da /W, quei glifi sparirebbero silenziosamente dal subset. Per chiudere quel buco il componente percorre ogni font Type0 il cui /DescendantFonts punta al CIDFont, richiede che il suo /Encoding sia /Identity-H o /Identity-V così un codice a 2 byte è il CID, e fonde i codici sorgente di ogni voce beginbfchar e beginbfrange della sua CMap /ToUnicode (§9.10.3) nel dominio. Qualunque altra codifica, o uno stream /ToUnicode che non si può decodificare entro il budget, preserva il font

Decisione delle prove per una CIDToGIDMap Identity nella creazione di subset dei font di PDFium Component per Delphi: uno stream di mappatura esplicito elenca già ogni glifo, Identity richiede un array di larghezze /W o /W2 o una bitmap /CIDSet, e i codici sorgente /ToUnicode chiudono il punto cieco della larghezza predefinita /DW
Una CIDToGIDMap Identity da sola non dice nulla sull'uso, quindi il subsetter esige prove /W o /CIDSet e fonde i codici /ToUnicode per coprire i glifi che /W omette a /DW

Font TrueType semplici: da Widths a glyph ID

I font /Subtype /TrueType a byte singolo vengono dimostrati attraverso il proprio intervallo di codici e la propria codifica, poi risolti in glyph ID attraverso il cmap del programma del font. Il componente richiede 0 <= FirstChar <= LastChar <= 255 e un array /Widths la cui lunghezza è esattamente LastChar - FirstChar + 1; un array corto, lungo o non numerico significa che il dominio dei codici non è dimostrato, e il font resta com'è

Ogni codice in quell'intervallo, più qualunque codice nominato in /Differences o coperto da /ToUnicode, viene poi mappato nel modo che ISO 32000-1 §9.6.6 descrive. I codici sotto /WinAnsiEncoding, /MacRomanEncoding o /StandardEncoding passano per la tabella corrispondente verso Unicode. I nomi di glifo di /Differences si risolvono attraverso l'Adobe Glyph List, i nomi uniXXXX e uXXXX, i 258 nomi di glifo Macintosh standard e la tabella dei nomi post formato 2.0 del font stesso. I valori risultanti si cercano nelle sottotabelle cmap formato 0, 4, 6 e 12, comprese Unicode (3,1), (3,10) e (0,*), Mac Roman (1,0), e Windows Symbol (3,0) a F000 più il codice

Quando un font non ha affatto una codifica di base, il componente marchia l'unione di ogni interpretazione plausibile invece di indovinarne una. Questo conserva qualche glifo probabilmente inutilizzato, che è la direzione giusta in cui sbagliare. Il caso opposto è rigido: un nome /Differences che non mappa a nessun glyph ID dimostrato rifiuta l'intero font, perché un nome non risolto significa che il dominio è incompleto

procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
  Options: TPdfDocumentFontSubsetOptions;
  Report: TPdfDocumentFontSubsetReport;
  Output: TBytes;
  I: Integer;
begin
  Options := TPdfDocumentFontSubsetOptions.Default;
  Options.SubsetOptions.MinimumSavingPercent := 10;  // per font
  Options.MinimumDocumentSavingPercent := 5;         // intero file
  if not SubsetPdfDocumentFonts(Source, Options, Output, Report) then
  begin
    Log.Add('Rejected: ' + Report.ErrorMessage);
    Exit;
  end;
  Log.Add(Format('Fonts %d, subset %d, preserved %d, glyphs %d',
    [Report.FontCount, Report.SubsetFontCount, Report.PreservedFontCount,
     Report.GlyphCount]));
  Log.Add(Format('Font bytes %d -> %d, file bytes %d -> %d',
    [Report.OriginalFontBytes, Report.SubsetFontBytes,
     Report.SourceByteCount, Report.OutputByteCount]));
  for I := 0 to High(Report.Issues) do
    Log.Add(Format('  object %d kept: %s',
      [Report.Issues[I].ObjectNumber, Report.Issues[I].Reason]));
end;

Cosa lascia in pace SubsetPdfDocumentFonts?

SubsetPdfDocumentFonts lascia in pace ogni font che non riesce a dimostrare, e riporta ciascuno in Report.Issues con il numero di oggetto e un motivo invece di far fallire il documento. Report.Complete è True solo quando nessun font è stato preservato. I casi preservati meritano di essere conosciuti prima di promettere una riduzione di dimensione:

  • Programmi Type 1 /FontFile e CFF o OpenType /FontFile3, riportati come Only TrueType FontFile2 programs are supported
  • CIDFont Identity senza /W, /W2 o /CIDSet, e CIDFont senza né stream di mappatura né /Identity
  • Programmi di font referenziati da qualcosa di diverso dai loro descrittori di font, e descrittori condivisi con un consumatore che non è CIDFontType2 o TrueType semplice
  • Font il cui subset non raggiunge SubsetOptions.MinimumSavingPercent, e ogni font quando il file ricostruito non raggiunge MinimumDocumentSavingPercent; in quel caso Report.Applied è False e l'output sono i byte originali

Due input falliscono in toto invece di essere preservati. I documenti firmati e i documenti con campi firma vengono rifiutati, perché sostituire gli oggetti font invaliderebbe il byte range di ogni firma, e i file cifrati richiedono prima una sorgente decifrata. Un'esecuzione riuscita scrive gli oggetti di sostituzione come aggiornamento sparso e poi ricostruisce il file attraverso lo stesso passaggio di raggiungibilità descritto nella guida alla potatura degli oggetti orfani e alla deduplicazione delle risorse, così i vecchi programmi di font non sopravvivono come oggetti irraggiungibili e il risultato è una singola revisione pulita

Creare subset del testo nuovo al salvataggio con saSubsetNewFonts

I font che PDFium Component incorpora durante la sessione corrente sono un caso a parte, gestito dall'opzione di salvataggio saSubsetNewFonts invece che da SubsetPdfDocumentFonts. saSubsetNewFonts corrisponde al flag PDFium FPDF_SUBSET_NEW_FONTS, che esegue il subsetter nativo HarfBuzz sui font che chiamate native come TPdf.AddText hanno caricato dal sistema, facce OpenType CFF comprese. Il PDFium upstream etichetta ancora il flag come sperimentale. Non tocca i font che erano già nel file

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);
    Pdf.AddText('Quarterly statement', 'Arial', 14, 72, 770, clBlack, $FF, 0.0);
    if not Pdf.SaveAs('statement.pdf', saSubsetNewFonts) then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

Nella sonda del componente, un documento con testo Arial è sceso da 590 KB a 22 KB sotto saSubsetNewFonts, e l'estrazione del testo dal file salvato continuava a combaciare esattamente. Un font a colori COLR v0, Segoe UI Emoji da 7,0 MB, ha percorso la stessa strada fino a 10,5 KB, renderizzato con i propri contorni base visto che il PDF non ha nozione di layer COLR. I font variabili sono l'eccezione, e il blocco sta davanti al subsetter: il GDI GetFontData ha restituito uno stream sostituto da 18,3 MB per il file Segoe UI Variable da 1,85 MB, e la creazione dell'oggetto testo è fallita prima che qualunque subset iniziasse. La guida passo passo all'incorporamento di font CID Type 2 da Delphi copre il percorso di incorporamento che quei font percorrono prima

Dove la prova si ferma

La prova dentro SubsetPdfDocumentFonts è esattamente forte quanto i metadati del font che il produttore ha scritto. Un CIDFont Identity la cui prova è solo /W ha ancora un punto cieco: un glifo la cui larghezza è pari a /DW, che non ha voce /ToUnicode, e che il descrittore non elenca in una /CIDSet. Un tale glifo non è dimostrabile dalle risorse, e il componente non può sapere che esiste senza lo scanner dei content stream che evita di proposito. I produttori che omettono sia /ToUnicode sia /CIDSet comprimendo /W sono rari, ma esistono, quindi per un archivio da un generatore sconosciuto, renderizzate un campione prima e dopo e confrontate pixel e testo estratto

Prima di eseguire un batch, aiuta anche sapere con cosa avete a che fare. L'approccio in leggere le proprietà dei font PDF con PDFium Component vi dice quali font sono incorporati in partenza, e Report.Issues da una prova a secco vi dice quali di loro saranno preservati e perché. Le opzioni predefinite delimitano anche il lavoro: 1024 font, circa un milione di glifi in totale, 128 MB di sorgente e un budget di lavoro fisso, con un CancellationToken facoltativo per lunghi job in batch

FAQ

SubsetPdfDocumentFonts cambia il testo o il layout del PDF?

No. Glyph ID, CID, larghezze e content stream originali vengono conservati, così estrazione del testo e rendering usano gli stessi glifi di prima; vengono tolti solo i contorni irraggiungibili e i nomi dei font ricevono un nuovo prefisso di subset

Perché un font incorporato non ha ricevuto il subset?

Leggete Report.Issues. Ogni font preservato porta un motivo come un /W o /CIDSet mancante su un CIDFont Identity, un programma non TrueType, un array /Widths malformato o un risparmio sotto la soglia

saSubsetNewFonts è la stessa cosa di SubsetPdfDocumentFonts?

No. saSubsetNewFonts crea subset dei font incorporati nella sessione corrente quando salvate, mentre SubsetPdfDocumentFonts lavora su uno stream PDF esistente e sostituisce i font già incorporati da un altro produttore

La creazione di subset dei font del documento, quella al salvataggio e il passaggio di potatura degli orfani dietro di loro arrivano tutti con il PDFium Component per Delphi e C++Builder, che incapsula PDFium per progetti Delphi, C++Builder e Lazarus