İki PDF'i elle birleştirin, tek bir sayfa nesnesini hedef belgeye taşıyın ve kopya, doğrudan bir erişim ihlaline yürür. PDFlibPas bunu CopyForeignObject içinde düzeltir: bir dolaylı nesneyi ve tüm başvuru kapanışını derin kopyalar, /Parent gibi döngüsel geri başvuruları özyinelemeye girmek yerine null olarak çözümler
Belgeler arası tek sayfa kopyalamak neden çöker?
Çünkü bir PDF sayfa ağacı, yalnızca aşağıya doğru okursanız ağaçtır. Özyinelemeli bir kopyalayıcının yaptığı gibi, her sözlükteki her değeri izleyerek gezerseniz sayfa sözlüğü size /Parent'i verir; o da geldiğiniz /Pages düğümünü işaret eder ve o düğüm size /Kids'i verir, o da sayfayı işaret eder. ISO 32000-1 §7.7.3, /Parent'i kök dışındaki her sayfa ağacı düğümünde zorunlu kılar; yani bu, reddedebileceğiniz bozuk bir dosya değildir — elinize geçecek her belgenin normal biçimidir
Sorunun ikinci yarısı numaralandırmadır. Dolaylı nesneler, tek bir dosyaya özgü bir nesne numarasıyla tanımlanır (ISO 32000-1 §7.3.10); bu yüzden A belgesinden B belgesine taşınan bir nesnenin yeniden numaralanması ve kopyalanan kapanış içindeki ona dair her başvurunun aynı şekilde yeniden numaralanması gerekir, yoksa eskiden tek paylaşımlı fontu işaret eden iki başvuru artık birbiriyle ilgisiz iki şeyi işaret eder. Bu yeniden numaralandırma, hızlı bir birleştirmenin bayt düzeyinde yaptığı işle aynıdır ve ikisini yan yana okumaya değer: hızlı PDF birleştirme için bayt düzeyi başvuru kaydırma sorunu bütün dosyaları çevirerek çözerken, nesne düzeyinde bir kopya bunu birer kenar hâlinde çözmek zorundadır
PDFlibPas CopyForeignObject aslında neyi kopyalar
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) bir dolaylı nesneyi ve oradan ulaşılabilen her şeyi — iç içe sözlükler, diziler, dizeler, adlar, sayılar ve sözlükleri eksiksiz akışlar — o an seçili belgeye klonlar ve yeni dolaylı başvuruya sıfırdan farklı bir tanıtıcı döndürür. Kaynak nesne numaraları, çağrı süresince tutulan canlı bir eşleme tablosu üzerinden yeniden eşlenir; böylece kapanışta iki kez ulaşılan bir nesne bir kez klonlanır ve iki kez paylaşılır. Kaynak belge kimliği bilinmiyorsa, kaynak seçili belgenin kendisiyse ya da ObjectNumber 1'in altındaysa istisna fırlatmadan sıfır döndürür
var
Lib: TPDFlib;
SourceDoc, TargetDoc, Handle: Integer;
begin
Lib := TPDFlib.Create;
try
TargetDoc := Lib.NewDocument;
if Lib.LoadFromFile('source.pdf', '') <> 1 then
Exit; // LoadFromFile başarıda 1 döndürür
SourceDoc := Lib.SelectedDocument; // yükleme, yüklediğini seçili kıldı
Lib.SelectDocument(TargetDoc); // kopya, seçili belgeyi hedefler
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
İki ayrıntı ilk denemede insanı ısırır. LoadFromFile bir belge kimliği değil 1 ya da 0 cevaplar; dolayısıyla ihtiyaç duyduğunuz tanıtıcı, yüklemenin hemen ardından SelectedDocument'tan gelir. Kopya ayrıca her zaman SelectDocument'un en son geçerli kıldığı belgeye yazar, asla yüklediğiniz belgeye değil. Dahili olarak özyineleme 64'lük sert bir derinlik sınırı da taşır; bu patolojik iç içe geçmişe karşı bir sigortadır, döngüleri yöneten mekanizma değildir — döngü yönetimi ayrı ve bilinçlidir
Nil eşlemesi ayırmak döngüyü neden kırmaz?
Çünkü eşleme tablosundaki Nil aynı anda iki farklı şey anlamına gelir ve kod bunları ayırt edemez. Döngüye karşı bariz savunma, nesneye özyinelemeden önce eşleme girdisini eklemektir; böylece geriye dolanan her şey girdiyi bulur ve durur. Ama girdi henüz gerçek hedefi tutamaz — hedef, altındaki kapanış yazılana kadar yoktur — bu yüzden Nil tutar ve geri kenarı yakalaması beklenen arama Nil okur ve nesnenin hiç eşlenmediği sonucuna varır
// Bozuk: ayrılmış bir Nil hedef, "henüz eşlenmedi" durumundan ayırt edilemez
NewRef := FindMapped(SrcRef.ObjNum);
if not Assigned(NewRef) then
begin
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // ayrılmış, hâlâ Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // yalnızca çıkışta geri doldurulur
end;
Bunu sayfa döngüsü üzerinden izleyin. Sayfanın klonu /Parent'e ulaşır, /Pages düğümüne özyineler; o düğüm /Kids'e ulaşır, o da sayfaya geri özyineler — sayfanın ayrılmış girdisi hâlâ Nil okur, bu yüzden ikinci kez, üçüncü kez klonlanır ve her düzey taze bir çerçeve ve taze bir yarı-bitmiş nesne iter. Gözlemlediğiniz şey temiz bir yığın taşması da değildir: dıştaki çerçeveler hedefi hiç atanmamış başvuruların üzerinde oturur; o yuvalardan biri üzerinden yapılan ilk yazış, ona neden olan sayfa kopyasıyla hiç ilgisi görünmeyen bir yerde erişim ihlali olur
Düzeltme: açık bir in-progress durumu
Onarım, Nil'i çok anlamlı kullanmayı bırakıp soruyu doğrudan sormaktır. Hedefi hâlâ atanmamış bir eşleme girdisi, bu nesne şu anda klonlanıyor demektir ve InProgress yüklemi, sıradan arama çalışmadan önce tam olarak bunu sınar. Doğru olduğunda kenar, geçerli klonun bir atasına dönen bir döngüdür ve PDFlibPas onu izlemek yerine bir null nesnesi üretir
// Nil hedefli bir eşleme girdisi, sürmekte olan bir klonu işaretler
function InProgress(Num: Integer): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to High(Map) do
if (Map[I].SourceObjNum = Num) and (not Assigned(Map[I].Target)) then
Exit(True);
end;
// ... CloneObject içinde, bir dolaylı başvuru için:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // döngüsel geri kenarı, özyineleme
NewRef := FindMapped(SrcRef.ObjNum);
if not Assigned(NewRef) then
begin
SrcInd := SourceDoc.FindObj(SrcRef.ObjNum, SrcRef.GenNum);
if (not Assigned(SrcInd)) or (not Assigned(SrcInd.Obj)) then
Exit(FStructure.NewNull); // asılı kalmış kaynak başvurusu
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // önce ayır, sonra özyinele
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // geri doldur
end;
Exit(NewRef);
Bu genelleme yalnızca PDF'in yapısal bir gerçeği sayesinde güvenlidir: nesne grafiğindeki döngüler geri bağlantılarda görünür, içerik kenarlarında değil. Sayfa ağacındaki /Parent ve ana hat zincirindeki /Prev yukarıya ya da geriye, ziyaret edilmiş bir şeye işaret eder; bir fontun, bir görüntü XObject'inin ya da form XObject'inin kapanışı aşağıya akar ve son bulur. Bu yüzden bir font tanımlayıcısının, bir renk uzayının ya da shading sözlüğünün kopyası null ikamesinden etkilenmez — o kapanışlarda hiçbir şey InProgress'e çarpmaz. Açık söylemek gerekirse maliyet şudur: döngüsel kenar kopyadan sağ çıkmaz. Bu şekilde klonlanan bir sayfa sözlüğü /Parent'i null nesne olarak getirir; ISO 32000-1 §7.3.9 bunu bulunmayan bir girdiyle eşdeğer kılar, yani kopyalanan sayfa, onu hedef /Pages düğümüne bağlayıp /Count'u kendiniz düzeltene kadar hiçbir sayfa ağacına ait olmayan geçerli bir nesnedir. Kopyalanan bir ana hat öğesi aynı şekilde /Prev'ini kaybeder ve kardeş zincirinin yeniden kurulması gerekir. Dürüst takas budur: CopyForeignObject size doğru bir kapanış verir ve yapısal yeniden ebeveynlemeyi çağırana bırakır; nesne numaralarını koruyarak sayfa değiştirme de aynı sınır içinde çalışır
Eşleme girdisi neden NewObjRef'ten önce ayrılmalı?
Bariz bir alternatif, tüm in-progress dansını dışarıda bırakır: önce boş bir kabuk nesnesi ayırır, gerçek numarasını eşlemeye kaydeder, çocuklar klonlandıktan sonra kabuğu doldurursunuz. Bu burada işlemez, çünkü TPDFIndObj.Obj salt okunurdur ve içeriği inşa sonrası değiştirilemez — doldurulacak kabuk yoktur. Numara ile içerik NewObjRef tarafından birlikte belirlenir; bu da eşleme girdisinin özyinelemeli çağrıdan önce oluşturulup ardından tamamlanması gerektiği anlamına gelir ve bu iki an arasındaki aralık, tam olarak InProgress'in kapsamak zorunda olduğu şeydir. Çıktıyı diff etmeden önce bilinmeye değer bir sonuç: NewObjRef çocuk kapanışı yazıldıktan sonra çalıştığı için hedefteki numaralandırma aşağıdan yukarıya çıkar ve nesne numaraları kaynak sırasını yansıtmaz. Dosya biçimi açısından hiçbir şey bunu umursamaz ama elle kurulmuş bir beklentiye karşı bayt karşılaştırması umursar. Bir çalıştırma, hiçbir şeye bağlamamaya karar verdiğiniz nesneleri bırakırsa bunlar bozuk değil, başvurusuzdur ve kaydetmeden önce onları temizleyen araç, ulaşılabilir olmayan PDF nesnelerinin mark-and-sweep toplamasıdır
Bunu kapsayan regresyon, TPDFlib'e karşı test yazanları şaşırtan bir ayrıntı ister: kurucu zaten bir varsayılan belge tutar; DocumentCount 1'den başlar ve iki belgeli bir fixture, = 2 değil >= 2 doğrulamalıdır. Başarılı kopyanın yanında test üç reddi sabitler — bilinmeyen kaynak kimliği, kaynak olarak seçili belgenin kendisi ve sıfır nesne numarası — hepsi istisna fırlatmak yerine 0 döndürür; çünkü bir birleştirme döngüsü, bir koruma yan tümcesinin fırlattığını keşfetmek için kötü bir yerdir
Bunun birleştirme hattındaki yeri
Nesne düzeyinde kopyalama, bütün dosya birleştirmenin fazla kaba kaldığı ilkel işlemdir: bir şablondan tek bir font programını çıkarırken, tek bir form XObject'i damga belgesine çekerken ya da bir ek açıklamayı görünüm akışlarıyla birlikte sayfanın gerisini sürüklemeden dosyalar arasında taşırken ona uzanırsınız. PDFlibPas bunu yüklenmiş belgelere karşı tek bir çağrı olarak ortaya koyar; düşük düzey nesne API'sinin geri kalanıyla nasıl oturduğunu PDFlibPas Delphi PDF Library başvurusunda görebilirsiniz