Tehnički članak

PDFium emoji i CJK znakovi oštećuju WideChar u Delphiju

Ako iz PDF-a izdvojite emoji ili japansko ime iz porodičnog registra kao tekst, izlaz prikazuje kvadrat, znak pitanja ili ništa tamo gde bi znak trebalo da bude. Za to je obično krivo svojstvo Character[] komponente PDFium Component: ono čita svaki glif preko FPDFText_GetUnicode, koji vraća punu Unicode kodnu tačku kao neoznačenu 32-bitnu vrednost, a zatim je izlaže Delphiju kao jednu 16-bitnu WideChar vrednost. Nijedna kodna tačka iznad U+FFFF ne može tako da prođe u jednom delu, a oštećenje se nikada ne vidi dok gledate prikazanu stranicu, jer prikazivanje i izdvajanje teksta u PDFium-u koriste odvojene putanje koda — dokument može savršeno da prikaže emoji, a da vam ipak preda besmislene podatke čim u petlji pročitate Character[] i od njega sastavite nisku

Osnovna višejezična ravan i zašto WideChar staje na U+FFFF

WideChar u Delphiju je 16-bitni tip koji može da sadrži samo jednu UTF-16 kodnu jedinicu. Osnovna višejezična ravan Unicode-a, u opsegu od U+0000 do U+FFFF, tačno staje u nju, zbog čega se latinica, ćirilica, grčki alfabet i uobičajeni blok CJK objedinjenih ideografa bez problema prenose kroz jednu vrednost WideChar. Dve porodice znakova u stvarnim dokumentima redovno izlaze iz tog opsega: emoji, od kojih se mnogi nalaze u bloku Emoticons koji počinje od U+1F600, i retki CJK ideografi iz proširenja B CJK objedinjenih ideografa, opsega od U+20000 do U+2A6DF, rezervisanog za manje uobičajene kineske, japanske i korejske znakove, uključujući mnoga lična imena i nazive mesta. UTF-16 obrađuje sve iznad U+FFFF pomoću surogatnog para — dve 16-bitne kodne jedinice, visokog surogata u opsegu od $D800 do $DBFF, iza kog sledi niski surogat u opsegu od $DC00 do $DFFF, koje zajedno kodiraju jednu kodnu tačku — a matematika tog uparivanja je dovoljno utvrđena da se direktno 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;

Provucite U+1F600, emoji nasmejanog lica, kroz tu funkciju i rezultat će biti visoki surogat $D83D i niski surogat $DE00, dve 16-bitne vrednosti, a ne jedna. Nijedna polovina sama po sebi nema značenje; usamljeni $D83D u niskoj sa kojom iza njega nema $DE00 predstavlja viseći surogat, a većina koda za obradu teksta koji naiđe na njega ili ga odbaci, zameni znakom za zamenu ili prijavi grešku

Zašto FPDFText_GetUnicode vraća vrednost koju Character[] ne može da sadrži?

FPDFText_GetUnicode vraća LongWord, punu 32-bitnu vrednost, zato što kodiranje PDF teksta već sadrži potpunu Unicode skalarnu vrednost za svaki glif. PDF-ova ToUnicode CMap preslikava kodove znakova u Unicode tekst, a kada glif predstavlja ono što se neformalno naziva znak ravni izvan osnovne ravni — bilo šta iznad osnovne višejezične ravni — to preslikavanje je puna kodna tačka, a ne 16-bitni fragment. PDFium interno dekodira tu vrednost nazad u skalar i vraća je preko granice DLL-a kroz FPDFText_GetUnicode, a upravo na toj granici 32-bitna vrednost mora da postane nešto što Delphi svojstvo može da vrati vašem kodu

Očigledna implementacija je WideChar(FPDFText_GetUnicode(TextPage, Index)), ali je ona takođe pogrešna. Tvrdo pretvaranje 32-bitne vrednosti u 16-bitni tip zadržava samo nižih 16 bitova i nečujno odbacuje ostatak, bez izuzetka i bez provere opsega. Za U+1F600 to znači zadržavanje vrednosti $F600 i gubitak informacije da je prava vrednost ikada bila iznad U+FFFF, čime nastaje kodna jedinica koja čak nije ni važeći viseći surogat, već nepovezan znak osnovne višejezične ravni koji slučajno deli te niže bitove. Nadovežite nekoliko hiljada takvih vrednosti u nisku i nizvodni kod više nema način da razlikuje oštećeni znak od legitimnog

Šta svojstva Character[] i Charcode[] sada vraćaju za kodne tačke izvan osnovne ravni

Svojstva Character[] i Charcode[] komponente PDFium Component vraćaju U+FFFD, Unicode znak zamene, kad god osnovna kodna tačka prelazi U+FFFF, umesto da je nečujno skrate. Ta zaštita se nalazi direktno u pristupniku 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 umesto skraćenog fragmenta jeste namerna, uska ispravka, a ne redizajn. Character[] i Charcode[] su tipizovani kao WideChar i u TPdf i u TPdfView, a proširivanje tog povratnog tipa radi prenosa pune kodne tačke pokvarilo bi svakog postojećeg pozivaoca koji očekuje da jedan glif po indeksu znači jednu 16-bitnu vrednost. U+FFFD je sopstveni označeni rezervni znak Unicode standarda upravo za ovu situaciju, pa pozivalac koji ga proverava dobija definisan i dokumentovan signal umesto nečujno pogrešnih podataka. Važno je znati jedan granični slučaj: U+FFFD je takođe legitiman znak sam po sebi, pa se na retkom dokumentu koji već sadrži pravi glif znaka zamene taj indeks po samoj vrednosti ne može razlikovati od skraćenog znaka izvan osnovne ravni

Kako u Delphiju ispravno izdvojiti emoji i tekst CJK Extension B?

Pozovite Text umesto prolaska kroz Character[] kad god je stvarni tekstualni sadržaj važan, jer Text čita preko FPDFText_GetText i vraća punu vrednost WString sa pravilnim surogatnim parovima za svaki znak izvan osnovne ravni u opsegu, umesto jedne vrednosti fiksne širine po indeksu. Pdf.Text(0, MaxInt), ili skraćeni oblik Pdf.Text, pravilno izdvaja celu stranicu u jednom pozivu, a Pdf.Text(StartIndex, Count) na isti način preuzima manji opseg. Character[] i dalje ima svrhu kada su vam na indeksu potrebni samo položaj, font ili podaci zastavice i nikada ne dodirujete samu kodnu tačku — CharacterOrigin[], FontSize[] i CharacterMapError[] nije važno da li je osnovni glif izvan osnovne ravni

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;

Provera za preskakanje generisanih i nepovezanih 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 Component; jedina promena je poslednja linija, koja direktno dodavanje Character[I] zamenjuje pozivom Text za jedan indeks, tako da znakovi izvan osnovne ravni stižu kao potpuni surogatni parovi umesto rezervnih znakova

Gde se problem stvarno pojavljuje: izvoz razgovora, lična imena i ugrađeni CJK fontovi

Emoji se pojavljuju svuda gde PDF beleži neformalnu komunikaciju: u izvezenim zapisima razgovora, izvodima recenzija iz prodavnica aplikacija i transkriptima sistema za tikete sačuvanim u PDF-u radi arhive usklađenosti. CJK Extension B pojavljuje se u užem, ali važnijem području, kod ličnih imena i naziva mesta, jer su japanski porodični registri, kineske evidencije domaćinstava i tajvanska lična dokumenta klasični izvori znakova koji nikada nisu ušli u uobičajeni CJK blok. Sistem za obračun zarada ili proveru identiteta koji izdvaja imena iz skeniranih državnih dokumenata upravo je takvo opterećenje u kom nečujno izmenjen znak dovodi do neuspešnog podudaranja, a ne samo do kozmetičkog problema

Retki CJK ideografi takođe često donose probleme sa fontovima, a ne samo sa kodiranjem, jer font mora da sadrži glif za kodnu tačku iz opsega U+20000 pre nego što ona uopšte može da se prikaže, a malo instaliranih sistemskih fontova to podržava. Ako već za svaki znak proveravate FontIsEmbedded[] na način opisan u članku čitanje svojstava PDF fontova pomoću PDFium Component, proverite na istom indeksu oba problema zajedno: indeks koji iz Character[] vraća U+FFFD i prijavljuje neugrađeni font označava dokument koji taj znak neće ni pravilno izdvojiti ni odštampati, a ispravka pripada uzvodno, načinu na koji je PDF proizveden, a ne vašem kodu za izdvajanje

Ovde opisana svojstva Character[], Charcode[] i Text deo su standardnog proizvoda PDFium Component za Delphi i C++Builder