Articol tehnic

Emoji și caractere CJK rup WideChar în PDFium în Delphi

Extrageți emoji sau un nume de registru familial japonez dintr-un PDF ca text, iar ieșirea arată o casetă, un semn de întrebare, sau nimic deloc unde ar trebui să fie caracterul. Proprietatea Character[] a componentei PDFium este de obicei motivul: citește fiecare glifă prin FPDFText_GetUnicode, care returnează un punct de cod Unicode complet ca o valoare fără semn pe 32 de biți, apoi îl expune Delphi ca un singur WideChar pe 16 biți. Orice punct de cod peste U+FFFF nu poate face acea călătorie într-o singură bucată, iar corupția nu apare niciodată în timp ce priviți pagina randată, pentru că randarea și extracția de text rulează prin căi de cod separate în PDFium — un document poate afișa emoji-urile sale perfect și totuși vă poate da gunoi în momentul în care citiți Character[] într-o buclă și construiți un șir din el

Planul Multilingv de Bază și de ce WideChar se oprește la U+FFFF

WideChar din Delphi este un tip pe 16 biți care poate ține doar o unitate de cod UTF-16. Planul Multilingv de Bază al Unicode, intervalul U+0000 la U+FFFF, se încadrează exact în asta, motiv pentru care latina, chirilica, greaca și blocul comun CJK Unified Ideographs fac toate drumul dus-întors printr-un singur WideChar fără incident. Două familii de caractere cad de obicei în afara acestuia în documente reale: emoji, multe dintre ele în blocul Emoticons începând la U+1F600, și ideograme CJK rare din CJK Unified Ideographs Extension B, intervalul U+20000 până la U+2A6DF rezervat pentru caractere chinezești, japoneze și coreene mai puțin comune, incluzând multe nume personale și de loc. UTF-16 gestionează orice peste U+FFFF cu o pereche surogat — două unități de cod pe 16 biți, un surogat înalt în intervalul $D800 până la $DBFF urmat de un surogat jos în $DC00 până la $DFFF, care împreună codifică un punct de cod — iar matematica din spatele acelei împerecheri este suficient de fixă încât să fie demonstrată direct în 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;

Alimentați U+1F600, emoji-ul cu față zâmbitoare, prin acea funcție, iar rezultatul este un surogat înalt de $D83D și un surogat jos de $DE00, două valori pe 16 biți, nu una. Niciuna din jumătăți nu înseamnă nimic de una singură; un $D83D singuratic așezat într-un șir fără niciun $DE00 în spatele lui este un surogat atârnat, iar majoritatea codului de manipulare a textului care întâlnește unul fie îl elimină, fie substituie o glifă de înlocuire, fie ridică o eroare

De ce returnează FPDFText_GetUnicode o valoare pe care Character[] nu o poate ține?

FPDFText_GetUnicode returnează un LongWord, o valoare completă pe 32 de biți, pentru că codificarea de text PDF poartă deja valoarea scalară Unicode completă pentru fiecare glifă. CMap-ul ToUnicode al unui PDF mapează coduri de caracter la text Unicode, iar când o glifă reprezintă ceea ce este numit informal un caracter de plan astral — orice dincolo de Planul Multilingv de Bază — acea mapare este un punct de cod complet, nu un fragment pe 16 biți. PDFium îl decodifică înapoi la o valoare scalară intern și îl returnează peste granița DLL prin FPDFText_GetUnicode, iar acea graniță este exact unde o valoare pe 32 de biți trebuie să devină ceva ce o proprietate Delphi poate returna codului dvs

Implementarea evidentă este WideChar(FPDFText_GetUnicode(TextPage, Index)), și este de asemenea cea greșită. O conversie forțată (hard cast) dintr-o valoare pe 32 de biți într-un tip pe 16 biți păstrează doar cei 16 biți inferiori și aruncă restul silențios, fără nicio excepție și nicio verificare de interval. Pentru U+1F600 asta înseamnă păstrarea $F600 și pierderea faptului că valoarea reală a fost vreodată peste U+FFFF, ceea ce produce o unitate de cod care nu este nici măcar un surogat atârnat valid, doar un caracter din Planul Multilingv de Bază fără legătură, care se întâmplă să împartă acei biți inferiori. Concatenați câteva mii din acestea într-un șir, iar codul din aval nu mai are nicio modalitate de a distinge un caracter corupt de unul legitim

Ce returnează Character[] și Charcode[] pentru puncte de cod din planul astral acum

Proprietățile Character[] și Charcode[] ale componentei PDFium returnează U+FFFD, caracterul de înlocuire Unicode, ori de câte ori punctul de cod de bază depășește U+FFFF, în loc să-l trunchieze silențios. Acea gardă stă direct în interiorul getter-ului de proprietate din spatele 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;

Returnarea U+FFFD în loc de un fragment trunchiat este o soluție deliberată, restrânsă, nu o reproiectare. Character[] și Charcode[] sunt tipate ca WideChar atât pe TPdf, cât și pe TPdfView, iar lărgirea acelui tip de retur pentru a purta un punct de cod complet ar rupe fiecare apelant existent care se așteaptă ca o glifă per index să însemne o singură valoare pe 16 biți. U+FFFD este propriul substituent desemnat al Standardului Unicode exact pentru această situație, așa că un apelant care verifică asta primește un semnal definit, documentat, în loc de date silențios greșite. Un caz limită care merită cunoscut: U+FFFD este de asemenea un caracter legitim în sine, așa că pe documentul rar care conține deja o glifă autentică de caracter de înlocuire, acel index este indistinguibil de un caracter astral trunchiat doar prin valoare

Cum extrageți corect emoji și text CJK Extension B în Delphi?

Apelați Text în loc să parcurgeți Character[] ori de câte ori conținutul textual efectiv contează, pentru că Text citește prin FPDFText_GetText și returnează un WString complet, cu perechi surogat corecte pentru fiecare caracter din planul astral din interval, în loc de o valoare de lățime fixă per index. Pdf.Text(0, MaxInt), sau prescurtarea Pdf.Text, extrage o pagină întreagă corect într-un singur apel, iar Pdf.Text(StartIndex, Count) extrage un interval mai mic la fel. Character[] încă își câștigă locul atunci când aveți nevoie doar de date de poziție, font, sau steag la un index și nu atingeți niciodată punctul de cod în sine — CharacterOrigin[], FontSize[] și CharacterMapError[] nu le pasă dacă glifa de bază a fost 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;

Verificarea de omitere-generat-și-nemapat din acea buclă este același tipar folosit pentru extracția de text simplu în extragerea de text din documente PDF cu componenta PDFium; singura schimbare este ultima linie, care schimbă un adăugat direct Character[I] pentru un apel cu un index în Text, astfel încât caracterele astrale sosesc ca perechi surogat complete, în loc de substituenți de înlocuire

Unde mușcă asta efectiv: exporturi de chat, nume personale și fonturi CJK încorporate

Emoji apar oriunde un PDF captează comunicare informală: jurnale de chat exportate, dump-uri de recenzii din app store, transcrieri de sistem de ticketing salvate ca PDF pentru o arhivă de conformitate. CJK Extension B apare într-un loc mai restrâns, dar cu miză mai mare, nume personale și de loc, pentru că registrele familiale japoneze, evidențele de înregistrare a gospodăriilor chinezești și documentele de identitate taiwaneze sunt surse clasice de caractere care nu au ajuns niciodată în blocul CJK comun. O conductă de salarizare sau verificare de identitate care extrage nume din documente guvernamentale scanate este exact genul de sarcină de lucru unde un caracter deteriorat silențios se transformă într-o potrivire eșuată, nu într-un defect cosmetic

Ideogramele CJK rare de asemenea tind să călătorească cu probleme de font, nu doar de codificare, pentru că un font trebuie să poarte o glifă pentru un punct de cod din intervalul U+20000 înainte ca orice să se poată reda deloc, iar puține fonturi de sistem instalate o fac. Oricine deja parcurge FontIsEmbedded[] per caracter așa cum descrie citirea proprietăților de font PDF cu componenta PDFium ar trebui să verifice același index pentru ambele probleme împreună: un index care returnează U+FFFD de la Character[] și raportează un font neîncorporat este un document care nu va extrage și nici nu va tipări corect acel caracter, iar soluția aparține în amonte modului în care a fost produs PDF-ul, nu codului dvs. de extracție

Proprietățile Character[], Charcode[] și Text descrise aici fac parte din componenta PDFium standard pentru Delphi și C++Builder