PDFium Component for Delphi legger inn systemfontene som TPdf.AddText bruker, som CID-fonter nøklet etter Unicode-kodepunkt, så hver CID bærer nøyaktig én ToUnicode-mapping. Det er det som stopper ekstraherte mellomrom fra å komme tilbake som U+00A0 (no-break space) og bindestreker som U+00AD (soft hyphen), både fra det levende dokumentet og fra den lagrede filen
Symptomet er dumt fordi det er usynlig. Et søkeindeks bommer på «two-x» fordi den lagrede strengen inneholder en myk bindestrek, en CSV-eksport deler annerledes, et diff-verktøy flagger linjer som ser identiske ut i alle visere. Ingenting i den renderede siden er feil; bare Unicode-en bak glyfene er det
Hvorfor kommer ekstraherte mellomrom tilbake som U+00A0?
Ekstraherte mellomrom blir til U+00A0 fordi ToUnicode CMap-en som PDFium genererer i FPDFText_LoadFont, er nøklet etter glyf, og én glyf kan nås fra to kodepunkter. I Arial serverer glyf 3 både U+0020 og U+00A0, og bindestrek-glyfen serverer både U+002D og U+00AD. Den genererte CMap-en mapper derfor samme CID to ganger, én gang gjennom en bfchar-oppføring og én gang gjennom en array-form bfrange, og den oppføringen leserens prioritetsregel begunstiger, blir den ekstraherte teksten
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Lenge var denne motsetningen ufarlig, siden PDFium-leseren lot den laveste mappingen vinne. En oppstrøms endring byttet lesere til sist-vinner, og fra det bygget og frem ekstraherte hvert mellomrom skrevet med AddText som NBSP og hver bindestrek som en myk bindestrek. Legg merke til mønsteret i parene: 0x20/0xA0 og 0x2D/0xAD skiller seg bare i den høye biten, som er nøyaktig det du ville forvente fra en font hvis cmap sender Latin-1 tvillingene til samme omriss. Var uttrekk-koden din fin i går og feiler den nå på usynlige tegn, dump kodepunktene i stedet for å stole på feilsøkervisningen; det grunnleggende i å hente ut tekst er dekket i å ekstrahere tekst fra PDF-dokumenter med PDFium i Delphi
uses
SysUtils, PDFium;
const
// Space/U+00A0 og hyphen/U+00AD deler én Arial-glyf, og det gjør
// gresk Omega (U+03A9) og Ohm-tegnet (U+2126) også
Sample: WString = 'two-x'#$00A0'y'#$00AD'z '#$03A9#$2126;
function CodePoints(const S: WString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + 'U+' + IntToHex(Ord(S[I]), 4) + ' ';
end;
var
Pdf: TPdf;
Live, Reloaded: WString;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(1, 595, 842);
Pdf.AddText(Sample, 'Arial', 12, 72, 770);
Live := Pdf.Text; // live, ulagret dokument
Pdf.SaveAs('codepoints.pdf');
finally
Pdf.Free;
end;
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'codepoints.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Reloaded := Pdf.Text; // etter en full lagring og gjeninnlesing
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Hvorfor det å lappe CMap-en etter lagring ikke var nok
Å lappe den lagrede filen fikser bare den lagrede filen, og bare hvis lappen holder CMap-strukturen intakt byte for byte. Den første fiksen, RepairSubsetToUnicodeCMaps i uniten FPdfCompress, kjører etter hver ikke-inkrementelle TPdf.SaveAs og løser hver konflikterende CID: bfchar-oppføringen vinner, et par som skiller seg bare i den høye biten løses til det mindre base-latinske kodepunktet, og alt annet beholder sin første mapping
Den interessante delen er det negative resultatet. Å bygge den konflikterende CMap-en pent opp igjen, i enten start-kode- eller array-form, så ut som det åpenbare trekket, og PDFium avviste hver gjenoppbygd CMap kontant og falt tilbake til Identity. Det eneste output-et den native leseren aksepterte, var en like-lang in-place-erstatning av de konflikterende heks-verdiene, med blokklayout og CID-dekning urørt. Den andre lærdommen var ydmykere: notatet vårt på den tiden skyldte på at tilfellet i minnet kom av at det levende dokumentet ikke hadde noen ToUnicode-strøm i det hele tatt. Å kalle DLL-en direkte motsa det, siden det levende dokumentet bærer den samme tvetydige strømmen, noe som betydde at den egentlige fiksen måtte skje før PDFium i det hele tatt genererte CMap-en. Reparasjonsrutinen blir værende i biblioteket som et forsvar for PDF-er produsert av andre PDFium-baserte verktøy
uses
Classes, FPdfCompress;
var
Source, Dest: TFileStream;
begin
Source := TFileStream.Create('from-other-tool.pdf',
fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create('repaired.pdf', fmCreate);
try
// Kun like-lange endringer; filer uten en reparerbar konflikt,
// og kryssreferansestrøm- eller objektstrømfiler, kopieres som de er
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Nøkle fonten etter kodepunkt i stedet for etter glyf
Rotfiksen er å slutte å be PDFium generere CMap-en i det hele tatt. TPdf.LoadCachedFont overleverer nå systemfont-bytene til TPdf.LoadUnicodeKeyedCidFont, som leser fontens egen sfnt cmap-tabell, med preferanse for en format 12-subtabell og fallback til format 4. Kodepunkter kommer tilbake sortert og avdubbet, og CID k+1 tildeles det k-te kodepunktet, med CID 0 igjen som .notdef. En eksplisitt CIDToGIDMap sender hver CID til sin glyf, så U+0020 og U+00A0 får to forskjellige CID-er som tegner samme omriss, og ToUnicode CMap-en mapper hver CID til bare ett kodepunkt. Fonten lastes så gjennom FPDFText_LoadCidType2Font, det samme inngangspunktet bak glyfnivå-skriving i CID Type 2 font-innbygging med eksplisitte CID-til-GID-mapper
// Komprimert fra TPdf.LoadUnicodeKeyedCidFont
SetLength(CidToGidMap, (Length(Entries) + 1) * 2); // CID 0 = .notdef
for I := 0 to High(Entries) do
begin
CidToGidMap[(I + 1) * 2] := Byte(Entries[I].GlyphID shr 8);
CidToGidMap[(I + 1) * 2 + 1] := Byte(Entries[I].GlyphID and $FF);
end;
ToUnicode := BuildUnicodeKeyedCidCMap(Entries); // én CID, ett kodepunkt
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Når FPDFText_SetText senere skriver en streng, lander oppslaget bakover på én enkelt CID per tegn, så NBSP, myk bindestrek og Ohm-tegnet overlever hver som seg selv under en hvilken som helst prioritetsregel, i minnet og etter enhver lagring. Fordi den lagrede filen bærer komponentens egen ToUnicode-strøm i stedet for en motor-generert, finner RepairSubsetToUnicodeCMaps ingenting å fikse i den
Hvorfor kan én bfrange-oppføring utslette en hel blokk?
En enkelt bfrange hvis CID-rekke krysser en xxFF-grense, får PDFium til å forkaste hele blokken den ligger i. ISO 32000-1 §9.10.3 lar bare den siste byten av destinasjonen variere innenfor et område, men CID-siden har sin egen felle: PDFiums HandleBeginBFRange avleder den høye CID-en som (low and $FFFFFF00) or (high and $FF). En rekke fra CID 00FE til 0101 leses derfor som 00FE til 0001, lav større enn høy, og hele blokken merkes som ugyldig. Feilen er stille: SetText lykkes, siden renderer perfekt, og uttrekket returnerer U+0000 for hvert tegn i den blokken
BuildUnicodeKeyedCidCMap avslutter en rekke før enten kodepunktet eller CID-en når en lav byte på FF, holder hver blokk innenfor 100-oppføringsgrensen i CMap-grammatikken og skriver kodepunkter i supplementært plan som individuelle bfchar-oppføringer med UTF-16 surrogatpar-destinasjoner, siden å inkrementere et surrogatpar inne i et område ikke har noen definert betydning; surrogatsiden av den historien finnes i håndtering av emoji, CJK og surrogatpar i Delphi. En kun-bfchar-CMap ville omgå grenseproblemet fullstendig, til flere ganger størrelsen
Hva dekker ikke den kodepunkt-nøkle fonten?
Kodepunkt-nøkle stien dekker hver font som eksponerer en Unicode-cmap-subtabell, og faller tilbake til den gamle glyf-nøkle oppførselen for resten. Grensene verdt å kjenne før du støtter deg til den:
- Symbolfonter med bare en (3,0) cmap, og enhver font CID-stien feiler å laste, går gjennom
FPDFText_LoadFontsom før, så en glyf delt av to kodepunkter kan fortsatt ekstraheres tvetydig der - Uten en format 12-subtabell er mappen begrenset til BMP-en, og oppføringsantallet er taksert til 65535 slik at hver CID får plass i to byte over null
- Inkrementelle lagringer (
saIncremental) hopper overRepairSubsetToUnicodeCMapsmed vilje, fordi en inkrementell revisjon må forbli append-only; de kodepunkt-nøkle fontene gjør det irrelevant for tekst komponenten skriver selv - TrueType Collections trenger ekstra omsorg: GDI
GetFontDatareturnerer hele .ttc-en, ogFPDFText_LoadCidType2Fonthar ingen face-indeksparameter, så å be om NSimSun fra simsun.ttc pleide å bygge inn og rendre SimSun, face 0. Komponenten matcher nå familienavnet mot navnetabellen (nameID 1 og 16) og trekker ut den etterspurte face-en som en frittstående sfnt før cmap-en parses; feiler parsingen, går samlingsbytene rett gjennom og oppførselen går tilbake til face 0
Tekstskriving, fontinnbygging og uttrekk deler én sidemodell på tvers av Delphi, C++Builder og Lazarus, og hele API-et er beskrevet på produktsiden for PDFium Component for Delphi