Artigo Técnico

PDFlibPas: Substituição Automática de Tipo de Letra CJK

O PDFlibPas resolve os caracteres que o tipo de letra selecionado não consegue desenhar, procurando numa cadeia de substituição de tipos de letra instalados, cluster a cluster, preservando a formação e a ordem dos segmentos bidirecionais. Ativa-se com SetAutomaticFontFallback, estende-se a cadeia com AddFontFallback, e apenas os tipos de letra de substituição efetivamente utilizados na saída são incorporados no ficheiro

O problema que resolve é aquele com que qualquer gerador de documentos se depara na primeira vez que chega o nome de um cliente num sistema de escrita que o tipo de letra do modelo nunca previu. A falha é silenciosa, e é isso que a torna dispendiosa

Porque desaparece o texto não suportado em vez de gerar um erro?

Porque o PDF não tem qualquer conceito de tipo de letra incapaz de desenhar um carácter. Um tipo de letra simples mapeia códigos de byte para nomes de glifos através de uma codificação; um tipo de letra composto mapeia códigos através de um CMap para índices de glifos. Se pedir um glifo que o tipo de letra não contém, obtém o índice de glifo zero, .notdef, que a maioria dos tipos de letra desenha como nada ou como uma caixa vazia. O ficheiro é estruturalmente válido, o operador de texto está bem formado e a página é renderizada. Fica apenas em branco onde o nome deveria estar

Nada na ISO 32000-1 obriga um produtor a detetar isto. Um gerador que escreve texto sem verificar a cobertura produz um PDF tecnicamente conforme que perdeu conteúdo silenciosamente, e essa perda surge no ecrã de um cliente semanas depois. É por isso que a funcionalidade de substituição e o relatório de glifos em falta são disponibilizados em conjunto: resolver o que pode ser resolvido é apenas metade do trabalho, e reportar o que não pôde ser resolvido é a outra metade

A substituição ocorre por cluster, não por ponto de código

A granularidade é o pormenor que separa uma implementação funcional de uma implementação apenas plausível. O texto não é uma sequência de caracteres independentes. Uma sílaba devanágari, um emoji com um modificador de tom de pele, uma letra base com marcas de combinação: cada um destes casos é um único cluster que tem de ser desenhado por um único tipo de letra, porque as decisões de formação no seu interior dependem de tabelas desse mesmo tipo de letra

O PDFlibPas resolve clusters, pelo que um cluster coberto por um tipo de letra de substituição é desenhado inteiramente por esse tipo de letra. Dividir a meio de um cluster e desenhar metade a partir do tipo de letra principal e metade a partir de um tipo de letra de substituição produziria um resultado tecnicamente presente mas visivelmente danificado, o que é discutivelmente pior do que o espaço em branco inicial. A ordem dos segmentos também é preservada, pelo que uma substituição dentro de um segmento da direita para a esquerda não reordena o texto circundante; o mesmo mecanismo sustenta o esquema vertical descrito em escrita vertical para japonês e chinês

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetAutomaticFontFallback(1);

    // Ordem de pesquisa: a primeira correspondência vence, por isso coloque os tipos mais abrangentes no fim
    Lib.AddFontFallback('Microsoft YaHei');   // Chinês simplificado
    Lib.AddFontFallback('Meiryo');            // Japonês
    Lib.AddFontFallback('Segoe UI Symbol');
    Lib.AddFontFallback('Segoe UI Emoji');

    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);

    Lib.AddTrueTypeFont('Arial', 1);          // 1 = incorporar o tipo de letra
    Lib.SetTextSize(11);
    Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
    Lib.DrawText(72, 700, 'Delivery status: on time');

    Lib.SaveToFile('invoice.pdf');
  finally
    Lib.Free;
  end;
end;

Ordene a cadeia de forma deliberada. A resolução escolhe o primeiro tipo de letra que cobre o cluster, pelo que um tipo de letra pan-Unicode abrangente colocado em primeiro lugar ganhará quase tudo, e os seus tipos de letra específicos por sistema de escrita, cuidadosamente escolhidos, nunca serão consultados. Coloque os tipos específicos primeiro e o genérico por último

Reportar ou abortar: que falha prefere?

SetMissingGlyphPolicy aceita PDF_MISSING_GLYPH_REPORT, a predefinição compatível, ou PDF_MISSING_GLYPH_ABORT. Sob a política de relatório, a operação de texto prossegue, os pontos de código não resolvidos são descartados como antes e cada um é registado. Sob a política de aborto, a operação de texto é rejeitada antes de qualquer conteúdo ser escrito e LastErrorCode é definido como 521

Escolha em função da finalidade do documento. Um lote de relatórios internos deve continuar a ser gerado e registar as lacunas, porque um relatório ligeiramente incompleto hoje é melhor do que nenhum relatório. Um contrato juridicamente vinculativo, uma fatura, ou qualquer documento com um nome envolvido, deve abortar, porque um carácter descartado silenciosamente no nome de uma parte é um defeito que se prefere descobrir no próprio processo em vez de num litígio. A política de aborto falha antes de escrever, pelo que nenhum fluxo de conteúdo mal formado fica para trás

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
    // ... construir o documento ...

    if Lib.DrawText(72, 660, CustomerName) <> 1 then
      if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
      begin
        Report := Lib.GetMissingGlyphReportJSON;
        // {"valid":false,"policy":1,"eventCount":1,"events":[
        //   {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
        //    "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
        //    "fontType":"TrueType","operation":"DrawText"}]}
        EscalateToOperator(Report);
      end;
  finally
    Lib.Free;
  end;
end;

O relatório é deliberadamente legível por máquina e limitado. Cada evento regista a página, o índice UTF-16 dentro da cadeia de texto, o ponto de código em forma numérica e no formato U+XXXX, o tipo de letra selecionado, o seu tipo e a operação que encontrou o problema, para que um pedido de suporte possa identificar o carácter exato em vez de descrever um sintoma. O rastreador mantém os 256 eventos mais recentes, o que é suficiente para diagnosticar um documento e suficientemente reduzido para que uma execução patológica não transforme diagnósticos num problema de memória

A medição e o desenho têm de coincidir

A medição de largura utiliza as mesmas decisões de substituição sensíveis a clusters que o desenho. Isto parece óbvio e é precisamente o que a maioria das camadas de substituição desenvolvidas internamente faz mal: corrigem o caminho de desenho, deixam a medição no tipo de letra principal, e cada caixa de texto, alinhamento à direita e coluna de tabela acaba por ser calculado a partir de larguras que não correspondem ao que foi efetivamente renderizado

Como ambos os caminhos partilham a resolução, uma cadeia de texto medida antes do desenho ocupa a largura para a qual foi medida, incluindo os segmentos de substituição. É isso que torna seguro ativar a substituição globalmente, e não apenas nos locais que auditou manualmente

Só é incorporado o que foi realmente utilizado

Os tipos de letra de substituição são incorporados de forma lenta: um tipo de letra na cadeia que nunca resolveu nenhum cluster não contribui em nada para a saída. Um documento contendo um carácter chinês e 5000 caracteres latinos não transporta um tipo de letra CJK completo; transporta apenas o que a passagem de subconjunto produziu para esse único glifo, o comportamento descrito em otimização do tamanho do ficheiro PDF e subconjuntos de tipos de letra

Essa economia torna barato configurar uma cadeia abrangente. Registe os tipos de letra que o seu conjunto de documentos possa necessitar em todas as localizações que serve, e cada PDF individual só paga pelo que efetivamente utilizou. Para documentos que não gerou, onde os tipos de letra em falta já se encontram dentro de um ficheiro existente, o caminho de reparação é diferente e é abordado em incorporar tipos de letra em falta num PDF existente

Vale a pena declarar claramente um aviso de implementação: a substituição resolve-se contra os tipos de letra instalados na máquina que executa o código. Um servidor sem tipos de letra CJK instalados não tem para onde recorrer, e o relatório indicá-lo-á logo no primeiro documento, em vez de depois da primeira reclamação. Distribua os tipos de letra de que depende e confirme o licenciamento para os incorporar

O PDFlibPas é uma biblioteca PDF para Delphi, C++Builder e Lazarus com interfaces DLL e ActiveX correspondentes, pelo que as APIs de substituição e de glifos em falta também estão disponíveis a partir de chamadas fora do Pascal. A documentação completa encontra-se na página da biblioteca PDF PDFlibPas para Delphi