Tehnični članak

Emoji in znaki CJK povzročijo težave z WideChar v Delphiju

Ko iz PDF-ja kot besedilo izvlečete emoji ali ime iz japonskega družinskega registra, se na mestu, kjer bi moral biti znak, lahko prikaže kvadratek, vprašaj ali pa nič. Za to je običajno kriva lastnost Character[] komponente PDFium Component: vsak glif prebere prek FPDFText_GetUnicode, ki vrne celotno kodno točko Unicode kot nepredznačeno 32-bitno vrednost, nato pa jo Delphiju izpostavi kot en sam 16-bitni WideChar. Nobene kodne točke nad U+FFFF ni mogoče v enem kosu spraviti skozi takšno pretvorbo, poškodba pa se med ogledom upodobljene strani nikoli ne pokaže, ker upodabljanje in izločanje besedila v PDFium potekata po ločenih poteh — dokument lahko svoje emoji prikaže popolnoma pravilno, vendar vam ob branju Character[] v zanki in sestavljanju niza iz njega vseeno izroči neuporabne podatke

Osnovna večjezična ravnina in zakaj se WideChar konča pri U+FFFF

Delphijev WideChar je 16-bitni tip, ki lahko vsebuje samo eno 16-bitno kodno enoto UTF-16. Osnovna večjezična ravnina Unicode, območje od U+0000 do U+FFFF, se vanj prilega natanko, zato se latinica, cirilica, grščina in običajni blok poenotenih ideografov CJK brez težav izmenično pretvorijo skozi en sam WideChar. V resničnih dokumentih sta zunaj tega območja pogosto dve družini znakov: emoji, med katerimi jih je veliko v bloku emotikonov z začetkom pri U+1F600, in redki ideografi CJK iz razširitve B poenotenih ideografov CJK, območja od U+20000 do U+2A6DF, rezerviranega za manj pogoste kitajske, japonske in korejske znake, vključno s številnimi osebnimi imeni in krajevnimi imeni. UTF-16 vse nad U+FFFF obdela z nadomestnim parom — z dvema 16-bitnima kodnima enotama, z visokim nadomestnim znakom v območju $D800 do $DBFF, ki mu sledi nizki nadomestni znak v območju $DC00 do $DFFF; skupaj kodirata eno kodno točko — matematika tega povezovanja pa je dovolj določna, da jo lahko neposredno pokažemo 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;

Če skozi to funkcijo pošljete U+1F600, emoji nasmejanega obraza, dobite visok nadomestni znak $D83D in nizki nadomestni znak $DE00, torej dve 16-bitni vrednosti, ne ene. Nobena polovica sama zase nima pomena; samostojen $D83D v nizu brez $DE00 za njim je viseči nadomestni znak, večina kode za obdelavo besedila pa ga bodisi odstrani, nadomesti z nadomestnim glifom ali sproži napako

Zakaj FPDFText_GetUnicode vrne vrednost, ki je Character[] ne more vsebovati

FPDFText_GetUnicode vrne LongWord, polno 32-bitno vrednost, ker kodiranje besedila PDF-ja že vsebuje celotno skalarno vrednost Unicode za vsak glif. CMap PDF-ja ToUnicode preslika kode znakov v besedilo Unicode in kadar glif predstavlja tako imenovani znak astralne ravnine — karkoli nad osnovno večjezično ravnino — je ta preslikava celotna kodna točka, ne 16-bitni del. PDFium jo interno dekodira nazaj v skalarno vrednost in jo prek FPDFText_GetUnicode vrne čez mejo DLL, prav na tej meji pa mora 32-bitna vrednost postati nekaj, kar lahko Delphijeva lastnost vrne vaši kodi

Očitna izvedba je WideChar(FPDFText_GetUnicode(TextPage, Index)), vendar je tudi napačna. Trda pretvorba 32-bitne vrednosti v 16-bitni tip tiho obdrži samo spodnjih 16 bitov in zavrže vse drugo, brez izjeme in preverjanja obsega. Pri U+1F600 to pomeni, da obdržite $F600 in izgubite podatek, da je bila prava vrednost sploh nad U+FFFF, zato dobite kodno enoto, ki ni niti veljaven viseči nadomestni znak, temveč nepovezan znak osnovne večjezične ravnine, ki ima po naključju iste spodnje bite. Ko jih nekaj tisoč združite v niz, nadaljnja koda ne more več razlikovati poškodovanega znaka od veljavnega

Kaj lastnosti Character[] in Charcode[] zdaj vrneta za kodne točke astralne ravnine

Lastnosti Character[] in Charcode[] komponente PDFium Component vrneta U+FFFD, nadomestni znak Unicode, kadar osnovna kodna točka presega U+FFFF, namesto da bi jo tiho skrajšali. Ta zaščita je neposredno v pridobivalniku lastnosti 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;

Vračanje U+FFFD namesto skrajšanega dela je namerna, ozka rešitev in ne preoblikovanje. Lastnosti Character[] in Charcode[] sta pri TPdf in TPdfView tipa WideChar, razširitev vrnjenega tipa za celotno kodno točko pa bi pokvarila vsakega obstoječega klicatelja, ki pričakuje, da en glif na indeks pomeni eno 16-bitno vrednost. U+FFFD je lastna določena oznaka standarda Unicode za prav tak primer, zato klicatelj, ki jo preveri, dobi opredeljen in dokumentiran signal namesto tiho napačnih podatkov. Vredno je poznati eno mejno okoliščino: U+FFFD je tudi sam veljaven znak, zato pri redkem dokumentu, ki že vsebuje pravi glif nadomestnega znaka, po sami vrednosti ni mogoče razlikovati tega indeksa od skrajšanega astralnega znaka

Kako v Delphiju pravilno izločite besedilo z emoji in znaki CJK iz razširitve B

Uporabite Text namesto sprehajanja po Character[], kadar je pomembna dejanska vsebina besedila, saj Text bere prek FPDFText_GetText in vrne poln WString z ustreznimi nadomestnimi pari za vsak astralni znak v območju, ne pa ene vrednosti stalne širine na indeks. Pdf.Text(0, MaxInt) ali krajši zapis Pdf.Text pravilno izloči celotno stran v enem klicu, Pdf.Text(StartIndex, Count) pa na enak način pridobi manjši obseg. Character[] je še vedno uporabna, kadar pri indeksu potrebujete samo podatke o položaju, pisavi ali zastavici in se same kodne točke nikoli ne dotaknete — CharacterOrigin[], FontSize[] in CharacterMapError[] ne zanimajo, ali je bil osnovni glif astralen

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;

Preverjanje za preskok ustvarjenih in nepreslikanih znakov v tej zanki je enak vzorec, kot ga uporablja izločanje navadnega besedila v članku izločanje besedila iz dokumentov PDF s komponento PDFium Component; spremeni se samo zadnja vrstica, ki neposredno dodajanje Character[I] zamenja s klicem Text za en indeks, tako da astralni znaki prispejo kot celotni nadomestni pari namesto kot nadomestni znaki

Kje se to dejansko pokaže: izvozi klepetov, osebna imena in vdelane pisave CJK

Emoji se pojavijo povsod, kjer PDF zajame neformalno komunikacijo: v izvoženih dnevnikih klepetov, izvozih ocen iz trgovin z aplikacijami in prepisih sistemov za obravnavo zahtevkov, shranjenih v PDF za arhiv skladnosti. Znaki CJK iz razširitve B se pojavljajo na ožjem, vendar pomembnejšem področju, pri osebnih in krajevnih imenih, saj so japonski družinski registri, kitajski gospodinjski registri in tajvanski osebni dokumenti klasični viri znakov, ki nikoli niso prišli v običajni blok CJK. Plačilni sistem ali postopek preverjanja identitete, ki iz uradnih skeniranih dokumentov izloča imena, je natanko tista vrsta obremenitve, pri kateri tiho popačen znak povzroči neuspešno ujemanje namesto zgolj kozmetične napake

Redke ideografe CJK pogosto spremljajo težave s pisavami, ne le s kodiranjem, saj mora pisava vsebovati glif za kodno točko iz območja U+20000, preden jo je sploh mogoče izrisati, nameščenih sistemskih pisav pa je malo. Kdor že pregleduje FontIsEmbedded[] za vsak znak na način, opisan v članku branje lastnosti pisav PDF s komponento PDFium Component, naj za oba problema preveri isti indeks: indeks, ki iz Character[] vrne U+FFFD in poroča o nevdelani pisavi, pripada dokumentu, ki tega znaka ne bo pravilno izločil niti natisnil, popravek pa je treba iskati pri nastanku PDF-ja in ne v vaši kodi za izločanje

Tu opisane lastnosti Character[], Charcode[] in Text so del standardne komponente PDFium Component za Delphi in C++Builder