Extraiga un emoji o un nombre de registro familiar japonés de un PDF como texto, y la salida muestra un cuadro, un signo de interrogación, o nada en absoluto donde debería estar el carácter. La propiedad Character[] del componente PDFium suele ser la razón: lee cada glifo mediante FPDFText_GetUnicode, que devuelve un punto de código Unicode completo como un valor sin signo de 32 bits, y luego lo expone a Delphi como un único WideChar de 16 bits. Ningún punto de código más allá de U+FFFF puede hacer ese viaje en una sola pieza, y la corrupción nunca aparece mientras se mira la página renderizada, porque el renderizado y la extracción de texto pasan por rutas de código separadas en PDFium; un documento puede mostrar su emoji perfectamente y aun así entregarle basura en el momento en que lee Character[] en un bucle y construye una cadena con eso
El Plano Multilingüe Básico y por qué WideChar se detiene en U+FFFF
El WideChar de Delphi es un tipo de 16 bits que solo puede contener una unidad de código UTF-16. El Plano Multilingüe Básico de Unicode, el rango U+0000 a U+FFFF, cabe exactamente ahí, que es por qué el latín, el cirílico, el griego, y el bloque común de Ideogramas CJK Unificados hacen todos el viaje de ida y vuelta a través de un solo WideChar sin incidentes. Dos familias de caracteres rutinariamente caen fuera de ese rango en documentos reales: los emoji, muchos de ellos en el bloque de Emoticonos que empieza en U+1F600, y los ideogramas CJK raros de la Extensión B de Ideogramas CJK Unificados, el rango U+20000 a U+2A6DF reservado para caracteres chinos, japoneses, y coreanos menos comunes, incluidos muchos nombres personales y de lugar. UTF-16 maneja cualquier cosa por encima de U+FFFF con un par sustituto (surrogate pair) —dos unidades de código de 16 bits, un sustituto alto en el rango $D800 a $DBFF seguido de un sustituto bajo en $DC00 a $DFFF, que juntos codifican un punto de código— y la matemática detrás de ese emparejamiento es lo bastante fija como para demostrarla directamente en 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;
Entréguele U+1F600, el emoji de cara sonriente, a esa función y el resultado es un sustituto alto de $D83D y un sustituto bajo de $DE00, dos valores de 16 bits, no uno. Ninguna de las dos mitades significa nada por sí sola; un $D83D solitario sentado en una cadena sin ningún $DE00 detrás es un sustituto colgante, y la mayoría del código de manejo de texto que se encuentra con uno o bien lo descarta, lo sustituye por un glifo de reemplazo, o lanza un error
¿Por qué FPDFText_GetUnicode devuelve un valor que Character[] no puede contener?
FPDFText_GetUnicode devuelve un LongWord, un valor completo de 32 bits, porque la codificación de texto de PDF ya lleva el valor escalar Unicode completo para cada glifo. El CMap ToUnicode de un PDF mapea códigos de carácter a texto Unicode, y cuando un glifo representa lo que informalmente se llama un carácter de plano astral —cualquier cosa más allá del Plano Multilingüe Básico— ese mapeo es un punto de código completo, no un fragmento de 16 bits. PDFium lo decodifica internamente de vuelta a un valor escalar y lo devuelve a través del límite de la DLL mediante FPDFText_GetUnicode, y ese límite es exactamente donde un valor de 32 bits tiene que convertirse en algo que una propiedad Delphi pueda devolver a su código
La implementación obvia es WideChar(FPDFText_GetUnicode(TextPage, Index)), y también es la incorrecta. Un cast forzado de un valor de 32 bits a un tipo de 16 bits conserva solo los 16 bits bajos y descarta el resto silenciosamente, sin ninguna excepción y sin ninguna comprobación de rango. Para U+1F600 eso significa conservar $F600 y perder el hecho de que el valor verdadero alguna vez estuvo por encima de U+FFFF, lo que produce una unidad de código que ni siquiera es un sustituto colgante válido, solo un carácter no relacionado del Plano Multilingüe Básico que resulta compartir esos bits bajos. Concatene unos pocos miles de esos en una cadena y el código aguas abajo ya no tiene forma de distinguir un carácter corrompido de uno legítimo
Qué devuelven ahora Character[] y Charcode[] para puntos de código de plano astral
Las propiedades Character[] y Charcode[] del componente PDFium devuelven U+FFFD, el carácter de reemplazo Unicode, cada vez que el punto de código subyacente excede U+FFFF, en lugar de truncarlo silenciosamente. Esa protección está directamente dentro del getter de propiedad detrá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;
Devolver U+FFFD en lugar de un fragmento truncado es una corrección deliberada y acotada en lugar de un rediseño. Character[] y Charcode[] están tipadas como WideChar tanto en TPdf como en TPdfView, y ampliar ese tipo de retorno para llevar un punto de código completo rompería cada llamador existente que espera que un glifo por índice signifique un valor de 16 bits. U+FFFD es el propio marcador de posición designado por el Estándar Unicode exactamente para esta situación, así que un llamador que lo comprueba recibe una señal definida y documentada en lugar de datos silenciosamente equivocados. Un caso límite que vale la pena conocer: U+FFFD también es un carácter legítimo por derecho propio, así que en el documento raro que ya contiene un glifo de carácter de reemplazo genuino, ese índice es indistinguible por valor solo de un carácter astral truncado
¿Cómo se extrae correctamente en Delphi texto con emoji y CJK Extensión B?
Llame a Text en lugar de recorrer Character[] cada vez que importe el contenido textual real, porque Text lee mediante FPDFText_GetText y devuelve un WString completo con pares sustitutos apropiados para cada carácter de plano astral en rango, en lugar de un valor de ancho fijo por índice. Pdf.Text(0, MaxInt), o el atajo Pdf.Text, extrae una página entera correctamente en una llamada, y Pdf.Text(StartIndex, Count) extrae un rango más pequeño de la misma manera. Character[] sigue ganándose su lugar cuando solo se necesitan datos de posición, fuente, o bandera en un índice y nunca se toca el punto de código en sí —CharacterOrigin[], FontSize[], y CharacterMapError[] no les importa si el glifo subyacente 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;
La comprobación de omitir-generado-y-sin-mapear en ese bucle es el mismo patrón usado para la extracción de texto plano en extraer texto de documentos PDF con el componente PDFium; el único cambio es la última línea, que cambia un anexado directo de Character[I] por una llamada de un índice a Text para que los caracteres astrales lleguen como pares sustitutos completos en lugar de marcadores de reemplazo
Dónde esto realmente muerde: exportaciones de chat, nombres personales, y fuentes CJK incrustadas
Los emoji aparecen dondequiera que un PDF capture comunicación informal: registros de chat exportados, volcados de reseñas de tiendas de aplicaciones, transcripciones de sistemas de tickets guardadas a PDF para un archivo de cumplimiento. La Extensión B de CJK aparece en un lugar más estrecho pero de mayor riesgo, nombres personales y de lugar, porque los registros familiares japoneses, los registros de hogar chinos, y los documentos de identidad taiwaneses son fuentes clásicas de caracteres que nunca llegaron al bloque CJK común. Un pipeline de nómina o verificación de identidad que extrae nombres de papeleo gubernamental escaneado es exactamente el tipo de carga de trabajo donde un carácter silenciosamente corrompido se convierte en una coincidencia fallida en lugar de un fallo cosmético
Los ideogramas CJK raros también tienden a viajar con problemas de fuente, no solo de codificación, porque una fuente tiene que llevar un glifo para un punto de código del rango U+20000 antes de que algo pueda renderizarse en absoluto, y pocas fuentes de sistema instaladas lo hacen. Cualquiera que ya esté recorriendo FontIsEmbedded[] por carácter de la forma en que describe leer propiedades de fuente PDF con el componente PDFium debería comprobar el mismo índice para ambos problemas juntos: un índice que devuelve U+FFFD desde Character[] y reporta una fuente no incrustada es un documento que no extraerá ni imprimirá correctamente ese carácter, y la corrección pertenece aguas arriba, en cómo se produjo el PDF, no en su código de extracción
Las propiedades Character[], Charcode[], y Text descritas aquí son parte del componente PDFium estándar para Delphi y C++Builder