HotPDF Delphi Bileşeni, Delphi içinde yüklediğiniz herhangi bir PDF dosyasından Unicode metni iki çağrı üzerinden çıkarır: ExtractLoadedPageText bir sayfanın okuma akışındaki metnini döndürür, ExtractLoadedPageTextLayout (v2.263.0 ile eklendi) ise sayfanın görsel düzenini düz metin olarak yeniden kurar, böylece sütunlar, girintiler ve tablo hizası çıktıda korunur. İkisi de HotPDF tarafından oluşturulmamış belgelerde çalışır ki asıl önemli durum budur: bir müşterinin e-postayla gönderdiği fatura, bir tarama bürosunun teslim ettiği rapor, artık kimsenin adını anımsayamadığı bir yazılımın ürettiği sözleşme
Buraya gelmek iki imzanın düşündürdüğünden fazla mekanizma gerektirdi, çünkü bir PDF metni bir metin dosyasının sakladığı gibi saklamaz. Bu yazı önce her iki çıkarma kipini anlatır, sonra altındaki üç parçanın — CMap okuyucusu, içerik akışı yorumlayıcısı ve font çözme yedek zinciri — kaputunu açar, çünkü eşlemenin nasıl çalıştığını bilmek, bozuk çıktıya omuz silkmekle onu teşhis etmek arasındaki farktır
Metin çıkarmak neden dosyadan dize okumaktan zordur?
Bir PDF içerik akışı karakterleri değil karakter kodlarını kaydeder. Tj ve TJ operatörleri (ISO 32000-1 §9.4.3), anlamı tümüyle kendinden önceki Tf ile seçilen fonta bağlı bayt dizeleri taşır: 0x41 baytı WinAnsi altında A harfi, bir alt küme fontunda keyfi bir glif ya da bileşik bir CJK fontunda iki baytlık bir CID değerinin yarısı olabilir. ISO 32000-1 §9.10 metin çıkarmayı tam olarak bu çözme sorunu olarak tanımlar — her kodu, font sözlüğünün sağladığı bilgiyle Unicode değerine geri eşlemek — ve standart, uyumlu bir dosyanın bunu yapmaya yetecek bilgiyi sağlamak zorunda olmadığını açıkça söyler
O son cümle, bugüne dek gördüğünüz her "bu PDF dosyasından kopyala yapıştır neden anlamsız çıkıyor" hata bildirimini açıklar. /ToUnicode tablosu olmayan bir alt küme fontunu gömen bir üretici, kusursuz işlenen ama saçma çıkan bir dosya yazmıştır, çünkü koddan glife eşleme vardır ama koddan Unicode değerine eşleme hiç gönderilmemiştir. Dolayısıyla dürüst her çıkarma API bileşeni, elinden gelenin en iyisini yapan bir yedek zinciridir ve işe yarar soru, zincirin ne kadar derine indiğidir
ExtractLoadedPageText ile okuma akışı çıkarma
Arama dizinleme, anahtar sözcük eşleştirme ya da metni bir çözümleme hattına verme için istediğiniz çağrı ExtractLoadedPageText olur. İmza şöyledir: function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — sayfa dizinleri sıfır tabanlıdır, sonuç yerel bir Delphi UnicodeString olarak gelir ve sayfanın okunabilir bir içerik akışı yoksa işlev istisna yükseltmek yerine False döndürür
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText artık belgenin okuma akışındaki metnini tutar
finally
Pdf.Free;
end;
end;
Çıktıdaki satır sonları bilinçli olarak basit bir geometrik kuraldan gelir. HotPDF v2.766.79'dan bu yana, glif başlangıç noktası yazma yönü boyunca metin yüksekliğinin yarısından fazlasına yayıldığında bir yeni satır eklenir; metin yüksekliği, bu glifin ve önceki glifin çıkıntıdan inişe (ascent-to-descent) kutu yüksekliklerinden büyüğü olarak, sayfa birimlerinde alınır. O sürümden önce dikey hareket, Tf'ye verilen font boyutunun yarısıyla karşılaştırılırdı; metni metin matrisiyle boyutlandıran belgelerde bu çoğu zaman 1'dir, dolayısıyla yukarı fırlamış bir üst simge yeni satır açar ve döndürülmüş metin her karakteri kendi satırına koyardı. Sözcük boşlukları da v2.766.76'dan bu yana eşleşen bir kuralı izler: üretici bir boşluk karakteri göstermek yerine metin konumunu kaydırarak — bir TJ ayarı ya da Td ile — sözcükleri ayırıyorsa, önceki glifin kendi genişliğinden sonra taban çizgisi boyunca kalan boşluk glifin kutu yüksekliğinin 0.15'ini aştığında bir boşluk eklenir ve iki Çince, Japonca veya Korece karakterin arasına asla eklenmez. Çözücünün çözemediği karakterler yok olmak yerine boşluğa dönüşür, böylece tek tek glifler ayakta kalmasa bile sözcük sınırları korunur. Bu kipin denemediği şey, okuma sırası kümeleme ya da çok sütunlu düzen algılamadır: iki sütunlu bir sayfa, içerik akışı sırasına göre iç içe geçmiş çıkar ki bu genellikle, ama her zaman değil, görsel sıradır
Bunun yerine ne zaman düzen koruyan çıkarma kullanmalısınız?
ExtractLoadedPageTextLayout, konumun anlam taşıdığı her yerde doğru çağrıdır: tablolar, formlar, kod listeleri, fark almayı, grep yapmayı ya da sütuna göre ayrıştırmayı düşündüğünüz her şey. Glifleri bir akışa düzleştirmek yerine onları taban çizgilerinde kümeler, her taban çizgisini X eksenine göre sıralar ve yatay ile dikey boşlukları, ortanca glif ilerlemesi ve font boyutundan boyutlandırılmış eş aralıklı bir karakter kılavuzunda yeniden üretir. Aynı taban çizgisindeki kesimler arasındaki geniş boşluklar boşluk dizilerine, taban çizgileri arasındaki büyük boşluklar ise boş satırlara dönüşür. Sonuç, sayfanın göründüğü gibi okunur
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Sütunlar, girintiler ve tablo hizası karakter kılavuzunda
// boşluklar ve boş satırlar olarak korunur
end;
İki kip, çözme mekanizmasının her baytını paylaşır ve yalnızca çözülmüş glifleri nasıl dizdikleriyle ayrılır; dolayısıyla bu tercih doğruluktan hiçbir şey götürmez. Yalnızca sözcükler önemliyse ExtractLoadedPageText, düzen önemliyse ExtractLoadedPageTextLayout seçin. Çok sütunlu okuma sırası algılama ikisinin de kapsamı dışında kalır — iki sütunlu bir sayfanın kılavuz görüntüsü size iki sütunu yan yana, sadakatle gösterir ki bu fark almak için tam olarak doğru, düzyazıyı yeniden akıtmak için değildir
HotPDF karakter kodlarını Unicode değerine nasıl çözer?
HotPDF Delphi Bileşeni her karakter kodunu öncelik sırasına dizilmiş bir yedek zinciriyle çözer: önce fontun gömülü /ToUnicode CMap tablosu, sonra /Encoding girdisi (akış ya da adlandırılmış CMap), ardından — bileşik fontlar için — Adobe-GB1, Adobe-CNS1, Adobe-Japan1 ve Adobe-KR gibi karakter koleksiyonlarına ait Adobe standart CMap dosyaları ve en sonda basit fontlar için yerleşik WinAnsi ile MacRoman tabloları. Yanıt veremeyen bir strateji istisna yükseltmek yerine sessizce bir sonrakine geçer ve zincirin tamamını tüketen bir kod 0 değerine çözülür, böylece çağıran taraf tahmin yürütmek yerine ıskalananları sayabilir
/ToUnicode CMap tablosu (ISO 32000-1 §9.10.3) ilk sırada durur, çünkü üreticinin özellikle çıkarma için yazdığı tek eşleme odur. Adobe standart CMap yolu, herhangi bir şey gömmek yerine UniGB-UTF16-H gibi önceden tanımlı CMap tabloları kullanan CJK belgeleri için önemlidir: HotPDF koleksiyon dosyalarını resources\CMap dizini altında gönderir, çalışma zamanında bunları yürütülebilire göre bulur ve ayrıştırılan her tabloyu süreç başına önbelleğe alır — bunu bilmeye değer, çünkü en büyükleri olan Adobe-GB1 tablosu kabaca 2 MB kaynak metindir ve onu sayfa başına yeniden ayrıştırmak istemezsiniz. Dizin yoksa çözücü diske dayalı CMap tablolarını atlar ve gömülü tablolar ile yerleşik kodlamalarla çalışır. Bu, HotPDF ile karmaşık yazı sistemi metin şekillendirme yazısında ele alınan şekillendirme sorununun okuma tarafındaki aynasıdır; orada aynı kod ile glif ayrımıyla yazma anında karşılaşılır
Bilmeye değer iki CMap sözdizimi tuzağı
CMap dosyaları kolayca ayrıştırılabilir görünür ama değildir ve ilk denemedeki ayrıştırıcı başarısızlıklarının çoğunu iki ayrıntı açıklar. Birincisi, kayıt sayısının bölüm anahtar sözcüğünden önce gelmesidir: bir bölüm beginbfchar 2 değil, 2 beginbfchar diye okunur. Sayının anahtar sözcükten sonra geleceğini bekleyen bir ayrıştırıcı sayıyı başıboş bir belirteç olarak yutar, sonra her bölümde sıfır girdi bulur. Sağlam yaklaşım — HotPDF okuyucusunun benimsediği yaklaşım — sayıyı tümüyle yok saymak ve eşleşen endbfchar / endbfrange anahtar sözcüğüne kadar döngü kurmaktır; bunun bir yararı da sayıları düpedüz yanlış olan gerçek dünya dosyalarına tahammül etmesidir
İkinci tuzak, bfchar ve bfrange hedeflerinin tamsayı değil UTF-16BE dizeleri olmasıdır. <D83DDE00> hedefi U+1F600 demektir — tek bir kod noktasına yeniden birleştirilmesi gereken bir vekil çifti — ve o dört baytı büyük sonlu bir tamsayı olarak okumak, Temel Çok Dilli Düzlemin dışındaki her kod noktasında anlamsız bir değer üretir. PDF dosyalarındaki emoji artık egzotik değildir, dolayısıyla vekil yeniden birleştirmeyi atlayan bir çözücü kullanıcılarınızın gerçekten sahip olduğu dosyalarda çuvallar. HotPDF önce onaltılık değişmezi ham baytlara ayrıştırır, sonra UTF-16BE kod birimlerini yeniden birleştirir; bu aynı zamanda bitişik harf eşlemelerinin ürettiği çok karakterli hedefleri de kapsar
ExtractLoadedPageGlyphs ile glif düzeyine inmek
Her iki metin çağrısı da ExtractLoadedPageGlyphs üzerine kuruludur ve altındaki THPDFGlyphArray sizin kodunuza da açıktır. Her THPDFGlyphRecord; çözülmüş Unicode kod noktasını, ham karakter kodunu, kodun bayt genişliğini (CMap tablosunun codespacerange tanımına göre 1, 2 ya da 4), etkin font kaynak anahtarı ile boyutunu, kullanıcı uzayındaki X ve Y başlangıç noktasını ve yatay ilerlemeyi taşır; v2.766.76'dan bu yana ayrıca GlyphEndX ile GlyphEndY'yi taşır — karakter ve sözcük aralığı olmadan glifin kendi genişliğinin sayfa uzayındaki sonu, yukarıdaki sözcük boşluğu kuralının ölçümü bundan alır. Bu, içerik akışına kendiniz dokunmadan sözcük sınırı algılama, konumlandırılmış vurgulama ya da özel bir düzen algoritması kurmaya yeter
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Yukarıdaki gibi Unicode = 0 kayıtlarını saymak, metne aşağı akışta güvenmeden önce belirli bir belgede çıkarma kalitesini ölçmenin dürüst yoludur. Glif kayıtları ayrıca her karakteri içerik akışındaki kaynak işlenene bağlar; HotPDF içindeki yüklü belge metin arama ve değiştirme işini aynı temel üzerinde mümkün kılan da budur
Hangi PDF dosyaları metnini vermez?
Bazı dosyalar her çıkarıcıyı yener ve onları tespit etmek, çıktılarını yayına almaktan iyidir. Taranmış belgeler en yalın durumdur: tek büyük bir görüntüden ibaret bir sayfa hiç metin operatörü içermez, dolayısıyla çıkarma doğru biçimde boş bir dize döndürür — çözüm OCR uygulamaktır ve yüklü PDF dosyasından sayfa görüntülerini çıkarmak o hattın ilk adımıdır. /ToUnicode tablosu olmayan alt küme fontları daha zor durumdur: /Encoding yolu ve standart CMap tabloları da boş çıkarsa o glifler 0 değerine çözülür ve metin çağrılarında boşluk olarak görünür. Şifrelenmiş belgeler, onları LoadFromFile aşırı yüklemesiyle parolalarını vererek yüklediğiniz sürece normal biçimde çıkarılır, çünkü akışlar yorumlayıcı onları görmeden önce çözülür
Daha dar bir sınırı açıkça söylemekte yarar var: çözme zinciri CMap ve içerik akışlarını HotPDF içindeki Flate yolundan okur, dolayısıyla ToUnicode akışı alışılmadık bir süzgeç kullanan bir font sayfayı düşürmek yerine bir sonraki stratejiye geçer. Uygulamada FlateDecode son yirmi yılda üretilen neredeyse her şeyi kapsar ve bu geçiş tasarım gereği sessizdir — bir istisna değil, dosyanın izin verdiği en iyi metni alırsınız. Burada font sözlüklerini çözen aynı okuma tarafı nesne mekanizması yüklü belgelerde meta veri düzenlemeyi de besler, böylece bir belge alım hattı tek geçişte çıkarabilir, inceleyebilir ve açıklama ekleyebilir
Metin çıkarma, düzen koruyan işleme, glif düzeyinde erişim ve bunların üzerine kurulu arama ve değiştirme özelliklerinin tamamı, Delphi ve C++Builder için standart HotPDF Delphi Bileşeni kapsamındadır — harici DLL yok, işletim sistemi metin hizmetleri yok, kuyruğunuza tuhaf bir dosya düştüğünde adım adım izleyebileceğiniz Object Pascal var