Extrayez un emoji ou un nom de registre familial japonais d'un PDF sous forme de texte, et la sortie montre un rectangle, un point d'interrogation, ou rien du tout là où le caractère devrait être. La propriété Character[] du composant PDFium en est habituellement la cause : elle lit chaque glyphe via FPDFText_GetUnicode, qui renvoie un point de code Unicode complet sous forme de valeur non signée 32 bits, puis l'expose à Delphi comme un unique WideChar de 16 bits. Tout point de code au-delà de U+FFFF ne peut pas faire ce trajet en une seule pièce, et la corruption n'apparaît jamais pendant que vous regardez la page rendue, car le rendu et l'extraction de texte empruntent des chemins de code séparés dans PDFium — un document peut afficher ses emoji parfaitement et tout de même vous renvoyer du charabia dès l'instant où vous lisez Character[] dans une boucle et construisez une chaîne à partir de cela
Le plan multilingue de base et pourquoi WideChar s'arrête à U+FFFF
Le WideChar de Delphi est un type 16 bits qui ne peut contenir qu'une seule unité de code UTF-16. Le plan multilingue de base d'Unicode, la plage U+0000 à U+FFFF, tient exactement là-dedans, ce qui explique pourquoi le latin, le cyrillique, le grec, et le bloc commun des idéogrammes unifiés CJK font tous l'aller-retour via un seul WideChar sans incident. Deux familles de caractères tombent couramment en dehors dans les documents réels : les emoji, beaucoup d'entre eux dans le bloc Emoticons commençant à U+1F600, et les idéogrammes CJK rares de l'extension B des idéogrammes unifiés CJK, la plage U+20000 à U+2A6DF réservée aux caractères chinois, japonais, et coréens moins courants, y compris de nombreux noms de personnes et de lieux. UTF-16 gère tout ce qui est au-dessus de U+FFFF avec une paire de substitution — deux unités de code 16 bits, une substitution haute dans la plage $D800 à $DBFF suivie d'une substitution basse dans $DC00 à $DFFF, qui ensemble encodent un point de code — et les mathématiques derrière cet appariement sont assez fixes pour être démontrées directement 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;
Injectez U+1F600, l'emoji visage souriant, dans cette fonction et le résultat est une substitution haute de $D83D et une substitution basse de $DE00, deux valeurs 16 bits, pas une. Aucune des deux moitiés ne signifie quoi que ce soit seule ; un $D83D isolé assis dans une chaîne sans $DE00 derrière lui est une substitution pendante, et la plupart du code de traitement de texte qui en rencontre une l'abandonne, la remplace par un glyphe de substitution, ou lève une erreur
Pourquoi FPDFText_GetUnicode renvoie-t-il une valeur que Character[] ne peut pas contenir ?
FPDFText_GetUnicode renvoie un LongWord, une valeur 32 bits complète, car l'encodage de texte PDF porte déjà la valeur scalaire Unicode complète pour chaque glyphe. La CMap ToUnicode d'un PDF fait correspondre les codes de caractère au texte Unicode, et lorsqu'un glyphe représente ce qu'on appelle familièrement un caractère de plan astral — tout ce qui dépasse le plan multilingue de base — cette correspondance est un point de code complet, pas un fragment 16 bits. PDFium le décode en interne en valeur scalaire et le renvoie à travers la frontière de la DLL via FPDFText_GetUnicode, et cette frontière est exactement là où une valeur 32 bits doit devenir quelque chose qu'une propriété Delphi peut restituer à votre code
L'implémentation évidente est WideChar(FPDFText_GetUnicode(TextPage, Index)), et c'est aussi la mauvaise. Une conversion forcée d'une valeur 32 bits vers un type 16 bits ne conserve que les 16 bits de poids faible et jette silencieusement le reste, sans exception ni vérification de plage. Pour U+1F600, cela signifie conserver $F600 et perdre le fait que la valeur réelle ait jamais dépassé U+FFFF, ce qui produit une unité de code qui n'est même pas une substitution pendante valide, juste un caractère du plan multilingue de base sans rapport qui se trouve partager ces bits de poids faible. Concaténez quelques milliers de ceux-ci dans une chaîne et le code en aval n'a plus aucun moyen de distinguer un caractère corrompu d'un légitime
Ce que renvoient désormais Character[] et Charcode[] pour les points de code du plan astral
Les propriétés Character[] et Charcode[] du composant PDFium renvoient U+FFFD, le caractère de remplacement Unicode, chaque fois que le point de code sous-jacent dépasse U+FFFF, plutôt que de le tronquer silencieusement. Cette protection se trouve directement à l'intérieur du getter de propriété derrière 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;
Renvoyer U+FFFD plutôt qu'un fragment tronqué est une correction délibérée et étroite plutôt qu'une refonte. Character[] et Charcode[] sont typées WideChar à la fois sur TPdf et TPdfView, et élargir ce type de retour pour porter un point de code complet casserait chaque appelant existant qui attend qu'un glyphe par index signifie une valeur 16 bits. U+FFFD est le propre substitut désigné par le standard Unicode précisément pour cette situation, si bien qu'un appelant qui le vérifie obtient un signal défini et documenté au lieu de données silencieusement fausses. Un cas limite qui mérite d'être connu : U+FFFD est aussi un caractère légitime en soi, si bien que sur le rare document qui contient déjà un véritable glyphe caractère-de-remplacement, cet index est indiscernable d'un caractère astral tronqué par la seule valeur
Comment extraire correctement les emoji et le texte de l'extension B CJK en Delphi ?
Appelez Text plutôt que de parcourir Character[] chaque fois que le contenu textuel réel importe, car Text lit via FPDFText_GetText et renvoie un WString complet avec des paires de substitution correctes pour chaque caractère de plan astral dans la plage, plutôt qu'une valeur de largeur fixe par index. Pdf.Text(0, MaxInt), ou le raccourci Pdf.Text, extrait une page entière correctement en un seul appel, et Pdf.Text(StartIndex, Count) extrait une plage plus petite de la même façon. Character[] mérite toujours sa place lorsque vous n'avez besoin que de la position, de la police, ou des données de drapeau à un index et ne touchez jamais au point de code lui-même — CharacterOrigin[], FontSize[], et CharacterMapError[] ne se soucient pas de savoir si le glyphe sous-jacent était 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 vérification saut-généré-et-non-mappé dans cette boucle est le même schéma utilisé pour l'extraction de texte pur dans l'extraction de texte de documents PDF avec le composant PDFium ; le seul changement est la dernière ligne, qui échange un ajout direct de Character[I] contre un appel à un seul index dans Text afin que les caractères astraux arrivent sous forme de paires de substitution complètes plutôt que de substituts de remplacement
Où cela mord réellement : exports de conversation, noms personnels, et polices CJK intégrées
Les emoji apparaissent partout où un PDF capture une communication informelle : journaux de conversation exportés, extraits d'avis de magasin d'applications, transcriptions de système de billetterie enregistrées en PDF pour une archive de conformité. L'extension B CJK apparaît dans un endroit plus étroit mais à enjeux plus élevés, les noms de personnes et de lieux, car les registres familiaux japonais, les registres d'enregistrement des ménages chinois, et les documents d'identité taïwanais sont des sources classiques de caractères qui n'ont jamais fait leur entrée dans le bloc CJK commun. Un pipeline de paie ou de vérification d'identité qui extrait des noms de documents administratifs scannés est exactement le genre de charge de travail où un caractère silencieusement dénaturé se transforme en échec de correspondance plutôt qu'en simple glitch cosmétique
Les idéogrammes CJK rares ont aussi tendance à voyager avec des problèmes de police, pas seulement d'encodage, car une police doit porter un glyphe pour un point de code de la plage U+20000 avant que quoi que ce soit puisse se rendre du tout, et peu de polices système installées le font. Quiconque parcourt déjà FontIsEmbedded[] par caractère de la façon décrite dans la lecture des propriétés de police PDF avec le composant PDFium devrait vérifier le même index pour les deux problèmes ensemble : un index qui renvoie U+FFFD depuis Character[] et signale une police non intégrée est un document qui n'extraira ni n'imprimera correctement ce caractère, et la correction se trouve en amont dans la façon dont le PDF a été produit, pas dans votre code d'extraction
Les propriétés Character[], Charcode[], et Text décrites ici font partie du composant PDFium standard pour Delphi et C++Builder