PDF'leri art arda eklemek ucuz olmalıymış gibi görünür. Sayfa içeriği zaten yerleştirilmiştir, fontlar zaten gömülüdür, görseller zaten sıkıştırılmıştır. İlke olarak birleştirme sadece muhasebe işidir: iki dosyanın numaralandırma alanları çakışmasın diye nesneleri yeniden numaralandır, sayfa ağaçlarını birbirine dik, cross-reference tablosunu düzelt ve yaz. Pratikte çoğu merge kodu bu ucuzluğu çöpe atar. Her girdi dosyasındaki her nesne için token'lara ayrılmış tam nesne ağacına ayrıştırma yapar, birkaç dolaylı başvuruyu değiştirir, sonra ağacı tekrar baytlara serileştirir. Pahalı olan yarılar parse ile yeniden serileştirmedir; nesnelerin büyük çoğunluğu için bunların ürettiği bayt dizisi, girene neredeyse birebir aynıdır
PDFlibPas, Delphi ve C++Builder için yerel bir Object Pascal PDF motorudur ve hızlı merge yolu, bunun kanıtlanabilir biçimde güvenli olduğu her yerde bu gidiş dönüşü atlamak için vardır. Fikir dardır ama bütün belge kümelerinde karşılığını verir: değiştirilmemiş, stream olmayan nesne için özgün kaynak baytlarını aynen alıp, içlerindeki dolaylı başvuruları tek seferlik bayt düzeyi yeniden yazımla değiştirin; her N G R ifadesini (N+Offset) G R ifadesine çevirin. Tokenizer yok, nesne ağacı yok, serializer yok. Bu makale, bu kısayolun nerede yasal olduğunu, bayt yeniden yazımını hiçbir şeyi bozmadan yapan parser durum makinesini, yer imi birleştirmenin neden bütünüyle farklı mekanizma gerektirdiğini ve sıradan merge yolunun aynı anda kareselden lineere nasıl yeniden kurulduğunu anlatır
Nesne yeniden numaralandırması neden merge'in asıl maliyetidir
Her PDF kendi nesne numaralandırma alanını taşır. Dosya A'da nesne 1, nesne 2 ve devamı vardır; dosya B'de de kendi nesne 1'i, 2'si ve devamı vardır. B'nin nesnelerini A'nın dosyasına değiştirmeden bırakamazsınız; çünkü numaralar çakışır ve B'nin içindeki her dolaylı başvuru artık yanlış nesneye çözülür. Çözüm bir ofsettir: A, Offset nesne sayısında bitiyorsa B'nin N nesnesi çıktıda N+Offset nesnesi olur ve B nesnelerinin herhangi bir yerinde görünen her N G R başvurusu da buna uyacak şekilde (N+Offset) G R başvurusuna kaydırılmalıdır
Bu kaydırma, gövdeyi birleştirmenin bütün anlamsal işidir. Sayfa ağacı düzeltmeleri ile AcroForm merge, bir avuç nesne üzerinde yapılan küçük ve sınırlı düzenlemelerdir. Asıl kütle işi, binlerce nesne boyunca başvuruları yeniden yazmaktır ve bunu saf yoldan yapmanın yöntemi, başvuruları yapısal olarak bulabilmek için her nesneyi ayrıştırmaktır. PDFlibPas içindeki MergeFileListFast yordamı ters görüşü benimser: yeterince dikkatli olursanız başvurular ham baytlarda da bulunabilir; önemli olan, rakam-boşluk-rakam-boşluk-R dizisinin gerçekte başvuru olmadığı bağlamları ayırmaktır. Parse'i atlayın, yerinde kaydırın ve nesne başına maliyet, zaten kopyalayacağınız baytlar üzerinde tek lineer taramaya düşsün
Kaynak baytlarını yeniden kullanmak hangi durumda kanıtlı biçimde güvenlidir
Bayt yolu, yalnızca takip eden belgeden çıkışa kopyalanan nesne için üç koşulun da sağlandığı durumda alınır. Bunlardan herhangi biri başarısız olduğunda nesne tam decode-and-reserialize yoluna geri gönderilir; dolayısıyla doğruluk her zaman hızdan önce gelir:
Doc2.IsChangedObject(X)değeri False olmalıdır. Merge motoru nesneyi bellekte zaten değiştirdiyse — örneğin/Parentalanı yeniden işaretlenmiş sayfa nesnesi gibi — bellekteki ağaç doğruluk kaynağıdır ve özgün baytlar bayat kalmıştır. Yalnızca dokunulmamış nesneler uygundur- Kaynak baytlar
streamanahtar sözcüğünü içermemelidir. Stream nesnesinin gövdesi,stream/endstreamile çerçevelenmiş opak ikili veridir ve sıkıştırılmış veya şifrelenmiş stream verisi üzerinde saf başvuru taraması, başvuruya benzeyen bayt desenlerini seve seve bulup bozar. Stream nesneleri özgün stream farkındalıklı yolda kalır - Kaynak baytlar ne
/StructTreeRootne de/StructElemiçermelidir. Hızlı profilde tagged-PDF structure tree merge edilmek yerine bırakılır; bu nedenle bu nesnelerin, motorun bunları bilinçli biçimde null yapabildiği decode yolundan geçmesi gerekir
Karar, nesne başına kopyalama döngüsünde yaşar. Üç denetim de geçerse nesnenin baytları doğrudan ShiftIndRefsInSource içine ve oradan yazıcıya gider; aksi halde baytlar atılır ve nesne GetObject ile yeniden kurulur, ShiftIndRef ile kaydırılır ve serileştirilir. Bu dalın yapısını görmekte fayda var; güvenliği sağlayan şey denetimlerin sırasıdır:
ObjectData := '';
if not Doc2.IsChangedObject(X) then
begin
ObjectData := FastMergeObjectSource(Reader2, X);
if (PLPos('stream', ObjectData) > 0) or
((not PreserveStructTree) and (PLPos('/StructTreeRoot', ObjectData) > 0)) or
((not PreserveStructTree) and (PLPos('/StructElem', ObjectData) > 0)) then
ObjectData := '' // fall back to decode
else
ObjectData := ShiftIndRefsInSource(ObjectData, Offset);
end;
if ObjectData <> '' then
Writer.AddObject(X + Offset, Doc2.GetGenNum(X), ObjectData)
else
begin
Obj := Doc2.GetObject(X, TempStruct); // full parse path
// ... null out struct-tree objects, ShiftIndRef, Obj.Output ...
end;
Boş bir ObjectData, bayt yolunun nesneyi reddettiğinin işaretidir. Bu tek işaret değeri, hızlı ve yavaş yolların birbirinden kopmasını engeller: karar veren tam bir yer vardır ve geri düşme de tam bir yerdedir
Başvuru kaydırma durum makinesi ve köşe durumları
Dolaylı başvuruların bayt düzeyinde yeniden yazımı aldatıcı biçimde kolay görünür; çünkü R ve rakam dizileri, başvuru olmayan bağlamlarda da PDF nesnesinin her yanında görünür. ShiftIndRefsInSource, baytları bir kez yürüyen küçük el yazımı tarayıcıdır ve bir sayıyı yalnızca sonrasında PDF boşluğu ile ayrılmış başka bir sayı ve ardından R ayıracı geldiğinde yeniden yazar. Ucuz çıkışlar en başta gelir: ofset sıfırsa veya kaynak boşsa, tarayıcıya hiç girmeden baytlar dokunulmadan döndürülür
Tarayıcının doğruluğu, başvuru şeklindeki dizinin dokunulmadan bırakılması gereken bağlamları tanımaya dayanır. Kaçırması en kolay sınırlar bunlardır ve her biri açıkça ele alınır:
- Literal string'ler,
(ile)arasında sınırlanır ve aynen kopyalanır; iç içe geçme derinliği izlenir, ayrıca backslash escape dikkate alınır; böylece escape edilmiş parantez derinlik sayımını bozmaz.(see object 3 0 R for details)gibi bir dizge, kitap gibi başvuru desenini aslında düz yazı olarak taşır ve bayt bayt korunmalıdır - Hexadecimal string'ler,
<ile>arasında sınırlanır ve yorumlanmadan geçirilir. Hex string içindeki52baytlarıRiçin ASCII kodudur; hex yükünü metin gibi ele alan tarayıcı hayalet başvuru üretebilir. Sözlüğün açılışındaki<<işareti önce tanınır; böylece sözlük, hex string sanılmaz - Name nesneleri,
/ile başlar ve slash'tan sonraki ilk boşluk ya da ayıraca kadar bütünüyle tüketilir. Bu olmazsa/Rgibi bir ad — yaygın bir resource anahtarıdır — başvurununRkısmı gibi okunabilir - Yorumlar,
%ile başlar, satır sonuna kadar gider ve opak metin olarak atlanır - Sayı-sonra-R testi katıdır Bir başvuru ancak
NboşlukGboşlukRbiçimindeyse veR, boşlukla, ayıraçla veya giriş sonuyla sonlanıyorsa tanınır. Nesil numarası yoksa ya daRsonrası harfse, rakamlar olduğu gibi yazılır./Length 1234içindeki tamsayıyı veMediaBoxiçindeki dört sayıyı sessizce artırmaktan koruyan şey budur
Bu katı testin kalbi, neredeyse belirtim cümlesinin anlattığı gibi okunur:
if (P <= N) and (Source[P] = 'R') and
((P = N) or PLIsPdfWhite(Source[P + 1]) or PLIsPdfDelimiter(Source[P + 1])) then
Obj1 := PLStrToIntDef(PLCopy(Source, I, E1 - I), -1);
if Obj1 >= 0 then
begin
AppendStr(PLIntToStr(Obj1 + Offset)); // shifted object number
AppendBytes(E1, P - E1); // original whitespace + generation
AppendBytes(P, 1); // the 'R'
end;
Yalnızca nesne numarası yeniden yazılır; nesil numarası ve token'lar arasındaki özgün boşluğun aynısı korunur. Böylece çıktı, değişmek zorunda olan tek tamsayı dışında girdinin bayt düzeyinde aynısı olur. Bütün amaç budur — kaynak baytlarını yeniden kullanmayı tam yeniden serileştirmeye sadece yakın değil, gerçekten denk kılan şey budur. Davranış; yalın başvuruları, dizi içindeki başvuruları, başvuru olmayan sayıları, literal string'leri, hex string'leri ve sıfır olmayan nesil numaralarını ofset uygulanmış halde sınayan odaklı unit test kümesiyle kapsanır
Yer imleri neden AppendOutline'ı yeniden kullanamadı
Birden çok belgenin yer imlerini tek outline tree içinde birleştirmek, mevcut AppendOutline yardımcısı için doğal iş gibi görünür; çünkü o zaten bir belgenin üst seviye yer imlerini diğerine aşılamayı bilir. Burada yanlış araç odur ve neden, katmanlama uyuşmazlığındaki ince noktadır. AppendOutline, özgün dosya baytları üzerinden reader'ı yürüyerek mevcut son üst seviye yer imini bulur. Oysa hızlı merge, düzenlemelerini ChangeObject aracılığıyla new-objects tamponunda sahneler; reader bu düzenlemeleri hiç görmez. Üç veya daha fazla belge zincirlerseniz her append, ilk belgenin özgün son yer imini en yeni belgeye yeniden işaretler; böylece aradaki tüm belgelerin yer imleri zincirden düşer. Toplam /Count yine doğru kaldığı için hata, biri yer imi panelini açana kadar fark edilmesi zor şekilde gizlenir
Hızlı yol bunu, reader üzerinde yeniden yürümeyen iki aşamalı metadata tabanlı enjeksiyonla çözer. Tüm girdiler üzerindeki ilk geçiş, belge başına outline root nesnesini ve nesil numaralarını, ilk ve son üst seviye yer imi numaralarını ve root'un /Count değerini toplar. Bu özetten kod, inşa etmesi gereken her bağlantının global nesne numaralarını hesaplar — her belgenin üst seviye /Parent bağlantısı paylaşılan root'a, ilk yer iminin /Prev bağlantısı önceki belgenin son yer imine, son yer iminin /Next bağlantısı sonraki belgenin ilk yer imine — ve bunu saf nesne numarası aritmetiğiyle yapar. Bunun arkasında yazma sırası kısıtı vardır: ilk belgenin nesneleri, sonraki belge daha açılmadan yazıldığı için ilk belgenin tüm outline düzenlemeleri (root /Count ve /Last ile eski son yer iminin /Next değeri), elde daha sonraki belge yokken yalnızca aritmetikle ifade edilebilir olmalıdır. Sonraki her belgenin düzenlemesi ise belge açıldıktan sonra ama yazılmadan önce yerinde uygulanır; dolayısıyla aynı change-object yolu üzerinden dışarı taşınır
Her şeyi bağlayan ofset hizalama değişmezi
Hem başvuru kaydırma hem yer imi enjeksiyonu tek aritmetik değişmeze dayanır; bu da tasarımın en kırılgan varsayımıdır. Takip eden belge içine enjekte edilen başvuru, hedef global nesne numarası eksi o belgenin Offset değeri olarak yazılır; böylece nesne daha sonra ShiftIndRef(Offset) ile kaydırıldığında değer hedef global numaraya iner. İlk belge Offset = 0 alır ve global numaraları doğrudan kullanır. Bu çıkarmanın doğru olması için enjeksiyon sırasında kullanılan akış ofset dizisinin, nesneler gerçekten yazılırken kullanılan ofset dizisiyle birebir eşleşmesi gerekir
Eşleşir; çünkü sayfa ve form merge işlerinin çalışma biçimi buna izin veren özelliğe sahiptir: AddPages, AddFields ve AddFieldFonts yalnızca ilk belgenin mevcut nesnelerini değiştirir; yeni nesne eklemez. Dolayısıyla ilk belgenin nesne sayısı sayfa-merge aşamasında değişmez ve her takip eden belgenin ofseti — ondan önce gelen tüm belgelerin nesne sayılarının toplamı — enjeksiyondan yazmaya kadar sabit kalır. Bunu bozarsanız, yani merge ortasında yeni nesne oluşturan aşama eklerseniz, aşağı akıştaki her sayfa ve yer imi başvurusu, eklediğiniz nesne sayısı kadar kayar. Değişmez sessizdir ama taşıyıcıdır
Tek motor üzerinde üç giriş noktası
Hızlı yol, merge kodunun ayrı çatalı değildir. Aynı çalışma çizgisinde bayt düzeyi motor, tek bir dahili yordam olan MergeFileListInternal(ListName, OutputFileName, PreserveStructTree, StrictMode) içine ayrıştırıldı; kamuya açık API'ler ise iki bayrak seçen ince sarmalayıcılara dönüştü:
MergeFileListFast, structure-tree korumayı kapalı olarak motoru çağırır; bu en yalın yoldur ve tagged-PDF tree'si bırakıldığı için bayt yolu en çok nesneye uygulanabilirMergeFileList, korumayı açık çağırır; böylece structure tree yaşar ve sonuç kullanılabilir tagged PDF olarak kalır. Bu sıradan yol aynı zamanda çok belgeli yer imi ve form merge işini de devralırMergeFileListStrict, strict mode'u açar: ilk metadata geçişi temiz merge rapor etmeyen ilk girdide durur; böylece kötü dosyayı atlayıp devam etmek yerine yalnızca ondan önce toplanmış belgeler dahil edilir
Yolları birleştirmek, sıradan merge'in de ikili O(N²) döngüden — dosya bir ile ikiyi birleştir, çıkan sonucu üçle birleştir ve böyle sürdür; her adımda büyüyen birikeni yeniden ayrıştır — tek lineer geçişe yeniden kurulmasını sağladı; her girdi yalnızca bir kez açılır. Uzun süredir var olan iki dosyalı ve iki stream'li giriş noktaları, MergeFiles ile MergeStreams, dokunulmadan kalmıştır ve gerçekten pairwise merge isteyen çağıranlar için hâlâ mevcuttur
Structure-tree davranışı hakkında dürüst bir not, çünkü test takımını burası vurdu. Hızlı yolun "bırakması" tam silme değildir: ilk belgenin catalog içindeki /StructTreeRoot başvurusunu kaldırır, ama structure-tree nesnesinin kendisi yine de orphan olarak yazılır. Bu yüzden hızlı çıktının baytları hâlâ /StructTreeRoot dizgesini içerir ve hızlı çıktıyla sıradan çıktıyı bu dizgeyi arayarak ayıramazsınız. Gerçek fark, catalog'un hâlâ structure tree'ye ulaşıp ulaşmadığıdır; dosyanın gezilebilir tagged PDF olarak kalıp kalmadığını belirleyen şey budur
Hangi yol ne zaman seçilmeli
Bayt yolu, tagged-PDF structure tree'nin korunması gerekmeyen çok belgeli birleştirmelerde verim optimizasyonudur: rapor paketleme, ekstre koşuları, toplu art arda ekleme gibi. Orta ve büyük boyutlu girdi kümelerinde tekrar eden ölçümlerde, bayt yeniden kullanımı nesne karışımına bağlı olarak duvar saati süresinden yaklaşık yüzde dört ile on üç arasında kırptı; küçük veya bozuk girdilerde yeni arıza üretmedi, çünkü tarayıcının güvenli olduğunu kanıtlayamadığı her nesne tam parse yoluna geri düşer. Erişilebilirlik için structure tree'nin sağlam kalması gerekiyorsa, onu koruyan sıradan tagged-PDF merge yolunu kullanın; ve çok sayıda girdi yerine çok büyük tek dosyalarla çalışıyorsanız, doğrudan dosya erişimiyle büyük PDF birleştirme ve bölme yazısında anlatılan bayt kopyalama teknikleri aynı "baytı kopyala, tam nesne ağacından kaçın" felsefesini dosya ölçeğinde uygular
Merge yordamları ile onların hızlı ve strict varyantları, PDFlibPas Delphi PDF Library ürününün parçasıdır; dokümantasyonu burada anlatılan file-list API'si ve merge seçenekleri için tam başvuruyu taşır