PDFium Component til Delphi indlejrer de systemfonts, som TPdf.AddText bruger, som CID-fonts nøglet efter Unicode-kodepunkt, så hver CID bærer præcis én ToUnicode-mapping. Det er dét, der forhindrer udtrukne mellemrum i at komme tilbage som U+00A0 (hårdt mellemrum) og bindestreger som U+00AD (blødt bindestregstegn), både fra det levende dokument og fra den gemte fil
Symptomet er ubehageligt, fordi det er usynligt. Et søgeindeks missér "two-x", fordi den gemte streng indeholder et blødt bindestregstegn, en CSV-eksport deler anderledes, et diff-værktøj flagger linjer, der ser identiske ud i alle viewere. Intet i den renderede side er forkert; kun Unicode bag glyfferne er det
Hvorfor kommer udtrukne mellemrum tilbage som U+00A0?
Udtrukne mellemrum bliver til U+00A0, fordi ToUnicode CMap'en, som PDFium genererer i FPDFText_LoadFont, er nøglet efter glyf, og én glyf kan nås fra to kodepunkter. I Arial betjener glyf 3 både U+0020 og U+00A0, og bindestregs-glyffen betjener både U+002D og U+00AD. Den genererede CMap mapper derfor samme CID to gange, én gang gennem en bfchar-entry og én gang gennem en array-formet bfrange, og dén entry, som læserens prioritetsregel begunstiger, bliver den udtrukne tekst
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Længe var denne modstrid harmløs, da PDFium-læseren lod den laveste mapping vinde. En upstream-ændring skiftede læseren til last-wins, og fra den build af udtrak hvert mellemrum skrevet med AddText som NBSP og hver bindestreg som et blødt bindestregstegn. Bemærk mønsteret i parrene: 0x20/0xA0 og 0x2D/0xAD adskiller sig kun i den høje bit, hvilket er præcis, hvad man ville forvente fra en font, hvis cmap sender Latin-1 look-alikes til samme outline. Var din udtrækskode fin i går og fejler nu på usynlige tegn, så dump kodepunkterne frem for at stole på debugger-viewen; grundlæggende om at trække tekst ud er dækket i udtræk af tekst fra PDF-dokumenter med PDFium i Delphi
uses
SysUtils, PDFium;
const
// Space/U+00A0 og bindestreg/U+00AD deler én Arial-glyf, og det gør
// græsk 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; // levende, ugemt 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; // efter en fuld gemning og genindlæsning
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Hvorfor det ikke var nok at lappe CMap'en efter gemning
At lappe den gemte fil retter kun den gemte fil, og kun hvis lappen holder CMap-strukturen byte-for-byte intakt. Det første fix, RepairSubsetToUnicodeCMaps i uniten FPdfCompress, kører efter hver ikke-inkrementel TPdf.SaveAs og resolver hver konfliktende CID: bfchar-entryen vinder, et par, der kun adskiller sig i den høje bit, resolser til det mindre base-Latin-kodepunkt, og alt andet beholder sin første mapping
Den interessante del er det negative resultat. At genbygge den konfliktende CMap rent, i enten start-kode- eller array-form, lignede det oplagte træk, og PDFium afviste hver genbygget CMap kategorisk og faldt tilbage til Identity. Det eneste output, den native læser accepterede, var en ligelang erstatning på stedet af de konfliktende hex-værdier, med bloklayout og CID-dækning urørt. Anden lektie var mere ydmygende: vores note på daværende tidspunkt bebrejdede in-memory-tilfældet det levende dokument slet ikke at have en ToUnicode-stream. At kalde DLL'en direkte modbeviste det, da det levende dokument bærer samme tvetydige stream, hvilket betød, at det rigtige fix skulle ske, før PDFium nogensinde genererede CMap'en. Reparationsrutinen forbliver i biblioteket som et værn mod PDF'er produceret af andre PDFium-baserede værktøjer
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 ligelange redigeringer; filer uden reparerbar konflikt,
// og cross-reference-stream- eller object-stream-filer, kopieres som de er
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
At nøgle fonten efter kodepunkt i stedet for efter glyf
Rodfixet er at holde op med at bede PDFium om overhovedet at generere CMap'en. TPdf.LoadCachedFont giver nu systemfontens bytes til TPdf.LoadUnicodeKeyedCidFont, som læser fontens egen sfnt cmap-tabel, foretrækker en format 12-undertabel og falder tilbage til format 4. Kodepunkter kommer tilbage sorteret og deduplikerede, og CID k+1 tildeles det k'te kodepunkt, med CID 0 efterladt som .notdef. En eksplicit CIDToGIDMap sender hver CID til sin glyf, så U+0020 og U+00A0 får to forskellige CID'er, der tegner samme outline, og ToUnicode CMap'en mapper hver CID til ét kodepunkt. Fonten indlæses derefter gennem FPDFText_LoadCidType2Font, samme indgangspunkt bag glyf-niveau-skrivning i CID Type 2 font embedding med eksplicitte CID-til-GID-maps
// Komprimeret 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, ét kodepunkt
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Når FPDFText_SetText senere skriver en streng, lander det omvendte opslag på én enkelt CID pr. tegn, så NBSP, blødt bindestregstegn og Ohm-tegnet hver overlever som sig selv under begge prioritetsregler, i hukommelsen og efter enhver gemning. Fordi den gemte fil bærer komponentens egen ToUnicode-stream frem for en motorgenereret, finder RepairSubsetToUnicodeCMaps intet at rette i den
Hvorfor kan én bfrange-entry udviske en hel blok?
En enkelt bfrange, hvis CID-run krydser en xxFF-grænse, får PDFium til at kassere hele blokken, den sidder i. ISO 32000-1 §9.10.3 tillader kun, at destinationens sidste byte varierer inden for et interval, men CID-siden har sin egen fælde: PDFiums HandleBeginBFRange afleder den høje CID som (low and $FFFFFF00) or (high and $FF). Et run fra CID 00FE til 0101 læses derfor som 00FE til 0001, lav større end høj, og hele blokken markeres ugyldig. Fejlen er lydløs: SetText lykkes, siden renderer perfekt, og udtræk returnerer U+0000 for hvert tegn i den blok
BuildUnicodeKeyedCidCMap afslutter et run, før enten kodepunktet eller CID'en når en lav byte på FF, holder hver blok inden for 100-entry-grænsen i CMap-grammatikken og skriver supplementary-plane kodepunkter som enkelte bfchar-entries med UTF-16 surrogate par-destinationer, da det at inkrementere et surrogate par inden for et interval har ingen defineret betydning; surrogate-siden af den historie står i emoji, CJK og surrogate pair-håndtering i Delphi. En CMap med kun bfchar ville omgå grænseproblemet helt, til flere gange størrelsen
Hvad dækker den kodepunkts-nøglede font ikke?
Den kodepunkts-nøglede sti dækker enhver font, der eksponerer en Unicode cmap-undertabel, og falder tilbage til den gamle glyf-nøglede adfærd for resten. Grænserne, der er værd at kende, før du støtter dig til den:
- Symbolfonts med kun en (3,0) cmap, og enhver font, CID-stien fejler at indlæse, går gennem
FPDFText_LoadFontsom før, så en glyf delt af to kodepunkter stadig kan udtrække tvetydigt dér - Uden en format 12-undertabel er mappen begrænset til BMP, og entry-antallet cappes ved 65535, så hver CID er to bytes over nul
- Inkrementelle gemninger (
saIncremental) springerRepairSubsetToUnicodeCMapsover af design, for en inkrementel revision skal forblive append-only; de kodepunkts-nøglede fonts gør det irrelevant for tekst, komponenten selv skriver - TrueType Collections kræver ekstra omhu: GDI
GetFontDatareturnerer hele .ttc'en, ogFPDFText_LoadCidType2Fonthar ingen face index-parameter, så en forespørgsel efter NSimSun fra simsun.ttc indlejrede og renderede tidligere SimSun, face 0. Komponenten matcher nu familienavnet mod name-tabellen (nameID 1 og 16) og udtrækker den efterspurgte face som en selvstændig sfnt, før cmap'en parses; fejler parsingen, passeres samlingsbytes, og adfærden vender tilbage til face 0
Tekstskrivning, font-indlejring og udtræk deler én sidemodel på tværs af Delphi, C++Builder og Lazarus, og det fulde API er beskrevet på PDFium Component for Delphi produktsiden