Vytáhněte emoji nebo jméno z japonského rodinného rejstříku z PDF jako text, a výstup ukáže rámeček, otazník, nebo úplně nic tam, kde měl znak být. Vlastnost Character[] v PDFium Component je obvykle důvod: čte každou glyfu přes FPDFText_GetUnicode, která vrací plný bod kódu Unicode jako neznaménkovou 32bitovou hodnotu, a pak jej vystaví Delphi jako jediný 16bitový WideChar. Jakýkoli bod kódu za U+FFFF nemůže tuto cestu urazit vcelku, a poškození se nikdy neprojeví, zatímco se díváte na vykreslenou stránku, protože vykreslování a extrakce textu v PDFium běží přes oddělené cesty kódu — dokument dokáže svá emoji zobrazovat dokonale a přesto vám podat odpad v okamžiku, kdy si přečtete Character[] ve smyčce a postavíte z toho řetězec
Základní vícejazyčná rovina a proč WideChar končí na U+FFFF
WideChar v Delphi je 16bitový typ, který dokáže držet jen jednu jednotku kódu UTF-16. Základní vícejazyčná rovina Unicode, rozsah U+0000 až U+FFFF, se do toho vejde přesně, a proto latinka, cyrilice, řečtina a běžný blok CJK Unified Ideographs projdou obousměrným převodem přes jediný WideChar bez incidentu. Dvě rodiny znaků ve skutečných dokumentech rutinně vypadnou mimo tento rozsah: emoji, mnohé z nich v bloku Emoticons začínajícím na U+1F600, a vzácné ideografy CJK z bloku CJK Unified Ideographs Extension B, rozsah U+20000 až U+2A6DF vyhrazený pro méně běžné čínské, japonské a korejské znaky, včetně mnoha osobních a místních jmen. UTF-16 zvládá cokoli nad U+FFFF pomocí náhradní dvojice (surrogate pair) — dvě 16bitové jednotky kódu, vysoký náhradník v rozsahu $D800 až $DBFF následovaný nízkým náhradníkem v $DC00 až $DFFF, které společně kódují jeden bod kódu — a matematika za tímto párováním je dost pevná na to, aby se dala přímo předvést v Pascalu
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;
Podejte U+1F600, emoji usmívající se tváře, přes tuto funkci a výsledkem je vysoký náhradník $D83D a nízký náhradník $DE00, dvě 16bitové hodnoty, ne jedna. Ani jedna polovina neznamená nic sama o sobě; osamocené $D83D sedící v řetězci bez $DE00 za sebou je visící náhradník, a většina kódu pro práci s textem, který na něj narazí, jej buď zahodí, nahradí náhradní glyfou, nebo vyvolá chybu
Proč FPDFText_GetUnicode vrací hodnotu, kterou Character[] nedokáže pojmout?
FPDFText_GetUnicode vrací LongWord, plnou 32bitovou hodnotu, protože kódování textu PDF už nese kompletní skalární hodnotu Unicode pro každou glyfu. CMap ToUnicode souboru PDF mapuje kódy znaků na text Unicode, a když glyfa reprezentuje to, čemu se neformálně říká znak astrální roviny — cokoli za Základní vícejazyčnou rovinou — je toto mapování plný bod kódu, ne 16bitový fragment. PDFium jej interně dekóduje zpátky na skalární hodnotu a vrátí přes hranici DLL skrz FPDFText_GetUnicode, a tato hranice je přesně místo, kde se 32bitová hodnota musí stát něčím, co dokáže vlastnost Delphi vrátit vašemu kódu
Zjevná implementace je WideChar(FPDFText_GetUnicode(TextPage, Index)), a je to zároveň ta špatná. Tvrdý cast z 32bitové hodnoty na 16bitový typ ponechá jen dolních 16 bitů a zbytek tiše zahodí, bez výjimky a bez kontroly rozsahu. U U+1F600 to znamená ponechat $F600 a ztratit fakt, že skutečná hodnota kdy byla nad U+FFFF, což vyprodukuje jednotku kódu, která není ani platný visící náhradník, jen nesouvisející znak Základní vícejazyčné roviny, který náhodou sdílí tyto dolní bity. Zřetězte pár tisíc takových do řetězce a navazující kód už nemá způsob, jak odlišit poškozený znak od legitimního
Co teď vrací Character[] a Charcode[] pro body kódu astrální roviny
Vlastnosti Character[] a Charcode[] v PDFium Component vrátí U+FFFD, náhradní znak Unicode, kdykoli podkladový bod kódu přesáhne U+FFFF, místo aby jej tiše useknuly. Tato ochrana sedí přímo uvnitř gettru 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átit U+FFFD místo useknutého fragmentu je záměrná, úzká oprava, ne redesign. Character[] a Charcode[] jsou typované jako WideChar jak na TPdf, tak na TPdfView, a rozšíření tohoto návratového typu, aby nesl plný bod kódu, by rozbilo každého existujícího volajícího, který očekává, že jedna glyfa na index znamená jednu 16bitovou hodnotu. U+FFFD je vlastní designovaný zástupný znak Unicode standardu přesně pro tuto situaci, takže volající, který jej kontroluje, dostane definovaný, zdokumentovaný signál místo tiše špatných dat. Jeden okrajový případ, který stojí za znalost: U+FFFD je také legitimní znak sám o sobě, takže na vzácném dokumentu, který už obsahuje skutečnou glyfu náhradního znaku, je tento index podle hodnoty samotné nerozlišitelný od useknutého astrálního znaku
Jak v Delphi správně extrahovat emoji a text CJK Extension B?
Volejte Text místo procházení Character[] kdykoli záleží na skutečném textovém obsahu, protože Text čte přes FPDFText_GetText a vrátí plné WString se správnými náhradními dvojicemi pro každý znak astrální roviny v rozsahu, místo jedné hodnoty pevné šířky na index. Pdf.Text(0, MaxInt), nebo zkratka Pdf.Text, extrahuje celou stránku správně jedním voláním, a Pdf.Text(StartIndex, Count) vytáhne menší rozsah stejným způsobem. Character[] si pořád vydobude své místo, když na indexu potřebujete jen pozici, font, nebo data příznaku a nikdy se nedotknete samotného bodu kódu — CharacterOrigin[], FontSize[] a CharacterMapError[] je jedno, zda byla podkladová glyfa astrální
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 přeskočení generovaných a nenamapovaných znaků v této smyčce je stejný vzor používaný pro extrakci prostého textu v extrakci textu z dokumentů PDF s PDFium Component; jediná změna je poslední řádek, který vyměňuje přímé připojení Character[I] za volání Text na jeden index, takže astrální znaky dorazí jako kompletní náhradní dvojice místo náhradních zástupných znaků
Kde to skutečně kouše: exporty chatů, osobní jména a vložené fonty CJK
Emoji se objevují všude, kde PDF zachycuje neformální komunikaci: exportované chatovací záznamy, výpisy recenzí z app store, přepisy z ticketového systému uložené do PDF pro kompliance archiv. CJK Extension B se objevuje na užším, ale vyšší sázce místě, osobní a místní jména, protože japonské rodinné rejstříky, čínské záznamy registrace domácnosti a tchajwanské doklady totožnosti jsou klasické zdroje znaků, které se nikdy nedostaly do běžného bloku CJK. Pipeline pro zpracování mezd nebo ověření identity, která extrahuje jména ze skenovaných vládních dokumentů, je přesně ten druh zátěže, kde se tiše zkomolený znak promění na neúspěšnou shodu místo kosmetické závady
Vzácné ideografy CJK také obvykle cestují s problémy s fontem, ne jen s kódováním, protože font musí nést glyfu pro bod kódu z rozsahu U+20000 dřív, než se vůbec cokoli vykreslí, a málo nainstalovaných systémových fontů to dělá. Kdokoli už prochází FontIsEmbedded[] po znaku tak, jak popisuje čtení vlastností fontů PDF s PDFium Component, by měl na stejném indexu zkontrolovat oba problémy zároveň: index, který z Character[] vrátí U+FFFD a hlásí nevložený font, je dokument, který tento znak nebude ani extrahovat, ani tisknout správně, a oprava patří proti proudu do toho, jak bylo PDF vyprodukováno, ne do vašeho kódu extrakce
Vlastnosti Character[], Charcode[] a Text popsané zde jsou součástí standardní komponenty PDFium pro Delphi a C++Builder