Delphin PDFium Component upottaa TPdf.AddTextin käyttämät järjestelmäfontit CID-fontteina jotka on avainattu Unicode-koodipisteellä, joten jokainen CID kantaa täsmälleen yhden ToUnicode-kuvauksen. Juuri se estää ekstrahoitujen välilyöntien palaamisen arvona U+00A0 (no-break space) ja väliviivojen arvona U+00AD (soft hyphen), sekä elävästä asiakirjasta että tallennetusta tiedostosta
Oire on ilkeä koska se on näkymätön. Hakemisto eksyy kohdasta ”two-x” koska tallennettu merkkijono sisältää pehmeän väliviivan, CSV-export jakautuu eri tavalla, diff-työkalu liputtaa rivejä jotka näyttävät identtisiltä jokaisessa katselimessa. Renderöidyltä sivulta ei puutu mitään; vain glyyfien takana oleva Unicode on pielessä
Miksi ekstrahoidut välilyönnit palaavat arvona U+00A0?
Ekstrahoidut välilyönnit muuttuvat arvoksi U+00A0 koska ToUnicode-CMap jonka PDFium generoi FPDFText_LoadFontissa on avainattu glyyfillä, ja yhteen glyyfiin pääsee kahdesta koodipisteestä. Arialissa glyyfi 3 palvelee sekä U+0020:a että U+00A0:a, ja väliviivaglyyfi palvelee sekä U+002D:tä että U+00AD:tä. Generoitu CMap kuvaa siksi saman CID:n kahdesti, kerran bfchar-merkinnän kautta ja kerran taulukkomuotoisen bfrangein kautta, ja kumpi tahansa merkintä jota lukijan etusijasääntö suosii muuttuu ekstrahoiduksi tekstiksi
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Pitkään tämä ristiriita oli harmiton, koska PDFiumin lukija antoi pienimmän kuvauksen voittaa. Ylävirran muutos käänsi lukijan viimeinen voittaa -käytäntöön, ja siitä käännöksestä alkaen jokainen AddTextilla kirjoitettu välilyönti ekstrahoitui NBSP:nä ja jokainen väliviiva pehmeänä väliviivana. Huomaa kaava pareittain: 0x20/0xA0 ja 0x2D/0xAD eroavat vain korkeimmassa bitissä, mikä on juuri sitä mitä voi odottaa fontilta jonka cmap lähettää Latin-1-kaksoiset samaan ääriviivaan. Jos ekstraktiokoodisi toimi eilen ja epäonnistuu nyt näkymättömissä merkeissä, vedosta koodipisteet sen sijaan että luottaisit debuggaajan näkymään; tekstin ulos vetämisen perusteet käsitellään artikkelissa tekstin ekstrahointi PDF-asiakirjoista PDFiumilla Delphissä
uses
SysUtils, PDFium;
const
// Välilyönti/U+00A0 ja väliviiva/U+00AD jakavat yhden Arial-glyyfin, samoin
// kreikkalainen Omega (U+03A9) ja Ohm-merkki (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; // elävä, tallentamaton asiakirja
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; // täyden tallennuksen ja uudelleenlatauksen jälkeen
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Miksi CMapin paikkaaminen tallennuksen jälkeen ei riittänyt
Tallennetun tiedoston paikkaaminen korjaa vain tallennetun tiedoston, ja vain jos paikkaus pitää CMapin rakenteen tavu tavulta ennallaan. Ensimmäinen korjaus, RepairSubsetToUnicodeCMaps FPdfCompress-yksikössä, ajetaan jokaisen ei-inkrementaalisen TPdf.SaveAsin jälkeen ja ratkaisee jokaisen ristiriitaisen CID:n: bfchar-merkintä voittaa, pari joka eroaa vain korkeimmassa bitissä ratkeaa pienempään base-Latin-koodipisteeseen, ja kaikki muu säilyttää ensimmäisen kuvauksensa
Kiinnostava osa on negatiivinen tulos. Ristiriitaisen CMapin rakentaminen puhtaasti uudelleen, joko start-code- tai taulukkomuodossa, näytti ilmeiseltä siirrolta, ja PDFium hylkäsi jokaisen uudelleenrakennetun CMapin täysin palaten Identityyn. Ainoa tuotos jota natiivilukija hyväksyi oli samanpituisena paikallaan tapahtuva korvaus ristiriitaisia heksa-arvoja, lohkoasettelun ja CID-kattavuuden ollessa koskemattomia. Toinen opetus oli nöyrempi: muistiinpanomme syytti silloin muistissa olevaa tapausta siitä ettei elävällä asiakirjalla ollut lainkaan ToUnicode-streamia. DLL:n suora kutsuminen kumosi sen, koska elävä asiakirja kantaa samaa monimerkityksistä streamia, mikä tarkoitti että oikean korjauksen oli tapahduttava ennen kuin PDFium ylipäätään generoi CMapin. Korjausrutiini jää kirjastoon puolustukseksi muiden PDFium-pohjaisten työkalujen tuottamia PDF:ejä vastaan
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
// Vain samanpituiset muokkaukset; tiedostot ilman korjattavaa ristiriitaa,
// sekä ristiviittausstream- tai objektistream-tiedostot kopioidaan sellaisenaan
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Fontin avainaminen koodipisteellä glyyfin sijaan
Juurikorjaus on lopettaa PDFiumilta CMapin generoinnin pyytäminen ylipäätään. TPdf.LoadCachedFont luovuttaa nyt järjestelmäfontin tavut TPdf.LoadUnicodeKeyedCidFontille, joka lukee fontin oman sfnt cmap-taulun suosien format 12 -alitaulua ja palaten format 4:ään. Koodipisteet palaavat lajiteltuina ja tupliana poistettuina, ja CID k+1 osoitetaan k:nnelle koodipisteelle, CID 0:n jäädessä .notdefiksi. Eksplisiittinen CIDToGIDMap lähettää jokaisen CID:n glyyfilleen, joten U+0020 ja U+00A0 saavat kaksi eri CID:iä jotka piirtävät saman ääriviivan, ja ToUnicode-CMap kuvaa jokaisen CID:n vain yhteen koodipisteeseen. Fontti ladataan sitten FPDFText_LoadCidType2Fontin kautta, sama sisääntulopiste glyyfitason kirjoituksen takana artikkelissa CID Type 2 -fonttien upotus eksplisiittisillä CID-GID-kuvauksilla
// Tiivistetty funktiosta 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); // yksi CID, yksi koodipiste
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
Kun FPDFText_SetText myöhemmin kirjoittaa merkkijonon, käänteinen haku osuu yhteen CID:iin per merkki, joten NBSP, pehmeä väliviiva ja Ohm-merkki selviävät kukin itsenään kummalla tahansa etusijasäännöllä, muistissa ja minkä tahansa tallennuksen jälkeen. Koska tallennettu tiedosto kantaa komponentin omaa ToUnicode-streamia moottorin generoiman sijaan, RepairSubsetToUnicodeCMaps ei löydä siitä mitään korjattavaa
Miksi yksi bfrange-merkintä voi pyyhkiä kokonaisen lohkon?
Yksittäinen bfrange jonka CID-ajo ylittää xxFF-rajan saa PDFiumin hylkäämään kokonaisen lohkon jonka sisällä se istuu. ISO 32000-1 §9.10.3 antaa vain kohteen viimeisen tavun vaihdella alueen sisällä, mutta CID-puolella on oma ansansa: PDFiumin HandleBeginBFRange johtaa korkean CID:n muodossa (low and $FFFFFF00) or (high and $FF). Ajo CID:stä 00FE arvoon 0101 luetaan siksi muodossa 00FE arvoon 0001, pienempi suurempi, ja koko lohko merkitään kelvottomaksi. Vika on hiljainen: SetText onnistuu, sivu renderöityy täydellisesti, ja ekstraktio palauttaa arvon U+0000 jokaiselle kyseisen lohkon merkille
BuildUnicodeKeyedCidCMap päättää ajon ennen kuin koodipiste tai CID saavuttaa FF:n alatavun, pitää jokaisen lohkon CMapin kieliopin 100 merkinnän rajan sisällä ja kirjoittaa täydentävien tasojen koodipisteet yksittäisiksi bfchar-merkinnöiksi UTF-16-surrogaariparein kohteina, koska surrogaariparin kasvattamisella alueen sisällä ei ole määriteltyä merkitystä; surrogaaripuoli siitä tarinasta on artikkelissa emoji, CJK ja surrogaariparien käsittely Delphissä. Pelkkä bfchar-CMap kiertäisi rajan ongelman kokonaan, useankertaisella koolla
Mitä koodipisteellä avainattu fontti ei kata?
Koodipisteellä avainattu polku kattaa jokaisen fontin joka paljastaa Unicode-cmap-alitaulun, ja palaa vanhaan glyyfillä avainattuun käyttäytymiseen muille. Rajat jotka kannattaa tuntea ennen kuin luotat siihen:
- Symbolfontit joilla on vain (3,0)-cmap, ja mikä tahansa fontti jonka CID-polku ei lataudu, kulkevat
FPDFText_LoadFontin kautta kuten ennenkin, joten kahden koodipisteen jakama glyyfi voi yhä ekstrahoitua monimerkityksisesti siellä - Ilman format 12 -alitaulua kuvaus rajoittuu BMP:hen, ja merkintöjen määrä katkaistaan arvoon 65535 jotta jokainen CID mahtuu kahteen tavuun nollan yläpuolella
- Inkrementaaliset tallennukset (
saIncremental) ohittavatRepairSubsetToUnicodeCMapsin suunnitellusti, koska inkrementaalisen revision on pysyttävä vain liittävänä; koodipisteellä avainatut fontit tekevät siitä merkityksettömän tekstille jota komponentti itse kirjoittaa - TrueType Collections vaativat lisähuomiota: GDI:n
GetFontDatapalauttaa koko .ttc:n, jaFPDFText_LoadCidType2Fontilla ei ole face-indeksiparametria, joten NSimSunin pyytäminen simsun.ttc:stä upotti ja renderöi aiemmin SimSunin, facen 0. Komponentti vertaa nyt perhenimeä name-tauluun (nameID 1 ja 16) ja ekstrahoi pyydetyn facen itsenäiseksi sfntiksi ennen kuin cmap jäsentyy; jos jäsennys epäonnistuu, kokoelman tavut kulkevat läpi ja käytös palaa faceen 0
Tekstin kirjoittaminen, fonttien upotus ja ekstraktio jakavat yhden sivumallin Delphissä, C++Builderissa ja Lazaruksessa, ja koko API on kuvattu PDFium Component for Delphi -tuotesivulla