Műszaki cikk

A PDFium emoji és CJK karakterei elrontják a WideChar-t Delphiben

Húzz ki emojit vagy egy japán családi anyakönyvi nevet egy PDF-ből szövegként, és a kimenet egy dobozt, egy kérdőjelet, vagy egyáltalán semmit nem mutat ott, ahol a karakternek lennie kellene. A PDFium Component Character[] tulajdonsága általában ennek az oka: minden glyph-et az FPDFText_GetUnicode-on keresztül olvas, amely egy teljes Unicode-kódpontot ad vissza előjel nélküli 32 bites értékként, majd egyetlen 16 bites WideChar-ként teszi elérhetővé Delphinek. Egy U+FFFF fölötti kódpont nem tudja ezt az utat egy darabban megtenni, és a sérülés soha nem mutatkozik meg, amíg a renderelt oldalt nézed, mert a renderelés és a szövegkinyerés külön kódútvonalakon fut a PDFiumban — egy dokumentum tökéletesen megjelenítheti emojiját, és mégis szemetet adhat neked abban a pillanatban, amikor a Character[]-t egy ciklusban olvasod, és egy sztringet építesz belőle

Az Alapvető Többnyelvű Sík, és miért áll meg a WideChar U+FFFF-nél

A Delphi WideChar-ja egy 16 bites típus, amely csak egy UTF-16 kódegységet tud tárolni. A Unicode Alapvető Többnyelvű Síkja (Basic Multilingual Plane), az U+0000-tól U+FFFF-ig terjedő tartomány, pontosan belefér ebbe, ezért a latin, cirill, görög, és a közönséges CJK Unified Ideographs blokk mind hiba nélkül mennek oda-vissza egyetlen WideChar-on keresztül. Két karaktercsalád rutinszerűen esik ezen kívülre valódi dokumentumokban: az emojik, sokuk az U+1F600-nál kezdődő Emoticons blokkban, és a ritka CJK ideogramok a CJK Unified Ideographs Extension B-ből, az U+20000-től U+2A6DF-ig terjedő tartományból, amit a kevésbé gyakori kínai, japán, és koreai karakterek számára tartanak fenn, beleértve sok személy- és helynevet. Az UTF-16 mindent U+FFFF fölött egy szurrogátpárral kezel — két 16 bites kódegység, egy magas szurrogát a $D800-tól $DBFF-ig terjedő tartományban, amit egy alacsony szurrogát követ $DC00-tól $DFFF-ig, amelyek együtt kódolnak egy kódpontot —, és a párosítás mögötti matematika elég rögzített ahhoz, hogy közvetlenül bemutassuk Pascalban

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;

Táplálj be U+1F600-at, a vigyorgó arc emojit, azon a függvényen, és az eredmény egy $D83D magas szurrogát és egy $DE00 alacsony szurrogát, két 16 bites érték, nem egy. Egyik fele sem jelent semmit önmagában; egy magányos $D83D, amely egy sztringben ül, $DE00 nélkül mögötte, egy lógó szurrogát, és a legtöbb szövegkezelő kód, amely eggyel találkozik, vagy eldobja, helyettesítő glyph-fel cseréli, vagy hibát dob

Miért ad vissza az FPDFText_GetUnicode olyan értéket, amit a Character[] nem tud tartani?

Az FPDFText_GetUnicode egy LongWord-öt ad vissza, egy teljes 32 bites értéket, mert a PDF-szövegkódolás már hordozza a teljes Unicode skalárértéket minden glyph-hez. Egy PDF ToUnicode CMap-je karakterkódokat képez le Unicode-szövegre, és amikor egy glyph azt reprezentálja, amit informálisan asztrálsíkú karakternek hívnak — bármit az Alapvető Többnyelvű Síkon túl —, az a leképezés egy teljes kódpont, nem egy 16 bites töredék. A PDFium belsőleg dekódolja azt vissza skalárértékké, és visszaadja a DLL-határon át az FPDFText_GetUnicode-on keresztül, és pontosan az a határ az, ahol egy 32 bites értéknek olyasmivé kell válnia, amit egy Delphi-tulajdonság vissza tud adni a kódodnak

A nyilvánvaló megvalósítás a WideChar(FPDFText_GetUnicode(TextPage, Index)), és ez egyben a rossz is. Egy kemény átalakítás egy 32 bites értékből egy 16 bites típusba csak az alsó 16 bitet tartja meg, és a többit csendben eldobja, kivétel és tartományellenőrzés nélkül. U+1F600 esetén ez azt jelenti, hogy megtartja az $F600-at, és elveszíti azt a tényt, hogy a valódi érték egyáltalán U+FFFF fölött volt, ami egy olyan kódegységet állít elő, amely nem is érvényes lógó szurrogát, csak egy nem kapcsolódó, Alapvető Többnyelvű Síkbeli karakter, amely véletlenül osztozik azokon az alsó biteken. Fűzz össze néhány ezret ezekből egy sztringbe, és a downstream kódnak nem marad módja megkülönböztetni egy sérült karaktert egy legitimtől

Mit ad vissza most a Character[] és a Charcode[] asztrálsíkú kódpontokra?

A PDFium Component Character[] és Charcode[] tulajdonságai U+FFFD-t, a Unicode helyettesítő karaktert adják vissza, valahányszor az alapul szolgáló kódpont meghaladja az U+FFFF-et, ahelyett hogy csendben csonkolnák. Ez az őr közvetlenül a Character[] mögötti tulajdonság-getterben ül

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;

Az U+FFFD visszaadása csonkolt töredék helyett egy szándékos, szűk javítás, nem egy újratervezés. A Character[] és a Charcode[] WideChar-ként vannak típusozva mind a TPdf-en, mind a TPdfView-n, és annak a visszatérési típusnak a szélesítése egy teljes kódpont hordozásához eltörne minden meglévő hívót, amely azt várja, hogy egy glyph indexenként egy 16 bites értéket jelentsen. Az U+FFFD a Unicode Szabvány saját, kifejezetten erre a helyzetre kijelölt helykitöltője, így egy hívó, amely ellenőrzi azt, egy definiált, dokumentált jelzést kap ahelyett hogy csendben rossz adatot kapna. Egy határeset, amit érdemes tudni: az U+FFFD önmagában is legitim karakter, így azon a ritka dokumentumon, amely már valódi helyettesítő-karakter glyph-et tartalmaz, az az index önmagában, érték alapján megkülönböztethetetlen egy csonkolt asztrálkarakterrel

Hogyan kinyerünk emojit és CJK Extension B szöveget helyesen Delphiben?

Hívd a Text-et a Character[] bejárása helyett, valahányszor a tényleges szöveges tartalom számít, mert a Text az FPDFText_GetText-en keresztül olvas, és egy teljes WString-et ad vissza megfelelő szurrogátpárokkal minden tartományon belüli asztrálsíkú karakterhez, nem pedig egy fix szélességű értéket indexenként. A Pdf.Text(0, MaxInt), vagy a rövidebb Pdf.Text, egy teljes oldalt helyesen kinyer egyetlen hívásban, és a Pdf.Text(StartIndex, Count) ugyanígy húz ki egy kisebb tartományt. A Character[] még mindig kiérdemli helyét, amikor csak pozíció-, betűtípus-, vagy jelzőadatra van szükséged egy indexnél, és soha nem nyúlsz magához a kódponthoz — a CharacterOrigin[], a FontSize[], és a CharacterMapError[] nem törődik azzal, hogy az alapul szolgáló glyph asztrál volt-e

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;

A kihagyd-a-generáltakat-és-le-nem-térképezetteket ellenőrzés abban a ciklusban ugyanaz a minta, amit a sima szövegkinyeréshez használnak a szöveg kinyerése PDF-dokumentumokból a PDFium Componenttel cikkben; az egyetlen változás az utolsó sor, amely egy közvetlen Character[I] hozzáfűzést cserél egy egy-indexű hívásra a Text-be, így az asztrálkarakterek teljes szurrogátpárokként érkeznek, nem helyettesítő-jelölőkként

Hol harap ez ténylegesen: chatexportok, személynevek, és beágyazott CJK-betűtípusok

Az emojik ott jelennek meg, ahol egy PDF informális kommunikációt rögzít: exportált chat-naplók, appstore-értékelés-dömpingek, jegykezelő-rendszer-átiratok, amelyeket megfelelőségi archívum céljából PDF-be mentettek. A CJK Extension B egy szűkebb, de magasabb tétű helyen jelenik meg, személy- és helyneveknél, mert a japán családi anyakönyvek, a kínai háztartás-nyilvántartási feljegyzések, és a tajvani személyazonossági dokumentumok klasszikus forrásai olyan karaktereknek, amelyek soha nem kerültek be a közönséges CJK blokkba. Egy bérszámfejtési vagy személyazonosság-ellenőrzési csővezeték, amely neveket nyer ki beszkennelt kormányzati papírmunkából, pontosan az a fajta munkaterhelés, ahol egy csendben eltorzult karakter meghiúsult egyezéssé válik, nem kozmetikai hibává

A ritka CJK ideogramok hajlamosak betűtípus-problémákkal is együtt utazni, nem csak kódolási problémákkal, mert egy betűtípusnak glyph-et kell hordoznia egy U+20000-tartományba eső kódponthoz, mielőtt egyáltalán bármi is renderelődhetne, és kevés telepített rendszer-betűtípus teszi ezt. Bárki, aki már bejárja a FontIsEmbedded[]-et karakterenként, ahogyan a PDF-betűtípus-tulajdonságok olvasása a PDFium Componenttel leírja, ugyanazt az indexet mindkét problémára együtt kellene ellenőriznie: egy index, amely U+FFFD-t ad vissza a Character[]-ból, és nem-beágyazott betűtípust jelent, egy olyan dokumentum, amely sem nem fogja azt a karaktert helyesen kinyerni, sem nyomtatni, és a javítás upstream tartozik, abban, hogyan állították elő a PDF-et, nem a kinyerő kódodban

Az itt leírt Character[], Charcode[], és Text tulajdonságok a Delphihez és C++Builderhez készült szabványos PDFium Komponens részei