Artigo Técnico

Incorporar fontes CID Type 2 em PDF a partir do Delphi

O PDFium Component grava texto em uma página PDF no nível de glifo com AddCidType2Text, incorporando dados TrueType que você fornece como uma fonte CID Type 2. Ele atribui a cada instância de glifo um CID sequencial, gera o mapa explícito CID-to-GID e o CMap ToUnicode, e mescla glifos na mesma linha de base em um único objeto de texto nativo. A subsetagem opcional mantém o arquivo pequeno, com uma política explícita para o que acontece quando a subsetagem falha

A gravação em nível de glifo é o que você precisa quando o texto já foi moldado (shaped). Árabe, devanágari e qualquer script com formas contextuais produzem uma sequência de identificadores de glifo que não mapeia mais um a um para caracteres, de modo que uma API que recebe uma string e um nome de fonte não consegue expressar o resultado. Entregar glifos e posições diretamente é a única maneira de colocar texto de script complexo corretamente moldado em um PDF

AddCidType2Text em Delphi atribui a cada instância de glifo shaped um CID sequencial, grava o CIDToGIDMap e o CMap ToUnicode, e funde a execução em um único objeto de texto de página que extrai limpo
Cada instância de glifo leva seu próprio CID, o dicionário da fonte mapeia CIDs para ids de glifo e de volta para Unicode, e a sequência pousa na página como um objeto de texto que extrai de volta ao texto pretendido

Por que cada instância de glifo recebe seu próprio CID

A otimização tentadora é deduplicar: um CID por identificador de glifo distinto, reutilizado onde quer que esse glifo apareça. Isso produz uma fonte menor e quebra a extração de texto, porque o mesmo glifo pode legitimamente corresponder a conteúdo Unicode diferente em lugares diferentes

Um glifo de ligadura é o caso claro. O mesmo glifo "ffi" pode representar os três caracteres em uma palavra e, após uma decisão de shaping diferente, outra coisa em outro lugar. O mapa ToUnicode é indexado por CID, então um CID compartilhado só pode carregar um mapeamento, e qualquer texto que perca essa disputa se torna inextraível

Assim, cada instância de glifo recebe seu próprio CID, e cada mapeamento é livre para ser várias unidades de código UTF-16. Um glifo sem texto significativo — um elemento decorativo, um glifo apenas de posicionamento — mapeia explicitamente para U+200B, um espaço de largura zero, de modo que todo CID tem um resultado de extração observável, em vez de uma lacuna

uses
  PDFium;

var
  Pdf: TPdf;
  Glyphs: TPdfCidGlyphs;
  Options: TPdfCidFontOptions;
  Report: TPdfCidFontReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'label.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;

    // Uma entrada por glifo moldado: o id do glifo, o texto que ele
    // representa e seu avanço e deslocamentos no espaço de texto
    SetLength(Glyphs, 3);
    Glyphs[0].GlyphID := 402;  Glyphs[0].UnicodeText := 'ffi';
    Glyphs[0].Advance := 18.4;
    Glyphs[1].GlyphID := 71;  Glyphs[1].UnicodeText := 'c';
    Glyphs[1].Advance := 9.8;
    Glyphs[2].GlyphID := 74;   Glyphs[2].UnicodeText := 'e';
    Glyphs[2].Advance := 9.2;

    Options := TPdfCidFontOptions.Default;   // subset preferido
    Options.VerifyExtraction := True;

    if Pdf.AddCidType2Text(LoadFileBytes('NotoSans.ttf'), Glyphs,
         11, 72, 700, Options, Report) then
      Writeln(Format('%d glyphs, %d unique, font %d -> %d bytes, match=%s',
        [Report.GlyphCount, Report.UniqueGlyphCount,
         Report.OriginalFontBytes, Report.EmbeddedFontBytes,
         BoolToStr(Report.ExtractionMatches, True)]));
  finally
    Pdf.Free;
  end;
end;

Subsetagem, e as verificações que um código de retorno não dá

Fazer o subset de uma fonte apenas para os glifos realmente usados é a diferença entre incorporar 300 KB e incorporar 12 KB, e em um documento com várias fontes isso decide se o arquivo pode ser enviado por e-mail. O caminho de subsetagem da plataforma aceita uma lista de glifos diretamente, o que se encaixa perfeitamente nessa API, já que o chamador já conhece todo identificador de glifo em uso

O que isso não dá é confiança. Uma chamada de subsetagem pode relatar sucesso e retornar uma saída inutilizável, então o componente valida três propriedades antes de aceitar o resultado: a saída precisa ser menor que a original, precisa ser reanalisada como um sfnt válido com uma tabela maxp legível, e precisa reter glifos até o identificador original mais alto solicitado. Qualquer falha significa que o subset é rejeitado

O que acontece a seguir é a política do chamador. Em pcfemSubsetPreferred, o padrão, um subset rejeitado recorre à incorporação da fonte completa, de modo que a página fica correta e apenas maior. Em pcfemSubsetRequired, a operação falha antes de a página ser modificada, que é o que um pipeline com restrição de tamanho quer. pcfemFull pula a subsetagem inteiramente. O relatório diz qual caminho foi seguido por meio de SubsetAttempted, SubsetApplied, UsedFullFontFallback e SubsetErrorCode

Subsetting de fontes CID Type 2 em Delphi passa três verificações de validação de tamanho, integridade sfnt e cobertura de glifos antes da aceitação, e um subset rejeitado segue a política pcfem de fallback de fonte completa ou falha dura
Um subconjunto é aceito apenas depois que as verificações de tamanho, sfnt e cobertura de glifos passam, e um subconjunto rejeitado então segue a política do chamador — recorrer à fonte completa ou falhar antes de a página ser modificada

Verificação que não pode produzir um falso positivo

Com VerifyExtraction ativado, o componente confirma que o que gravou pode ser lido de volta. O jeito ingênuo de fazer isso é extrair a página inteira e procurar a string esperada, e está errado: uma página que já continha esse texto passa na verificação mesmo quando o novo texto foi gravado incorretamente

Em vez disso, a página de texto é reconstruída e os identificadores de objeto inseridos por essa chamada são lidos individualmente, na ordem de inserção, e concatenados. O resultado é comparado com o texto esperado, e o relatório expõe ambas as strings junto com o booleano, de modo que uma discrepância pode ser diagnosticada, em vez de meramente detectada

Ative isso durante o desenvolvimento e em qualquer pipeline onde a extraibilidade seja uma exigência — arquivos pesquisáveis, conformidade de acessibilidade, mineração de texto posterior. O custo é uma reconstrução de página de texto por chamada, razão pela qual não vem ativado por padrão em loops apertados

Orçamentos e falha atômica

Bytes de fonte, instâncias de glifo e unidades de código Unicode têm cada um um teto antes da alocação, e o formato de fonte, o índice TTC, a faixa de identificador de glifo e os valores de geometria são validados antes de qualquer coisa ser gravada. A geometria precisa ser finita, o que soa óbvio até um motor de shaping entregar a você um avanço NaN vindo de uma fonte malformada

A falha é atômica no nível da página. Se o carregamento da fonte, a gravação de objetos, a geração de conteúdo ou a verificação de extração falharem, todo objeto inserido por essa chamada é removido em ordem reversa e o conteúdo da página é regenerado. Uma chamada malsucedida deixa a página como estava, não com meia sequência de texto nela

Onde isso se encaixa em um pipeline de texto

Vale a pena declarar a divisão de trabalho claramente. Shaping — transformar caracteres em glifos posicionados — não é o trabalho dessa API; pertence a um motor de shaping, e o suporte bidirecional e de script complexo do próprio componente é abordado em tratamento de emoji, CJK e pares substitutos. Essa API é o que você chama depois, com a sequência de glifos que o shaper produziu

Para texto latino comum sem shaping contextual, as APIs de texto mais simples baseadas em string são a ferramenta certa e produzem código menor. Recorra à gravação CID Type 2 quando você tiver saída moldada, quando precisar de controle exato dos identificadores de glifo, ou quando a fonte precisar ser incorporada a partir de bytes que você já tem, em vez de resolvida por nome — a alternativa de resolução de fonte sendo o mecanismo de provedor descrito em controlando a substituição de fontes em PDF

Uma observação de implantação: incorporar uma fonte é uma questão de licenciamento tanto quanto técnica. As fontes variam quanto a se a incorporação é permitida, permitida apenas para visualização, ou permitida para edição. A biblioteca vai incorporar quaisquer bytes que você fornecer a ela, e verificar a licença é responsabilidade sua, não do formato de arquivo

A gravação de texto em nível de glifo, o provisionamento de fontes e a extração de texto compartilham o mesmo modelo de página no Delphi, no C++Builder e no Lazarus; a API completa está descrita na página do PDFium Component para Delphi