Wyciągnij emoji lub japońskie imię z rejestru rodzinnego z PDF jako tekst, a wynik pokaże kwadracik, znak zapytania, albo nic tam, gdzie powinien być znak. Właściwość Character[] komponentu PDFium zwykle jest tego przyczyną: odczytuje każdy glif przez FPDFText_GetUnicode, który zwraca pełny punkt kodowy Unicode jako wartość 32-bitową bez znaku, a następnie udostępnia go Delphi jako pojedynczy 16-bitowy WideChar. Żaden punkt kodowy powyżej U+FFFF nie może odbyć tej podróży w jednym kawałku, a uszkodzenie nigdy nie ujawnia się, gdy patrzysz na wyrenderowaną stronę, ponieważ renderowanie i wyodrębnianie tekstu przechodzą przez osobne ścieżki kodu w PDFium — dokument może wyświetlać swoje emoji doskonale i wciąż podać ci śmieci w chwili, gdy odczytasz Character[] w pętli i zbudujesz z tego łańcuch znaków
Podstawowa Płaszczyzna Wielojęzyczna i dlaczego WideChar kończy się na U+FFFF
WideChar w Delphi to typ 16-bitowy, który może pomieścić tylko jedną jednostkę kodu UTF-16. Podstawowa Płaszczyzna Wielojęzyczna Unicode (Basic Multilingual Plane), zakres U+0000 do U+FFFF, mieści się w niej dokładnie, dlatego łacina, cyrylica, greka i wspólny blok Zunifikowanych Ideogramów CJK przechodzą w obie strony przez pojedynczy WideChar bez incydentu. Dwie rodziny znaków rutynowo wypadają poza nią w rzeczywistych dokumentach: emoji, wiele z nich w bloku Emotikonów zaczynającym się od U+1F600, oraz rzadkie ideogramy CJK z Rozszerzenia B Zunifikowanych Ideogramów CJK, zakresu U+20000 do U+2A6DF zarezerwowanego dla mniej powszechnych znaków chińskich, japońskich i koreańskich, w tym wielu nazwisk osobowych i nazw miejscowości. UTF-16 obsługuje wszystko powyżej U+FFFF za pomocą pary surogatów — dwóch 16-bitowych jednostek kodu, wysokiego surogatu w zakresie $D800 do $DBFF, po którym następuje niski surogat w $DC00 do $DFFF, które razem kodują jeden punkt kodowy — a matematyka stojąca za tym parowaniem jest wystarczająco ustalona, by zademonstrować ją bezpośrednio w 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;
Podaj U+1F600, emoji uśmiechniętej buzi, przez tę funkcję, a wynikiem będzie wysoki surogat $D83D i niski surogat $DE00, dwie wartości 16-bitowe, nie jedna. Żadna z tych połówek nic sama w sobie nie znaczy; samotny $D83D siedzący w łańcuchu znaków bez $DE00 za nim to zwisający surogat, a większość kodu obsługującego tekst, który na taki natrafi, albo go odrzuca, albo zastępuje glifem zastępczym, albo zgłasza błąd
Dlaczego FPDFText_GetUnicode zwraca wartość, której Character[] nie może pomieścić?
FPDFText_GetUnicode zwraca LongWord, pełną wartość 32-bitową, ponieważ kodowanie tekstu PDF już niesie kompletną wartość skalarną Unicode dla każdego glifu. CMap ToUnicode pliku PDF mapuje kody znaków na tekst Unicode, a gdy glif reprezentuje to, co nieformalnie nazywa się znakiem z płaszczyzny astralnej — cokolwiek poza Podstawową Płaszczyzną Wielojęzyczną — to mapowanie jest pełnym punktem kodowym, nie 16-bitowym fragmentem. PDFium dekoduje go z powrotem do wartości skalarnej wewnętrznie i zwraca przez granicę DLL poprzez FPDFText_GetUnicode, i to dokładnie ta granica, w której wartość 32-bitowa musi stać się czymś, co właściwość Delphi może zwrócić twojemu kodowi
Oczywista implementacja to WideChar(FPDFText_GetUnicode(TextPage, Index)), i jest też niewłaściwa. Twardy rzut z wartości 32-bitowej na typ 16-bitowy zachowuje tylko dolne 16 bitów i po cichu odrzuca resztę, bez wyjątku i bez sprawdzenia zakresu. Dla U+1F600 oznacza to zachowanie $F600 i utratę faktu, że prawdziwa wartość kiedykolwiek była powyżej U+FFFF, co produkuje jednostkę kodu, która nie jest nawet prawidłowym zwisającym surogatem, tylko niepowiązanym znakiem z Podstawowej Płaszczyzny Wielojęzycznej, który akurat dzieli te dolne bity. Połącz kilka tysięcy takich w łańcuch znaków, a kod niżej w potoku nie ma już sposobu, by odróżnić uszkodzony znak od prawidłowego
Co teraz zwracają Character[] i Charcode[] dla punktów kodowych z płaszczyzny astralnej
Właściwości Character[] i Charcode[] komponentu PDFium zwracają U+FFFD, unicode'owy znak zastępczy, za każdym razem, gdy bazowy punkt kodowy przekracza U+FFFF, zamiast po cichu go obcinać. To zabezpieczenie siedzi bezpośrednio wewnątrz gettera właściwości stojącego 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;
Zwracanie U+FFFD zamiast obciętego fragmentu to celowa, wąska poprawka, a nie przeprojektowanie. Character[] i Charcode[] są typowane jako WideChar zarówno w TPdf, jak i TPdfView, a rozszerzenie tego typu zwracanego, by nieść pełny punkt kodowy, złamałoby każdego istniejącego wywołującego, który oczekuje, że jeden glif na indeks oznacza jedną wartość 16-bitową. U+FFFD to własny, wyznaczony przez standard Unicode symbol zastępczy dokładnie dla tej sytuacji, więc wywołujący, który go sprawdza, otrzymuje zdefiniowany, udokumentowany sygnał zamiast po cichu błędnych danych. Jeden przypadek graniczny warty poznania: U+FFFD jest też prawowitym znakiem samym w sobie, więc w rzadkim dokumencie, który już zawiera prawdziwy glif znaku zastępczego, ten indeks jest nierozróżnialny od obciętego znaku astralnego wyłącznie po wartości
Jak poprawnie wyodrębnić emoji i tekst z Rozszerzenia B CJK w Delphi?
Wywołaj Text zamiast przechodzić przez Character[] za każdym razem, gdy liczy się faktyczna treść tekstowa, ponieważ Text czyta przez FPDFText_GetText i zwraca pełny WString z prawidłowymi parami surogatów dla każdego znaku astralnego w zakresie, zamiast jednej wartości o stałej szerokości na indeks. Pdf.Text(0, MaxInt), albo skrót Pdf.Text, wyodrębnia całą stronę poprawnie w jednym wywołaniu, a Pdf.Text(StartIndex, Count) pobiera mniejszy zakres w ten sam sposób. Character[] wciąż zdobywa swoje miejsce, gdy potrzebujesz tylko danych pozycji, czcionki lub flagi przy indeksie i nigdy nie dotykasz samego punktu kodowego — CharacterOrigin[], FontSize[] i CharacterMapError[] nie obchodzi, czy bazowy glif był astralny
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;
Sprawdzenie pomijające znaki wygenerowane i niezmapowane w tej pętli to ten sam wzorzec, jaki jest używany do wyodrębniania zwykłego tekstu w wyodrębnianiu tekstu z dokumentów PDF za pomocą komponentu PDFium; jedyna zmiana to ostatnia linia, która wymienia bezpośrednie dołączenie Character[I] na jednoindeksowe wywołanie Text, tak aby znaki astralne przychodziły jako kompletne pary surogatów zamiast zastępczych placeholderów
Gdzie to faktycznie gryzie: eksporty czatów, imiona i nazwiska oraz osadzone czcionki CJK
Emoji pojawiają się wszędzie tam, gdzie PDF przechwytuje nieformalną komunikację: wyeksportowane logi czatu, zrzuty recenzji ze sklepu z aplikacjami, transkrypty systemów zgłoszeń zapisane do PDF na potrzeby archiwum zgodności. Rozszerzenie B CJK pojawia się w węższym, ale ważniejszym miejscu, w imionach i nazwiskach oraz nazwach miejscowości, ponieważ japońskie rejestry rodzinne, chińskie rejestry gospodarstw domowych i tajwańskie dokumenty tożsamości to klasyczne źródła znaków, które nigdy nie trafiły do powszechnego bloku CJK. Potok naliczania płac lub weryfikacji tożsamości, który wyodrębnia imiona i nazwiska ze skanowanych dokumentów rządowych, to dokładnie ten rodzaj obciążenia, w którym po cichu okaleczony znak zamienia się w nieudane dopasowanie, a nie kosmetyczną usterkę
Rzadkie ideogramy CJK mają też tendencję do podróżowania z problemami czcionek, nie tylko kodowania, ponieważ czcionka musi nieść glif dla punktu kodowego z zakresu U+20000, zanim cokolwiek w ogóle może się wyrenderować, a niewiele zainstalowanych czcionek systemowych to robi. Każdy, kto już przechodzi przez FontIsEmbedded[] na znak, tak jak opisuje odczyt właściwości czcionek PDF za pomocą komponentu PDFium, powinien sprawdzić ten sam indeks pod kątem obu problemów naraz: indeks, który zwraca U+FFFD z Character[] i zgłasza czcionkę niebędącą osadzoną, to dokument, który nie wyodrębni ani nie wydrukuje tego znaku poprawnie, a poprawka należy wyżej w potoku, do tego, jak PDF został wyprodukowany, nie do twojego kodu ekstrakcji
Opisane tutaj właściwości Character[], Charcode[] i Text są częścią standardowego komponentu PDFium dla Delphi i C++Buildera