Teknisk artikel

PDFium emoji och CJK-tecken förstör WideChar i Delphi

Dra ut emoji eller ett japanskt familjeregisternamn ur en PDF som text, och utdatan visar en ruta, ett frågetecken, eller inget alls där tecknet borde vara. PDFium-komponentens Character[]-egenskap är vanligtvis varför: den läser varje glyf genom FPDFText_GetUnicode, som returnerar en fullständig Unicode-kodpunkt som ett osignerat 32-bitarsvärde, och exponerar den sedan för Delphi som en enda 16-bitars WideChar. Ingen kodpunkt bortom U+FFFF kan göra den resan i ett stycke, och korruptionen visar sig aldrig medan man tittar på den rendrerade sidan, eftersom rendrering och textextraktion körs genom separata kodvägar i PDFium — ett dokument kan visa sina emoji perfekt och ändå ge dig skräp i det ögonblick du läser Character[] i en loop och bygger en sträng av det

Basic Multilingual Plane och varför WideChar stannar vid U+FFFF

Delphis WideChar är en 16-bitars typ som bara kan hålla en UTF-16-kodenhet. Unicodes Basic Multilingual Plane, intervallet U+0000 till U+FFFF, passar exakt inuti det, vilket är varför latin, kyrilliska, grekiska, och det vanliga CJK Unified Ideographs-blocket alla gör resan genom en enda WideChar utan incidenter. Två teckenfamiljer hamnar rutinmässigt utanför det i riktiga dokument: emoji, många av dem i Emoticons-blocket som börjar vid U+1F600, och sällsynta CJK-ideogram från CJK Unified Ideographs Extension B, intervallet U+20000 till U+2A6DF reserverat för mindre vanliga kinesiska, japanska, och koreanska tecken inklusive många person- och platsnamn. UTF-16 hanterar allt ovanför U+FFFF med ett surrogatpar — två 16-bitars kodenheter, en hög surrogat i intervallet $D800 till $DBFF följd av en låg surrogat i $DC00 till $DFFF, som tillsammans kodar en kodpunkt — och matematiken bakom den parningen är tillräckligt fast för att visa direkt i Pascal

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;

Mata U+1F600, det flinande-ansikte-emojin, genom den funktionen och resultatet är en hög surrogat på $D83D och en låg surrogat på $DE00, två 16-bitarsvärden, inte ett. Ingendera halvan betyder något på egen hand; ett ensamt $D83D som sitter i en sträng utan något $DE00 bakom sig är en dinglande surrogat, och det mesta av texthanteringskod som stöter på en sådan antingen tappar den, ersätter den med en ersättningsglyf, eller kastar ett fel

Varför returnerar FPDFText_GetUnicode ett värde Character[] inte kan hålla?

FPDFText_GetUnicode returnerar en LongWord, ett fullständigt 32-bitarsvärde, eftersom PDF-textkodning redan bär det fullständiga Unicode-skalärvärdet för varje glyf. En PDF:s ToUnicode-CMap mappar teckenkoder till Unicode-text, och när en glyf representerar vad som informellt kallas ett astral-plane-tecken — allt bortom Basic Multilingual Plane — är den mappningen en full kodpunkt, inte ett 16-bitars fragment. PDFium avkodar det tillbaka till ett skalärvärde internt och returnerar det över DLL-gränsen genom FPDFText_GetUnicode, och den gränsen är exakt där ett 32-bitarsvärde måste bli något en Delphi-egenskap kan ge tillbaka till din kod

Den uppenbara implementationen är WideChar(FPDFText_GetUnicode(TextPage, Index)), och det är också den fel. En hård cast från ett 32-bitarsvärde till en 16-bitars typ behåller bara de låga 16 bitarna och kastar bort resten tyst, utan något undantag och ingen intervallkontroll. För U+1F600 betyder det att behålla $F600 och förlora faktumet att det sanna värdet någonsin var ovanför U+FFFF, vilket producerar en kodenhet som inte ens är en giltig dinglande surrogat, bara ett orelaterat Basic Multilingual Plane-tecken som råkar dela de låga bitarna. Sammanfoga några tusen av dem till en sträng och nedströms kod har inget sätt kvar att skilja ett korrumperat tecken från ett legitimt

Vad Character[] och Charcode[] returnerar för astral-plane-kodpunkter nu

PDFium-komponentens Character[]- och Charcode[]-egenskaper returnerar U+FFFD, Unicode-ersättningstecknet, närhelst den underliggande kodpunkten överstiger U+FFFF, istället för att tyst trunkera det. Det skyddet sitter direkt inuti egenskapshämtaren bakom 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;

Att returnera U+FFFD istället för ett trunkerat fragment är en medveten, smal fix snarare än en omdesign. Character[] och Charcode[] är typade som WideChar på både TPdf och TPdfView, och att bredda den returtypen för att bära en full kodpunkt skulle bryta varje befintlig anropare som förväntar sig en glyf per index att betyda ett 16-bitarsvärde. U+FFFD är Unicode-standardens egen designerade platshållare för exakt den här situationen, så en anropare som kontrollerar den får en definierad, dokumenterad signal istället för tyst felaktig data. Ett gränsfall värt att känna till: U+FFFD är också ett legitimt tecken i sin egen rätt, så på det sällsynta dokument som redan innehåller en genuin ersättningstecken-glyf är det indexet inte skiljbart från ett trunkerat astral-tecken enbart genom värdet

Hur extraherar man emoji- och CJK Extension B-text korrekt i Delphi?

Anropa Text istället för att gå igenom Character[] närhelst det faktiska textinnehållet spelar roll, eftersom Text läser genom FPDFText_GetText och returnerar en full WString med korrekta surrogatpar för varje astral-plane-tecken i intervallet, snarare än ett fast-breddat värde per index. Pdf.Text(0, MaxInt), eller förkortningen Pdf.Text, extraherar en hel sida korrekt i ett anrop, och Pdf.Text(StartIndex, Count) drar ett mindre intervall på samma sätt. Character[] förtjänar fortfarande sin plats när du bara behöver position, typsnitt, eller flaggdata vid ett index och aldrig rör kodpunkten själv — CharacterOrigin[], FontSize[], och CharacterMapError[] bryr sig inte om huruvida den underliggande glyfen var astral

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;

Hoppa-över-genererat-och-omappat-kontrollen i den loopen är samma mönster som används för vanlig textextraktion i att extrahera text från PDF-dokument med PDFium-komponenten; den enda ändringen är sista raden, som byter ett direkt Character[I]-tillägg mot ett engångsindexanrop in i Text så astral-tecken anländer som fullständiga surrogatpar istället för ersättningsplatshållare

Var det här faktiskt biter: chattexporter, personnamn, och inbäddade CJK-typsnitt

Emoji dyker upp överallt där en PDF fångar informell kommunikation: exporterade chattloggar, app-butiksrecensionsdumper, ärendehanteringssystemtranskript sparade till PDF för ett efterlevnadsarkiv. CJK Extension B dyker upp på ett smalare men mer högriskerat ställe, person- och platsnamn, eftersom japanska familjeregister, kinesiska hushållsregistreringsposter, och taiwanesiska identitetsdokument är klassiska källor för tecken som aldrig kom med i det vanliga CJK-blocket. En lönehanterings- eller identitetsverifieringspipeline som extraherar namn från inskannad myndighetspapper är precis den typen av arbetsbelastning där ett tyst förvanskat tecken förvandlas till en misslyckad matchning snarare än ett kosmetiskt fel

Sällsynta CJK-ideogram tenderar också att resa med typsnittsproblem, inte bara kodningsproblem, eftersom ett typsnitt måste bära en glyf för en kodpunkt i U+20000-intervallet innan något alls kan rendreras, och få installerade systemtypsnitt gör det. Alla som redan går igenom FontIsEmbedded[] per tecken på det sätt att läsa PDF-typsnittsegenskaper med PDFium-komponenten beskriver bör kontrollera samma index för båda problemen tillsammans: ett index som returnerar U+FFFD från Character[] och rapporterar ett icke-inbäddat typsnitt är ett dokument som varken kommer extrahera eller skriva ut det tecknet korrekt, och fixen hör hemma uppströms i hur PDF:en producerades, inte i din extraktionskod

Character[]-, Charcode[]-, och Text-egenskaperna som beskrivs här är en del av standard-PDFium-komponenten för Delphi och C++Builder