Teknisk artikel

PDFium-emoji og CJK-tegn ødelægger WideChar i Delphi

Træk emoji eller et japansk familieregister-navn ud af en PDF som tekst, og outputtet viser en boks, et spørgsmålstegn, eller intet overhovedet, hvor tegnet burde være. PDFium-komponentens Character[]-egenskab er som regel årsagen: den læser hver glyf gennem FPDFText_GetUnicode, som returnerer et fuldt Unicode-kodepunkt som en unsigned 32-bit-værdi, og eksponerer det derefter til Delphi som en enkelt 16-bit WideChar. Ethvert kodepunkt forbi U+FFFF kan ikke foretage den tur i ét stykke, og korruptionen viser sig aldrig, mens man kigger på den gengivne side, fordi gengivelse og tekstudtrækning kører gennem separate kodeveje i PDFium — et dokument kan vise sine emoji perfekt og stadig give dig skrammel, i det øjeblik man læser Character[] i en løkke og bygger en streng ud af den

Basic Multilingual Plane, og hvorfor WideChar stopper ved U+FFFF

Delphis WideChar er en 16-bit type, der kun kan holde én UTF-16-kodeenhed. Unicodes Basic Multilingual Plane, intervallet U+0000 til U+FFFF, passer nøjagtig indeni det, hvilket er grunden til, at latin, kyrillisk, græsk, og den almindelige CJK Unified Ideographs-blok alle går tur-retur gennem en enkelt WideChar uden hændelser. To familier af tegn falder rutinemæssigt uden for det i rigtige dokumenter: emoji, mange af dem i Emoticons-blokken der starter ved U+1F600, og sjældne CJK-ideogrammer fra CJK Unified Ideographs Extension B, intervallet U+20000 til U+2A6DF reserveret til mindre almindelige kinesiske, japanske og koreanske tegn, inklusive mange personlige og stednavne. UTF-16 håndterer alt over U+FFFF med et surrogatpar — to 16-bit kodeenheder, en høj surrogat i intervallet $D800 til $DBFF efterfulgt af en lav surrogat i $DC00 til $DFFF, som sammen koder ét kodepunkt — og matematikken bag den parring er fast nok til at demonstrere direkte 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;

Fodr U+1F600, den grinende-ansigt-emoji, gennem den funktion, og resultatet er en høj surrogat på $D83D og en lav surrogat på $DE00, to 16-bit-værdier, ikke én. Ingen af halvdelene betyder noget alene; en enlig $D83D siddende i en streng uden noget $DE00 bag sig er en dinglende surrogat, og det meste tekst-håndterende kode, der støder på en, dropper den enten, erstatter den med en erstatningsglyf, eller kaster en fejl

Hvorfor returnerer FPDFText_GetUnicode en værdi Character[] ikke kan holde?

FPDFText_GetUnicode returnerer en LongWord, en fuld 32-bit værdi, fordi PDF-tekst-encoding allerede bærer den fulde Unicode-skalarværdi for hver glyf. En PDFs ToUnicode-CMap mapper tegnkoder til Unicode-tekst, og når en glyf repræsenterer, hvad der uformelt kaldes et astral-plan-tegn — alt forbi Basic Multilingual Plane — er den mapning et fuldt kodepunkt, ikke et 16-bit fragment. PDFium dekoder det tilbage til en skalarværdi internt og returnerer det på tværs af DLL-grænsen gennem FPDFText_GetUnicode, og den grænse er præcis, hvor en 32-bit værdi skal blive til noget, en Delphi-egenskab kan give tilbage til ens kode

Den indlysende implementering er WideChar(FPDFText_GetUnicode(TextPage, Index)), og det er også den forkerte. En hård cast fra en 32-bit værdi ind i en 16-bit type beholder kun de nederste 16 bits og smider resten væk i stilhed, uden nogen undtagelse og intet intervaltjek. For U+1F600 betyder det at beholde $F600 og miste det faktum, at den sande værdi nogensinde var over U+FFFF, hvilket producerer en kodeenhed, der ikke engang er en gyldig dinglende surrogat, bare et urelateret Basic Multilingual Plane-tegn, der tilfældigvis deler de lave bits. Sammenkæd et par tusind af dem til en streng, og nedstrøms kode har ingen måde tilbage at skelne et korrupt tegn fra et legitimt et

Hvad returnerer Character[] og Charcode[] for astral-plan-kodepunkter nu?

PDFium-komponentens Character[]- og Charcode[]-egenskaber returnerer U+FFFD, Unicode-erstatningstegnet, hver gang det underliggende kodepunkt overstiger U+FFFF, i stedet for i stilhed at afkorte det. Den vagt sidder direkte inde i egenskabs-getteren bag 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;

At returnere U+FFFD i stedet for et afkortet fragment er en bevidst, snæver fix frem for et redesign. Character[] og Charcode[] er typet som WideChar på både TPdf og TPdfView, og at udvide den returtype til at bære et fuldt kodepunkt ville ødelægge hver eksisterende kalder, der forventer, at én glyf pr. indeks betyder én 16-bit værdi. U+FFFD er selve Unicode-standardens udpegede pladsholder til netop denne situation, så en kalder der tjekker for det, får et defineret, dokumenteret signal i stedet for i stilhed forkert data. Én grænsesag værd at kende: U+FFFD er også et legitimt tegn i sin egen ret, så på det sjældne dokument, der allerede indeholder en ægte erstatningstegn-glyf, er det indeks umuligt at skelne fra et afkortet astral-tegn ved værdi alene

Hvordan udtrækker man emoji og CJK Extension B-tekst korrekt i Delphi?

Kald Text i stedet for at gennemgå Character[], hver gang det faktiske tekstindhold betyder noget, fordi Text læser gennem FPDFText_GetText og returnerer en fuld WString med korrekte surrogatpar for hvert astral-plan-tegn i intervallet, frem for én fast-bredde-værdi pr. indeks. Pdf.Text(0, MaxInt), eller genvejen Pdf.Text, udtrækker en hel side korrekt i ét kald, og Pdf.Text(StartIndex, Count) trækker et mindre interval på samme måde. Character[] fortjener stadig sin plads, når man kun har brug for position, skrifttype eller flag-data ved et indeks og aldrig rører selve kodepunktet — CharacterOrigin[], FontSize[] og CharacterMapError[] er ligeglade med, om den underliggende glyf 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;

Spring-genereret-og-ikke-mappet-tjekket i den løkke er det samme mønster brugt til ren-tekst-udtrækning i udtrækning af tekst fra PDF-dokumenter med PDFium-komponenten; den eneste ændring er den sidste linje, som bytter en direkte Character[I]-tilføjelse for et et-indeks-kald ind i Text, så astral-tegn ankommer som komplette surrogatpar frem for erstatnings-pladsholdere

Hvor dette rent faktisk bider: chat-eksporter, personlige navne og indlejrede CJK-skrifttyper

Emoji dukker op, hvor end en PDF indfanger uformel kommunikation: eksporterede chatlogs, app-store-anmeldelses-dumps, ticketing-system-transskripter gemt som PDF til et compliance-arkiv. CJK Extension B dukker op i et snævrere, men højere-indsats-sted, personlige navne og stednavne, fordi japanske familieregistre, kinesiske husstandsregistreringsregistre, og taiwanske identitetsdokumenter er klassiske kilder til tegn, der aldrig kom ind i den almindelige CJK-blok. En lønnings- eller identitetsverificerings-pipeline, der udtrækker navne fra scannede offentlige papirer, er præcis den slags arbejdsbyrde, hvor et i-stilhed-forvansket tegn bliver til et mislykket match frem for en kosmetisk fejl

Sjældne CJK-ideogrammer har også en tendens til at rejse med skrifttype-problemer, ikke kun encoding-problemer, fordi en skrifttype skal bære en glyf til et U+20000-interval-kodepunkt, før noget som helst kan gengives overhovedet, og få installerede systemskrifttyper gør. Alle der allerede gennemgår FontIsEmbedded[] per tegn på den måde, at læse PDF-skrifttype-egenskaber med PDFium-komponenten beskriver, bør tjekke det samme indeks for begge problemer sammen: et indeks der returnerer U+FFFD fra Character[] og rapporterer en ikke-indlejret skrifttype, er et dokument, der hverken vil udtrække eller udskrive det tegn korrekt, og fixen hører hjemme opstrøms i, hvordan PDF'en blev produceret, ikke i ens udtrækningskode

Character[]-, Charcode[]- og Text-egenskaberne beskrevet her er en del af standard-PDFium-komponenten til Delphi og C++Builder