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
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
Lengthdo 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
MaxLenque 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
// 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
| Valor | Unidades UTF-16 | Scalars (células) | Glifos típicos |
|---|---|---|---|
| Lam + alef (لا) | 2 | 2 | 1 ligadura |
| Beh + fatha (بَ) | 2 | 2 | 2, marca presa à base |
| U+20BB7 (𠮷) | 2 | 1 | 1 |
| Decomposed e + U+0301 | 2 | 2 | 1 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
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