Teknik Makale

ToUnicode Anahtarlama: NBSP ve Soft Hyphen Metin Çıkarma

Yeni üretilmiş bir PDF'de "two-x" araması, sayfa o beş karakteri açıkça gösterirken sıfır sonuç döndürebilir. Çıkarılan dizeyi kod noktaları olarak dökünce neden ortaya çıkar: tire U+00AD SOFT HYPHEN, ardından gelen boşluk U+00A0 NO-BREAK SPACE olarak geri dönmüştür. İkisi de ekranda birebir aynı görünür. Fark ToUnicode CMap'tedir, okuyucunun karakter kodlarını geriye Unicode'a çevirmek için baktığı tabloda ve tek bir font glyph'inin iki kod noktası tarafından paylaşılmasından doğar

Düz dize araçları uyuşmazlığı daha da büyütür. Bir CSV dışa aktarımı satırı başka türlü böler, bir diff aracı her görüntüleyicide birebir aynı okunan iki satırı işaretler ve bir yazım denetleyici soft hyphen'li "two-x"i bilinmeyen bir sözcük sayar. Unicode normalizasyonu yarı yarıya yardım eder: NFKC, U+00A0'yı sıradan bir boşluğa indirger çünkü NBSP uyumluluk ayrışmasına sahiptir; ama U+00AD hiçbir ayrışmaya sahip değildir, dolayısıyla soft hyphen her normalizasyon biçiminden sağ çıkar

Çıkarılan boşluklar neden U+00A0 olarak dönüyor?

Çıkarılan boşluklar, ToUnicode CMap glyph'e göre anahtarlandığında ve bir glyph'e iki kod noktasından ulaşılabildiğinde U+00A0'ya dönüşür. 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. Glyph tarafından kurulan bir CMap, CID 0003 hakkında bir şey söylemek zorundadır ve FPDFText_LoadFont içindeki PDFium üreteci sonuçta iki şey söyler: onu U+0020'ye eşleyen bir bfchar girdisi ve aynı CID'yi U+00A0'ya eşleyen dizi biçimli bir bfrange

Tek bir Arial glyph'inin Delphi'de PDF metin çıkarmasını nasıl şaşırttığı: U+0020 ile U+00A0 3 numaralı glyph'e, U+002D ile U+00AD tire glyph'ine ulaşır; üretilen ToUnicode CMap CID 0003'ü bir bfchar girdisiyle ve dizi bfrange'le iki kez eşler ve hangi kod noktasının çıkarılacağına okuyucunun öncelik kuralı karar verir
En-düşük-kazanır önceliğinde boşluk düz kalır; en-son-kazanırda, yani PDFium okuyucusunun uyguladığı kuralda, glyph-anahtarlı bir CMap her boşluğu NBSP ve her tireyi soft hyphen olarak çıkarır
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange

ISO 32000-1 §9.10.3, bfchar ve bfrange'in kodları Unicode'a nasıl eşlediğini anlatır ama ikisi aynı CID'yi talep ettiğinde hangisinin kazanacağını söylemez. Okuyucular kendi öncelik kurallarını seçer. En düşük ya da ilk eşlemeyi tutan bir okuyucu düz bir boşluk çıkarır; PDFium okuyucusu son eşlemeyi tutar ve dizi aralığı bfchar bloğunu izlediği için glyph-anahtarlı bir CMap üzerinden yazılan her boşluk NBSP, her tire soft hyphen olarak çıkar. Aynı metin, aynı dosyanın iki görüntüleyicisinde bu yüzden farklı çıkabilir

Çiftler bir örüntü izler. 0x20/0xA0 ile 0x2D/0xAD yalnızca yüksek bittte farklıdır; bir fontun cmap'i, Latin-1 benzerlerini ASCII karakter için kullanılan kontura gönderdiğinde ortaya tam olarak bu çıkar. Yunan büyük Omega harfi (U+03A9) ile Ohm işareti (U+2126) de Arial'de bir glyph paylaşır. Dünden iyi çalışan çıkarma kodunuz bugün göremediğiniz karakterlerde başarısız oluyorsa, debugger'ın dize görünümüne güvenmek yerine kod noktalarını dökün; bir sayfadan metin okumanın temelleri Delphi'de PDFium ile PDF belgelerinden metin çıkarmak yazısında

PDFium Component sistem fontlarını kod noktasına göre nasıl anahtalar

PDFium Component, Unicode cmap taşıyan sistem fontları için motordan o CMap'i üretmesini hiç istermez. TPdf.AddText bir fonta gereksinim duyduğunda component, fontun kendi sfnt cmap tablosunu okur; format 12 alt tablosunu tercih eder, gerekirse format 4'e düşer. 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 component'in yazdığı ToUnicode CMap her CID'yi tam olarak bir kod noktasına eşler

PDFium Component'ta kod-noktası-anahtarlı CID ataması: component fontun sfnt cmap'ini okur, CID 0 notdef olmak üzere her sıralı kod noktasına CID k+1 atar, U+0020 ile U+00A0'nın farklı CID'leri koruması için açık bir CIDToGIDMap bağlar ve her CID'ye tam olarak bir kod noktası veren bir ToUnicode CMap yazar
NBSP, soft hyphen ve Ohm işareti sonra her iki öncelik kuralı altında kendileri olarak hayatta kalır — canlı belgede ve her kayıttan sonra; CMap onarımının düzeltilecek bir şey bulamamasının nedeni budur

Font, 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ı. FPDFText_SetText sonra bir dize yazdığında ters arama karakter başına tek bir CID'ye düşer. Bunun hiçbir yanı okuyucunun öncelik kuralına bağlı değildir, çünkü kuralın seçecek ikinci bir eşlemesi yoktur. Kaydedilen dosya component'in kendi ToUnicode akışını da taşır; dolayısıyla yeniden yükleme sonrası çıkarma, canlı belgenin kullandığı aynı birebir tabloyu görür

Bedeli daha büyük bir CIDToGIDMap'tir: fontun kapladığı her kod noktası için iki bayt. Bir fontun kabaca 3.000 kod noktası eşlediğini düşünün. Harita o zaman 6 KB civarına gelir ve her fontla birlikte motora verilir. Bu takas her seferinde değecektir: birkaç kilobayt, PDFium'da da ilk eşlemeyi tutan görüntüleyicilerde de, gelecek yıl müşterinizin çalıştırdığı hangi indeksleyicide de aynı okunan bir çıkarma satın alır

Aşağıdaki gidiş dönüş, baş belası her çifti içeren bir dize yazar, kaydedilmemiş belgeden çıkarır, kaydeder, yeniden yükler ve yeniden çıkarır. İki dizenin de girdiyle tam olarak eşleşmesi gerekir; CodePoints yardımcısı yalnızca bir uyuşmazlığı okunabilir kılacak şekilde oradadır:

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.

Belgeleriniz onları nadiren taşısa bile NBSP ile soft hyphen'i örnek dizide tutun. Yalnızca ASCII yazan bir test, glyph-anahtarlı bir CMap'e karşı da geçer; çünkü içindeki hiçbir şey okuyucudan iki eşleme arasında seçim yapmasını istemez

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 ve CID tarafında PDFium ayrıştırıcısında buna eş bir tuzak vardır: 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, yüksekten büyüktür — ve bloğun tamamı geçersiz işaretlenir. SetText başarılı olur, sayfa doğru render edilir ve çıkarma o bloktaki her karakter için U+0000 döndürür

PDF CMap ayrıştırmasındaki bfrange tuzağı: 00FE'den 0101'e giden bir CID dizisi xxFF sınırını aşar, PDFium ayrıştırıcısı yüksek CID'yi 0001 olarak türetir, düşüğün yüksekten büyük olması bloğun tamamını geçersiz işaretler, SetText ile render hâlâ başarılıdır ve çıkarma bloktaki her karakter için U+0000 döndürür
PDFium Component CMap yazıcısı tuzağı, her diziyi düşük baytı FF olmadan bitirerek, blokları 100 girdilik sınırın içinde tutarak ve tamamlayıcı-düzlem kod noktalarını tek tek bfchar girdileri olarak yazarak önler

Kod-noktası-anahtarlı font tam olarak bunun dişe dişe dokunduğu yerdir. CID'ler yoğun ve ardışıktır; dolayısıyla 255'ten fazla kod noktası olan her font, yazıcı ardışık girdileri basitçe birleştirseydi 00FF'yi, 01FF'yi ve devamını aşacak CID dizileri üretirdi. PDFium Component'in yazdığı CMap, kod noktası ya da CID bir FF düşük baytına ulaşmadan önce diziyi bitirir ve girdileri en çok 100'lük bloklara böler; CMap gramerinin her beginbfrange ve beginbfchar bölümüne koyduğu sınır budur

U+FFFF üstündeki kod noktalar, hedefleri UTF-16 surrogate çifti olan tek tek bfchar girdileri alır; çünkü bir aralık içinde bir surrogate çiftini artırmanın tanımlı bir anlamı yoktur. 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; aralıkların ana biçim olarak kalması ve bölme kurallarının yükü taşıması bunun içindir

Başka araçlardan gelen dosyalarda ToUnicode çelişkilerini onarmak

FPdfCompress unit'indeki RepairSubsetToUnicodeCMaps, component'in yazmadığı dosyalardaki glyph-anahtarlı çelişkiyi düzeltir. TPdf.SaveAs her tam, artımlı olmayan kayıtta onu çalıştırır ve doğrudan herhangi bir akış üzerinde çağırabilirsiniz. İki hedefi taşıyan her CID için bfchar girdisi kazanır. CID için bir bfchar yoksa, yalnızca yüksek bittte farklı olan bir çift daha küçük taban-Latin kod noktasına çözülür. Diğer her çelişki ilk eşlemesini korur

Onarım, çelişen hex değerlerini eşit uzunlukta değişimlerle yerinde düzenler. Blok düzeni, CID kapsamı ve akışın diğer her baytı eski hâlinde kalır; böylece CMap, PDFium okuyucusunun zaten kabul ettiği biçimde kalır. Temizce yeniden kurulmuş bir CMap hex dökümünde daha derli toplu görünürdü. Ayrıca katı bir okuyucunun zaten kabul ettiği bir akışın yapısını değiştirirdi; yalnızca bir avuç hedef değeri yanlışken upside'sız bir kumardır

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.

Rutin, klasik bir çapraz referans tablosu yeniden kurar; bu yüzden çapraz-referans akışı, hibrit referans ya da object stream kullanan belgeler bayt bayt geçer. Artımlı kaydetmeler (saIncremental) tasarım gereği onarımı atlar, çünkü artımlı bir revizyon yalnızca-ekleme kalmalıdır. Component'in kendi yazdığı metin için ikisi de önemli değildir: kod-noktası-anahtarlı fontları, onarımın bulacağı bir çelişki hiç üretmez

Kod-noktası-anahtarlı yolun geçerli olduğu yer

Kod-noktası-anahtarlı yol, Unicode cmap alt tablosu açığa veren her fontu kapsar; geri kalanları PDFium'un glyph-anahtarlı yüklemesi karşılar. Buna güvenmeden önce bilinmeye değer ayrıntılar:

  • Yalnızca (3,0) cmap'i olan symbol fontlar ve CID yolunun yükleyemediği her font FPDFText_LoadFont'tan geçer; iki kod noktasının paylaştığı bir glyph orada okuyucunun öncelik kuralına tabidir
  • Yalnızca format 4 alt tablosu varken harita Basic Multilingual Plane'i kapsar; (3,10) ya da Unicode platformundaki bir format 12 alt tablosu tamamlayıcı-düzlem kod noktalarını ekler
  • Glyph 0 ile surrogate kod noktaları dışarıda bırakılır ve girdi sayısı 65535'te tavanlanır; böylece her CID, sıfırın üstünde iki bayta sığar
  • TrueType Collections ekstra özen ister: GDI GetFontData .ttc'nin tamamını döndürür ve FPDFText_LoadCidType2Font'un face index parametresi yoktur; koleksiyonu olduğu gibi geçirmek, simsun.ttc'den NSimSun isterken 0. face olan SimSun'u gömerdi. Component aile adını fontun name tablosuyla (nameID 1 ve 16) eşleştirir ve istenen face'i bağımsız bir sfnt olarak verir; parse edemediği bir koleksiyon bütün olarak geçer ve 0. face'i kullanır
  • İstenen adla okunabilir bir sistem font yoksa AddText, font adını PDFium'un standart fontları arasında bakan FPDFPageObj_NewTextObj'ye devreder

Görünmez karakterleri indekste katlayın, metinde tutun

Çıkarma gerçek kod noktalarını döndürmeye başladığında başka bir soru doğar: arama indeksi, yazarın kasıtlı yazdığı gerçek bir NBSP ya da soft hyphen ile ne yapmalı? Bunları indeksleme katmanında katlayın ve TPdf.Text'e dokunmayın. Çıkarılan dize, belgenin söylediği şeyin kaydıdır; indeks ise eşleştirme için kurulmuş kayıplı bir görünümdür ve katlama kuralları ona aittir

Aşağıdaki yardımcı, kendi kodunuza ekleyeceğiniz bir işlevdir. NBSP'yi boşluğa eşler ve soft hyphen'i düşürür; o da Unicode Standardı'na göre yalnızca bir satırın kırılabileceği yeri işaretler. U+2126'ya kasıtlı olarak dokunmaz. NFC normalizasyonu Ohm işaretini Omega'ya çevirirdi, çünkü U+2126'nın U+03A9'ya kanonik singleton ayrışması vardır ve bir veri sayfası aramasının ikisini eşleştirmesi çıkarma değil ürün kararıdır:

uses
  SysUtils, Classes, PDFium;

// Arama indeksi için kendi yardımcınız; PDFium Component'in parçası değildir
function FoldForIndex(const S: string): string;
begin
  Result := StringReplace(S, #$00A0, ' ', [rfReplaceAll]);
  Result := StringReplace(Result, #$00AD, '', [rfReplaceAll]);
end;

procedure IndexDocument(const FileName: string; Lines: TStrings);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    for P := 1 to Pdf.PageCount do
    begin
      Pdf.PageNumber := P;
      Lines.Add(FoldForIndex(Pdf.Text));   // tam metin Pdf.Text'te kalır
    end;
  finally
    Pdf.Free;
  end;
end;

IndexDocument'u ilk örnekteki codepoints.pdf dosyasında çalıştırın; indekslenen satır "two-x yz ΩΩ" biçiminde okunmalı, on bir karakter ve sonuncusu bir Ohm işareti olmalı; oysa Pdf.Text hâlâ NBSP ve soft hyphen dahil özgün on iki karakterin tamamını döndürür

Kod-noktası-anahtarlı fontlar, CMap onarımı ve metin çıkarma, Delphi, C++Builder ve Lazarus boyunca aynı sayfa modelinin üzerine oturur; font ve metin API'si eksiksiz biçimde Delphi için PDFium Component ürün sayfasında anlatılır