Belirti, HotPDF Bileşeni üzerine kurulmuş bir sayfa kopyalama yardımcı programında ortaya çıktı: üç sayfalık bir belgenin 1. sayfasını istemek sürekli olarak sayfa 2'yi üretti. İndeksleme (dizinleme) mantığını kontrol etmek yanlış bir şey bulmadı. Çağrı 0 tabanlı bir mantıksal dizin kullanıyordu, aritmetik doğruydu, sınır koşulları iyiydi. Yine de her seferinde yanlış sayfa çıktı
Hata kopyalama kodunda hiç değildi. Dosya yüklenirken HotPDF'in dahili sayfa dizisini nasıl oluşturduğuyla ilgiliydi

İki sıralama, tek bir karışıklık kaynağı
Bir PDF dosyası, her biri bir nesne numarasıyla tanımlanan dolaylı nesneler topluluğudur. Dosya yapısı, bu numaraların okuma sırasını yansıtması için hiçbir zorunluluk getirmez. Nesne 1, sayfa 2'yi tutabilir; nesne 20, sayfa 1'i tutabilir. Okuma sırasını asıl tanımlayan şey sayfa ağacıdır: /Kids dizileri sayfa başvurularını (referanslarını) bir görüntüleyicinin (viewer) bunları göstermesi gerektiği sırayla listeleyen /Pages sözlüklerinden oluşan bir hiyerarşi (ISO 32000-1 §7.7.3)
Hatayı tetikleyen belge şu sayfa ağacı yapısına sahipti:
{ Sayfa ağacı kökü, nesne 16 }
16 0 obj
<<
/Type /Pages
/Count 3
/Kids [20 0 R { mantıksal sayfa 1 }
1 0 R { mantıksal sayfa 2 }
4 0 R] { mantıksal sayfa 3 }
>>
endobj
Dosya tesadüfen nesne 1 ve nesne 4'ü bayt akışında nesne 20'den önce listeliyordu. Dolaylı nesneler arasında dosya sırasına göre yineleyen ve sayfa türü sözlükleri buldukça bunları bir PageArr dizisine damgalayan herhangi bir ayrıştırıcı (parser), dizin 0'da nesne 1, dizin 1'de nesne 4 ve dizin 2'de nesne 20 ile sonuçlanacaktır. Mantıksal sayfa 1 PageArr[2]'de bulunur. Sayfa dizini 0'ı istemek, bunun yerine mantıksal sayfa 2'yi getirir
HotPDF'in dahili ayrıştırma yollarının her ikisi de tam olarak bunu yapıyordu. PDF 1.3/1.4 dosyaları için kullanılan geleneksel yol ve nesne akışlı (object-stream) belgeler (PDF 1.5+) için kullanılan modern yol, her biri /Kids zincirini takip etmek yerine dosya fiziksel sırasında dolaylı nesneleri yürüyerek PageArr'i oluşturuyordu
Hipotezi doğrulamak
Herhangi bir düzeltmeye dokunmadan önce, uyuşmazlığın varsayılmak yerine kanıtlanması gerekiyordu. qpdf komut satırı aracı bunu kolaylaştırır:
{ kabuk (shell) }
qpdf --show-pages input.pdf
{ Çıktı Kids sırasını gösterir: 20 0 R, sonra 1 0 R, sonra 4 0 R }
qpdf --show-object="16 0 R" input.pdf
{ /Kids'i okuma sırasına göre Pages sözlüğünü gösterir }
Her sayfayı tek tek çıkarıp dosya boyutlarını kontrol etmek eşlemeyi doğruladı: PageArr[0]'ın ürettiği şey mantıksal sayfa 2'ye ait içerikti ve PageArr[2] mantıksal sayfa 1'i tutuyordu. Dairesel kayma bariz kanıttı. Bu aynı zamanda sorunun neden birden fazla farklı kaynak belgede ortaya çıktığını da açıkladı: sayfa nesnelerinin önceki bir mantıksal sayfadan daha düşük nesne numaralarına sahip olduğu herhangi bir PDF bunu tetikler
PDF'lerin bu duruma düşmesinin doğrudan bir nedeni vardır. Artımlı (incremental) kaydetmeler güncellenmiş nesneleri yeni nesne numaralarıyla ekler ve çapraz başvuru (cross-reference) tablosundaki eski yuvaları hiçbir yeri göstermeyecek şekilde bırakır. Kapak sayfası ekleyen düzenleyiciler, Kids dizisindeki konumuna bakılmaksızın bunu yüksek bir nesne numarasıyla ekler. Bazı oluşturucular sayfaları mantıksal sayfa sırası yerine içerik akışı için uygun bir sırayla yazarlar. PDF formatı aksi bir şey yapmalarını gerektirmez
Çözüm: Kids dizisini takip edin
Doğru yaklaşım, PageArr'yi dolaylı nesneleri tarayarak değil, katalog kökünden (catalog root) /Kids zincirini yürüyerek oluşturmaktır. Her iki ayrıştırma yolu da ilk geçişini tamamladıktan sonra, bir son işleme (post-processing) adımı mantıksal sırayı çözer:
procedure THotPDF.ReorderPageArrByPagesTree;
var
PagesObj : THPDFDictionaryObject;
KidsArray : THPDFArrayObject;
NewPageArr: array of THPDFDictArrItem;
I, J, PageIndex, KidsIndex: Integer;
RefObj : THPDFLink;
PageObjNum: Integer;
Found : Boolean;
begin
{ Kök /Pages sözlüğünü FRootIndex aracılığıyla bulun }
PagesObj := FindPagesRootFromCatalog;
if PagesObj = nil then Exit;
KidsIndex := PagesObj.FindValue('Kids');
if KidsIndex < 0 then Exit;
KidsArray := THPDFArrayObject(PagesObj.GetIndexedItem(KidsIndex));
SetLength(NewPageArr, KidsArray.Items.Count);
PageIndex := 0;
for I := 0 to KidsArray.Items.Count - 1 do
begin
RefObj := THPDFLink(KidsArray.GetIndexedItem(I));
PageObjNum := RefObj.Value.ObjectNumber;
Found := False;
for J := 0 to Length(PageArr) - 1 do
begin
if PageArr[J].PageLink.ObjectNumber = PageObjNum then
begin
NewPageArr[PageIndex] := PageArr[J];
Inc(PageIndex);
Found := True;
Break;
end;
end;
{ Sayfa olmayan Kid'ler (ara /Pages düğümleri) eşleşme sağlamaz; atla }
end;
if PageIndex > 0 then
begin
SetLength(PageArr, PageIndex);
for I := 0 to PageIndex - 1 do
PageArr[I] := NewPageArr[I];
end;
end;
Çağrı, tüm nesneler kataloglandıktan ancak herhangi bir sayfa işlemine hizmet verilmeden önce, her bir ayrıştırma yolunun sonunda yer alır:
{ Geleneksel yol }
ListExtDictionary(THPDFDictionaryObject(IndirectObjects.Items[I]), FPageslink);
ReorderPageArrByPagesTree;
Break;
{ Modern yol (nesne akışları) }
if TryParseModernPDF then
begin
Result := ModernPageCount;
ReorderPageArrByPagesTree;
Exit;
end;
Yeniden sıralama adımı, n'nin Kids sayısı ve m'nin geçerli PageArr uzunluğu olduğu O(n * m)'dir, ancak düz bir sayfa ağacına (tüm yapraklar derinlik 1'de, bu da gerçek dünyadaki PDF'lerin ezici çoğunluğunu kapsar) sahip herhangi bir belge için her ikisi de aynı değerdir ve maliyet göz ardı edilebilir düzeydedir. Derinden iç içe geçmiş sayfa ağaçları, burada gösterilen tek seviyeli yaklaşım yerine yinelemeli (recursive) bir yürüyüş gerektirir; üretim uygulaması bu durumu ayrı olarak ele alır
Düzeltmeden sonra CopyPageFromDocument'i kullanmak
ReorderPageArrByPagesTree yerindeyken, mantıksal sayfa dizinleri beklendiği gibi çalışır. Üst düzey CopyPageFromDocument, 0 tabanlı bir mantıksal dizin alır ve doğru sayfayı hedef belgeye kopyalar:
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
Source.LoadFromFile('source.pdf');
Dest.FileName := 'extracted.pdf';
Dest.BeginDoc;
{ Mantıksal sayfa 0'ı kopyala (kullanıcının gördüğü ilk sayfa) }
Dest.CopyPageFromDocument(Source, 0, 0);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
CopyPageFromDocument dahili olarak ham PageArr dizinine güvenmek yerine sayfa ağacı sırasını sorgular, bu nedenle fiziksel ve mantıksal sıranın ayrıldığı belgelere karşı bile doğru davranır. Toplu işlemler için InsertPagesFromDocument mantıksal dizinlerden oluşan bir diziyi kabul eder ve bunları tek geçişte kopyalar
Bu PDF ayrıştırma hakkında neyi ortaya koyuyor?
PDF belirtimi açıktır: mantıksal sayfa sırası, nesne numaraları veya bayt ofsetleriyle değil, sayfa ağacının /Kids dizisiyle tanımlanır (ISO 32000-1 §7.7.3.2). Kısayol olarak farklı bir sıralama kullanan herhangi bir ayrıştırıcı (parser) gördüğü belgelerin çoğunda doğru sonuçlar üretecektir, çünkü çoğu oluşturucu (generator) sayfaları doğal sırada yazar ve ardışık nesne numaraları atar. Hata, birisi artımlı (incremental) olarak düzenlenmiş, başka bir araç tarafından yeniden düzenlenmiş veya farklı bir düzen seçen bir yazılım tarafından oluşturulan bir PDF yükleyene kadar gizlenir
Yalnızca kendi kendine oluşturulan PDF'lere karşı test yapmak, bu tür sorunları tamamen kaçırır. Bu nedenle bir sayfa sıralaması gerilemesi (regression) düzeltmesi, çeşitli kaynaklardan gelen belgelere ihtiyaç duyar: artımlı kaydetmeler, eklenmiş kapak sayfalarına sahip taranmış belgeler, nesne grafiğini farklı şekilde doğrusallaştıran veya optimize eden araçlar tarafından üretilen PDF'ler. Orijinal hatayı tetikleyen bir belge, regresyon test paketinde kalıcı olarak kalmalıdır
HotPDF Bileşeni sayfası, CopyPageFromDocument, InsertPagesFromDocument ve MovePage dâhil olmak üzere sayfa işlemleri için tam API'yi kapsar