Ako iz PDF-a izdvojite emoji ili ime iz japanskog obiteljskog registra kao tekst, izlaz na mjestu znaka prikazuje okvir, upitnik ili baš ništa. Najčešći je uzrok svojstvo Character[] komponente PDFium Component: ono svaki glif čita kroz FPDFText_GetUnicode, koji vraća punu Unicode kodnu točku kao neoznačenu 32-bitnu vrijednost, a zatim je Delphiju izlaže kao jedan 16-bitni WideChar. Nijedna kodna točka iznad U+FFFF ne može tako proći u jednom komadu, a oštećenje se ne vidi dok gledate prikazanu stranicu jer se iscrtavanje i izdvajanje teksta u PDFiumu odvijaju kroz odvojene putanje — dokument može savršeno prikazati emoji, a ipak vam predati neispravan sadržaj čim u petlji pročitate Character[] i od njega sastavite niz
Osnovna višejezična ravnina i zašto WideChar staje na U+FFFF
Delphijev WideChar 16-bitni je tip koji može sadržavati samo jednu UTF-16 kodnu jedinicu. Unicodeova osnovna višejezična ravnina, raspon U+0000 do U+FFFF, točno stane u nju, pa se latinični, ćirilični i grčki znakovi te uobičajeni blok CJK Unified Ideographs bez problema prenose kroz jedan WideChar. U stvarnim dokumentima izvan tog raspona redovito se nalaze dvije skupine znakova: emoji, od kojih su mnogi u bloku Emoticons koji počinje na U+1F600, te rijetki CJK ideografi iz CJK Unified Ideographs Extension B, raspona U+20000 do U+2A6DF namijenjenog rjeđim kineskim, japanskim i korejskim znakovima, uključujući mnoga osobna imena i nazive mjesta. UTF-16 sve iznad U+FFFF obrađuje surogatnim parom — dvjema 16-bitnim kodnim jedinicama, visokim surogatom iz raspona $D800 do $DBFF i niskim surogatom iz raspona $DC00 do $DFFF, koje zajedno kodiraju jednu kodnu točku — a matematika tog sparivanja dovoljno je određena da se izravno prikaže u 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;
Provučete li kroz tu funkciju U+1F600, emoji nasmiješenog lica, rezultat je visoki surogat $D83D i niski surogat $DE00, dvije 16-bitne vrijednosti, a ne jedna. Nijedna polovica sama po sebi nema značenje; usamljeni $D83D u nizu iza kojeg nema $DE00 viseći je surogat, a većina koda za obradu teksta koji ga susretne ili ga odbaci, zamijeni zamjenskim glifom ili prijavi pogrešku
Zašto FPDFText_GetUnicode vraća vrijednost koju Character[] ne može sadržavati?
FPDFText_GetUnicode vraća LongWord, punu 32-bitnu vrijednost, jer kodiranje PDF teksta već sadrži potpunu Unicode skalarnu vrijednost za svaki glif. PDF-ov ToUnicode CMap preslikava kodove znakova u Unicode tekst, a kada glif predstavlja ono što se neformalno naziva znakom iz astralne ravnine — bilo što izvan osnovne višejezične ravnine — to je preslikavanje pune kodne točke, a ne 16-bitnog dijela. PDFium je interno dekodira natrag u skalarnu vrijednost i vraća je preko DLL granice kroz FPDFText_GetUnicode, a upravo se na toj granici 32-bitna vrijednost mora pretvoriti u nešto što Delphi svojstvo može vratiti vašem kodu
Očita je implementacija WideChar(FPDFText_GetUnicode(TextPage, Index)), ali ona je ujedno i pogrešna. Izravno pretvaranje 32-bitne vrijednosti u 16-bitni tip zadržava samo nižih 16 bitova i nečujno odbacuje ostatak, bez iznimke i provjere raspona. Za U+1F600 to znači zadržati $F600 i izgubiti informaciju da je stvarna vrijednost ikad bila iznad U+FFFF, čime nastaje kodna jedinica koja čak nije ni valjani viseći surogat, nego nepovezani znak osnovne višejezične ravnine koji slučajno dijeli te niže bitove. Spojite nekoliko tisuća takvih vrijednosti u niz i kod nizvodno više ne može razlikovati oštećeni znak od valjanog
Što Character[] i Charcode[] sada vraćaju za kodne točke izvan osnovne ravnine
Svojstva Character[] i Charcode[] komponente PDFium Component vraćaju U+FFFD, Unicodeov zamjenski znak, kad temeljna kodna točka prelazi U+FFFF, umjesto da je nečujno skrate. Ta se zaštita nalazi izravno u dohvatniku svojstva iza 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;
Vraćanje U+FFFD umjesto skraćenog dijela namjerno je usko rješenje, a ne redizajn. Character[] i Charcode[] na TPdf i TPdfView tipizirani su kao WideChar, a proširenje povratnog tipa kako bi nosio punu kodnu točku prekinulo bi svakog postojećeg pozivatelja koji očekuje da jedan glif po indeksu znači jednu 16-bitnu vrijednost. U+FFFD vlastiti je predviđeni zamjenski znak Unicode Standarda upravo za ovu situaciju, pa pozivatelj koji ga provjeri dobiva definirani, dokumentirani signal umjesto nečujno pogrešnih podataka. Važan rubni slučaj jest da je U+FFFD i sam po sebi valjan znak, pa se u rijetkom dokumentu koji već sadrži pravi glif zamjenskog znaka taj indeks po samoj vrijednosti ne može razlikovati od skraćenog astralnog znaka
Kako u Delphiju ispravno izdvojiti emoji i tekst CJK Extension B?
Pozovite Text umjesto prolaska kroz Character[] kad je važan stvarni tekstualni sadržaj, jer Text čita kroz FPDFText_GetText i vraća puni WString s pravilnim surogatnim parovima za svaki astralni znak u rasponu, umjesto jedne vrijednosti fiksne širine po indeksu. Pdf.Text(0, MaxInt), ili skraćeni oblik Pdf.Text, ispravno izdvaja cijelu stranicu jednim pozivom, a Pdf.Text(StartIndex, Count) na isti način dohvaća manji raspon. Character[] i dalje je koristan kad vam po indeksu trebaju samo položaj, font ili podaci o oznakama i ne pristupate samoj kodnoj točki — CharacterOrigin[], FontSize[] i CharacterMapError[] ne ovise o tome je li temeljni glif astralan
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;
Provjera preskakanja generiranih i neposlikanih znakova u toj petlji isti je obrazac koji se koristi za izdvajanje običnog teksta u članku izdvajanje teksta iz PDF dokumenata pomoću PDFium Componenta; jedina je promjena u zadnjem retku, gdje se izravno dodavanje Character[I] zamjenjuje pozivom Text za jedan indeks kako bi astralni znakovi stigli kao potpuni surogatni parovi umjesto zamjenskih oznaka
Gdje se problem stvarno pojavljuje: izvoz razgovora, osobna imena i ugrađeni CJK fontovi
Emoji se pojavljuju svugdje gdje PDF bilježi neformalnu komunikaciju: u izvezenim zapisima razgovora, izvodima recenzija iz trgovina aplikacija i prijepisima sustava za podršku spremljenima u PDF radi arhive usklađenosti. CJK Extension B pojavljuje se u užem, ali važnijem području, osobnim imenima i nazivima mjesta, jer su japanski obiteljski registri, kineske evidencije kućanstava i tajvanski identifikacijski dokumenti klasični izvori znakova koji nisu ušli u uobičajeni CJK blok. Sustav obračuna plaća ili provjere identiteta koji izdvaja imena iz skeniranih državnih dokumenata upravo je vrsta opterećenja kod koje nečujno iskrivljen znak dovodi do neuspjelog podudaranja, a ne samo neugodnog vizualnog nedostatka
Rijetki CJK ideografi često donose i probleme s fontovima, ne samo s kodiranjem, jer font mora sadržavati glif za kodnu točku iz raspona U+20000 da bi se išta uopće moglo iscrtati, a malo instaliranih sistemskih fontova to podržava. Ako već za svaki znak prolazite kroz FontIsEmbedded[] na način opisan u članku čitanje svojstava PDF fontova pomoću PDFium Componenta, provjerite oba problema na istom indeksu: indeks koji iz Character[] vraća U+FFFD i prijavljuje neugrađeni font pripada dokumentu koji taj znak neće ispravno ni izdvojiti ni ispisati, a rješenje treba tražiti uzvodno, u načinu stvaranja PDF-a, a ne u vašem kodu za izdvajanje
Ovdje opisana svojstva Character[], Charcode[] i Text dio su standardnog proizvoda PDFium Component za Delphi i C++Builder