PDFium Component pentru Delphi încorporează fonturile de sistem folosite de TPdf.AddText ca fonturi CID cheiate după code point Unicode, astfel încât fiecare CID poartă exact o mapare ToUnicode. Asta este ce oprește spațiile extrase să revină ca U+00A0 (spațiu fără rupere) și cratimele ca U+00AD (cratimă moale), atât din documentul viu, cât și din fișierul salvat
Simptomul este urât pentru că este invizibil. Un index de căutare ratează „two-x" pentru că șirul stocat conține o cratimă moale, un export CSV se împarte diferit, un instrument de diff marchează linii care arată identic în orice viewer. Nimic din pagina randată nu este greșit; doar Unicode-ul din spatele glifelor este
De ce revin spațiile extrase ca U+00A0?
Spațiile extrase se transformă în U+00A0 pentru că CMap-ul ToUnicode pe care PDFium îl generează în FPDFText_LoadFont este cheiat după glifă, iar o glifă poate fi atinsă din două code point-uri. În Arial, glifa 3 deservește atât U+0020, cât și U+00A0, iar glifa de cratimă deservește atât U+002D, cât și U+00AD. CMap-ul generat mapează prin urmare același CID de două ori, o dată printr-o intrare bfchar și o dată printr-un bfrange în formă de tablou, iar intrarea pe care regula de precedență a cititorului o favorizează devine textul extras
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Multă vreme contradicția aceasta a fost inofensivă, pentru că cititorul PDFium lăsa cea mai mică mapare să câștige. O schimbare upstream a trecut cititorul la cel-mai-ultim-câștigă, iar din acel build încoace fiecare spațiu scris cu AddText se extrăgea ca NBSP și fiecare cratimă ca o cratimă moale. Observați tiparul din perechi: 0x20/0xA0 și 0x2D/0xAD diferă doar în bitul înalt, exact ce v-ați aștepta de la un font al cărui cmap trimite simulacrele Latin-1 spre același contur. Dacă codul dvs. de extracție era în regulă ieri și acum eșuează pe caractere invizibile, dump-uiți code point-urile în loc să aveți încredere în vederea debugger-ului; bazele scoaterii textului sunt acoperite în extracția de text din documente PDF cu PDFium în Delphi
uses
SysUtils, PDFium;
const
// Spațiul/U+00A0 și cratima/U+00AD împart o glifă Arial, la fel
// ca Omega grecesc (U+03A9) și semnul 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; // document viu, nesalvat
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; // după o salvare completă și o reîncărcare
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
De ce nu a fost de ajuns repararea CMap-ului după salvare
Repararea fișierului salvat repară doar fișierul salvat, și doar dacă repararea păstrează structura CMap-ului intactă byte cu byte. Prima reparație, RepairSubsetToUnicodeCMaps din unit-ul FPdfCompress, rulează după fiecare TPdf.SaveAs non-incremental și rezolvă fiecare CID aflat în conflict: intrarea bfchar câștigă, o pereche care diferă doar în bitul înalt se rezolvă la code point-ul Latin-1 de bază mai mic, iar orice altceva își păstrează prima mapare
Partea interesantă este rezultatul negativ. Reconstruirea curată a CMap-ului aflat în conflict, fie în formă de cod de start, fie de tablou, părea mutarea evidentă, iar PDFium a respins categoric fiecare CMap reconstruit, căzând pe Identity. Singura ieșire acceptată de cititorul nativ a fost o înlocuire în loc, de lungime egală, a valorilor hex aflate în conflict, cu layout-ul de blocuri și acoperirea de CID neatinse. A doua lecție a fost mai smerită: nota noastră de atunci învinuia cazul din memorie pe documentul viu care nu avea deloc un flux ToUnicode. Apelarea directă a DLL-ului a infirmat asta, deoarece documentul viu poartă același flux ambiguu, ceea ce însemna că reparația reală trebuia să se întâmple înainte ca PDFium să genereze vreodată CMap-ul. Rutina de reparare rămâne în bibliotecă ca apărare pentru PDF-urile produse de alte unelte bazate pe 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
// Doar editări de lungime egală; fișierele fără un conflict reparabil,
// precum și fișierele cu flux de referințe încrucișate sau flux de obiecte, sunt copiate ca atare
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Cheierea fontului după code point în loc de glifă
Reparația de rădăcină este să nu mai ceri deloc PDFium să genereze CMap-ul. TPdf.LoadCachedFont predă acum byte-ii fontului de sistem lui TPdf.LoadUnicodeKeyedCidFont, care citește tabelul sfnt cmap propriu al fontului, preferând un sub-tabel format 12 și căzând pe formatul 4. Code point-urile revin sortate și dedublate, iar CID-ului k+1 i se atribuie code point-ul k, cu CID 0 lăsat ca .notdef. Un CIDToGIDMap explicit trimite fiecare CID la glifa lui, astfel încât U+0020 și U+00A0 primesc două CID-uri diferite care desenează același contur, iar CMap-ul ToUnicode mapează fiecare CID la un singur code point. Fontul este apoi încărcat prin FPDFText_LoadCidType2Font, același punct de intrare din spatele scrierii la nivel de glifă din încorporarea de fonturi CID Type 2 cu hărți CID-to-GID explicite
// Condensat din 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); // un CID, un code point
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Când FPDFText_SetText scrie mai târziu un șir, căutarea inversă aterizează pe un singur CID per caracter, astfel încât NBSP, cratima moale și semnul Ohm supraviețuiesc fiecare ca ele însele sub oricare regulă de precedență, în memorie și după orice salvare. Pentru că fișierul salvat poartă fluxul ToUnicode al componentei, nu unul generat de motor, RepairSubsetToUnicodeCMaps nu găsește nimic de reparat în el
De ce poate o intrare bfrange șterge un bloc întreg?
Un singur bfrange a cărui alergare de CID traversează o graniță xFF face ca PDFium să arunce întregul bloc în care stă. ISO 32000-1 §9.10.3 lasă doar ultimul byte al destinației să varieze în interiorul unui interval, dar partea de CID are propria capcană: HandleBeginBFRange al PDFium derivă CID-ul înalt ca (low and $FFFFFF00) or (high and $FF). O alergare de la CID 00FE la 0101 este citită prin urmare ca 00FE la 0001, micul mai mare decât înaltul, iar blocul întreg este marcat invalid. Eșecul este silențios: SetText reușește, pagina se randează perfect, iar extracția întoarce U+0000 pentru fiecare caracter din blocul acela
BuildUnicodeKeyedCidCMap termină o alergare înainte ca fie code point-ul, fie CID-ul să atingă un byte mic de FF, ține fiecare bloc în limita de 100 de intrări a gramaticii CMap și scrie code point-urile din planurile suplimentare ca intrări bfchar individuale cu destinații perechi surogat UTF-16, întrucât incrementarea unei perechi surogat în interiorul unui interval nu are un sens definit; partea de surogat a poveștii aceleia este în tratarea emoji, CJK și a perechilor surogat în Delphi. Un CMap doar-cu-bfchar ar ocoli cu totul problema graniței, la de câteva ori mărimea
Ce nu acoperă fontul cheiat după code point?
Calea cheiată după code point acoperă fiecare font care expune un sub-tabel cmap Unicode, și cade pe vechiul comportament cheiat după glifă pentru restul. Limitele care merită știute înainte să vă bazați pe ea:
- Fonturile simbol cu doar un cmap (3,0), și orice font pe care calea CID eșuează să îl încarce, trec prin
FPDFText_LoadFontca înainte, astfel încât o glifă partajată de două code point-uri poate încă să se extragă ambiguu acolo - Fără un sub-tabel format 12, harta este limitată la BMP, iar numărul de intrări este plafonat la 65535, astfel încât fiecare CID încape în doi byte peste zero
- Salvările incrementale (
saIncremental) săritRepairSubsetToUnicodeCMapsdin design, pentru că o revizuire incrementală trebuie să rămână doar-adaugă; fonturile cheiate după code point fac asta irelevant pentru textul pe care componenta îl scrie ea însăși - TrueType Collections au nevoie de grijă în plus:
GetFontDatadin GDI întoarce tot .ttc-ul, iarFPDFText_LoadCidType2Fontnu are parametru de index de feță, astfel încât cererea NSimSun din simsun.ttc încorpora și randa SimSun, feța 0. Componenta potrivește acum numele familiei contra tabelului de nume (nameID 1 și 16) și extrage feța cerută ca sfnt de sine stătător înainte ca cmap-ul să fie parsat; dacă parsarea eșuează, byte-ii colecției trec și comportamentul revine la feța 0
Scrierea de text, încorporarea de fonturi și extracția împart un singur model de pagină în Delphi, C++Builder și Lazarus, iar API-ul complet este descris pe pagina de produs PDFium Component pentru Delphi