Artigo Técnico

Campos comb em árabe e hebraico no Delphi com HotPDF

O HotPDF preenche campos comb AcroForm com árabe, hebraico, marcas combinantes e texto de direção mista via THPDFHeadlessDocument.SetTextFieldValueWithFamilies. A grade comb é contada em Unicode scalars, uma ligadura ou uma letra base com as suas marcas é centralizada como um cluster nas k células que os seus scalars possuem, e a linha inteira passa pelo Unicode Bidirectional Algorithm antes de qualquer célula ser atribuída

Cada uma dessas três regras substitui um atalho que parece bom numa demo latina e desmorona no primeiro formulário real. Imagine uma solicitação governamental com um campo comb de dez caixas para um nome escrito em árabe. Preencha com lógica por caractere e você recebe letras em formas isoladas marchando da esquerda para a direita, uma ligadura lam-alef espremida numa caixa enquanto a vizinha fica vazia, e um fatha flutuando 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 em árabe com HotPDF no Delphi: o atalho por caractere esmaga a ligadura lam-alef, o preenchimento com shaping centraliza cada cluster
contar Unicode scalars e reordenar a linha antes de atribuir células mantém a ligadura centrada nas duas caixas dela e não deixa célula vazia

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

A flag Comb pede ao writer dividir o campo em exatamente MaxLen posições igualmente espaçadas e pôr um caractere em cada. A ISO 32000-1 §12.7.4.3 define Comb como bit 25 da Tabela 228, as field flags específicas de campos de texto, e o torna significativo apenas 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 de 32 bits do /Ff; se você já viu campos comb se comportarem mal depois de uma ida e volta por uma variável de ponto flutuante, o artigo sobre field flags AcroForm de 32 bits explica por que esse bit é o primeiro a ir embora

A especificação diz "character" e para por aí. Ela foi escrita com caixas latinas de passo fixo em mente e não diz o que é um caractere quando um par de letras árabe vira um glifo, ou quando uma marca de vogal não tem largura própria. O HotPDF precisa escolher uma unidade, e o faz impondo as regras estruturais com rigor: um campo comb que também é multiline, ou que não carrega um MaxLen positivo, é rejeitado com um diagnóstico, e campos password não podem ser comb de forma alguma

Por que contar Unicode scalars em vez de unidades UTF-16, glifos ou graphemes?

O HotPDF conta células comb em Unicode scalars porque essa é a mesma unidade que ele usa para impor o MaxLen, e a grade e a checagem de comprimento precisam concordar, senão um valor que passa na validação não cabe nas caixas dele. As três alternativas tentadoras quebram esse acordo de maneiras diferentes

  • Unidades de código UTF-16 são o que o Length do Delphi devolve, então um caractere suplementar como U+20BB7 devoraria duas caixas e duas vagas do orçamento do campo
  • Contagens de glifos dependem da fonte: a maioria das fontes árabes transforma lam mais alef num único glifo, então a contagem de células mudaria no momento em que você trocasse de typeface
  • Contagens de graphemes fundem uma letra base com as suas marcas, o que significa que a grade discordaria de uma checagem de MaxLen que conta todo scalar que o usuário digitou

A validação de MaxLen no THPDFHeadlessDocument só percorre a string quando o comprimento UTF-16 dela excede o limite, e quando o faz, conta um par surrogate válido uma vez e rejeita um surrogate sem par de imediato. Se a sua UI quer avisar antes da chamada em vez de capturar uma exceção depois, conte do mesmo jeito

Matriz comparando unidades UTF-16, glifos e graphemes contra Unicode scalars para células comb do HotPDF no Delphi, com lam-alef, beh mais fatha, U+20BB7 e e acentuado
só a coluna de Unicode scalar concorda com a checagem de MaxLen — um par surrogate conta uma vez e a contagem de células nunca depende da fonte
// Conta Unicode scalars do jeito que a grade comb faz:
// um par surrogate válido é um scalar
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 o quanto as unidades se afastam, e por que só a coluna de scalars bate com o que um campo comb orçamenta

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

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

Um cluster com shaping que cobre k scalars ocupa k células contíguas e é centralizado como uma unidade nelas. Depois do shaping, todo cluster é mapeado de volta ao trecho exato de texto de origem de onde veio, então o HotPDF sabe quais scalars, e portanto quais células, ele possui. A ligadura lam-alef de cima recebe duas caixas e fica centrada nas duas. Um beh carregando um shadda e um fatha são três scalars, então ele recebe três caixas, com as marcas ainda presas à letra

A centralização é onde a implementação óbvia erra. Centralizar cada glifo na própria caixa arranca uma marca da base dela, porque a posição da marca só significa algo relativo à letra que ela decora. O HotPDF mantém todo glifo dentro de um cluster no avanço com shaping e nos offsets X e Y dele, e então centraliza o cluster usando a extensão horizontal combinada do avanço natural, das origens posicionadas dos glifos e da tinta real dos glifos. O cluster se move como um bloco; nada dentro dele é rearranjado

Uma consequência é fácil de perder. Controles direcionais como U+200F RIGHT-TO-LEFT MARK, U+061C ARABIC LETTER MARK e os controles de embedding e isolate são Unicode scalars sem glifo. Eles contam contra o MaxLen e seguram uma célula que fica em branco. Se os seus dados de origem vêm de um sistema que espalha essas marcas em nomes, remova-as antes de preencher, ou o campo vai mostrar uma lacuna e rejeitar valores um caractere antes do que o usuário espera

Por que a reordenação bidi precisa acontecer antes de as células serem atribuídas?

O HotPDF passa a linha completa pelo Unicode Bidirectional Algorithm (UAX #9) primeiro, e só então dá a cada scalar a célula da posição visual dele. Atribuir células em ordem lógica e inverter o resultado depois funciona para uma string puramente árabe e falha assim que a linha contém dígitos ou um token latino, porque esses runs precisam continuar lendo da esquerda para a direita dentro da linha da direita para a esquerda. Um valor como um código de referência com um prefixo árabe e um número mantém os dígitos na ordem certa só se a reordenação vê a linha inteira de uma vez. O panorama do shaping de texto em scripts complexos cobre o problema de ordem lógica versus visual em 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
rode o algoritmo bidirecional na linha inteira primeiro, depois dê a cada scalar a célula da posição visual dele para que dígitos embutidos mantenham a ordem

O alinhamento então posiciona a grade inteira de células usadas dentro do campo. A entrada /Q herdada do campo vence quando presente; Options.Alignment se aplica só quando o form 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 mais à esquerda, o que é correto pela especificação e quase nunca o que o designer do formulário pretendia. Conserte o /Q do form, ou defina htaRight quando o campo não tem quadding próprio. A rotação do widget vem da entrada /MK /R, e a 90 ou 270 graus o viewport lógico de texto troca largura e altura enquanto a lógica de células continua a mesma

Preenchendo um campo comb de fontes mistas do Delphi

Texto comb de fontes mistas usa uma grade de células, um tamanho de fonte e uma baseline para toda fonte fornecida. Com FallbackFontFamilies definido como True, o HotPDF escolhe uma fonte para cada grapheme completo entre as famílias fornecidas na ordem configurada delas, então uma face árabe pode carregar as letras enquanto uma face latina carrega ASCII embutido. Cada trecho de fonte é moldado com a linha inteira como contexto, uma marca nunca é separada da base dela entre fontes, e nenhuma ligadura é inventada através de uma fronteira de fonte. Compor cada trecho com a própria grade local ou escalá-lo para o próprio bounding box seria mais simples, e quebraria tanto as posições das células quanto os campos rotacionados no momento em que o fallback trocasse de fonte no 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);                  // de uma a dezesseis 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);              // precisa diferir de SourceFile
  finally
    Doc.Free;
  end;
end;

Note o que o exemplo não define. Durante o preenchimento do documento, Width, Height, Rotation e CombCells são derivados de cada retângulo de widget, da entrada /MK dele e do MaxLen do campo, então valores que você puser nesses membros são sobrescritos. Um FontSize zero pede dimensionamento automático, que confere a extensão de cada cluster contra a largura das k células dele e a altura métrica e de tinta compartilhada de todas as fontes participantes; um tamanho fixo é simplesmente clipado ao widget. CharacterSpacing e WordSpacing nunca movem posições comb, já que só a grade decide onde um cluster aterrissa. 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 uma aparência comb com shaping não garante?

Uma aparência comb com shaping garante atribuição correta de células, clusters intactos e um valor lógico preservado; ela não garante junções cursivas perfeitas nem extração de texto impecável em toda ferramenta. Os dois limites decoram do que um campo comb é, e você deve conhecê-los antes de prometer qualquer coisa a um cliente

Junções cursivas entre células são o primeiro. As letras árabes ainda são moldadas com contexto de linha cheia, então cada uma recebe a 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 vão se encontrar como no texto corrido. Isso é inerente a desenhar um script de ligação em caixas, e nenhuma quantidade de centralização esconde

A extração de texto é o segundo. O /V do campo mantém o valor lógico puro, e o Form XObject pai da aparência carrega um trecho /ActualText (ISO 32000-1 §14.9.4) com a entrada completa; os Forms filhos não carregam nada. Extratores que honram o ActualText recuperam a string original. Extratores que o ignoram reconstroem o texto das posições dos glifos e podem devolver uma ordem diferente ou duplicar texto de cluster, porque um cluster de múltiplos glifos não tem um único valor Unicode óbvio por glifo. Se um sistema a jusante precisa do valor, leia o campo, não o texto da página

As fronteiras restantes são mecânicas. Texto comb precisa ser de linha única sem tabs, e um valor com mais scalars que células falha em vez de ser truncado. Falhas são transacionais: um glifo ausente, uma fonte inválida, um estouro de orçamento ou um cancelamento deixam o valor e as aparências anteriores intocados, então um preenchimento rejeitado nunca deixa um campo meio desenhado para trás

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 um dos erros o campo mantém o valor e a aparência anteriores

Onde campos comb com shaping se encaixam num pipeline de formulários

Suporte a comb com shaping importa mais para formulários que já existem: declarações de imposto, pedidos de visto, kits de onboarding bancário e registros de identidade desenhados com campos em caixas e agora preenchidos em árabe, hebraico, persa ou urdu. Conte em scalars, mantenha o valor original no /V, deixe uma grade governar todas as fontes e teste com um valor que mistura letras da direita para a esquerda, dígitos e uma marca combinante, porque essa única string exercita toda regra deste artigo. A API headless de documentos, o shaping comb e o fallback de fontes descritos aqui são parte do HotPDF Delphi PDF component