PDFium Component for Delphi įdeda TPdf.AddText naudojamus sistemos šriftus kaip CID šriftus, raktuojamus Unicode kodo tašku, tad kiekvienas CID neša lygiai vieną ToUnicode atvaizdavimą. Būtent todėl ištraukti tarpai nebegrįžta kaip U+00A0 (nepertraukiamasis tarpas), o brūkšneliai — kaip U+00AD (minkštasis brūkšnelis), tiek gyvame dokumente, tiek išsaugotame faile
Simptomas baisus, nes nematomas. Paieškos indeksas praleidžia „two-x“, nes saugomoje eilutėje yra minkštasis brūkšnelis, CSV eksportas skyla kitaip, diff įrankis žymi eilutes, kurios kiekvienoje žiūryklėje atrodo identiškai. Atvaizduotame puslapyje niekas negerai; netinkamas tik Unicode už glifų
Kodėl ištraukti tarpai grįžta kaip U+00A0?
Ištraukti tarpai virsta U+00A0, nes ToUnicode CMap, kurį PDFium sugeneruoja FPDFText_LoadFont, raktuojamas glifu, o prie vieno glifo galima prieiti iš dviejų kodo taškų. Arial glifas 3 aptarnauja ir U+0020, ir U+00A0, o brūkšnelio glifas — ir U+002D, ir U+00AD. Sugeneruotas CMap todėl tą patį CID atvaizduoja du kartus — kartą per bfchar įrašą ir kartą per masyvinės formos bfrange — ir tas įrašas, kurį pasirenka skaitytuvo pirmenybės taisyklė, tampa ištrauktu tekstu
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Ilgą laiką šis prieštaravimas buvo nekenksmingas, nes PDFium skaitytuvas leisdavo mažiausiam atvaizdavimui laimėti. Pakeitimas aukščiau persjungė skaitytuvą į last-wins, ir nuo to build'o kiekvienas AddText parašytas tarpas ištraukiamas kaip NBSP, o kiekvienas brūkšnelis — kaip minkštasis brūkšnelis. Pastebėkite modelį porose: 0x20/0xA0 ir 0x2D/0xAD skiriasi tik aukštesniuoju bitu, ir būtent to tikėtumėsi iš šrifto, kurio cmap lotyniškus atitikmenis siunčia į tą patį kontūrą. Jei jūsų ištraukos kodas vakar buvo geras, o dabar klysta dėl nematomų simbolių, išspausdinkite kodo taškus, užuot pasitikėję derintuvės rodiniu; teksto ištraukimo pagrindus dengia teksto ištraukimas iš PDF dokumentų su PDFium Delphi
uses
SysUtils, PDFium;
const
// Tarpas/U+00A0 ir brūkšnelis/U+00AD dalijasi vienu Arial glifu, taip pat
// graikų Omega (U+03A9) ir Omo ženklas (U+2126)
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; // gyvas, neišsaugotas dokumentas
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; // po pilno išsaugojimo ir pakartotinio įkėlimo
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Kodėl CMap lopymas po išsaugojimo nebuvo pakankamas
Išsaugoto failo lopymas sutvarko tik išsaugotą failą, ir tik jei lopas CMap struktūrą palieka baitas į baitą nesugadintą. Pirmoji pataisa, RepairSubsetToUnicodeCMaps FPdfCompress modulyje, paleidžiama po kiekvieno ne inkrementinio TPdf.SaveAs ir išsprendžia kiekvieną konfliktuojantį CID: bfchar įrašas laimi, pora, besiskirianti tik aukštesniuoju bitu, išsprendžiama į mažesnįjį bazinį lotynų kodo tašką, o visa kita išlaiko pirmąjį atvaizdavimą
Įdomiausia dalis — neigiamas rezultatas. Konfliktuojančio CMap tvarkingas perstatymas, start-code arba masyvo forma, atrodė kaip akivaizdus žingsnis, ir PDFium atmetė kiekvieną perstatytą CMap iš karto, nusileisdamas į Identity. Vienintelė išvestis, kurią savoji skaitytojas priėmė, buvo vienodo ilgio vietinė konfliktuojančių heksų reikšmių pakeitimas, su nepaliestu bloko išdėstymu ir CID danga. Antra pamoka buvo kuklesnė: mūsų to meto pastaba kaltino atminties atvejį tuo, kad gyvas dokumentas apskritai neturi ToUnicode srauto. Tiesioginis DLL kvietimas tai paneigė, nes gyvas dokumentas neša tą patį dviprasmišką srautą — tai reiškė, kad tikroji pataisa turi įvykti dar prieš PDFium sugeneruojant CMap. Taisymo procedūra lieka bibliotekoje kaip gynyba nuo kitų ant PDFium pagrindo veikiančių įrankių pagamintų PDF
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
// Tik vienodo ilgio pakeitimai; failai be taisomo konflikto,
// taip pat kryžminių ar objektų srautų failai, kopijuojami tokie, kokie yra
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Šrifto raktavimas pagal kodo tašką vietoj glifo
Šakninė pataisa — apskritai nustoti prašyti PDFium generuoti CMap. TPdf.LoadCachedFont dabar sistemos šrifto baitus atiduoda TPdf.LoadUnicodeKeyedCidFont, kuri skaito paties šrifto sfnt cmap lentelę, teikdama pirmenybę 12 formato sublentelei ir nusileisdama į 4 formatą. Kodo taškai grįžta surūšiuoti ir be dublių, o CID k+1 priskiriamas k-tajam kodo taškui, CID 0 paliekamas .notdef. Aiški CIDToGIDMap kiekvieną CID nukreipia į jo glifą, tad U+0020 ir U+00A0 gauna du skirtingus CID, piešiančius tą patį kontūrą, o ToUnicode CMap kiekvieną CID atvaizduoja tik į vieną kodo tašką. Tada šriftas įkeliamas per FPDFText_LoadCidType2Font — tą patį įėjimo tašką, slypintį už glifo lygio rašymo CID Type 2 šriftų įdėjime su aiškiais CID-į-GID atvaizdais
// Sutrumpinta iš 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); // vienas CID, vienas kodo taškas
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Kai FPDFText_SetText vėliau rašo eilutę, atvirkštinė paieška nusileidžia ant vieno CID simboliui, tad NBSP, minkštasis brūkšnelis ir Omo ženklas kiekvienas išgyvena kaip patys, esant bet kuriai pirmenybės taisyklei, atmintyje ir po bet kokio išsaugojimo. Kadangi išsaugotas failas neša paties komponento ToUnicode srautą, o ne variklio sugeneruotą, RepairSubsetToUnicodeCMaps jame nebemato, ką taisyti
Kodėl vienas bfrange įrašas gali nušluoti visą bloką?
Vienas bfrange, kurio CID seka kerta xFF ribą, priverčia PDFium išmesti visą bloką, kuriame jis sėdi. ISO 32000-1 §9.10.3 leidžia kisti tik paskutiniam tikslo baitui ruože, bet CID pusė turi savus spąstus: PDFium HandleBeginBFRange aukštąjį CID išveda kaip (low and $FFFFFF00) or (high and $FF). Seka nuo CID 00FE iki 0101 todėl skaitoma kaip 00FE iki 0001 — žemas didesnis už aukštą, ir visas blokas pažymimas netinkamu. Nesėkmė tyli: SetText pavyksta, puslapis atvaizduojamas tobulai, o ištrauka grąžina U+0000 kiekvienam to bloko simboliui
BuildUnicodeKeyedCidCMap seką užbaigia dar prieš kodo taškui ar CID pasiekus žemąjį FF baitą, laiko kiekvieną bloką CMap gramatikos 100 įrašų riboje ir papildomųjų plokštumų kodo taškus rašo kaip atskirus bfchar įrašus su UTF-16 surrogate porų tikslais, nes surrogate poros didinimas ruožo viduje neturi apibrėžtos reikšmės; surrogate pusė tos istorijos — emoji, CJK ir surrogate porų apdorojime Delphi. Vien tik bfchar CMap ribų problemą apeitų visai, tik kelis kartus didesnio dydžio
Ko kodo tašku raktuotas šriftas nedengia?
Kodo tašku raktuotas kelias dengia kiekvieną šriftą, atveriantį Unicode cmap sublentelę, o likusiems nusileidžia į senąjį glifu raktuotą elgesį. Ribos, vertos žinoti, prieš jumis pasitikint juo:
- Simbolių šriftai tik su (3,0) cmap ir bet koks šriftas, kurio CID kelias nesugeba įkelti, eina per
FPDFText_LoadFontkaip anksčiau, tad glifas, dalijamas dviejų kodo taškų, ten vis tiek gali ištraukti dviprasmiškai - Be 12 formato sublentelės atvaizdas ribojamas BMP, o įrašų skaičius ribojamas 65535, kad kiekvienas CID teltų į du baitus virš nulio
- Inkrementiniai įrašymai (
saIncremental) praleidžiaRepairSubsetToUnicodeCMapssąmoningai, nes inkrementinė revizija privalo likti tik prijungiamąja; kodo tašku raktuoti šriftai tai daro nereikšmingą tekstui, kurį rašo pats komponentas - TrueType Collections reikalauja papildomos priežiūros: GDI
GetFontDatagrąžina visą .ttc, oFPDFText_LoadCidType2Fontneturi face indekso parametro, tad NSimSun prašymas iš simsun.ttc anksčiau įdėdavo ir atvaizduodavo SimSun, face 0. Komponentas dabar šeimos vardą sugretina su name lentele (nameID 1 ir 16) ir ištraukia prašomąjį face kaip savarankišką sfnt, dar neskanuojant cmap; jei skaitymas žlunga, kolekcijos baitai praeina toliau, o elgesys grįžta į face 0
Teksto rašymas, šriftų įdėjimas ir ištrauka dalijasi vienu puslapio modeliu tarp Delphi, C++Builder ir Lazarus, o visa API aprašyta PDFium Component for Delphi produkto puslapyje