Delphi için PDFium Component, TPdf.AddText'in kullandığı sistem fontlarını Unicode kod noktasına göre anahtarlanmış CID fontlar olarak gömer; böylece her CID tam olarak bir ToUnicode eşlemesi taşır. Çıkarılan boşlukların U+00A0 (bölünemez boşluk) olarak ve tirelerin U+00AD (soft hyphen) olarak geri dönmesini önleyen şey budur — hem canlı belgede hem kaydedilen dosyada
Belirti iğrençtir, çünkü görünmezdir. Bir arama indeksi "two-x"i bulamaz, çünkü saklanan string soft hyphen içerir; bir CSV dışa aktarımı başka türlü böler; bir diff aracı her görüntüleyicide birebir aynı görünen satırları işaretler. Render edilen sayfada yanlış olan hiçbir şey yoktur; yanlış olan, glyph'lerin ardındaki Unicode'dur
Çıkarılan boşluklar neden U+00A0 olarak dönüyor?
Çıkarılan boşluklar U+00A0'ya dönüşür, çünkü PDFium'un FPDFText_LoadFont içinde ürettiği ToUnicode CMap glyph'e göre anahtarlanmıştır ve bir glyph'e iki kod noktasından ulaşılabilir. Arial'de 3 numaralı glyph hem U+0020'yi hem U+00A0'yı karşılar, tire glyph'i de hem U+002D'yi hem U+00AD'yi karşılar. Üretilen CMap bu yüzden aynı CID'yi iki kez eşler: bir kez bir bfchar girdisiyle, bir kez dizi biçimli bir bfrange ile; okuyucunun öncelik kuralının hangisini tuttuğu, çıkarılan metin olur
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
Uzun süre bu çelişki zararsızdı, çünkü PDFium okuyucusu en düşük eşlemenin kazanmasına izin veriyordu. Upstream bir değişiklik okuyucuyu en-son-kazanır'a çevirdi ve o derlemeden itibaren AddText ile yazılan her boşluk NBSP, her tire soft hyphen olarak çıkarıldı. Çiftlerdeki örüntüye dikkat: 0x20/0xA0 ile 0x2D/0xAD yalnızca yüksek bittte farklıdır; bu, cmap'i Latin-1 benzerlerini aynı kontura gönderen bir fonttan tam olarak beklenecek şeydir. Çıkarma kodunuz dün iyi çalışıyorduysa ve şimdi görünmez karakterlerde başarısız oluyorsa debugger görünümüne güvenmek yerine kod noktalarını dökün; metin çekmenin temelleri Delphi'de PDFium ile PDF belgelerinden metin çıkarmak yazısında işleniyor
uses
SysUtils, PDFium;
const
// Space/U+00A0 ve hyphen/U+00AD tek bir Arial glyph'i paylaşır,
// Yunan Omega (U+03A9) ile Ohm işareti (U+2126) de paylaşır
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; // canlı, kaydedilmemiş belge
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; // tam kaydetme ve yeniden yüklemeden sonra
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
Kaydettikten sonra CMap'i yamak neden yetmedi
Kaydedilen dosyayı yamak yalnızca kaydedilen dosyayı düzeltir ve yama, CMap yapısını bayt-bayt sağlam tutarsa. İlk düzeltme, FPdfCompress unit'indeki RepairSubsetToUnicodeCMaps, artımlı olmayan her TPdf.SaveAs'ten sonra çalışır ve çelişen her CID'yi çözer: bfchar girdisi kazanır, yalnızca yüksek bittte farklı olan bir çift daha küçük taban-Latin kod noktasına çözülür, geri kalan her şey ilk eşlemesini korur
İlginç olan kısmı olumsuz sonuç. Çelişen CMap'i, ister başlangıç-kodu ister dizi biçiminde olsun, temizce yeniden kurmak bariz hamle gibi görünüyordu ve PDFium her yeniden kurulan CMap'i düpedüz reddedip Identity'ye döndü. Yerli okuyucunun kabul ettiği tek çıktı, çelişen hex değerlerinin blok düzeni ile CID kapsamı dokunulmadan eşit-uzunluklu yerinde değişimiydi. İkinci ders daha mütevazıydı: o günkü notumuz bellek-içi vakayı, canlı belgenin hiç ToUnicode stream'i olmamasına bağlıyordu. DLL'i doğrudan çağırmak bunu çürüttü, çünkü canlı belge aynı belirsiz stream'i taşıyordu; gerçek düzeltmenin PDFium CMap'i daha üretmeden önce olması gerekiyordu. Onarım rutini, diğer PDFium tabanlı araçların ürettiği PDF'lere karşı savunma olarak kütüphanede kalıyor
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
// Yalnızca eşit-uzunluklu düzenlemeler; onarılabilir çelişkisi olmayan dosyalar
// ve çapraz-referans-stream ya da object-stream dosyaları olduğu gibi kopyalanır
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Fontu glyph yerine kod noktasına göre anahtarlama
Kök düzeltme, PDFium'dan CMap üretmesini hiç istememeyi gerektiriyor. TPdf.LoadCachedFont artık sistem font baytlarını TPdf.LoadUnicodeKeyedCidFont'a veriyor; bu da fontun kendi sfnt cmap tablosunu okuyor, format 12 alt tablosunu tercih edip format 4'e düşüyor. Kod noktalar sıralanmış ve tekilleştirilmiş olarak geri gelir, CID k+1 k. kod noktasına atanır ve CID 0 .notdef olarak bırakılır. Açık bir CIDToGIDMap her CID'yi kendi glyph'ine gönderir; U+0020 ile U+00A0 böylece aynı konturu çizen iki farklı CID alır ve ToUnicode CMap her CID'yi yalnızca bir kod noktasına eşler. Font sonra FPDFText_LoadCidType2Font üzerinden yüklenir — açık CID-to-GID map'lerle CID Type 2 font gömme yazısındaki glyph-düzeyi yazımın arkasındaki aynı giriş noktası
// TPdf.LoadUnicodeKeyedCidFont'tan yoğunlaştırıldı
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); // tek CID, tek kod noktası
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
FPDFText_SetText sonra bir string yazdığında ters arama karakter başına tek bir CID'ye düşer; NBSP, soft hyphen ve Ohm işareti böylece her iki öncelik kuralı altında kendileri olarak hayatta kalır — bellekte ve her kayıttan sonra. Kaydedilen dosya, motor-üretili bir stream yerine component'in kendi ToUnicode stream'ini taşıdığı için RepairSubsetToUnicodeCMaps onda düzeltilecek bir şey bulamaz
Tek bir bfrange girdisi bütün bir bloğu nasıl silebilir?
CID dizisi bir xxFF sınırını aşan tek bir bfrange, PDFium'u içinde bulunduğu bloğun tamamını çöpe atmaya iter. ISO 32000-1 §9.10.3 yalnızca hedefin son baytının bir aralık içinde değişmesine izin verir ama CID tarafının kendi tuzağı vardır: PDFium'un HandleBeginBFRange'si yüksek CID'yi (low and $FFFFFF00) or (high and $FF) olarak türetir. CID 00FE'den 0101'e giden bir dizi bu yüzden 00FE'den 0001'e okunur — düşük büyüktür yüksekten — ve bloğun tamamı geçersiz işaretlenir. Başarısızlık sessizdir: SetText başarılı olur, sayfa kusursuz render edilir ve çıkarma o bloktaki her karakter için U+0000 döndürür
BuildUnicodeKeyedCidCMap, kod noktası ya da CID düşük baytı FF'ye ulaşmadan önce diziyi bitirir, her bloğu CMap gramerinin 100 girdilik sınırının içinde tutar ve tamamlayıcı-düzlem kod noktalarını UTF-16 surrogate çifti hedefli tek tek bfchar girdileri olarak yazar; bir aralık içinde bir surrogate çiftini artırmanın tanımlı bir anlamı yoktur. O hikâyenin surrogate tarafı Delphi'de emoji, CJK ve surrogate çifti işleme yazısında. Yalnızca-bfchar bir CMap sınır sorununu bütünüyle atlatırdı — birkaç kat büyüklük pahasına
Kod-noktası-anahtarlı font neleri kapsamaz?
Kod-noktası-anahtarlı yol, Unicode cmap alt tablosu açığa veren her fontu kapsar ve geri kalanlar için eski glyph-anahtarlı davranışa döner. Buna güvenmeden önce bilinmeye değer sınırlar:
- Yalnızca (3,0) cmap'i olan symbol fontlar ve CID yolunun yükleyemediği her font eskisi gibi
FPDFText_LoadFont'tan geçer; iki kod noktasının paylaştığı bir glyph orada hâlâ belirsiz çıkarılabilir - Format 12 alt tablosu olmadan harita BMP ile sınırlıdır ve girdi sayısı 65535'te tavanlanır; böylece her CID, sıfırın üstünde iki bayta sığar
- Artımlı kaydetmeler (
saIncremental) tasarım gereğiRepairSubsetToUnicodeCMaps'i atlar; artımlı bir revizyon yalnızca-ekleme kalmalıdır. Kod-noktası-anahtarlı fontlar, component'in kendi yazdığı metin için bunu önemsiz kılar - TrueType Collections ekstra özen ister: GDI
GetFontData.ttc'nin tamamını döndürür veFPDFText_LoadCidType2Font'un face index parametresi yoktur; simsun.ttc'den NSimSun istemek eskiden SimSun'u — 0. face'i — gömer ve render ederdi. Component artık aile adını name tablosuyla (nameID 1 ve 16) eşleştirir ve istenen face'i, cmap parse edilmeden önce bağımsız bir sfnt olarak çıkarır; parse başarısız olursa koleksiyon baytları olduğu gibi geçer ve davranış 0. face'e döner
Metin yazımı, font gömme ve çıkarma, Delphi, C++Builder ve Lazarus boyunca tek bir sayfa modelini paylaşır; tam API Delphi için PDFium Component ürün sayfasında anlatılır