Fatura numarasındaki tek bir yanlış karakter ve elinizdeki tek düzenleme primitive'i bütün text run'ını yeniden yazar. PDF Library for Delphi bu boşluğu kapatır: GetTextBlockCharContentLocation, çıkarılan her UTF-16 konumunu onu üreten content-stream instruction'ına, operand'a ve encoded byte range'e geri eşler; ReplaceTextBlockCharSourceBytes ise yalnızca o aralığın üzerine yazar. Metin çıkarma normalde bunun için gereken her şeyi atar. Unicode, genişlik ve geometriyi alırsınız ama provenance kaybolur; 3 numaralı bloğun 7. konumundaki karakter yalnızca bir karakterdir. Onu hangi akış üretti, hangi instruction, hangi operand, o operand içindeki hangi bayt: hepsi yoktur. Bunun üstüne kurulan her nokta düzenleme stratejisi, genellikle decoded content içinde bir alt diziyi arayıp tam olarak bir kez geçtiğini umarak tahmin etmek zorunda kalır. Gerçek bir sayfada böyle olmaz
Bütün bir text run'ını yeniden yazmak sayfayı neden bozar?
Çünkü run yalnızca metin değildir. ISO 32000-1 §9.4.3 içindeki text-showing operator'ları arasında, operand'ı string'leri sayısal ayarlamalarla dönüşümlü taşıyan bir dizi olan TJ bulunur ve dizgi düzenini bu sayılar yapar. [(AB) -120 (CD)] TJ olarak yerleştirilmiş bir satır iki string arasında em'in binde 120'si kadar bir kern taşır. Birleştirilmiş metinle yeni bir Tj üretirseniz kern kaybolur, satır çok az yeniden akar ve bir formda değer kutusundan dışarı kayar. Aynı itiraz font için de geçerlidir: operand baytları Unicode değil, Tf'nin seçtiği kodlamadaki kodlardır; composite font için okuduğunuz karakterle hiçbir ilişkisi olmayan iki baytlık CID'ler olabilir. Run'ı yeniden üretmek fontun encoding'inde, /ToUnicode haritasında ve glyph kapsamındaki her şeyi doğru bilmenizi gerektirir. Nokta düzenleme, byte domain'den hiç çıkmayarak bütün bunları atlar
GetTextBlockCharContentLocation ne döndürür?
Metot tek bir karakteri, her alanı bir değer değil bir adres olan dokuz alanlı bir record'a çözer. ContentLayer, sayfanın /Contents dizisindeki 1 tabanlı index'tir; karakter iç içe içerikten geldiyse 0'dır. StreamObjectNumber ve StreamGeneration kapsayan akışı tanımlar. InstructionIndex decoded content programındaki 0 tabanlı konumdur, OperandIndex text-string operand'ıdır ve ArrayElementIndex bir TJ dizisi içindeki elemandır; doğrudan string operand için -1 olur. Ardından SourceByteOffset ve SourceByteLength, decoded string içindeki bayt aralığını adlandırır
Var
Lib: TPDFlib;
ListID, Block, CharPos: Integer;
ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
Lib:= TPDFlib.Create;
Try
Lib.LoadFromFile('invoice.pdf', '');
Lib.SelectPage(1);
ListID:= Lib.ExtractPageTextBlocks(3);
Try
// Block ve CharPos, GetTextBlockText üzerinde yaptığınız taramadan gelir
If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags)= 1 Then
Begin
// ContentLayer = 0, glyph'ün iç içe bir Form XObject'te yaşadığını gösterir
// ArrayElementIndex = -1, TJ dizisi değil düz bir Tj operand'ı demektir
End;
Finally
Lib.ReleaseTextBlocks(ListID);
End;
Finally
Lib.Free;
End;
End;
Arama sorgu zamanında hiçbir maliyet getirmez. Renderer her content layer'ı çözerken yürüdüğü mantıksal aralıkları kaydeder; bu nedenle konum sorgusu, her karakterde bütün content span'lerini doğrusal taramak yerine sıralı bir interval listesi üzerinde ikili aramadır. Sorduğunuzda yeniden ayrıştırma yapılmaz; harita zaten ücretini ödediğiniz extraction geçişi sırasında oluşturulmuştur. Zaten vuruş koordinatlarını döndüren PDF metin aramasıyla eşleşmeleri sayıyorsanız, her vuruşa bir content location eklemenin maliyeti yok denecek kadar azdır
Unicode'u değil baytları düzenlemek
ReplaceTextBlockCharSourceBytes, etkin PDF font encoding içindeki ham replacement baytlarından oluşan bir AnsiString alır. Tasarımın tamamı budur ve bilerek böyledir. Hiçbir şey transcode edilmez, yeniden encode edilmez, font hakkında tahmin yapılmaz. Kütüphane baytlarınızı hedef string'in adlandırılmış aralığı üzerine ekler ve kapsayan content layer'ı yeniden yayımlar. Aynı TJ dizisindeki komşu string'ler ve aralarındaki sayısal kern'ler byte özdeş bırakılır. Yukarıdaki düzeni alın: [(AB) -120 (CD)] TJ içindeki B'yi bulmak ArrayElementIndex 0, SourceByteOffset 1 ve SourceByteLength 1 verir. Onu Z ile değiştirirseniz yayımlanan içerikte (AZ) bulunur; ardından gelen -120 ve (CD) hâlâ dokunulmadan durur. Regresyon paketi tam olarak bunu doğrular, çünkü "kerning'i koruduk" türü bir iddia fark ettirmeden doğru olmaktan çıkabilir
Function EditableHere(Flags: Integer): Boolean;
Begin
Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;
// ...
If EditableHere(Flags) Then
Begin
If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
Begin
// Eski listedeki her location artık geçersiz. Yeniden çıkar.
Lib.ReleaseTextBlocks(ListID);
ListID:= Lib.ExtractPageTextBlocks(3);
End
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
// Çıkarma işleminden beri layer bizim altımızda değişti
Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
// Kontrol etmediğimiz veya sonraki bir sürümün eklediği bir bayrak
End;
İçselleştirmeye değer iki operasyonel ayrıntı var. Çağrı, text list'in çıkarıldığı sayfaya geçici olarak geçer ve başarıda da başarısızlıkta da önceden seçilmiş sayfayı geri yükler; böylece imlecinizi sessizce taşımaz. Başarılı olduğunda page element snapshot'larını da temizler ve daha önceki bir enumeration geçişinden tuttuğunuz tüm tutamaçları geçersiz kılar
Hangi karakterler düzenlenemez?
Altı kategori vardır ve kütüphane her birini belirsiz biçimde başarısız olmak yerine Flags bit maskesinde adlandırır. Bu, mutlu yoldan daha önemlidir; çünkü gerçek belgelerde eşlenemeyen durumlar yaygındır ve her birinin farklı bir nedeni vardır
PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: çıkarılan birkaç UTF-16 konumu tek bir kaynak glyph'ünden genişler. Bir kodufi'ye eşleyen/ToUnicodegirdisi, aynı bayt aralığını paylaşan iki karakter verir; bunları tek bir kaynak glyph'ü olarak ele alın ve aralığı bir kez düzenleyinPDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: karakter layout sırasında sentezlenmiştir. Çıkarılan kelime boşlukları olağan örnektir ve hiç kaynak baytı yoktur; bu yüzdenSourceByteOffset-1,SourceByteLengthise 0 gelirPDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: okuduğunuz metin bir/ActualTextreplacement'ından gelmiştir. Değiştirilmiş string'den kaynak baytlarına benzersiz bir ters eşleme yoktur; dolayısıyla location yalnızca tanısaldırPDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glyph bir Form XObject'in içindedir. Baytlar adreslenebilir, ancak Form birkaç sayfa tarafından çizilebilir; high-level API üzerinden onu düzenlemek istemediğiniz bir düzenleme olurPDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operand, mevcut extraction yolunun font eşlemesinden önce çözdüğü UTF-16BE byte order mark taşıyan bir hex string'dir. Decoded sonuca ofset vermek artık özgün baytları adreslemez, bu yüzden valid bayrağı temizlenirPDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: string operand ile text-showing operator iki farklı akışta yaşar
Sonuncusu ayrı bir cümleyi hak eder, çünkü mühendisler bunun olamayacağını sık sık varsayar. ISO 32000-1 §7.8.2, bir sayfanın /Contents dizisindeki akışların birleştirildiğini ve aralarındaki bölmenin yalnızca lexical boundary üzerinde olması gerektiğini söyler. Bu nedenle bir akışta BT /F1 16 Tf 220 340 Td (CrossLayer), sonraki akışta Tj ET bulunması tamamen geçerli bir sayfadır. Eşleme tanısal konumu korur ama onu salt okunur işaretler; çünkü operator'ün instruction index'i operand'ın baytlarından farklı bir layer'a aittir ve birini diğeriyle adreslemek dosyayı bozar
Kütüphane haritanın hâlâ geçerli olduğunu nasıl bilir?
Fingerprint'ler sayesinde ve yazmadan hemen önce kontrol ederek. Her extraction listesi kaynak sayfayı, ayrıca her content layer için layer uzunluğunu ve iki bağımsız rolling hash'i kaydeder: bir FNV-1a hash'i ve bir DJB2-style xor hash'i. ReplaceTextBlockCharSourceBytes herhangi bir şeyi ayrıştırmadan önce hedef layer'ı yeniden okur ve üç değerin tamamını karşılaştırır. O layer'ın herhangi bir yerinde bir bayt değişirse PDFLIB_ERROR_TEXT_LOCATION_STALE döner ve yazma gerçekleşmez. Bu bilerek muhafazakârdır: kontrol instruction başına değil layer başınadır; bu yüzden aynı content stream içindeki ilgisiz bir düzenleme bile location'ınızı geçersiz kılar. Bu doğru ödündür: bir bayt bile kaymış bir akıştaki ofset küçük bir yanılma değil, sessiz bozulmadır. Aynı disiplin, CTM ve clipping için content-stream state tracker dahil düzenleme yüzeyinin geri kalanını da yönetir. Başarılı her replacement'tan sonra listeyi bırakın ve yeniden çıkarın
Direct Access üzerinden salt okunur eşleme
DAGetTextBlockCharContentLocation, Direct Access yoluyla açılmış bir sayfa için aynı flag sözlüğüyle aynı record'u verir. Tasarım gereği bu yalnızca tanısaldır: ReplaceTextBlockCharSourceBytes seçili düzenlenebilir belge üzerinde çalışır ve Direct Access bir okuma yoludur. Location verisi, dosya tutamacı kapatıldıktan sonra da text block listesinde kalır; bu da onu offline denetim için kullanılabilir kılar
FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
PageRef:= Lib.DAFindPage(FileHandle, 1);
DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
Try
Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
ContentLayer, StreamObjectNumber, StreamGeneration,
InstructionIndex, OperandIndex, ArrayElementIndex,
SourceByteOffset, SourceByteLength, Flags);
// Location'lar DACloseFile sonrasında okunabilir kalır
Finally
Lib.DAReleaseTextBlocks(DirectList);
End;
Finally
Lib.DACloseFile(FileHandle);
End;
Bunu bir şeyi değiştirmek için değil, soruları yanıtlamak için kullanın. Hangi sayfalarda yerinde asla düzenleyemeyeceğiniz metinler var? Bu corpus'un ne kadarı /ActualText override'larıyla geliyor? Hangi üreticinin çıktısı operator'leri content layer'lar arasında bölüyor? Her karakterin bir adresi olduğunda bu sorgular ucuzdur ve bir düzeltme pipeline'ına karar vermeden önce çalıştırılmaya değerdir
Nokta düzenlemenin durduğu yer
Nokta düzenleme bir metin motoru değil, bisturidir. Baytları yerinde değiştirdiği için özgün metinden daha geniş veya dar bir replacement yeniden akmaz, yeniden satırlanmaz ve çevresindeki kern'leri güncellemez. Monospace bir alanda bir rakamı diğeriyle değiştirmek iyi bir uyumdur. Bir paragrafı yeniden yazmak değildir. Ayrıca kesinlikle bir güvenlik aracı değildir: glyph baytlarının üzerine yazmak özgün baytları dosyanın revision history'sinden kurtarılabilir bırakır; bu nedenle gizlilik gerektiren her şey üstünü kapatmak yerine içeriği kaldıran gerçek karartmaya aittir. Bu sınırların karşılığında dürüstlük kazanırsınız. Her karakterin üzerinde işlem yapabileceğiniz bir bayt adresi vardır veya neden olmadığını söyleyen adlandırılmış bir bayrak vardır; fingerprint kontrolü de eski bir haritayı bozuk sayfa yerine hard error'a çevirir. Karakterden içerik baytına eşleme ve yerinde kaynak baytı replacement'ı, Delphi, C++Builder ve Lazarus için native Object Pascal PDF kütüphanesi olan PDF Library for Delphi metin çıkarma ve content editing yüzeyinin parçası olarak sunulur