Techninis straipsnis

PDFium jaustukai ir CJK simboliai sugadina WideChar Delphi kalboje

Iš PDF ištraukus jaustuką arba japoniškame šeimos registre esantį vardą kaip tekstą, ten, kur turėtų būti simbolis, išvestyje matoma dėžutė, klaustukas arba visiškai nieko. Dažniausiai dėl to kalta PDFium Component ypatybė Character[]: ji kiekvieną glifą nuskaito per FPDFText_GetUnicode, kuris grąžina visą Unicode kodo tašką kaip beženklę 32 bitų reikšmę, o tada pateikia jį Delphi kaip vieną 16 bitų WideChar. Bet kuris kodo taškas už U+FFFF ribos negali vienu gabalu pereiti šio kelio, o sugadinimo nematyti žiūrint į atvaizduotą puslapį, nes PDFium atvaizdavimas ir teksto ištraukimas vykdomi atskirais kodo keliais — dokumentas gali nepriekaištingai rodyti jaustuką ir vis tiek grąžinti šiukšles vos tik cikle perskaitysite Character[] bei iš jo sukursite eilutę

Pagrindinė daugiakalbė plokštuma ir kodėl WideChar sustoja ties U+FFFF

Delphi WideChar yra 16 bitų tipas, galintis talpinti tik vieną UTF-16 kodo vienetą. Unicode pagrindinė daugiakalbė plokštuma, apimanti nuo U+0000 iki U+FFFF, tiksliai į ją telpa, todėl lotyniški, kirilicos, graikų ir įprasto CJK suvienodintų ideografų bloko simboliai be problemų pereina pirmyn ir atgal per vieną WideChar. Tikruose dokumentuose į ją dažnai netelpa dvi simbolių grupės: jaustukai, kurių daug yra nuo U+1F600 prasidedančiame jaustukų bloke, ir reti CJK ideografai iš CJK suvienodintų ideografų B plėtinio, apimančio nuo U+20000 iki U+2A6DF ir skirto retesniems kinų, japonų bei korėjiečių simboliams, įskaitant daugybę asmenvardžių ir vietovardžių. UTF-16 viską, kas yra už U+FFFF, apdoroja naudodamas surogatų porą — du 16 bitų kodo vienetus: aukštąjį surogatą nuo $D800 iki $DBFF, po kurio eina žemasis surogatas nuo $DC00 iki $DFFF; kartu jie užkoduoja vieną kodo tašką — o šios poros matematika yra pakankamai nekintama, kad ją būtų galima tiesiogiai parodyti Pascal kalba

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;

Perduokite U+1F600, besišypsančio veido jaustuką, šiai funkcijai ir gausite aukštąjį surogatą $D83D bei žemąjį surogatą $DE00, tai yra dvi 16 bitų reikšmes, o ne vieną. Nė viena pusė pati savaime nieko nereiškia: eilutėje vienas $D83D be po jo esančio $DE00 yra kabantis surogatas, o dauguma su tokiu simboliu susidūrusio teksto apdorojimo kodo jį arba pašalina, arba pakeičia pakaitiniu glifu, arba iškelia klaidą

Kodėl FPDFText_GetUnicode grąžina reikšmę, kurios negali talpinti Character[]

FPDFText_GetUnicode grąžina LongWord, visą 32 bitų reikšmę, nes PDF teksto koduotėje jau yra visa kiekvieno glifo Unicode skaliarinė reikšmė. PDF ToUnicode CMap susieja simbolių kodus su Unicode tekstu, o kai glifas reiškia taip neoficialiai vadinamą papildomos plokštumos simbolį — bet ką už pagrindinės daugiakalbės plokštumos ribų — tas susiejimas yra visas kodo taškas, o ne 16 bitų fragmentas. PDFium viduje jį iškoduoja į skaliarinę reikšmę ir grąžina per DLL ribą naudodamas FPDFText_GetUnicode, todėl būtent šioje riboje 32 bitų reikšmė turi virsti tuo, ką Delphi ypatybė gali perduoti jūsų kodui

Akivaizdi realizacija yra WideChar(FPDFText_GetUnicode(TextPage, Index)), tačiau ji taip pat yra neteisinga. Kietas 32 bitų reikšmės pavertimas į 16 bitų tipą išsaugo tik apatinius 16 bitų ir tyliai išmeta likusią dalį, be išimties ir diapazono patikros. U+1F600 atveju tai reiškia, kad išsaugoma $F600 ir prarandama informacija, jog tikroji reikšmė apskritai buvo didesnė už U+FFFF, todėl gaunamas kodo vienetas nėra net galiojantis kabantis surogatas, o tik nesusijęs pagrindinės daugiakalbės plokštumos simbolis, atsitiktinai turintis tuos pačius apatinius bitus. Sujungus kelis tūkstančius tokių reikšmių į eilutę, tolesnis kodas nebeturi būdo atskirti sugadinto simbolio nuo teisėto

Ką dabar grąžina Character[] ir Charcode[] papildomos plokštumos kodo taškams

PDFium Component ypatybės Character[] ir Charcode[], kai pagrindinė kodo taško reikšmė viršija U+FFFF, grąžina U+FFFD, Unicode pakaitinį simbolį, o ne tyliai sutrumpintą reikšmę. Ši apsauga yra tiesiogiai Character[] pagrindinėje ypatybės prieigos programoje

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;

U+FFFD grąžinimas vietoj sutrumpinto fragmento yra sąmoningas, siauras pataisymas, o ne pertvarkymas. Character[] ir Charcode[] tiek TPdf, tiek TPdfView klasėse yra WideChar tipo, o praplėtus grąžinamą tipą, kad jis galėtų perduoti visą kodo tašką, nutrūktų kiekvienas esamas iškvietimas, kuriam vienas glifas viename indekse reiškia vieną 16 bitų reikšmę. U+FFFD yra paties Unicode standarto nustatytas vietos rezervavimo simbolis būtent tokiai situacijai, todėl jį tikrinantis iškvietėjas gauna apibrėžtą, dokumentuotą signalą, o ne tyliai neteisingus duomenis. Verta žinoti vieną ribinį atvejį: U+FFFD yra ir teisėtas savarankiškas simbolis, todėl retame dokumente, kuriame jau yra tikras pakaitinio simbolio glifas, vien pagal reikšmę neįmanoma atskirti šio indekso nuo sutrumpinto papildomos plokštumos simbolio

Kaip Delphi kalboje teisingai ištraukti jaustukų ir CJK B plėtinio tekstą

Kai svarbus tikrasis tekstinis turinys, naudokite Text, o ne eikite per Character[], nes Text per FPDFText_GetText grąžina visą WString su tinkamomis surogatų poromis kiekvienam diapazone esančiam papildomos plokštumos simboliui, o ne po vieną fiksuoto pločio reikšmę kiekvienam indeksui. Pdf.Text(0, MaxInt) arba trumpesnis Pdf.Text vienu iškvietimu teisingai ištraukia visą puslapį, o Pdf.Text(StartIndex, Count) tokiu pat būdu paima mažesnį diapazoną. Character[] vis dar naudingas, kai indekse reikia tik padėties, šrifto arba vėliavėlių duomenų ir pats kodo taškas nenaudojamas — CharacterOrigin[], FontSize[] ir CharacterMapError[] nesvarbu, ar pagrindinis glifas buvo papildomos plokštumos

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;

Ciklo patikra, praleidžianti sugeneruotus ir nesusietus simbolius, yra tas pats įprasto teksto ištraukimo šablonas, naudojamas teksto ištraukimui iš PDF dokumentų naudojant PDFium Component; pasikeičia tik paskutinė eilutė: tiesioginis Character[I] pridėjimas pakeičiamas vieno indekso iškvietimu į Text, todėl papildomos plokštumos simboliai atkeliauja kaip pilnos surogatų poros, o ne kaip pakaitiniai simboliai

Kur tai iš tikrųjų sukelia problemų: pokalbių eksportai, asmenvardžiai ir įterptieji CJK šriftai

Jaustukai pasirodo visur, kur PDF fiksuoja neformalų bendravimą: pokalbių eksportuose, programėlių parduotuvių apžvalgų išrašuose ir pagalbos sistemų pokalbių stenogramose, išsaugotose PDF formatu atitikties archyvui. CJK B plėtinio simboliai pasitaiko rečiau, bet svarbesnėje srityje, asmenvardžiuose ir vietovardžiuose, nes japoniški šeimos registrai, Kinijos namų ūkių registracijos dokumentai ir Taivano tapatybės dokumentai yra klasikiniai simbolių, nepatekusių į įprastą CJK bloką, šaltiniai. Darbo užmokesčio arba tapatybės tikrinimo procesas, ištraukiantis vardus iš nuskaitytų valstybinių dokumentų, yra būtent toks, kuriame tyliai sugadintas simbolis tampa nepavykusiu sutapdinimu, o ne tik kosmetiniu trūkumu

Reti CJK ideografai dažnai susiję ne tik su koduotės, bet ir su šriftų problemomis, nes šriftas turi turėti U+20000 srities kodo taško glifą, kad apskritai būtų galima jį atvaizduoti, o tokių glifų yra nedaugelyje įdiegtų sistemos šriftų. Jei jau tikrinate FontIsEmbedded[] kiekvienam simboliui taip, kaip aprašyta straipsnyje PDF šriftų savybių skaitymas naudojant PDFium Component, tame pačiame indekse patikrinkite abi problemas kartu: dokumentas, kurio Character[] grąžina U+FFFD, o šriftas nurodomas kaip neįterptas, negalės nei teisingai ištraukti, nei išspausdinti to simbolio, todėl taisymas priklauso PDF kūrimo procesui, o ne jūsų ištraukimo kodui

Character[], Charcode[] ir Text ypatybės, aprašytos čia, yra standartinio PDFium Component rinkinio, skirto Delphi ir C++Builder, dalis