A Delphihez készült PDFium Component a TPdf.AddText által használt rendszerfontokat CID fontokként ágyazza be, Unicode kódpont szerint kulcsolva, így minden CID pontosan egy ToUnicode leképezést hordoz. Ez az, ami megakadályozza, hogy a kinyert szóközök U+00A0-ként (nem törhető szóköz) és a kötőjelek U+00AD-ként (lágy kötőjel) jöjjenek vissza, egyszerre az élő dokumentumból és a mentett fájlból
A tünet kellemetlen, mert láthatatlan. Egy keresési index nem találja a „two-x"-et, mert a tárolt string lágy kötőjelet tartalmaz, egy CSV export másképp bont, egy diff eszköz olyan sorokat jelöl, amelyek minden nézegetőben azonosnak tűnnek. A kirajzolt oldalon semmi nincs elrontva; csak a jelek mögötti Unicode az
Miért térnek vissza a kinyert szóközök U+00A0-ként?
A kinyert szóközök U+00A0-vá változnak, mert a PDFium által az FPDFText_LoadFont-ban generált ToUnicode CMap jel (glyph) szerint van kulcsolva, és egy jelet két kódpont is elérhet. Az Arialban a 3-as jel az U+0020-et és az U+00A0-t is kiszolgálja, a kötőjel jele pedig az U+002D-t és az U+00AD-t. A generált CMap ezért ugyanazt a CID-et kétszer képezi le, egyszer egy bfchar bejegyzésen, egyszer egy tömbalakú bfrange-en keresztül, és amelyik bejegyzést az olvasó elsőbbségi szabálya előnyben részesíti, az lesz a kinyert szöveg
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Sokáig ez az ellentmondás ártalmatlan volt, mivel a PDFium olvasója a legalacsonyabb leképezést hagyta nyerni. Egy upstream változás az utolsó-nyer szabályra kapcsolta az olvasót, és attól a buildtől kezdve minden, AddText-tel írt szóköz NBSP-ként, minden kötőjel lágy kötőjelként nyert ki. Figyeljen a párok mintázatára: a 0x20/0xA0 és a 0x2D/0xAD csak a felső bitben tér el, ami pontosan azt jelzi, amire egy olyan fonttól várhatunk, amelynek cmap-je a Latin-1 hasonmásokat ugyanarra a kontúrra küldi. Ha a kinyerő kódja tegnap még jó volt, és most láthatatlan karaktereken bukik, dumpolja a kódpontokat ahelyett, hogy a debugger nézetére hagyatkozna; a szöveg kihúzásának alapjait a szöveg kinyerése PDF dokumentumokból PDFiummal Delphiben cikk fedi le
uses
SysUtils, PDFium;
const
// A szóköz/U+00A0 és a kötőjel/U+00AD egy Arial jelet oszt meg,
// akárcsak a görög Omega (U+03A9) és az Ohm jel (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; // élő, mentetlen dokumentum
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; // teljes mentés és újratöltés után
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Miért nem volt elég a CMap foltozása mentés után
A mentett fájl foltozása csak a mentett fájlt javítja, és csak akkor, ha a foltozás a CMap struktúrát bájtonkénti épségben tartja. Az első javítás, a FPdfCompress egység RepairSubsetToUnicodeCMaps-e minden nem inkrementális TPdf.SaveAs után fut, és minden ütköző CID-et felold: a bfchar bejegyzés nyer, egy párt, amely csak a felső bitben tér el, a kisebb alap-Latin kódpont old fel, minden más megtartja az első leképezését
Az érdekes rész a negatív eredmény. Az ütköző CMap tiszta újjáépítése, akár start-kód, akár tömb alakban, kézenfekvő lépésnek tűnt, és a PDFium minden újjáépített CMap-ot elutasított, Identity-re esve vissza. Az egyetlen kimenet, amelyet a natív olvasó elfogadott, az ütköző hex értékek egyenlő hosszú, helyben végzett cseréje volt, blokkelrendezés és CID lefedettség érintetlenül. A második tanulság szerényebb volt: akkori jegyzetünk a memóriabeli esetet arra fogta, hogy az élő dokumentumnak egyáltalán nincs ToUnicode folyamja. A DLL közvetlen meghívása ezt megcáfolta, hiszen az élő dokumentum ugyanazt a kétértelmű folyamot hordozza, ami azt jelentette, hogy a valódi javításnak még azelőtt kellett megtörténnie, hogy a PDFium egyáltalán legenerálná a CMap-ot. A javító rutin a könyvtárban marad védelemként más PDFium-alapú eszközök által készített PDF-ekhez
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
// Csak egyenlő hosszú szerkesztések; a javítható ütközés nélküli fájlok,
// valamint a kereszthivatkozási- vagy objektumfolyamos fájlok, változatlanul másolódnak
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
A font kulcsolása jel helyett kódpont szerint
A gyökérjavítás az, hogy egyáltalán nem kérjük a PDFiumtól a CMap generálását. A TPdf.LoadCachedFont mostantól a rendszerfont bájtjait a TPdf.LoadUnicodeKeyedCidFont-nak adja, amely a font saját sfnt cmap tábláját olvassa, előnyben részesítve egy 12-es formátumú altáblát, és 4-es formátumra esve vissza. A kódpontok rendezve és duplikátummentesen jönnek vissza, a CID k+1 a k-adik kódpontot kapja, a CID 0 pedig .notdef marad. Egy explicit CIDToGIDMap minden CID-et a jeléhez köt, tehát az U+0020 és az U+00A0 két különböző CID-et kap, amelyek ugyanazt a kontúrt rajzolják, és a ToUnicode CMap minden CID-et egyetlen kódpontra képez le. A font ezután az FPDFText_LoadCidType2Font-on keresztül töltődik, ugyanazon a belépési ponton, amely a CID Type 2 font beágyazás mögött explicit CID-GID leképezésekkel áll a jel szintű írás hátterében
// A TPdf.LoadUnicodeKeyedCidFont-ból sűrítve
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); // egy CID, egy kódpont
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Amikor a FPDFText_SetText később stringet ír, a visszirányú keresés karakterenként egyetlen CID-re ér, tehát az NBSP, a lágy kötőjel és az Ohm jel egyaránt önmagaként él túl bármely elsőbbségi szabály alatt, memóriában és bármely mentés után. Mivel a mentett fájl a komponens saját ToUnicode folyamát hordozza a motor által generált helyett, a RepairSubsetToUnicodeCMaps nem talál benne mit javítani
Hogyan törölhet ki egyetlen bfrange bejegyzés egy egész blokkot?
Egyetlen bfrange, amelynek CID futása átlép egy xxFF határon, arra készteti a PDFiumot, hogy eldobja azt a teljes blokkot, amelyben ül. Az ISO 32000-1 §9.10.3 csak azt engedi, hogy a cél utolsó bájtja változzon a tartományon belül, de a CID oldalon is van csapda: a PDFium HandleBeginBFRange-je a magas CID-et (low and $FFFFFF00) or (high and $FF) alakban származtatja. Egy 00FE-től 0101-ig futó futás ezért 00FE-től 0001-ig olvasódik, az alsó nagyobb a felsőnél, és az egész blokk érvénytelennek jelölődik. A hiba csendes: a SetText sikerül, az oldal tökéletesen rajzol, és a kinyerés a blokk minden karakterére U+0000-t ad vissza
A BuildUnicodeKeyedCidCMap minden futást lezár, még mielőtt a kódpont vagy a CID FF alacsony bájtjához érne, minden blokkot a CMap nyelvtan 100 bejegyzéses határán belül tart, a kiegészítő síkbeli kódpontokat pedig egyenként bfchar bejegyzésként írja UTF-16 szürrogátpár célokkal, mivel egy szürrogátpár növelése egy tartományon belül nincs definiálva; ennek a történetnek a szürrogát oldala a emoji, CJK és szürrogátpár kezelés Delphiben cikkben olvasható. Egy csak bfchar-os CMap teljesen elkerülné a határproblémát, jóval nagyobb méret árán
Mit nem fed le a kódpont szerint kulcsolt font?
A kódpont szerint kulcsolt út minden olyan fontot lefed, amely Unicode cmap altáblát tár fel, és a többinél a régi, jel szerinti viselkedésre esik vissza. A határok, amelyeket érdemes ismerni, mielőtt ráépítene:
- A csak (3,0) cmap-mel rendelkező symbol fontok, és minden font, amelyet a CID út nem tud betölteni, a régi módon az
FPDFText_LoadFont-on mennek át, tehát ott két kódpont által megosztott jel továbbra is kétértelműen nyerhető ki - 12-es formátumú altábla nélkül a leképezés a BMP-re korlátozódik, és a bejegyzésszám 65535-nél tetőzik, hogy minden CID két bájtban, nullától felfelé elférjen
- Az inkrementális mentések (
saIncremental) tervezésből kihagyják aRepairSubsetToUnicodeCMaps-t, mert egy inkrementális revíziónak csak hozzáfűzőnek kell maradnia; a kódpont szerint kulcsolt fontok ezt irrelevánssá teszik azokra a szövegekre, amelyeket maga a komponens ír - A TrueType Collection-ök külön gondoskodást igényelnek: a GDI
GetFontData-ja a teljes .ttc-t adja vissza, azFPDFText_LoadCidType2Font-nak pedig nincs face index paramétere, így az NSimSun kérése a simsun.ttc-ből korábban SimSun-t, a 0. facét ágyazta be és rajzolta. A komponens mostantól a családnevet a névtáblához (nameID 1 és 16) illeszti, és a kért facét önálló sfnt-ként nyeri ki, mielőtt a cmap elemzése megtörténne; ha az elemzés elbukik, a kollekció bájtjai átmegy, és a viselkedés a 0. facára esik vissza
A szövegírás, a font beágyazás és a kinyerés egyetlen oldalmodellen osztozik Delphiben, C++Builderben és Lazarusban egyaránt, a teljes API pedig a PDFium Component Delphihez termékoldalon van leírva