PDFium Component za Delphi vgradi sistemske pisave, ki jih uporablja TPdf.AddText, kot pisave CID, ključane po kodni točki Unicode, tako da vsak CID nosi točno eno preslikavo ToUnicode. To je tisto, kar preprečuje, da bi izvlečeni presledki prihajali nazaj kot U+00A0 (neprelomni presledek) in vezaji kot U+00AD (mehki vezaj), tako iz živega dokumenta kot iz shranjene datoteke
Simptom je grd, ker je neviden. Iskalni indeks spregleda »two-x«, ker shranjeni niz vsebuje mehki vezaj, izvoz CSV se razcepi drugače, orodje za primerjavo označi vrstice, ki v vsakem pregledovalniku izgledajo identično. Nič na upodobljeni strani ni narobe; narobe je le Unicode za glifi
Zakaj izvlečeni presledki pridejo nazaj kot U+00A0?
Izvlečeni presledki se spremenijo v U+00A0, ker je CMap ToUnicode, ki ga PDFium ustvari v FPDFText_LoadFont, ključan po glifu in do enega glifa se je mogoče dokopati iz dveh kodnih točk. V Arialu glif 3 streže obema U+0020 in U+00A0, glif vezaja pa obema U+002D in U+00AD. Ustvarjeni CMap torej preslika isti CID dvakrat, enkrat skozi vnos bfchar in enkrat skozi bfrange v obliki tabele, vnos, ki mu nakloni pravilo prednosti bralca, pa postane izvlečeno besedilo
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Dolgo časa je bilo to protislovje nenevarno, saj je bralec PDFium pustil, da zmaguje najnižja preslikava. Zgornja sprememba je bralca preklopila na zadnji-zmaga in od te gradnje naprej se je vsak presledek, zapisan z AddText, izvlekel kot NBSP in vsak vezaj kot mehki vezaj. Poglejte vzorec v parih: 0x20/0xA0 in 0x2D/0xAD se razlikujeta le v visokem bitu, točno to pa bi pričakovali od pisave, katere cmap pošilja dvojnikom Latin-1 isti oris. Če je bila Vaša izvlečna koda včeraj v redu in zdaj odpoveduje na nevidnih znakih, odvrzite kodne točke, namesto da zaupate pogledu razhroščevalnika; osnove izvlečenja besedila pokriva izvlečenje besedila iz dokumentov PDF s PDFium v Delphiju
uses
SysUtils, PDFium;
const
// Presledka/U+00A0 in vezaj/U+00AD si delita en glif Arial, enako
// grška Omega (U+03A9) in znak Ohm (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; // živ, neshranjen 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; // po polnem shranjevanju in ponovnem nalaganju
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Zakaj krpanje CMapa po shranjevanju ni zadoščalo
Krpanje shranjene datoteke popravi le shranjeno datoteko in to le, če krpa obdrži strukturo CMapa bajt za bajtom nedotaknjeno. Prvi popravek, RepairSubsetToUnicodeCMaps v enoti FPdfCompress, teče po vsakem ne-inkrementalnem TPdf.SaveAs in razreši vsak spor CID: vnos bfchar zmaga, par, ki se razlikuje le v visokem bitu, se razreši na manjšo osnovno-latinsko kodno točko, karkoli drugega pa obdrži svojo prvo preslikavo
Zanimiv del je negativen rezultat. Čista znovogradnja spornega CMapa, v obliki začetne kode ali tabele, je videla kot očitna poteza, PDFium pa je vsak znovograjeni CMap odklonil na mestu in padel nazaj na Identity. Edini izhod, ki ga je domači bralec sprejel, je bila zamenjava spornih šestnajstiških vrednosti na mestu, enake dolžine, s postavitvijo bloka in pokritostjo CID nedotaknjeni. Druga lekcija je bila skromnejša: naša takratna opomba je krivdo za primer v pomnilniku valila na živi dokument, ki naj ne bi imel sploh nobenega toka ToUnicode. Neposredni klic DLL je to ovrgel, saj živi dokument nosi isti dvoumen tok, kar je pomenilo, da se mora pravi popravek zgoditi, preden PDFium sploh ustvari CMap. Popravljalna rutina ostane v knjižnici kot obramba za PDF, ki jih ustvarijo druga orodja na osnovi PDFium
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
// Le urejanja enake dolžine; datoteke brez popravljivega spora
// in datoteke s tokovi navzkrižnih sklicev ali objektov se kopirajo, kakršne so
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Ključanje pisave po kodni točki namesto po glifu
Korenski popravek je, da prenehamo prositi PDFium, naj sploh ustvari CMap. TPdf.LoadCachedFont zdaj bajte sistemske pisave poda TPdf.LoadUnicodeKeyedCidFont, ki prebere lastno tabelo sfnt cmap pisave, pri čemer raje izbere podtabelo formata 12 in pade nazaj na format 4. Kodne točke pridejo nazaj razvrščene in brez dvojnikov, CID k+1 pa je dodeljen k-ti kodni točki, CID 0 pa ostane kot .notdef. Izrecni CIDToGIDMap pošlje vsak CID k njegovemu glifu, tako da U+0020 in U+00A0 dobita dva različna CID, ki rišeta isti oris, CMap ToUnicode pa preslika vsak CID na eno samo kodno točko. Pisava se nato naloži prek FPDFText_LoadCidType2Font, iste vstopne točke, ki stoji za pisanjem na ravni glifov v vgrajevanju pisav CID Type 2 z izrecnimi preslikavami CID-v-GID
// Skrčeno iz 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); // en CID, ena kodna točka
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Ko FPDFText_SetText pozneje zapiše niz, obratno iskanje pristane na enem samem CID na znak, tako da NBSP, mehki vezaj in znak Ohm preživijo vsak kot sam sebi pod katerim koli pravilom prednosti, v pomnilniku in po vsakem shranjevanju. Ker shranjena datoteka nosi lasten tok ToUnicode komponente in ne pogonsko ustvarjenega, RepairSubsetToUnicodeCMaps v njej ne najde ničesar za krpanje
Zakaj lahko en sam vnos bfrange izbriše cel blok?
En sam bfrange, katerega zaporedje CID prečka mejo xxFF, naredi, da PDFium zavrne celoten blok, v katerem stoji. ISO 32000-1 §9.10.3 pusti variirati le zadnji bajt cilja znotraj razpona, stran CID pa ima svojo past: HandleBeginBFRange PDFium izpelje visoki CID kot (low and $FFFFFF00) or (high and $FF). Zaporedje od CID 00FE do 0101 se zato prebere kot 00FE do 0001, nižji večji od visokega, in celoten blok je označen kot neveljaven. Odpoved je tiha: SetText uspe, stran se izriše popolnoma, izvlečenje pa vrne U+0000 za vsak znak v tem bloku
BuildUnicodeKeyedCidCMap konča zaporedje, preden kodna točka ali CID doseže nizki bajt FF, drži vsak blok znotraj omejitve 100 vnosov slovnice CMap in dopolnilnoravninske kodne točke zapiše kot posamezne vnose bfchar s cilji surrogatnih parov UTF-16, saj povečevanje surrogatnega para znotraj razpona nima definiranega pomena; surrogatna stran te zgodbe je v ravnanju z emoji, CJK in surrogatnimi pari v Delphiju. CMap samo iz bfchar bi problem meje v celoti zaobšel, ob večkratni velikosti
Česa pisava ključana po kodni točki ne pokriva?
Pot ključana po kodni točki pokriva vsako pisavo, ki izpostavi podtabelo cmap Unicode, za preostanek pa pade nazaj na staro vedenje ključano po glifih. Meje, vredne poznavanja, preden se zanjo zanesete:
- Simbolne pisave s samo (3,0) cmap in vsaka pisava, ki je pot CID ne zna naložiti, gredo skozi
FPDFText_LoadFontkot prej, tako da lahko glif, ki si ga delita dve kodni točki, tam še vedno izvleče dvoumno - Brez podtabele formata 12 je preslikava omejena na BMP in števec vnosov je omejen na 65535, tako da se vsak CID prilega v dva bajta nad ničlo
- Inkrementalna shranjevanja (
saIncremental) preskočijoRepairSubsetToUnicodeCMapspo zasnovi, ker mora inkrementalna revizija ostati samo-dodajna; pisave ključane po kodni točki naredijo to nepomembno za besedilo, ki ga komponenta piše sama - Zbirke TrueType potrebujejo dodatno skrb: GDI
GetFontDatavrne celotno .ttc inFPDFText_LoadCidType2Fontnima parametra za indeks face, zato je zahteva po NSimSun iz simsun.ttc vgrajevala in izrisovala SimSun, face 0. Komponenta zdaj ime družine ujema proti tabeli imen (nameID 1 in 16) in izvleče zahtevano face kot samostojni sfnt, preden se cmap razčleni; če razčlenjevanje spodleti, bajti zbirke gredo skozi in vedenje se vrne na face 0
Pisanje besedila, vgrajevanje pisav in izvlečenje si delijo en model strani skozi Delphi, C++Builder in Lazarus, celoten API pa je opisan na strani izdelka PDFium Component for Delphi