Artigo Técnico

Campos comb árabe e hebraico em Delphi com HotPDF

O HotPDF preenche campos comb AcroForm com árabe, hebraico, marcas combinantes e texto de direção mista através do THPDFHeadlessDocument.SetTextFieldValueWithFamilies. A grelha comb é contada em escalares Unicode, uma ligadura ou uma letra base com as suas marcas é centrada como um cluster único nas k células que os seus escalares possuem, e a linha inteira passa pelo Algoritmo Bidirecional Unicode antes de qualquer célula ser atribuída

Cada uma dessas três regras substitui um atalho que parece bem numa demo latina e desmorona-se no primeiro formulário real. Imagine um requerimento governamental com um campo comb de dez caixas para um nome escrito em árabe. Preencha-o com lógica por caráter e obtém letras em formas isoladas a marchar da esquerda para a direita, uma ligadura lam-alef espremida numa caixa enquanto a vizinha fica vazia, e uma fatha a flutuar sobre a célula errada. Cada um desses defeitos vem de contar a unidade errada ou ordenar o texto no momento errado

Duas formas de preencher um campo comb árabe com o HotPDF em Delphi: o atalho por caráter esmaga a ligadura lam-alef, o preenchimento shaped centra cada cluster
contar escalares Unicode e reordenar a linha antes de as células serem atribuídas mantém a ligadura centrada nas suas duas caixas e não deixa célula vazia

O que pede realmente a flag Comb a um writer PDF?

A flag Comb pede ao writer para dividir o campo em exatamente MaxLen posições igualmente espaçadas e pôr um caráter em cada. O ISO 32000-1 §12.7.4.3 define Comb como bit 25 na Tabela 228, as field flags específicas de campos de texto, e torna-o significativo só quando a entrada MaxLen da Tabela 229 está presente e as flags Multiline, Password e FileSelect estão limpas. O bit em si é $1000000 no valor /Ff de 32 bits; se alguma vez viu campos comb portar-se mal depois de uma ida e volta por uma variável de vírgula flutuante, o artigo sobre field flags AcroForm de 32 bits explica porque esse bit é o primeiro a ir abaixo

A especificação diz "caráter" e fica por aí. Foi escrita com caixas latinas de passo fixo em mente e não diz o que é um caráter quando um par de letras árabe se torna um glyph, ou quando uma marca de vogal não tem largura própria. O HotPDF tem de escolher uma unidade, e impõe as regras estruturais estritamente ao fazê-lo: um campo comb que também é multiline, ou que não traz um MaxLen positivo, é rejeitado com um diagnóstico, e campos password não podem ser comb de forma nenhuma

Porque contar escalares Unicode em vez de unidades UTF-16, glyphs ou grafemas?

O HotPDF conta células comb em escalares Unicode porque é a mesma unidade que usa para impor o MaxLen, e a grelha e a verificação de comprimento têm de concordar ou um valor que passe a validação não caberá nas suas caixas. As três alternativas tentadoras cada uma quebra esse acordo à sua maneira

  • Unidades de código UTF-16 são o que o Length do Delphi devolve, por isso um caráter suplementar como U+20BB7 comeria duas caixas e duas posições do orçamento do campo
  • Contagens de glyphs dependem da fonte: a maioria das fontes árabes transforma lam mais alef num único glyph, por isso a contagem de células mudaria no momento em que mudasse de tipografia
  • Contagens de grafemas fundem uma letra base com as suas marcas, o que significa que a grelha discordaria de uma verificação MaxLen que conta todos os escalares que o utilizador escreveu

A validação MaxLen no THPDFHeadlessDocument só percorre a string quando o seu comprimento UTF-16 excede o limite, e quando o faz, conta um par surrogate válido uma vez e rejeita um surrogate a solo de imediato. Se a sua UI quiser avisar antes da chamada em vez de apanhar uma exceção depois, conte da mesma maneira

Matriz a comparar unidades UTF-16, glyphs e grafemas com escalares Unicode para células comb do HotPDF em Delphi, com lam-alef, beh mais fatha, U+20BB7 e e acentuado
só a coluna de escalares Unicode concorda com a verificação MaxLen — um par surrogate conta uma vez e a contagem de células nunca depende da fonte
// Conta escalares Unicode da mesma forma que a grelha comb:
// um par surrogate válido é um escalar
function CountScalars(const S: UnicodeString): Integer;
var
  I: Integer;
begin
  Result := 0;
  I := 1;
  while I <= Length(S) do
  begin
    if (Ord(S[I]) >= $D800) and (Ord(S[I]) <= $DBFF) and
       (I < Length(S)) and
       (Ord(S[I + 1]) >= $DC00) and (Ord(S[I + 1]) <= $DFFF) then
      Inc(I, 2)
    else
      Inc(I);
    Inc(Result);
  end;
end;

Alguns valores mostram quanto as unidades se afastam, e porque só a coluna de escalares casa com o que um campo comb orçamenta

ValorUnidades UTF-16Escalares (células)Glyphs típicos
Lam + alef (لا)221 ligadura
Beh + fatha (بَ)222, marca anexada à base
U+20BB7 (𠮷)211
Decomposed e + U+0301221 ou 2, depende da fonte

Como partilham as suas células as ligaduras e marcas combinantes?

Um cluster shaped que cubra k escalares ocupa k células contíguas e é centrado como uma unidade única nelas. Depois do shaping, cada cluster é mapeado de volta ao trecho exato de texto-fonte de onde veio, por isso o HotPDF sabe que escalares, e portanto que células, possui. A ligadura lam-alef acima recebe duas caixas e assenta centrada em ambas. Um beh com um shadda e um fatha são três escalares, por isso recebe três caixas, com as marcas ainda anexadas à letra

O centrar é onde a implementação óbvia erra. Centrar cada glyph na sua própria caixa arranca uma marca da sua base, porque a posição da marca só significa algo relativo à letra que decora. O HotPDF mantém cada glyph dentro de um cluster no seu avanço shaped e nos seus offsets X e Y, depois centra o cluster usando a extensão horizontal combinada do avanço natural, das origens de glyph posicionadas e da tinta real do glyph. O cluster move-se em bloco; nada dentro dele é rearranjado

Uma consequência é fácil de perder. Controlos direcionais como U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK, e os controlos de embedding e isolate são escalares Unicode sem glyph. Contam para o MaxLen e seguram uma célula que fica em branco. Se os seus dados-fonte vêm de um sistema que semeia essas marcas nos nomes, limpe-as antes de preencher, ou o campo mostrará um intervalo vazio e rejeitará valores um caráter antes do que o utilizador espera

Porque tem a reordenação bidi de acontecer antes de as células serem atribuídas?

O HotPDF passa a linha completa pelo Algoritmo Bidirecional Unicode (UAX #9) primeiro, e só depois dá a cada escalar a sua célula a partir da sua posição visual. Atribuir células em ordem lógica e inverter o resultado depois funciona para uma string puramente árabe e falha logo que a linha contenha dígitos ou um token latino, porque esses runs têm de manter a leitura da esquerda para a direita dentro da linha da direita para a esquerda. Um valor como um código de referência com prefixo árabe e um número mantém os seus dígitos pela ordem certa só se a reordenação vir a linha inteira de uma vez. A visão geral do shaping de texto de scripts complexos cobre o problema de ordem lógica contra visual com mais profundidade

Atribuição de células comb para um valor árabe com dígitos no HotPDF para Delphi: a ordem lógica inverte o número, enquanto o UAX 9 primeiro mantém os dígitos intactos
corra o algoritmo bidirecional na linha inteira primeiro, depois dê a cada escalar a célula da sua posição visual para os dígitos embutidos manterem a ordem

O alinhamento depois posiciona a grelha inteira de células usadas dentro do campo. A entrada /Q herdada do campo vence quando presente; o Options.Alignment aplica-se só quando o formulário não tem nenhuma. Isso importa para formulários da direita para a esquerda: um valor árabe curto num campo comb alinhado à esquerda preenche as caixas da esquerda, o que está correto pela especificação e quase nunca é o que o desenhador do formulário pretendia. Corrija o /Q do formulário, ou ponha htaRight quando o campo não tem quadding próprio. A rotação do widget vem da entrada /R do /MK, e a 90 ou 270 graus o viewport lógico de texto troca largura com altura enquanto a lógica de células se mantém

Preencher um campo comb de fontes mistas a partir de Delphi

Texto comb de fontes mistas usa uma grelha de células, um tamanho de fonte e uma baseline para cada fonte fornecida. Com FallbackFontFamilies em True, o HotPDF escolhe uma fonte para cada grafema completo das famílias fornecidas pela ordem configurada, por isso uma face árabe pode carregar as letras enquanto uma face latina carrega ASCII embutido. Cada trecho de fonte é shaped com a linha inteira como contexto, uma marca nunca é separada da sua base entre fontes, e nenhuma ligadura é inventada através de uma fronteira de fonte. Compor cada trecho com a sua própria grelha local ou escalá-lo à sua própria bounding box seria mais simples, e partiria tanto as posições de células como os campos rodados no momento em que o fallback trocasse de fonte a meio da linha

uses
  System.SysUtils, System.IOUtils,
  HPDFHeadlessDocument, HPDFHeadlessAppearance;

procedure FillArabicComb(const SourceFile, TargetFile, Value: string);
var
  Doc: THPDFHeadlessDocument;
  Field: THPDFHeadlessFormField;
  Families: THPDFHeadlessRichTextFontFamilies;
  Options: THPDFHeadlessTextAppearanceOptions;
  FieldIndex: Integer;
begin
  Doc := THPDFHeadlessDocument.Create;
  try
    Doc.LoadFromFile(SourceFile);
    FieldIndex := Doc.FindFormField('applicant.name_ar');
    if FieldIndex < 0 then
      raise Exception.Create('Field not found');
    Field := Doc.GetFormField(FieldIndex);
    if (Field.Flags and $1000000) = 0 then   // bit 25: Comb
      raise Exception.Create('Not a comb field');

    SetLength(Families, 2);                  // uma a dezasseis famílias
    Families[0].FamilyName := 'Noto Naskh Arabic';
    Families[0].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoNaskhArabic-Regular.ttf');
    Families[1].FamilyName := 'Noto Sans';
    Families[1].Fonts[hrfsRegular] :=
      TFile.ReadAllBytes('NotoSans-Regular.ttf');

    Options := THPDFHeadlessTextAppearanceOptions.Default;
    Options.ShapeComplexText := True;        // já é o default
    Options.FallbackFontFamilies := True;    // o default é False
    Options.FontSize := 0;                   // auto-ajuste, limitado a 12
    Options.Alignment := htaRight;           // usado só quando /Q está ausente

    Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
    Doc.SaveToFile(TargetFile);              // tem de diferir de SourceFile
  finally
    Doc.Free;
  end;
end;

Repare no que o exemplo não define. Durante o preenchimento de documento carregado, Width, Height, Rotation e CombCells derivam de cada retângulo de widget, da sua entrada /MK e do MaxLen do campo, por isso valores que puser nesses membros são sobrescritos. Um FontSize zero pede dimensionamento automático, que verifica a extensão de cada cluster contra a largura das suas k células e a altura métrica e de tinta partilhada de todas as fontes participantes; um tamanho fixo é simplesmente recortado ao widget. O CharacterSpacing e o WordSpacing nunca movem posições comb, já que só a grelha decide onde um cluster aterra. O HotPDF carrega os seus runtimes HarfBuzz e FriBidi só quando um valor realmente exige shaping, e valores que não precisam de shaping mantêm o layout comb direto existente inalterado. Para o fluxo geral de preenchimento de PDF carregado, incluindo permissões e gravação, veja definir valores de campos de formulário num PDF carregado

O que é que uma aparência comb shaped não garante?

Uma aparência comb shaped garante atribuição correta de células, clusters intactos e o valor lógico preservado; não garante junções cursivas perfeitas nem extração de texto perfeita em toda e qualquer ferramenta. Ambos os limites decorrem do que um campo comb é, e deve conhecê-los antes de prometer alguma coisa a um cliente

As junções cursivas entre células são o primeiro. As letras árabes continuam shaped com contexto de linha inteira, por isso cada uma recebe a sua forma inicial, medial ou final correta, mas o passo fixo das células põe espaço entre clusters separados e os traços de ligação não se vão encontrar como se encontram em texto corrido. Isso é inerente a desenhar um script de ligação em caixas, e nenhum centrar o esconde

A extração de texto é o segundo. O /V do campo mantém o valor lógico simples, e o Form XObject pai da aparência traz um trecho /ActualText (ISO 32000-1 §14.9.4) com a entrada completa; os Forms filhos não trazem nenhum. Extratores que honram o ActualText recuperam a string original. Extratores que o ignoram reconstruem o texto a partir de posições de glyph e podem devolver outra ordem ou texto de cluster duplicado, porque um cluster de múltiplos glyphs não tem um único valor Unicode óbvio por glyph. Se um sistema a jusante precisa do valor, leia o campo, não o texto da página

As restantes fronteiras são mecânicas. O texto comb tem de ser uma linha única sem tabs, e um valor com mais escalares que células falha em vez de ser truncado. As falhas são transacionais: um glyph em falta, uma fonte inválida, um orçamento excedido ou um cancelamento deixa o valor e as aparências anteriores intactos, por isso um preenchimento rejeitado nunca deixa para trás um campo meio desenhado

try
  Doc.SetTextFieldValueWithFamilies(FieldIndex, Value, Families, Options);
except
  on E: EHPDFHeadlessDocumentError do
    if E.Code = hdLimitExceeded then
      ShowFieldError('The value has more characters than the field allows')
    else
      raise;
  on E: EHPDFHeadlessAppearanceError do
    if E.Failure = haMissingGlyph then
      ShowFieldError('None of the supplied fonts covers this text')
    else
      raise;
end;
// Em qualquer dos erros o campo mantém o valor e a aparência anteriores

Onde os campos comb shaped cabem num pipeline de formulários

O suporte comb shaped importa mais para formulários que já existem: declarações de impostos, pedidos de visto, packs de onboarding bancário e registos de ID desenhados com campos em caixas e agora preenchidos em árabe, hebraico, persa ou urdu. Conte em escalares, mantenha o valor original em /V, deixe uma única grelha governar todas as fontes, e teste com um valor que misture letras da direita para a esquerda, dígitos e uma marca combinante, já que essa única string exerce todas as regras deste artigo. A API de documento headless, o shaping comb e o fallback de fontes descritos aqui são parte do HotPDF Delphi PDF component