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
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
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
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
/FontFilee CFF o OpenType/FontFile3, riportati comeOnly TrueType FontFile2 programs are supported - CIDFont Identity senza
/W,/W2o/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 è
CIDFontType2oTrueTypesemplice - Font il cui subset non raggiunge
SubsetOptions.MinimumSavingPercent, e ogni font quando il file ricostruito non raggiungeMinimumDocumentSavingPercent; in quel casoReport.AppliedèFalsee 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