Technisch artikel

PDFium: emoji en CJK-tekens breken WideChar in Delphi

Haal emoji of een Japanse familieregisternaam als tekst uit een PDF, en de uitvoer toont een vakje, een vraagteken, of helemaal niets op de plek waar het teken zou moeten staan. De eigenschap Character[] van PDFium Component is meestal de oorzaak: deze leest elk glyph via FPDFText_GetUnicode, dat een volledig Unicode-codepunt teruggeeft als een unsigned 32-bits waarde, en stelt dit vervolgens aan Delphi bloot als een enkele 16-bits WideChar. Elk codepunt voorbij U+FFFF kan die reis niet in één stuk maken, en de beschadiging komt nooit naar boven terwijl u naar de gerenderde pagina kijkt, omdat rendering en tekstextractie via aparte codepaden in PDFium lopen — een document kan zijn emoji perfect weergeven en u toch rommel overhandigen zodra u Character[] in een lus leest en er een string van opbouwt

Het Basic Multilingual Plane en waarom WideChar bij U+FFFF stopt

WideChar van Delphi is een 16-bits type dat slechts één UTF-16-code-eenheid kan bevatten. Het Basic Multilingual Plane van Unicode, het bereik U+0000 tot U+FFFF, past daar precies in, en dat is waarom Latijns, Cyrillisch, Grieks, en het gewone CJK Unified Ideographs-blok allemaal probleemloos via één WideChar heen en weer reizen. Twee families tekens vallen in echte documenten routinematig daarbuiten: emoji, waarvan vele in het Emoticons-blok beginnend bij U+1F600, en zeldzame CJK-ideogrammen uit CJK Unified Ideographs Extension B, het bereik U+20000 tot en met U+2A6DF, gereserveerd voor minder gangbare Chinese, Japanse en Koreaanse tekens, inclusief veel persoonlijke namen en plaatsnamen. UTF-16 handelt alles boven U+FFFF af met een surrogaatpaar — twee 16-bits code-eenheden, een hoge surrogaat in het bereik $D800 tot $DBFF gevolgd door een lage surrogaat in $DC00 tot $DFFF, die samen één codepunt coderen — en de wiskunde achter die koppeling is vast genoeg om rechtstreeks in Pascal te demonstreren

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;

Voer U+1F600, de grijnzende-gezicht-emoji, door die functie en het resultaat is een hoge surrogaat van $D83D en een lage surrogaat van $DE00, twee 16-bits waarden, niet één. Geen van beide helften betekent op zichzelf iets; een losse $D83D in een string zonder $DE00 erachter is een bungelende surrogaat, en de meeste tekstverwerkende code die er eentje tegenkomt, laat deze vallen, vervangt deze door een vervangingsglyph, of werpt een fout op

Waarom geeft FPDFText_GetUnicode een waarde terug die Character[] niet kan bevatten?

FPDFText_GetUnicode geeft een LongWord terug, een volledige 32-bits waarde, omdat PDF-tekstcodering al de volledige Unicode-scalaire waarde voor elk glyph draagt. De ToUnicode-CMap van een PDF koppelt tekencodes aan Unicode-tekst, en wanneer een glyph staat voor wat informeel een astraal-vlak-teken wordt genoemd — alles voorbij het Basic Multilingual Plane — is die koppeling een volledig codepunt, geen 16-bits fragment. PDFium decodeert dit intern terug naar een scalaire waarde en geeft dit terug over de DLL-grens via FPDFText_GetUnicode, en precies die grens is waar een 32-bits waarde iets moet worden dat een Delphi-eigenschap aan uw code kan teruggeven

De voor de hand liggende implementatie is WideChar(FPDFText_GetUnicode(TextPage, Index)), en het is ook de verkeerde. Een harde cast van een 32-bits waarde naar een 16-bits type behoudt alleen de laagste 16 bits en gooit de rest stilzwijgend weg, zonder uitzondering en zonder bereikcontrole. Voor U+1F600 betekent dat het behouden van $F600 en het verliezen van het feit dat de werkelijke waarde ooit boven U+FFFF lag, wat een code-eenheid oplevert die zelfs geen geldige bungelende surrogaat is, gewoon een ongerelateerd teken uit het Basic Multilingual Plane dat toevallig diezelfde lage bits deelt. Voeg een paar duizend daarvan samen tot een string en downstream-code heeft geen manier meer om een beschadigd teken van een legitiem teken te onderscheiden

Wat geven Character[] en Charcode[] nu terug voor astraal-vlak-codepunten?

De eigenschappen Character[] en Charcode[] van PDFium Component geven U+FFFD terug, het Unicode-vervangingsteken, wanneer het onderliggende codepunt U+FFFF overschrijdt, in plaats van dit stilzwijgend af te kappen. Die bewaking zit direct in de property-getter achter 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;

U+FFFD teruggeven in plaats van een afgekapt fragment is een doelbewuste, nauw begrensde fix, geen herontwerp. Character[] en Charcode[] zijn getypeerd als WideChar op zowel TPdf als TPdfView, en dat retourtype verbreden om een volledig codepunt te dragen, zou elke bestaande aanroeper breken die verwacht dat één glyph per index één 16-bits waarde betekent. U+FFFD is de eigen aangewezen plaatshouder van de Unicode-standaard voor precies deze situatie, dus een aanroeper die hierop controleert, krijgt een gedefinieerd, gedocumenteerd signaal terug in plaats van stilzwijgend verkeerde data. Eén grensgeval is het waard om te kennen: U+FFFD is ook een legitiem teken op zichzelf, dus op het zeldzame document dat al een echte vervangingstekenglyph bevat, is die index door waarde alleen niet te onderscheiden van een afgekapt astraal teken

Hoe extraheert u emoji en CJK Extension B-tekst correct in Delphi?

Roep Text aan in plaats van door Character[] te lopen wanneer de daadwerkelijke tekstuele inhoud ertoe doet, omdat Text leest via FPDFText_GetText en een volledige WString teruggeeft met correcte surrogaatparen voor elk astraal-vlak-teken binnen bereik, in plaats van één waarde met vaste breedte per index. Pdf.Text(0, MaxInt), of de verkorte vorm Pdf.Text, extraheert een hele pagina correct in één aanroep, en Pdf.Text(StartIndex, Count) haalt een kleiner bereik op dezelfde manier op. Character[] verdient nog steeds zijn plek wanneer u alleen positie-, lettertype-, of vlaggegevens op een index nodig heeft en het codepunt zelf nooit aanraakt — CharacterOrigin[], FontSize[], en CharacterMapError[] geven niet om of het onderliggende glyph astraal was

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;

De skip-generated-en-unmapped-controle in die lus is hetzelfde patroon dat wordt gebruikt voor platte-tekstextractie in het extraheren van tekst uit PDF-documenten met PDFium Component; de enige wijziging is de laatste regel, die een directe Character[I]-toevoeging inruilt voor een aanroep van Text op één index, zodat astrale tekens als volledige surrogaatparen aankomen in plaats van vervangingsplaceholders

Waar dit werkelijk pijn doet: chatexports, persoonlijke namen, en ingebedde CJK-lettertypen

Emoji duiken overal op waar een PDF informele communicatie vastlegt: geëxporteerde chatlogs, dumps van app-store-recensies, transcripties uit ticketingsystemen die voor een compliance-archief als PDF zijn opgeslagen. CJK Extension B duikt op een smallere maar hoger-risico plek op, persoonlijke namen en plaatsnamen, omdat Japanse familieregisters, Chinese huishoudregistratiegegevens, en Taiwanese identiteitsdocumenten klassieke bronnen zijn van tekens die het nooit tot het gewone CJK-blok hebben geschopt. Een loonadministratie- of identiteitsverificatiepijplijn die namen extraheert uit gescande overheidsdocumenten is precies het soort werklast waarbij een stilzwijgend verminkt teken uitmondt in een mislukte match in plaats van een cosmetisch glitchje

Zeldzame CJK-ideogrammen gaan doorgaans ook gepaard met lettertypeproblemen, niet alleen coderingsproblemen, omdat een lettertype een glyph moet dragen voor een codepunt in het U+20000-bereik voordat er überhaupt iets kan renderen, en weinig geïnstalleerde systeemlettertypen doen dat. Iedereen die al per teken FontIsEmbedded[] doorloopt zoals het lezen van PDF-lettertype-eigenschappen met PDFium Component beschrijft, moet dezelfde index op beide problemen tegelijk controleren: een index die U+FFFD teruggeeft van Character[] en een niet-ingebed lettertype rapporteert, is een document dat dat teken noch correct zal extraheren noch correct zal afdrukken, en de oplossing hoort stroomopwaarts thuis, in hoe de PDF is geproduceerd, niet in uw extractiecode

De hier beschreven eigenschappen Character[], Charcode[], en Text maken deel uit van de standaard PDFium Component voor Delphi en C++Builder