Artigo Técnico

Incorporar Fontes CID Tipo 2 em PDF a partir do Delphi

O PDFium Component escreve texto numa página PDF ao nível do glifo com AddCidType2Text, incorporando dados TrueType que o utilizador fornece como uma fonte CID Tipo 2. Atribui a cada instância de glifo um CID sequencial, gera o mapa explícito CID-to-GID e o CMap ToUnicode, e junta os glifos na mesma linha de base num único objeto de texto nativo. A subconjuntação opcional mantém o ficheiro pequeno, com uma política explícita para o que acontece quando a subconjuntação falha

A escrita ao nível do glifo é o que se precisa quando o texto já foi moldado (shaped). O árabe, o devanágari e qualquer escrita com formas contextuais produzem uma sequência de identificadores de glifo que já não mapeia um para um com os carateres, pelo que uma API que recebe uma string e um nome de fonte não consegue expressar o resultado. Entregar diretamente glifos e posições é a única forma de colocar num PDF texto de escrita complexa corretamente moldado

O AddCidType2Text em Delphi atribui a cada instância de glifo moldada um CID sequencial, escreve o CIDToGIDMap e o CMap ToUnicode e funde o trecho num único objeto de texto da página que se extrai limpo
Cada instância de glifo toma o seu próprio CID, o dicionário do tipo de letra mapeia CIDs para ids de glifo e de volta para Unicode, e a sequência aterra na página como um objeto de texto que se extrai de volta para o texto pretendido

Porque é que cada instância de glifo recebe o seu próprio CID

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

Um glifo de ligadura é o caso claro. O mesmo glifo "ffi" pode representar os três carateres numa palavra e, após uma decisão de moldagem diferente, outra coisa noutro sítio. O mapa ToUnicode é indexado por CID, pelo que um CID partilhado só pode transportar um mapeamento, e o texto que perder essa disputa torna-se inextraível

Assim, cada instância de glifo recebe o seu próprio CID, e cada mapeamento é livre de 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, pelo que todos os CID têm 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 representa,
    // e o seu avanço e desvios 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 preferred
    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;

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

Subconjuntar uma fonte aos glifos realmente utilizados é a diferença entre incorporar 300 KB e incorporar 12 KB, e num documento com várias fontes isso decide se o ficheiro é enviável por email. O percurso de subconjuntação da plataforma aceita diretamente uma lista de glifos, o que se ajusta perfeitamente a esta API, já que quem chama já conhece todos os identificadores de glifo em uso

O que isso não dá é confiança. Uma chamada de subconjuntação pode reportar sucesso e devolver uma saída inutilizável, pelo que o componente valida três propriedades antes de aceitar o resultado: a saída tem de ser mais pequena do que a original, tem de voltar a analisar-se como um sfnt válido com uma tabela maxp legível, e tem de reter glifos até ao identificador original mais alto pedido. Qualquer falha significa que o subconjunto é rejeitado

O que acontece a seguir é política de quem chama. Sob pcfemSubsetPreferred, a predefinição, um subconjunto rejeitado recorre à incorporação da fonte completa, pelo que a página fica correta e apenas maior. Sob pcfemSubsetRequired, a operação falha antes de a página ser modificada, que é o que um pipeline limitado em tamanho quer. pcfemFull ignora a subconjuntação por completo. O relatório diz-lhe que percurso foi tomado através de SubsetAttempted, SubsetApplied, UsedFullFontFallback e SubsetErrorCode

A criação de subconjuntos de tipos de letra 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 subconjunto rejeitado segue a política pcfem de fallback para o tipo de letra completo ou falha dura
Um subconjunto só é aceite depois de as verificações de tamanho, sfnt e cobertura de glifos passarem, e um subconjunto rejeitado segue então a política do chamador — recuar para o tipo de letra completo 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 escreveu pode ser lido de volta. A forma ingénua de o fazer é extrair a página inteira e procurar a string esperada, e está errada: uma página que já continha esse texto passa a verificação mesmo quando o novo texto foi escrito incorretamente

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

Ative-a durante o desenvolvimento e em qualquer pipeline onde a extraibilidade seja um requisito — arquivos pesquisáveis, conformidade de acessibilidade, mineração de texto a jusante. O custo é uma reconstrução da página de texto por chamada, razão pela qual não está ativa por predefinição em ciclos apertados

Orçamentos e falha atómica

Os bytes da fonte, as instâncias de glifo e as unidades de código Unicode têm cada um um limite antes da alocação, e o formato da fonte, o índice TTC, o intervalo de identificadores de glifo e os valores de geometria são validados antes de algo ser escrito. A geometria tem de ser finita, o que parece óbvio até um motor de moldagem entregar um avanço NaN vindo de uma fonte malformada

A falha é atómica ao nível da página. Se o carregamento da fonte, a escrita de objetos, a geração de conteúdo ou a verificação de extração falharem, todos os objetos inseridos por essa chamada são removidos por ordem inversa e o conteúdo da página é regenerado. Uma chamada falhada deixa a página como estava, não com meia sequência de texto nela

Onde isto se encaixa num pipeline de texto

Vale a pena declarar a divisão de trabalho com clareza. A moldagem — transformar carateres em glifos posicionados — não é trabalho desta API; pertence a um motor de moldagem, e o próprio suporte bidirecional e de escrita complexa do componente está descrito em tratamento de emoji, CJK e pares substitutos. Esta API é o que se chama depois, com a sequência de glifos que o moldador produziu

Para texto latino comum sem moldagem contextual, as APIs de texto mais simples, baseadas em strings, são a ferramenta certa e produzem código mais pequeno. Recorra à escrita CID Tipo 2 quando tiver saída moldada, quando precisar de controlo exato dos identificadores de glifo, ou quando a fonte tiver de ser incorporada a partir de bytes que já tem, em vez de ser resolvida pelo nome — sendo a alternativa de resolução de fontes o mecanismo de fornecedor descrito em controlar a substituição de fontes em PDF

Uma nota de implementação: incorporar uma fonte é uma questão de licenciamento tanto quanto técnica. As fontes variam quanto a permitirem, ou não, a incorporação, a permitirem-na apenas para visualização, ou a permitirem-na para edição. A biblioteca incorpora quaisquer bytes que lhe forneça, e verificar a licença é da sua responsabilidade, não do formato de ficheiro

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