Vytiahnite emoji alebo japonské meno z rodinného registra z PDF ako text, a výstup zobrazí štvorček, otáznik, alebo úplne nič tam, kde by mal byť znak. Za tým je zvyčajne vlastnosť Character[] komponentu PDFium: číta každý glyf cez FPDFText_GetUnicode, ktorá vracia plný bod kódu Unicode ako 32-bitovú hodnotu bez znamienka, a potom ju sprístupňuje Delphi ako jediný 16-bitový WideChar. Akýkoľvek bod kódu za U+FFFF nedokáže túto cestu prejsť vcelku, a poškodenie sa nikdy neprejaví, kým sa pozeráte na vykreslenú stránku, pretože vykresľovanie a extrakcia textu v PDFium bežia cez oddelené cesty kódu — dokument môže svoje emoji zobrazovať dokonale a stále vám odovzdať nezmysel vo chvíli, keď v slučke prečítate Character[] a postavíte z toho reťazec
Základná viacjazyčná rovina a prečo WideChar končí na U+FFFF
WideChar v Delphi je 16-bitový typ, ktorý dokáže držať iba jednu jednotku kódu UTF-16. Základná viacjazyčná rovina Unicode, rozsah U+0000 až U+FFFF, sa doňho zmestí presne, čo je dôvod, prečo latinka, cyrilika, gréčtina, a bežný blok CJK Unified Ideographs prechádzajú cez jediný WideChar bez problémov. Dve rodiny znakov v skutočných dokumentoch bežne padajú mimo tejto roviny: emoji, mnohé z nich v bloku Emoticons začínajúcom na U+1F600, a vzácne ideogramy CJK z CJK Unified Ideographs Extension B, rozsahu U+20000 až U+2A6DF vyhradeného pre menej bežné čínske, japonské, a kórejské znaky vrátane mnohých osobných mien a názvov miest. UTF-16 zvláda čokoľvek nad U+FFFF pomocou náhradnej dvojice — dve 16-bitové jednotky kódu, vysoký náhradník v rozsahu $D800 až $DBFF nasledovaný nízkym náhradníkom v $DC00 až $DFFF, ktoré spolu kódujú jeden bod kódu — a matematika za týmto párovaním je natoľko pevná, že sa dá priamo demonštrovať v Pascale
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;
Nakŕmte touto funkciou U+1F600, emoji usmievavej tváre, a výsledkom je vysoký náhradník $D83D a nízky náhradník $DE00, dve 16-bitové hodnoty, nie jedna. Ani jedna polovica sama osebe nič neznamená; osamotený $D83D sediaci v reťazci bez $DE00 za ním je visiaci náhradník, a väčšina kódu na spracovanie textu, ktorý naň narazí, ho buď zahodí, nahradí náhradným glyfom, alebo vyvolá chybu
Prečo FPDFText_GetUnicode vracia hodnotu, ktorú Character[] nedokáže udržať?
FPDFText_GetUnicode vracia LongWord, plnú 32-bitovú hodnotu, pretože kódovanie textu PDF už nesie kompletnú skalárnu hodnotu Unicode pre každý glyf. CMap ToUnicode v PDF mapuje kódy znakov na text Unicode, a keď glyf reprezentuje to, čo sa neformálne nazýva znak astrálnej roviny — čokoľvek za základnou viacjazyčnou rovinou — toto mapovanie je plný bod kódu, nie 16-bitový fragment. PDFium ho interne dekóduje späť na skalárnu hodnotu a vracia ho cez hranicu DLL prostredníctvom FPDFText_GetUnicode, a práve táto hranica je miesto, kde sa 32-bitová hodnota musí stať niečím, čo vlastnosť Delphi dokáže vrátiť vášmu kódu
Zjavná implementácia je WideChar(FPDFText_GetUnicode(TextPage, Index)), a je zároveň nesprávna. Tvrdý cast z 32-bitovej hodnoty do 16-bitového typu ponechá iba spodných 16 bitov a zvyšok ticho zahodí, bez akejkoľvek výnimky a bez kontroly rozsahu. Pre U+1F600 to znamená ponechať $F600 a stratiť skutočnosť, že skutočná hodnota kedy bola nad U+FFFF, čo vyprodukuje jednotku kódu, ktorá nie je ani platný visiaci náhradník, iba nesúvisiaci znak základnej viacjazyčnej roviny, ktorý náhodou zdieľa tieto spodné bity. Zreťazte niekoľko tisíc takýchto do reťazca a downstream kód už nemá žiadny spôsob, ako rozlíšiť poškodený znak od legitímneho
Čo teraz Character[] a Charcode[] vracajú pre body kódu z astrálnej roviny
Vlastnosti Character[] a Charcode[] komponentu PDFium vracajú U+FFFD, náhradný znak Unicode, vždy, keď podkladový bod kódu prekročí U+FFFF, namiesto toho, aby ho ticho orezali. Táto stráž sedí priamo vnútri gettera vlastnosti za 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;
Vrátenie U+FFFD namiesto oreznutého fragmentu je zámerná, úzka oprava, nie prepracovanie. Character[] a Charcode[] sú typované ako WideChar na oboch TPdf aj TPdfView, a rozšírenie tohto návratového typu tak, aby niesol plný bod kódu, by pokazilo každého existujúceho volajúceho, ktorý očakáva, že jeden glyf na index znamená jednu 16-bitovú hodnotu. U+FFFD je vlastný, samotným štandardom Unicode určený zástupný znak presne pre túto situáciu, takže volajúci, ktorý ho kontroluje, dostane definovaný, zdokumentovaný signál namiesto ticho nesprávnych dát. Jeden hraničný prípad sa oplatí poznať: U+FFFD je aj samo osebe legitímny znak, takže na vzácnom dokumente, ktorý už obsahuje skutočný glyf náhradného znaku, je tento index od orezaného astrálneho znaku podľa samotnej hodnoty nerozlíšiteľný
Ako správne extrahovať emoji a text CJK Extension B v Delphi?
Vždy, keď záleží na skutočnom textovom obsahu, zavolajte Text namiesto prechádzania Character[], pretože Text číta cez FPDFText_GetText a vracia plné WString so správnymi náhradnými dvojicami pre každý znak astrálnej roviny v rozsahu, namiesto jednej hodnoty pevnej šírky na index. Pdf.Text(0, MaxInt), alebo skratka Pdf.Text, extrahuje celú stránku správne v jednom volaní, a Pdf.Text(StartIndex, Count) vytiahne menší rozsah rovnakým spôsobom. Character[] si stále zaslúži svoje miesto vtedy, keď na indexe potrebujete iba pozíciu, písmo, alebo príznakové dáta a nikdy sa nedotýkate samotného bodu kódu — CharacterOrigin[], FontSize[], a CharacterMapError[] je jedno, či bol podkladový glyf astrálny
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;
Kontrola preskočiť-generované-a-nemapované v tejto slučke je ten istý vzor, aký sa používa pre extrakciu obyčajného textu v extrakcii textu z dokumentov PDF s komponentom PDFium; jediná zmena je posledný riadok, ktorý vymieňa priame pripojenie Character[I] za jednoindexové volanie do Text, aby astrálne znaky prišli ako kompletné náhradné dvojice namiesto náhradných zástupných znakov
Kde to skutočne hryzie: exporty chatu, osobné mená, a vložené písma CJK
Emoji sa objavujú všade, kde PDF zachytáva neformálnu komunikáciu: exportované chatové logy, výpisy recenzií z app-store, prepisy z tiketovacieho systému uložené do PDF pre compliance archív. CJK Extension B sa objavuje na užšom, no vyššie rizikovom mieste, v osobných menách a názvoch miest, pretože japonské rodinné registre, čínske záznamy registrácie domácností, a taiwanské identifikačné dokumenty sú klasické zdroje znakov, ktoré sa nikdy nedostali do bežného bloku CJK. Mzdová alebo overovacia pipeline identity, ktorá extrahuje mená z naskenovaných vládnych dokumentov, je presne ten typ záťaže, kde sa ticho zmrzačený znak zmení na neúspešnú zhodu namiesto kozmetickej chyby
Vzácne ideogramy CJK majú tiež tendenciu cestovať s problémami písma, nielen kódovania, pretože písmo musí niesť glyf pre bod kódu v rozsahu U+20000 skôr, než sa vôbec dá čokoľvek vykresliť, a málokedy to robia nainštalované systémové písma. Ktokoľvek, kto už prechádza FontIsEmbedded[] na znak spôsobom, aký opisuje čítanie vlastností písiem PDF s komponentom PDFium, by mal skontrolovať ten istý index na oba problémy naraz: index, ktorý z Character[] vráti U+FFFD a zároveň hlási nevložené písmo, je dokument, ktorý tento znak nedokáže ani extrahovať, ani správne vytlačiť, a oprava patrí vyššie prúdu do toho, ako bolo PDF vyprodukované, nie do vášho extrakčného kódu
Vlastnosti Character[], Charcode[], a Text opísané tu sú súčasťou štandardného komponentu PDFium pre Delphi a C++Builder