O PDFium Component faz subset de fontes que já estão embutidas em um PDF existente por meio do SubsetPdfDocumentFonts na unit FPdfDocumentFontSubset. A função substitui um programa TrueType /FontFile2 embutido por um menor apenas quando os próprios recursos da fonte provam todo glifo que o documento pode referenciar; uma fonte cujo domínio de glifos não pode ser provado é deixada exatamente como estava
O motivo habitual é um acervo que você não produziu. Faturas, extratos e relatórios de geradores mais antigos frequentemente carregam uma Arial completa ou uma face CJK inteira em cada arquivo, e algumas centenas de kilobytes de fonte por documento de duas páginas somam rápido ao longo de alguns milhões de documentos. Renderizar de novo a partir da origem raramente é uma opção. O que você tem é o PDF, e a única pergunta segura é quanto de cada fonte embutida pode ir sem que uma única página perca um caractere
Por que fazer subset de um PDF existente arrisca perder glifos?
Fazer subset de um PDF existente arrisca perder glifos porque o pós-processador não sabe que glifos o writer original usou. O produtor que embutiu a fonte conhecia o próprio texto; uma ferramenta rodando depois precisa reconstruir esse conhecimento, e qualquer glifo que perder vira uma caixa vazia ou uma forma .notdef em uma página que ninguém olha até um cliente olhar
A abordagem óbvia é interpretar todo content stream, todo Form XObject, toda aparência de anotação e toda aparência padrão de campo, coletar os códigos mostrados com cada fonte, e manter esses glifos. Funciona até encontrar um recurso de fonte compartilhado entre páginas e forms, um campo de formulário cuja aparência um viewer regenera do /DA, ou um content stream com uma sequência de operadores que o scanner não modela. O SubsetPdfDocumentFonts deliberadamente não varre content streams. Ele deriva o domínio de glifos dos metadados que o PDF precisa carregar sobre cada fonte de qualquer forma: o /CIDToGIDMap, os arrays de largura /W e /W2, o bitmap /CIDSet, o CMap /ToUnicode, o array /Widths de fonte simples e o /Encoding. Quando esses metadados não limitam o domínio, a fonte fica intocada
Mantendo glyph IDs originais para os content streams ficarem intocados
O backend de subset por trás do SubsetPdfDocumentFonts, o TPdfTrueTypeSubsetBackend, mantém todo slot de glyph ID original, então nenhum content stream, CID ou /CIDToGIDMap jamais precisa ser regravado. Ele reconstrói as tabelas glyf e loca, dá a todo glifo não usado uma faixa de loca de comprimento zero e deixa o maxp.numGlyphs inalterado. O glifo 4711 continua sendo o glifo 4711 depois do subset; ele só tem contornos se o documento consegue alcançá-lo
Essa escolha remove a parte mais arriscada de fazer subset de um documento pronto. Renumerar glifos significaria regravar toda string em todo content stream que usa a fonte, mais o stream de mapeamento, mais qualquer coisa que cacheie glyph IDs. IDs retidos custam um pouco de tamanho, porque a loca mantém uma entrada por glifo original, mas o volume de um programa TrueType são dados de contorno, e é isso que vai embora. Glifos compostos são seguidos até seus componentes, então uma letra acentuada mantém a letra base e o acento de que é construída; referências compostas cíclicas ou fundas demais reprovam a fonte em vez de produzir um fechamento quebrado
A fonte renomeada também segue a ISO 32000-1 §9.6.4, que exige que o nome de uma fonte subset comece com seis letras maiúsculas e um sinal de mais. O componente deriva as seis letras de um digest SHA-256 do novo programa de fonte, remove primeiro qualquer prefixo ABCDEF+ existente, resolve colisões entre fontes no mesmo documento, e grava o novo nome tanto no /FontName do descritor quanto no /BaseFont do dicionário de fonte, incluindo o pai Type0 de um 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;
// A saída é sempre um PDF completo: o arquivo reconstruído quando Report.Applied
// é True, caso contrário uma cópia byte a byte da origem
Target := TFileStream.Create(TargetName, fmCreate);
try
if Length(Output) > 0 then
Target.WriteBuffer(Output[0], Length(Output));
finally
Target.Free;
end;
end;
Como o SubsetPdfDocumentFonts prova o domínio de glifos de um CIDFont?
O SubsetPdfDocumentFonts prova o domínio de glifos de uma fonte CIDFontType2 a partir de seu mapeamento CID para glifo, e o mapeamento decide quanta evidência extra é necessária. Um stream /CIDToGIDMap explícito (ISO 32000-1 §9.7.4.2) já é uma lista de todo glifo que a fonte pode endereçar, então todo GID no stream entra no domínio. Um stream de mapeamento maior que 65536 entradas, ou um com contagem de bytes ímpar, reprova no perfil limitado e a fonte é preservada
O /CIDToGIDMap /Identity é o caso difícil, e também o mais comum em arquivos modernos. Identity diz que CID iguala GID, o que não diz nada sobre quais CIDs o documento usa. O componente portanto exige evidência primária: um array de largura /W ou /W2 explícito (§9.7.4.3), cujas entradas de CID marcam glifos para os quais o produtor declarou métricas, ou um stream /CIDSet no descritor de fonte (§9.8.3), um bitmap com um bit por CID, bit mais significativo primeiro. Uma fonte Identity bare sem nenhum dos dois é preservada com a razão Identity CIDToGIDMap requires an explicit W array or CIDSet stream
A armadilha em usar o /W como evidência é o /DW. Um produtor pode deixar de fora todo CID cuja largura iguala a largura padrão, e um writer bem otimizado faz exatamente isso para as larguras de glifo mais comuns. Se o domínio viesse do /W sozinho, esses glifos desapareceriam silenciosamente do subset. Para fechar esse buraco, o componente percorre toda fonte Type0 cujo /DescendantFonts aponta para o CIDFont, exige que seu /Encoding seja /Identity-H ou /Identity-V para um código de 2 bytes ser o CID, e mescla os códigos de origem de toda entrada beginbfchar e beginbfrange de seu CMap /ToUnicode (§9.10.3) no domínio. Qualquer outra codificação, ou um stream /ToUnicode que não possa ser decodificado dentro do orçamento, preserva a fonte
Fontes TrueType simples: de Widths a glyph IDs
Fontes /Subtype /TrueType de byte único são provadas por sua faixa de códigos e sua codificação, e então resolvidas a glyph IDs pelo cmap do próprio programa de fonte. O componente exige 0 <= FirstChar <= LastChar <= 255 e um array /Widths cujo comprimento é exatamente LastChar - FirstChar + 1; um array curto, longo ou não numérico significa que o domínio de códigos não está provado, e a fonte fica como está
Cada código nessa faixa, mais qualquer código nomeado no /Differences ou coberto pelo /ToUnicode, é então mapeado do jeito que a ISO 32000-1 §9.6.6 descreve. Códigos sob /WinAnsiEncoding, /MacRomanEncoding ou /StandardEncoding passam pela tabela correspondente para Unicode. Nomes de glifo do /Differences são resolvidos pela Adobe Glyph List, nomes uniXXXX e uXXXX, os 258 nomes de glifo padrão Macintosh e a tabela de nomes post formato 2.0 da própria fonte. Os valores resultantes são buscados em subtables cmap de formato 0, 4, 6 e 12, incluindo Unicode (3,1), (3,10) e (0,*), Mac Roman (1,0), e Windows Symbol (3,0) em F000 mais o código
Quando uma fonte não tem codificação base de forma alguma, o componente marca a união de toda interpretação plausível em vez de chutar uma. Isso mantém alguns glifos que provavelmente não são usados, que é a direção certa para errar. O caso oposto é estrito: um nome de /Differences que mapeia para nenhum glyph ID provado reprova a fonte inteira, porque um nome não resolvido significa que o domínio está incompleto
procedure ReportFontSubset(Source: TStream; Log: TStrings);
var
Options: TPdfDocumentFontSubsetOptions;
Report: TPdfDocumentFontSubsetReport;
Output: TBytes;
I: Integer;
begin
Options := TPdfDocumentFontSubsetOptions.Default;
Options.SubsetOptions.MinimumSavingPercent := 10; // por fonte
Options.MinimumDocumentSavingPercent := 5; // arquivo inteiro
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;
O que o SubsetPdfDocumentFonts deixa em paz?
O SubsetPdfDocumentFonts deixa em paz toda fonte que não consegue provar, e reporta cada uma em Report.Issues com o número de objeto e uma razão em vez de reprovar o documento. O Report.Complete é True apenas quando nenhuma fonte foi preservada. Os casos preservados valem conhecer antes de prometer uma redução de tamanho:
- Programas Type 1
/FontFilee CFF ou OpenType/FontFile3, reportados comoOnly TrueType FontFile2 programs are supported - CIDFonts Identity sem
/W,/W2ou/CIDSet, e CIDFonts sem stream de mapeamento nem/Identity - Programas de fonte referenciados de algum lugar que não seus descritores de fonte, e descritores compartilhados com um consumidor que não é
CIDFontType2nemTrueTypesimples - Fontes cujo subset perde menos que
SubsetOptions.MinimumSavingPercent, e toda fonte quando o arquivo reconstruído perde menos que oMinimumDocumentSavingPercent; nesse caso oReport.AppliedéFalsee a saída são os bytes originais
Duas entradas falham de vez em vez de serem preservadas. Documentos assinados e documentos com campos de assinatura são rejeitados, porque substituir objetos de fonte invalidaria a faixa de bytes de toda assinatura, e arquivos criptografados precisam primeiro de uma origem decifrada. Uma rodada bem-sucedida grava os objetos de substituição como uma atualização esparsa e então reconstrói o arquivo pela mesma passada de alcançabilidade descrita no guia de podar objetos órfãos e deduplicar recursos, então os programas de fonte antigos não sobrevivem como objetos inalcançáveis e o resultado é uma única revisão limpa
Fazendo subset de texto novo ao salvar com saSubsetNewFonts
Fontes que o PDFium Component embute durante a sessão atual são um caso separado, tratado pela opção de salvar saSubsetNewFonts em vez de pelo SubsetPdfDocumentFonts. O saSubsetNewFonts mapeia para a flag FPDF_SUBSET_NEW_FONTS do PDFium, que roda o subsetter nativo HarfBuzz sobre fontes que chamadas nativas como o TPdf.AddText carregaram do sistema, incluindo faces OpenType CFF. O PDFium upstream ainda rotula a flag como experimental. Ela não toca em fontes que já estavam no arquivo
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;
Na sonda do componente, um documento com texto Arial caiu de 590 KB para 22 KB sob saSubsetNewFonts, e a extração de texto do arquivo salvo ainda casava exatamente. Uma fonte colorida COLR v0, Segoe UI Emoji de 7.0 MB, passou pelo mesmo caminho para 10.5 KB, renderizada com seus contornos base já que o PDF não tem noção de camadas COLR. Fontes variáveis são a exceção, e o bloqueio senta na frente do subsetter: o GetFontData da GDI devolveu um stream substituto de 18.3 MB para o arquivo Segoe UI Variable de 1.85 MB, e a criação do objeto de texto falhou antes de qualquer subset começar. O passo a passo de embutir fontes CID Type 2 a partir do Delphi cobre o caminho de embutimento por que essas fontes passam primeiro
Onde a prova para
A prova dentro do SubsetPdfDocumentFonts é exatamente tão forte quanto os metadados de fonte que o produtor gravou. Um CIDFont Identity cuja evidência é o /W sozinho ainda tem um ponto cego: um glifo cuja largura iguala o /DW, que não tem entrada no /ToUnicode, e que o descritor não lista em um /CIDSet. Tal glifo não pode ser provado a partir dos recursos, e o componente não pode saber que ele existe sem a varredura de content streams que evita de propósito. Produtores que omitem tanto o /ToUnicode quanto o /CIDSet enquanto comprimem o /W são raros, mas existem, então para um acervo de um gerador desconhecido, renderize uma amostra antes e depois e compare pixels e texto extraído
Antes de rodar um lote, também ajuda saber com o que você está lidando. A abordagem em ler propriedades de fontes PDF com o PDFium Component diz quais fontes estão embutidas de forma alguma, e o Report.Issues de uma rodada seca diz quais delas serão preservadas e por quê. As opções padrão também limitam o trabalho: 1024 fontes, cerca de um milhão de glifos no total, 128 MB de origem e um orçamento de trabalho fixo, com um CancellationToken opcional para jobs em lote longos
FAQ
O SubsetPdfDocumentFonts muda o texto ou o layout do PDF?
Não. Glyph IDs, CIDs, larguras e content streams originais são mantidos, então extração de texto e renderização usam os mesmos glifos de antes; apenas contornos inalcançáveis são removidos e os nomes das fontes recebem um prefixo de subset novo
Por que uma fonte embutida não recebeu subset?
Leia o Report.Issues. Cada fonte preservada carrega uma razão como um /W ou /CIDSet ausente em um CIDFont Identity, um programa não TrueType, um array /Widths malformado ou uma economia abaixo do limiar
O saSubsetNewFonts é o mesmo que SubsetPdfDocumentFonts?
Não. O saSubsetNewFonts faz subset de fontes embutidas na sessão atual quando você salva, enquanto o SubsetPdfDocumentFonts trabalha sobre um stream PDF existente e substitui fontes que já haviam sido embutidas por outro produtor
Subset de fontes de documento, subset de fontes ao salvar e a passada de poda de órfãos por trás deles vêm todos com o PDFium Component for Delphi and C++Builder, que embrulha o PDFium para projetos Delphi, C++Builder e Lazarus