Extraia um emoji ou um nome de registro familiar japonês de um PDF como texto, e a saída mostra uma caixa, um ponto de interrogação, ou nada onde o caractere deveria estar. A propriedade Character[] do Componente PDFium geralmente é o motivo: ela lê cada glifo por meio de FPDFText_GetUnicode, que retorna um ponto de código Unicode completo como um valor sem sinal de 32 bits, depois o expõe ao Delphi como um único WideChar de 16 bits. Qualquer ponto de código além de U+FFFF não consegue fazer essa viagem em uma única peça, e a corrupção nunca aparece enquanto você está olhando para a página renderizada, porque renderização e extração de texto rodam por caminhos de código separados no PDFium — um documento pode exibir seus emojis perfeitamente e ainda assim entregar lixo no instante em que você lê Character[] em um loop e constrói uma string a partir dele
O Plano Multilíngue Básico e por que o WideChar para em U+FFFF
O WideChar do Delphi é um tipo de 16 bits que só consegue guardar uma unidade de código UTF-16. O Plano Multilíngue Básico do Unicode, a faixa U+0000 a U+FFFF, cabe exatamente ali dentro, motivo pelo qual latim, cirílico, grego e o bloco comum de Ideogramas Unificados CJK todos fazem o percurso de ida e volta por um único WideChar sem incidentes. Duas famílias de caracteres rotineiramente ficam fora disso em documentos reais: emojis, muitos deles no bloco Emoticons começando em U+1F600, e ideogramas CJK raros da Extensão B de Ideogramas Unificados CJK, a faixa U+20000 a U+2A6DF reservada para caracteres chineses, japoneses e coreanos menos comuns, incluindo muitos nomes pessoais e de lugares. O UTF-16 trata qualquer coisa acima de U+FFFF com um par substituto (surrogate pair) — duas unidades de código de 16 bits, um substituto alto na faixa $D800 a $DBFF seguido de um substituto baixo em $DC00 a $DFFF, que juntos codificam um ponto de código — e a matemática por trás desse pareamento é fixa o bastante para demonstrar diretamente em Pascal
function ToSurrogatePair(CodePoint: LongWord; out Hi, Lo: WideChar): Boolean;
var
V: LongWord;
begin
Result := CodePoint > $FFFF;
if Result then
begin
V := CodePoint - $10000;
Hi := WideChar($D800 + (V shr 10));
Lo := WideChar($DC00 + (V and $3FF));
end;
end;
Alimente U+1F600, o emoji de rosto sorridente, por essa função e o resultado é um substituto alto de $D83D e um substituto baixo de $DE00, dois valores de 16 bits, não um. Nenhuma das metades significa nada sozinha; um $D83D solitário sentado em uma string sem nenhum $DE00 atrás dele é um substituto pendente (dangling surrogate), e a maioria do código de tratamento de texto que encontra um ou o descarta, ou o substitui por um glifo de reserva, ou levanta um erro
Por que o FPDFText_GetUnicode retorna um valor que o Character[] não consegue guardar?
FPDFText_GetUnicode retorna um LongWord, um valor completo de 32 bits, porque a codificação de texto do PDF já carrega o valor escalar Unicode completo para cada glifo. O CMap ToUnicode de um PDF mapeia códigos de caractere para texto Unicode, e quando um glifo representa o que informalmente se chama de caractere de plano astral — qualquer coisa além do Plano Multilíngue Básico — esse mapeamento é um ponto de código completo, não um fragmento de 16 bits. O PDFium o decodifica de volta para um valor escalar internamente e o retorna através da fronteira da DLL por meio de FPDFText_GetUnicode, e essa fronteira é exatamente onde um valor de 32 bits precisa virar algo que uma propriedade Delphi consiga devolver ao seu código
A implementação óbvia é WideChar(FPDFText_GetUnicode(TextPage, Index)), e também é a errada. Um cast direto de um valor de 32 bits para um tipo de 16 bits mantém apenas os 16 bits baixos e descarta o resto silenciosamente, sem exceção e sem verificação de faixa. Para U+1F600 isso significa manter $F600 e perder o fato de que o valor verdadeiro alguma vez esteve acima de U+FFFF, o que produz uma unidade de código que nem sequer é um substituto pendente válido, apenas um caractere não relacionado do Plano Multilíngue Básico que por acaso compartilha esses bits baixos. Concatene alguns milhares desses em uma string e o código downstream não tem mais como distinguir um caractere corrompido de um legítimo
O que Character[] e Charcode[] retornam para pontos de código de plano astral agora
As propriedades Character[] e Charcode[] do Componente PDFium retornam U+FFFD, o caractere de substituição Unicode, sempre que o ponto de código subjacente excede U+FFFF, em vez de truncá-lo silenciosamente. Essa proteção fica diretamente dentro do getter de propriedade por trás de Character[]
function TPdf.GetCharacter(Index: Integer): WideChar;
var
Code: LongWord;
begin
LoadTextPage;
Code := FPDFText_GetUnicode(FTextPage, Index);
if Code > $FFFF then
Result := #$FFFD // astral-plane code point: cannot fit in one WideChar
else
Result := WideChar(Code);
end;
Retornar U+FFFD em vez de um fragmento truncado é uma correção deliberada e restrita, e não um redesenho. Character[] e Charcode[] são tipados como WideChar tanto em TPdf quanto em TPdfView, e ampliar esse tipo de retorno para carregar um ponto de código completo quebraria todo chamador existente que espera um glifo por índice significando um único valor de 16 bits. U+FFFD é o próprio placeholder designado pelo Padrão Unicode exatamente para essa situação, de modo que um chamador que verifica por ele recebe um sinal definido e documentado, em vez de dado silenciosamente errado. Um caso de fronteira que vale a pena conhecer: U+FFFD também é um caractere legítimo por direito próprio, de modo que, no raro documento que já contém um glifo de caractere de substituição genuíno, esse índice é indistinguível de um caractere astral truncado apenas pelo valor
Como você extrai texto de emoji e da Extensão B CJK corretamente em Delphi?
Chame Text em vez de percorrer Character[] sempre que o conteúdo textual real importar, porque Text lê por meio de FPDFText_GetText e retorna uma WString completa com pares substitutos apropriados para cada caractere de plano astral na faixa, em vez de um valor de largura fixa por índice. Pdf.Text(0, MaxInt), ou o atalho Pdf.Text, extrai uma página inteira corretamente em uma chamada, e Pdf.Text(StartIndex, Count) puxa uma faixa menor da mesma forma. Character[] ainda ganha seu lugar quando você só precisa de dados de posição, fonte ou flag em um índice e nunca toca no próprio ponto de código — CharacterOrigin[], FontSize[], e CharacterMapError[] não se importam se o glifo subjacente era astral
function ExtractLineSafely(Pdf: TPdf): WString;
var
I: Integer;
begin
Result := '';
for I := 0 to Pdf.CharacterCount - 1 do
if not (Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I]) then
Result := Result + Pdf.Text(I, 1); // full code point, never a truncated WideChar
end;
A verificação de pular-gerado-e-não-mapeado nesse loop é o mesmo padrão usado para extração de texto simples em extraindo texto de documentos PDF com o Componente PDFium; a única mudança é a última linha, que troca um append direto de Character[I] por uma chamada de um índice em Text, de modo que caracteres astrais chegam como pares substitutos completos, em vez de placeholders de substituição
Onde isso de fato morde: exportações de chat, nomes pessoais e fontes CJK embutidas
Emojis aparecem em qualquer lugar onde um PDF captura comunicação informal: logs de chat exportados, despejos de avaliação de app store, transcrições de sistema de tickets salvas em PDF para um arquivo de conformidade. A Extensão B CJK aparece em um lugar mais restrito, mas de risco mais alto, nomes pessoais e de lugares, porque registros familiares japoneses, registros de domicílio chineses, e documentos de identidade taiwaneses são fontes clássicas de caracteres que nunca entraram no bloco CJK comum. Um pipeline de folha de pagamento ou verificação de identidade que extrai nomes de papelada governamental digitalizada é exatamente o tipo de carga de trabalho onde um caractere silenciosamente corrompido vira uma correspondência falhada, em vez de um defeito cosmético
Ideogramas CJK raros também tendem a viajar com problemas de fonte, não apenas de codificação, porque uma fonte precisa carregar um glifo para um ponto de código na faixa U+20000 antes de qualquer coisa poder renderizar, e poucas fontes de sistema instaladas o fazem. Quem já estiver percorrendo FontIsEmbedded[] por caractere da forma como lendo propriedades de fonte de PDF com o Componente PDFium descreve deveria verificar o mesmo índice para ambos os problemas juntos: um índice que retorna U+FFFD de Character[] e reporta uma fonte não embutida é um documento que não vai nem extrair nem imprimir aquele caractere corretamente, e a correção pertence rio acima, em como o PDF foi produzido, não no seu código de extração
As propriedades Character[], Charcode[] e Text descritas aqui fazem parte do Componente PDFium padrão para Delphi e C++Builder