Artigo Técnico

Renderizando composite glyphs e acentos CFF seac em PDF

O HotPDF renderiza letras acentuadas de fontes embutidas corretamente ao tratar dois mecanismos de fonte que constroem um glifo a partir de outros: composite glyphs TrueType, cujos offsets de componente precisam ser aplicados em unidades de glifo antes da matriz glifo-dispositivo, e a forma seac do endchar em CFF, que costura uma letra base e um acento pelo código StandardEncoding. Errar qualquer um dos dois e é, Å, ü ou uma aspa dupla curva sai faltando uma parte, enquanto as letras simples ao redor parecem perfeitas

Esse último detalhe é o que torna esses bugs caros. Um relatório renderiza, o corpo do texto está ok, e só “München” tem um buraco, ou uma citação abre com uma aspa simples e um vão. Ninguém desconfia do renderizador. A primeira teoria é sempre uma fonte danificada ou um produtor ruim, e o ticket fica quicando por uma semana. Este artigo percorre o que realmente dá errado em cada mecanismo, como o componente HotPDF Delphi o trata desde a v2.768.10 e v2.768.11, e a correção de subsetting relacionada na v2.770.83 que mantém composite glyphs intactos quando o HotPDF escreve fontes em vez de lê-las

Por que só o acento desaparece de um glifo PDF?

O acento desaparece porque é a única parte do glifo que carrega um offset diferente de zero. Fontes TrueType e CFF economizam espaço reutilizando contornos: é não é desenhado como uma forma nova, mas como “o glifo e, mais o glifo agudo deslocado para cima e para a direita”. A letra base quase sempre fica no offset (0, 0), então qualquer erro em como os offsets são aplicados a deixa intacta. O acento é o componente que se move, então é o componente que cai no lugar errado ou nunca é desenhado

O padrão de sintomas é um diagnóstico por si só. Se as letras base dos caracteres acentuados renderizam e os diacríticos não, ou se pontuação pareada como “ e ” aparece como uma marca em vez de duas, pare de olhar o arquivo da fonte. Uma fonte quebrada geralmente quebra glifos inteiros, e os quebra para todo viewer. Pedaços faltando de glifos compostos em um renderizador, enquanto o Acrobat mostra a página bem, apontam para o código que monta os componentes

Como composite glyphs TrueType posicionam os seus componentes?

Um composite glyph TrueType posiciona cada componente com a sua própria pequena transformação afim, expressa em unidades de fonte, e essa transformação precisa agir sobre o contorno do componente antes da matriz glifo-dispositivo. Na tabela glyf do OpenType, um composite glyph tem uma contagem de contornos negativa, seguida de uma lista de registros de componente. Cada registro guarda uma palavra de flags, o índice de glifo do componente, dois argumentos que são bytes ou words (ARG_1_AND_2_ARE_WORDS, 0x0001) e uma escala opcional: um valor F2Dot14 para WE_HAVE_A_SCALE (0x0008), dois para WE_HAVE_AN_X_AND_Y_SCALE (0x0040), ou uma matriz 2x2 completa para WE_HAVE_A_TWO_BY_TWO (0x0080). Quando ARGS_ARE_XY_VALUES (0x0002) está definido, os dois argumentos são um offset dx/dy em unidades de fonte. MORE_COMPONENTS (0x0020) diz que outro registro vem a seguir

Então, para cada componente você constrói uma matriz de componente CM a partir da parte de escala e do offset, e depois a combina com M, a matriz que mapeia unidades de fonte para pixels de dispositivo. A ordem dessa combinação é todo o bug. A multiplicação de matrizes interna do HotPDF usa a convenção de vetor coluna, então o produto de A e B aplica B primeiro e depois A. A composição correta aplica CM primeiro, no espaço do glifo, e M depois. Escrita ao contrário, M roda primeiro e o offset de CM é somado depois, já em pixels de dispositivo e nunca escalado

// Só ilustração: um record afim 2D com a convenção de vetor coluna,
// em que Mul(A, B) mapeia um ponto por B primeiro e depois por A
type
  TAffine = record
    a, b, c, d, e, f: Double;
  end;

function Mul(const A, B: TAffine): TAffine;
begin
  Result.a := A.a * B.a + A.c * B.b;
  Result.b := A.b * B.a + A.d * B.b;
  Result.c := A.a * B.c + A.c * B.d;
  Result.d := A.b * B.c + A.d * B.d;
  Result.e := A.a * B.e + A.c * B.f + A.e;
  Result.f := A.b * B.e + A.d * B.f + A.f;
end;

// M  : unidades de fonte -> pixels de dispositivo (escala minúscula, y invertido)
// CM : transformação de componente; e/f guardam dx/dy em unidades de fonte
Sub := Mul(M, CM);   // correto: offset escalado para baixo com o contorno
// Sub := Mul(CM, M); // errado: dx/dy caem como pixels de dispositivo brutos

Os números tornam a falha óbvia quando você os vê. O Arial usa um em de 2048 unidades, e o componente de acento do é carrega o seu offset nessas unidades de fonte. Com a ordem correta, esse offset encolhe junto com o contorno para alguns pixels no zoom normal. Com a ordem invertida, o valor bruto em unidades de fonte é somado como pixels de dispositivo, e no render de teste do HotPDF o acento do é do Arial acabou a 243 pixels da sua letra, bem fora do glifo e geralmente fora de qualquer coisa visível. A aspa dupla esquerda no Arial são duas cópias da forma de aspa simples, a segunda deslocada, então ela renderizava como um ‘ solitário seguido de espaço vazio

A parte linear raramente expõe o bug. Componentes de acento geralmente não carregam escala nenhuma, e uma escala uniforme comuta com M de qualquer jeito, então só a translação revela o produto invertido. É também por isso que um teste rápido com texto latino simples passa: a inversão estava sentada no código por muitos releases antes de a v2.768.10 corrigi-la, e só componentes com offsets a mostravam

Recursão e ciclos

Composite glyphs podem referenciar outros composite glyphs, então um renderizador os percorre recursivamente e precisa se defender. O HotPDF limita a profundidade de aninhamento a 5 níveis e a contagem total de componentes por glifo a 1024, pula um componente que nomeia o seu próprio índice de glifo e trata o modo de casamento por pontos raramente usado (argumentos que são números de ponto em vez de offsets) como um offset zero. Uma fonte hostil ou corrompida pode, portanto, perder um componente, mas não consegue travar o render da página

O que é a forma seac do endchar em CFF?

A forma seac do endchar é um jeito descontinuado de uma charstring Type 2 de CFF dizer “este glifo é uma letra base mais um acento”: quando o endchar tem quatro argumentos, eles são adx ady bchar achar. A Adobe Technical Note #5177 a descreve no seu apêndice sobre compatibilidade com Type 1. O nome vem do operador seac do Type 1 (standard encoding accented character), e subsets CFF convertidos de fontes Type 1 ainda o usam pesadamente para letras latinas acentuadas

Três detalhes derrubam implementadores. Primeiro, bchar e achar não são glyph IDs. São códigos na Adobe StandardEncoding, então o renderizador mapeia cada código para um nome de glifo e depois procura esse nome pelo charset da fonte para achar o GID. Segundo, o Type 2 não tem argumento asb, ao contrário do seac do Type 1, que recebe asb adx ady bchar achar. No Type 2, a origem do acento vai direto para (adx, ady) em relação à origem do glifo base, sem correção de side-bearing. Terceiro, fontes CFF CID-keyed não podem usar seac de forma alguma, porque não há charset de nomes de glifo pelo qual resolver códigos StandardEncoding

Depois vem a largura. Toda charstring Type 2 pode começar o seu primeiro operador de limpeza de pilha com um argumento opcional de largura de avanço, e o endchar é um desses operadores. Um endchar simples com um argumento carrega uma largura; um endchar seac com cinco argumentos carrega uma largura seguida dos quatro valores seac. A regra é contar argumentos contra a contagem fixa esperada, que é quatro para seac. Antes da v2.768.11, o HotPDF tratava qualquer endchar como “pegue a largura e pare”, então glifos seac produziam um contorno vazio: sem base, sem acento, só um vão do tamanho certo na linha

// Ilustração da decisão do endchar em um interpretador Type 2.
// A pilha guarda os operandos que precedem o endchar; Count é quantos.
if (Count >= 4) and not IsCIDFont then
begin
  // 5 operandos: o primeiro é a largura de avanço, o resto é seac
  if Count = 5 then
    TakeWidthAndShift;                 // descarte a largura, mantenha 4 operandos
  if StackDepth = 4 then
  begin
    BaseGID   := GIDForName(StdEncName(Round(Stack[2])));  // bchar
    AccentGID := GIDForName(StdEncName(Round(Stack[3])));  // achar
    DrawGlyph(BaseGID, Identity);
    DrawGlyph(AccentGID, Translate(Stack[0], Stack[1]));   // adx, ady
    Exit;
  end;
end;
// caso contrário: endchar comum, largura opcional, feche o path

No interpretador real, ambos os glifos de componente são executados com o mesmo motor de charstring, a matriz do acento é a matriz de glifo atual composta com uma translação para (adx, ady), e a matriz é restaurada depois, para que glifos seguintes na linha não sejam afetados. Se qualquer um dos nomes não resolver para um GID, esse componente é pulado em vez de falhar o glifo inteiro, que é o comportamento complacente que você quer quando um subset foi aparado um pouco agressivo demais

Conferindo a correção a partir do seu próprio código Delphi

O jeito mais rápido de verificar o tratamento de composite glyph e seac é renderizar uma página com texto acentuado de fontes embutidas pelos dois caminhos de saída públicos: THotPDF.RenderLoadedPageToBitmap para saída raster e THotPDF.BuildLoadedPageSVG com EmbedFonts definido como True para saída vetorial. Ambos compartilham o mesmo código de contorno de glifo, então ambos receberam as correções da v2.768.10 e v2.768.11. O pipeline como um todo é coberto em como o HotPDF renderiza glifos TrueType, CFF e Type 3 embutidos, e as opções de bitmap em renderização de páginas PDF para bitmaps em Delphi

uses
  System.Classes, Vcl.Graphics, HPDFDoc;

procedure RenderAccentProbe(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  Svg: AnsiString;
  Lines: TStringList;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;

    // Caminho raster: 144 DPI torna acentos deslocados fáceis de notar
    Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bmp <> nil then
    try
      Bmp.SaveToFile('accent-probe.bmp');
    finally
      Bmp.Free;
    end;

    // Caminho vetorial: EmbedFonts = True escreve contornos de glifo, então a
    // montagem de composite e seac fica visível nos próprios paths do SVG
    Svg := Pdf.BuildLoadedPageSVG(0, True);
    if Svg <> '' then
    begin
      Lines := TStringList.Create;
      try
        Lines.Text := string(Svg);
        Lines.SaveToFile('accent-probe.svg');
      finally
        Lines.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Escolha os documentos de teste deliberadamente. Você quer um PDF com um subset TrueType embutido contendo é, Å e aspas duplas curvas, e um com um subset CFF (FontFile3, Type1C) embutido produzido a partir de uma fonte Type 1, que é onde o seac ainda vive, tipicamente em saídas de diagramação antigas e alguns workflows de impressão. Texto composto em fontes que o produtor não embutiu não diz nada aqui, porque esses glifos vêm de fontes Windows instaladas em vez do programa de fonte embutido descrito na ISO 32000-1 §9.9

Por que composite glyphs quebravam durante o subsetting de fontes?

Composite glyphs quebravam durante o subsetting porque o subsetter percorria os registros de componente com o passo errado sempre que um componente carregava uma escala ou transformação 2x2, e depois lia errado o índice de glifo do próximo componente. O HotPDF corrigiu isso na v2.770.83. Subsetting é o gêmeo do lado da escrita do bug de renderização: quando o HotPDF embute um subset de uma fonte TrueType instalada, ele precisa manter todo glifo que um composite referencia e renumerar essas referências para os novos glyph IDs compactos

O walker antigo só olhava a flag de tamanho de argumento e a flag de mais-componentes, então avançava 6 ou 8 bytes por registro. Ele ignorava os 2, 4 ou 8 bytes de dados de transformação que as flags de escala adicionam. Depois de um componente escalado, o próximo “índice de glifo” que ele lia eram na verdade bytes de transformação, então o componente real podia ficar de fora do subset, e o passo de renumeração podia escrever um novo glyph ID direto na transformação. O resultado era uma letra acentuada que renderizava em alguns viewers com o acento errado, um acento distorcido ou nenhum, e só para fontes que escalam os seus componentes, que é o motivo de a maioria dos testes nunca tê-lo pego

Mais dois problemas sentavam ao lado do bug de passo. Renumerar um índice de glifo não é idempotente, e composites aninhados estavam sendo renumerados duas vezes: uma pela passada sobre todo composite marcado e outra por uma descida recursiva nele. O HotPDF agora renumera cada composite exatamente uma vez e comanda o passo “quais glifos este composite precisa” a partir de uma fila de trabalho, então um ciclo de composites que se referenciam termina, índices de glifo além da contagem de glifos do maxp da fonte são rejeitados e glifos vazios nunca são parseados como composites

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'accents-subset.pdf';
    Pdf.FontEmbedding := True;          // embute os glifos que são usados
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    // é, Å, ü e aspas curvas são todos composite glyphs no Arial
    Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
      'Café, Ångström, München, “quoted”');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Faça round-trip desse arquivo pelo probe acima e por um segundo viewer. Desde a v2.770.83 todo componente de todo composite glyph sobrevive ao subsetting, incluindo componentes atrás de uma escala ou transformação 2x2, então os acentos renderizam igual no HotPDF, no Acrobat e nos viewers de navegador

O que o tratamento de composite glyph não cobre

A correção de composite glyph cobre posicionamento baseado em offset, que é o que quase toda fonte real usa, e aproxima o resto de forma conservadora. Composites TrueType de casamento por pontos, em que os dois argumentos nomeiam pontos a alinhar em vez de dar um offset, são desenhados com offset zero. Seac em fontes CFF CID-keyed é ignorado de propósito porque a especificação o proíbe ali. Um seac cujo código de base ou de acento não tem glifo em um subset aparado desenha o componente que conseguir achar. Nenhum desses casos crasha o renderizador, mas se você produz PDFs para arquivamento ou uso PDF/A, o movimento mais seguro é embutir fontes que usam composites por offset e charstrings Type 2 sem seac, para que todo consumidor, e não apenas um renderizador, veja as mesmas letras

FAQ

Por que letras acentuadas renderizam sem os acentos no meu renderizador PDF?

Letras acentuadas costumam ser composite glyphs ou glifos seac de CFF, e o acento é o único componente com offset. Um renderizador que aplica o offset do componente depois da matriz de dispositivo, ou que ignora seac no endchar, descarta ou desloca o acento enquanto a letra base renderiza normalmente

O HotPDF suporta a forma seac do endchar em CFF?

Sim. Desde a v2.768.11 o HotPDF desenha o endchar com quatro argumentos, ou cinco incluindo a largura, como um glifo base mais um acento deslocado para (adx, ady), resolvendo ambos por nomes StandardEncoding e pelo charset da fonte, tanto na renderização de bitmap quanto na exportação SVG com fontes embutidas

Composite glyphs são preservados quando o HotPDF faz subsetting de fontes TrueType?

Sim. Desde a v2.770.83 o HotPDF pula as transformações de componente de 2, 4 ou 8 bytes corretamente, mantém todo componente referenciado no subset, renumera composites aninhados uma vez e para com segurança em referências cíclicas ou fora do intervalo

Renderização de composite glyphs, suporte a seac de CFF e subsetting TrueType seguro vêm como parte do HotPDF Delphi PDF Component, uma biblioteca VCL nativa para Delphi e C++Builder que cria, edita, extrai e renderiza documentos PDF sem DLLs externas