Teknik Makale

PDFlibPas MovePage: kalıtsal kutular instance paylaştığında

PDFlibPas'ta, yani Delphi PDF kütüphanesinde, MovePage ile taşınan bir sayfa eski Pages düğümünün tuttuğu MediaBox, CropBox ve Resources objectlerinin aynısını alıyordu; böylece taşınan sayfada sonradan yapılan bir SetPageBox ya da DrawText o düğümü ve hâlâ ondan kalıtan her kardeş sayfayı sessizce yeniden yazıyordu. v3.539.36'dan beri taşınan sayfa kendi kopyalarını alır ve bir indirect reference referans olarak kalır. Aynı sürüm iki ilişkili yolu daha kapatır: birkaç sayfanın paylaştığı indirect bir kutu üzerinde SetPageBox ve kaynak belgenin sayfalarını kendi Pages düğümüne bağlı bırakan, CropBox'ı da MediaBox'a bağlı bırakan CopyPageRanges

Buraya getiren raporlar hiçbir zaman object identity demez. "7. sayfayı kırptım, 8 ile 12 de kırpıldı" ya da "CropBox'ı daralttım, MediaBox da onunla oynadı" ya da en kafa karıştırıcısı olan "bir sayfayı yeni belgeye kopyaladım, özgün dosya değişti" derler. Hiçbir şey çökmüyor, hiçbir şey sızmıyor ve kaydedilen dosya kusursuz geçerli bir PDF. Sadece kimsenin istemediği geometri içeriyor

Tek sayfada SetPageBox kardeşleri neden yeniden boyutlandırır?

SetPageBox kardeşleri yeniden boyutlandırıyordu, çünkü page tree'nin iki girdisi bellekteki tek bir diziye işaret ediyordu ve SetPageBox hedef dizisini yerinde düzenliyor. Aynı instance'ı tutan her sayfa ya da Pages düğümü düzenlemeyi görüyordu. v3.539.36 öncesinde PDFlibPas'ta o paylaşımı üreten üç kod yolu vardı:

  • MovePage, kalıtsal attribute'ları sayfayı ebeveyinden ayırmadan önce sayfanın üzerine materyalleştirir ve ataların kendi objectlerini kopya değil bağlıyordu; böylece taşınan sayfa ile eski kardeşleri bir kutu dizisini ve bir Resources sözlüğünü paylaşıyordu
  • SetPageBox indirect reference'leri takip ediyor ve referans edilen diziyi düzenliyordu; dolayısıyla birkaç sayfanın tek bir /MediaBox 11 0 R object'ine işaret ettiği dosyada o sayfaların hepsi tek çağrıyla yeniden boyutlanıyordu, MovePage işin içinde olsun ya da olmasın
  • CopyPageRanges, kalıtsal değerleri sayfayı hedef belgeye klonlamadan önce kaynak sayfanın üzerine materyalleştirir ve Pages düğümü instance'larını kaynak sayfaya bağlıyordu; üstelik varsayılan CropBox olarak MediaBox instance'ının kendisini
PDFlibPas MovePage aliasing: taşınan sayfa ile eski kardeşi atasının kendi MediaBox dizi instance'ını tutuyordu; dolayısıyla SetPageBox bir sayfayı düzenleyip ötekini yeniden boyutlandırıyordu. v3.539.36'dan beri materyalleştirme çözülmüş kopyalar bağlıyor ve düzenlemeler dokunduğunuz sayfada kalıyor
Bellekteki tek bir diziye işaret eden iki page tree girdisi her düzenlemeyi her tutucuya düşürüyordu ve kaydedilen PDF bu arada hep geçerliydi

MovePage olgusunun kısa bir tarihi var. v3.539.27 öncesinde MovePage yanında yalnızca /Resources taşıyordu; böylece farklı bir ebeveyn altına taşınan sayfa sessizce o ebeveynin boyutunu ve rotasyonunu üzerine alıyordu. v3.539.27, eksik MediaBox, CropBox ve Rotate'i düzeltti — sayfaları yeniden sıralarken CollateDocumentsEx'in dayandığı da tam olarak bu — ama ataların değerlerini paylaşılan instance'lar olarak bağlıyordu. v3.539.36'nın kapattığı pencere budur. SetPageBox ve CopyPageRanges yolları daha eskidir; v3.539.36 öncesi her derlemede vardır

Direkt değerler, indirect reference'ler ve sayfa attribute kalıtımı

Kalıtsal bir sayfa attribute'ının doğru kopyası direkt değerleri çoğaltır ve indirect reference'leri referans olarak tutar; çünkü ISO 32000-1'in kendisinin çizdiği ayrım budur. Bir sözlüğün içinde yazılı [0 0 400 300] gibi bir direkt object yalnızca o sözlüğe aittir. Bir kez 11 0 obj olarak tanımlanan ve 11 0 R olarak anılan bir indirect object tasarımdan paylaşımlıdır: ISO 32000-1 §7.3.10 onu dosyanın herhangi bir yerinden adreslenebilir kılar ve her 11 0 R aynı objecti demektir

Sayfa attribute kalıtımı, ISO 32000-1 §7.7.3.4, üçüncü bir olgu ekler. Resources, MediaBox, CropBox ve Rotate bir Pages düğümünde durabilir ve kendininkini tanımlamayan her torun sayfaya uygulanır. Sayfa değeri tutmaz; değeri /Parent üzerinden arar. O arama zinciri, bir sayfa ebeveyn değiştirdiği anda kopar; MovePage ile BalancePageTree'nin önce etkin değerleri sayfanın kendisine yazması bu yüzden zorunludur. Soru yalnızca nasıl yazacaklarıdır

Bir object havuzu hatayı neden gizler?

PDFlibPas'ta ayrıştırılan ya da üretilen her PDF objecti belgenin TPDFStructure havuzuna aittir ve sözlükler ile diziler girdilerine sade işaretçiler saklar. TPDFDictionary.Add işaretçiyi kaydeder, başka hiçbir şey. Bir instance'ı iki ebeveyn kapsayıcıya eklemek bu yüzden çalışma zamanının denetleyebildiği her düzeyde legaldir: kapanışta çift free yok, bozulacak bir reference count yok, istisna yok. Serileştirme de aynı ölçüde hoşgörülüdür; her kapsayıcı paylaşılan instance'ın güncel değerini inline yazar ve herhangi bir düzenlemeden önce çıktı, doğru bir kopyanın üreteceğiyle bayt bayt aynıdır

Aliasing yalnızca biri paylaşılan instance'ı yerinde değiştirdiğinde yüzeye çıkar. SetPageBox tam olarak bunu, var olan dizinin üstünden bir dikdörtgen sarmalayıcısıyla yapar; bir sayfaya çizmek de bir font ya da image kaydedildiğinde aynısını Resources sözlüğüne yapar. Düzenleme, işaretçiyi tutan diğer her kapsayıcıya, sessizce düşer

PDFlibPas v3.539.36 paylaşım yerine kopyayı nasıl koyar?

PDFlibPas v3.539.36 sorunu iki uçtan da düzeltir: materyalleştirme artık kopyalar bağlıyor ve kutu yazımı artık yalnızca sayfanın sahibi olduğu bir diziyi düzenliyor. Her düzeltme ötekini kapsayamayacağı bir olguyu kapsar

Materyalleştirme yardımcısı PLInheritPageAttributes artık Value yerine Page.Owner.Decode(Value.Output) bağlıyor. Serileştirici üzerinden gidip gelmek, PDF semantiğini bedavaya getiren kaba ama kesin bir yoldur. Direkt bir dizi ya da sözlük literal metnine serileşir ve taze, bağımsız bir instance'a çözülür. Bir indirect reference 11 0 R'ye serileşir ve aynı 11 numaralı objecte işaret eden yeni bir reference objectine çözülür; dolayısıyla sayfa, inline edilmiş bir kopya almak yerine paylaşılan objecti referans etmeyi sürdürür ve v3.539.27'nin getirdiği referans davranışı korunur. Kopya tam olarak direkt yapının derinliğindedir: kopyalanan bir sözlüğün içindeki bir referans üzerinden ulaşılan her şey, dosya formatının niyet ettiği gibi paylaşımlı kalır. BalancePageTree yeniden ebeveylediği her sayfa için aynı yardımcıyı çağırır; orada materyalleştirilen sayfalar da ayrı instance'lar alır

PDFlibPas materyalleştirme gidiş-dönüşü: PLInheritPageAttributes, Page.Owner.Decode(Value.Output) bağlıyor. Direkt bir dizi literal metne serileşir ve taze bir instance'a çözülür; indirect bir 11 0 R ise serileşip paylaşılan 11 numaralı objecte hâlâ işaret eden yeni bir referansa çözülür
Serileştirip yeniden ayrıştırmak PDF object semantiğini bedavaya getirir: direkt değerler kopyalanır, referanslar referans kalır, tam olarak ISO 32000-1'in niyet ettiği gibi

Yalnızca kopyalamak yetmez, çünkü referans olgusu hâlâ paylaşılan bir objecte işaret eder. SetPageBox o referansı takip edip 11 numaralı objecti düzenlesydi, taşınan sayfa eski ebeveyni ve onun diğer çocuklarını yine yeniden boyutlandırırdı. Bu yüzden kutu yazıcısı artık copy-on-write uygular: yalnızca sayfanın kendi girdisi direkt bir dizi olduğunda yerinde düzenler ve indirect ya da eksik kutuyu yeni bir direkt diziyle değiştirir. 11 numaralı object, ona atıf yapan diğer her sayfa için dokunulmadan kalır

PDFlibPas SetPageBox copy-on-write kararı: sayfanın kendi girdisi direkt bir dizi olduğunda yerinde düzenlenir; indirect bir referans ya da eksik olduğunda yazıcı yerine yeni bir direkt dizi yazar, böylece paylaşılan 11 numaralı object ona atıf yapan diğer her sayfa için değerini korur
Referanslar paylaşılan objectlere işaret ettiği sürece materyalleştirmedeki kopya yetmez; kutu yazıcısı bu yüzden yalnızca sayfanın sahibi olanı düzenler
Kod yoluv3.539.36 öncesiv3.539.36'dan beri
MovePage materyalleştirmesiSayfa atanın kendi direkt instance'larını tutarSayfa çözülmüş kopyalar tutar; referanslar referans kalır
SetPageBoxBir referansı takip eder ve paylaşılan diziyi düzenlerYalnızca sayfadaki direkt diziyi düzenler, aksi hâlde yenisini yazar
CopyPageRanges kaynak sayfasıPages düğümü kutularını paylaşır; CropBox, MediaBox instance'ıdırKaynak sayfadaki her materyalleştirilmiş değer bir kopyadır
Sayfa kaynakları klonlanırken varsayılan kutularCropBox, BleedBox, TrimBox ve ArtBox tek diziyi paylaşırHer varsayılan kutu kendi dizisini alır

Son satır gizli olanıdır. Kütüphane bir sayfanın kaynaklarını sayfa yakalama ya da birleştirme için klonlarken eksik CropBox, BleedBox, TrimBox ve ArtBox girdilerini doldurur ve onlar eskiden aynı dizi instance'ıydı. Mevcut hiçbir çağıran o alias'ın düzenlenecek kadar yaşamasına izin vermedi ama sıradaki çağıran verecekti. O varsayılan kutu değerlerinin nasıl seçildiği kendi başına bir konudur; PDFlibPas TrimBox, BleedBox ve CropBox varsayılanları rehberinde kapsanır

MovePage aliasing'ini elle yazılmış bir PDF ile yeniden üretmek

Herhangi bir PDFlibPas derlemesini denemenin en hızlı yolu, LoadFromString ile yüklenen küçük elle yazılmış bir PDF'tir; her object numarası önceden bilinir. Aşağıdaki yardımcı, doğru hesaplanmış bayt ofsetleriyle klasik bir cross-reference tablosu yazar; böylece test, bozuk dosyalar için ayrıştırıcının kurtarma davranışına dayanmaz

uses
  System.SysUtils, PDFlibrary;

function BuildPdf(const Objects: array of AnsiString): AnsiString;
var
  Offsets: array of Integer;
  I, XRefPos: Integer;
begin
  Result := '%PDF-1.4'#10;
  SetLength(Offsets, Length(Objects));
  for I := 0 to High(Objects) do
  begin
    Offsets[I] := Length(Result);   // "N 0 obj" dizesinin 0 tabanlı bayt ofseti
    Result := Result + AnsiString(IntToStr(I + 1)) + ' 0 obj'#10 +
      Objects[I] + #10'endobj'#10;
  end;
  XRefPos := Length(Result);
  Result := Result + 'xref'#10'0 ' + AnsiString(IntToStr(Length(Objects) + 1)) +
    #10'0000000000 65535 f '#10;
  for I := 0 to High(Offsets) do      // her girdi tam olarak 20 bayttır
    Result := Result + AnsiString(Format('%.10d 00000 n ', [Offsets[I]])) + #10;
  Result := Result + 'trailer'#10'<< /Size ' +
    AnsiString(IntToStr(Length(Objects) + 1)) + ' /Root 1 0 R >>'#10 +
    'startxref'#10 + AnsiString(IntToStr(XRefPos)) + #10'%%EOF'#10;
end;

function StreamObj(const Content: AnsiString): AnsiString;
begin
  Result := '<< /Length ' + AnsiString(IntToStr(Length(Content))) +
    ' >>'#10'stream'#10 + Content + #10'endstream';
end;

Test belgesinin iki ara Pages düğümü vardır. 3. düğüm indirect bir MediaBox (11 numaralı object, 400'e 300 point), direkt bir CropBox ve direkt bir Resources sözlüğü taşır ve iki sayfaya sahiptir. 4. düğüm Letter boyutunda bir MediaBox'a sahiptir ve üçüncü sayfaya sahiptir. 1. sayfayı 3. konuma taşımak onu 4. düğümün altına yeniden ebeveynler; tam da materyalleştirme gereken hamle budur: onsuz sayfa bir Letter sayfasına dönüşür

procedure Check(Condition: Boolean; const Msg: string);
begin
  if not Condition then
    raise Exception.Create(Msg);
end;

procedure CheckMovedPageIsIsolated;
var
  Lib: TPDFlib;
  FontID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Check(Lib.LoadFromString(BuildPdf([
      '<< /Type /Catalog /Pages 2 0 R >>',
      '<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>',
      '<< /Type /Pages /Parent 2 0 R /Kids [5 0 R 6 0 R] /Count 2 ' +
        '/MediaBox 11 0 R /CropBox [10 20 390 280] /Resources << >> >>',
      '<< /Type /Pages /Parent 2 0 R /Kids [7 0 R] /Count 1 ' +
        '/MediaBox [0 0 612 792] >>',
      '<< /Type /Page /Parent 3 0 R /Contents 8 0 R >>',
      '<< /Type /Page /Parent 3 0 R /Contents 9 0 R >>',
      '<< /Type /Page /Parent 4 0 R /Contents 10 0 R >>',
      StreamObj('1 w'), StreamObj('2 w'), StreamObj('3 w'),
      '[0 0 400 300]']), '') = 1, 'load failed');

    Lib.SelectPage(1);
    Check(Lib.MovePage(3) = 1, 'MovePage failed');
    Lib.SelectPage(3);                       // az önce taşıdığımız sayfa
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'inherited MediaBox lost');

    Lib.SetPageBox(1, 0, 200, 200, 200);     // MediaBox 200 x 200
    Lib.SetPageBox(2, 0, 100, 100, 100);     // CropBox 100 x 100
    FontID := Lib.AddStandardFont(4);        // Helvetica
    Lib.SelectFont(FontID);
    Lib.SetTextSize(12);
    Lib.DrawText(20, 20, 'MOVED');

    // Eski parent'ı başka bir sayfa seçmeden ÖNCE inceleyin (aşağıya bakın)
    Check(Pos(AnsiString('/Font'), Lib.GetObjectToString(3)) = 0,
      'font registered in the old Pages node');

    Lib.SelectPage(1);                       // eski 2. sayfa, hâlâ 3. düğümün altında
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling MediaBox changed');
    Check(Abs(Lib.GetPageBox(2, 2) - 380) < 0.001, 'sibling CropBox changed');
    Check(Pos(AnsiString('400'), Lib.GetObjectToString(11)) > 0,
      'shared object 11 was rewritten');
  finally
    Lib.Free;
  end;
end;

GetPageBox(BoxType, Dimension) kutu tipi olarak MediaBox için 1, CropBox için 2; boyut olarak genişlik için 2 alır. Varsayılan sol-alt kökenle SetPageBox(1, 0, 200, 200, 200) demek sol 0, üst 200, 200 genişlik ve 200 yükseklik demektir. v3.539.27 ile v3.539.35 arasındaki derlemelerde kardeş denetimleri başarısız olur: CropBox düzenlemesi 3. düğümün direkt dizisine düşer ve MediaBox düzenlemesi 11 numaralı objecti referans üzerinden yeniden yazar

CopyPageRanges kaynak belgeyi değiştirir mi?

v3.539.36'dan beri CopyPageRanges kaynak sayfaların üzerine yazmayı sürdürür ama yazdığı her değer ayrı bir kopyadır; dolayısıyla kaynaktaki sonraki düzenlemeler düzenlediğiniz sayfada kalır. Yazmanın kendisi kasıtlıdır: kaynak sayfanın sözlüğü hedefe klonlanmadan önce açık MediaBox, CropBox, Rotate ve Resources'a ihtiyacı vardır, yoksa kopya kalıttığı her şeyi kaybederdi. Sayfayı hedefe yeniden numaralandırarak kopyalamak PDFlibPas'ta belgeler arası object derin kopyasında kapsanır; bu hata ise kopyanın yalnızca okuduğunu varsayan çoğunluğun aksine kaynak tarafında duruyordu

Çıktı bunu hiç göstermedi. Paylaşılan da kopyalanan da aynı serileştiği için iki belge de düzeltme öncesi ve sonrası bayt bayt aynı kaydedildi. Alias'ı yalnızca kopyadan sonra kaynak belgeye yapılan bir düzenleme ifşa eder:

procedure CheckSourceSurvivesCopy;
var
  Lib: TPDFlib;
  SourceID, TargetID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Check(Lib.LoadFromString(BuildPdf([
      '<< /Type /Catalog /Pages 2 0 R >>',
      '<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 ' +
        '/MediaBox [0 0 400 300] /Resources << >> >>',
      '<< /Type /Page /Parent 2 0 R /Contents 5 0 R >>',
      '<< /Type /Page /Parent 2 0 R /Contents 6 0 R >>',
      StreamObj('1 w'), StreamObj('2 w')]), '') = 1, 'load failed');
    SourceID := Lib.SelectedDocument;

    TargetID := Lib.NewDocument;             // seçilen belge olur
    Check(Lib.CopyPageRanges(SourceID, '1') = 1, 'copy failed');

    Lib.SelectDocument(SourceID);
    Lib.SelectPage(1);
    Lib.SetPageBox(2, 50, 250, 100, 100);    // yalnızca CropBox'ı daraltır
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'MediaBox followed CropBox');
    Lib.SetPageBox(1, 0, 200, 200, 200);

    Lib.SelectPage(2);
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'sibling page resized');

    Lib.SelectDocument(TargetID);            // kopya özgün boyutunu korur
    Lib.SelectPage(Lib.PageCount);
    Check(Abs(Lib.GetPageBox(1, 2) - 400) < 0.001, 'copied page resized');
  finally
    Lib.Free;
  end;
end;

v3.539.36 öncesinde buradaki iki sayfa da kök düğümün direkt MediaBox'ını kalıtıyordu; kopya o instance'ı kaynak 1. sayfaya bağlıyor ve bir kez daha 1. sayfanın CropBox'ı olarak bağlıyordu. CropBox'ı daraltmak bu yüzden MediaBox'ı daraltıyor, MediaBox'ı yeniden boyutlandırmak ise kök düğüm üzerinden 2. sayfayı yeniden boyutlandırıyordu. Sayfaları kopyalayıp kaynağı düzenlemeye devam eden iş akışları — özgünleri kırpmadan önce dubleks taramaları tek PDF'te harmanlamak gibi — bunun göründüğü yerdir

Instance aliasing'ini test etmek neden bu kadar zordur?

Instance aliasing'ini test etmek zordur, çünkü gözlenebilir etki belirli bir sırada üç adım ister: alias'ı kur, bir tarafı değiştir, sonra başka bir şey dokunmadan öteki tarafı incele. Testlerin çoğu yalnızca ilk adımı yapar ve kaydedilen çıktıyı karşılaştırır; o da alias varken yokken aynıdır

PDFlibPas'taki sıra tuzağı SelectPage'dir. Sayfa seçmek geçerli fontu SelectFont üzerinden yeniden uygular ve bu da o fontu sayfanın kaynaklarına kaydeder. Kendi /Resources'u olmayan bir sayfa ebeveyninin sözlüğüne çözülür; böyle bir sayfayı seçmek Pages düğümüne usulünce /Font ekler. Yukarıdaki MovePage testinde eski 2. sayfayı seçmek 3. düğüme Helvetica girdisini ekler; bu doğru davranıştır, sızıntı değil. GetObjectToString(3) denetimi bu yüzden SelectPage(1)den önce koşar; ikisini değiştirin test, düzeltilmiş derlemede başarısız olur

O kural aynı zamanda v3.539.36'nın kasıtlı olarak dokunmadığı şeyi de işaretler. Resources sözlüğünü kalıtan bir sayfaya kaynak yazmak, ataların sözlüğüne yazar ve her kardeş yeni girdiyi görür. Bu, belirtildiği gibi çalışan kalıtımıdır, instance paylaşımı değil; paylaşılan bir sözlüğe font ya da image adı eklemek diğer sayfaların nasıl render edeceğini değiştirmediği için de zararsızdır. Bir sayfanın kalıtımı bırakmasını istiyorsanız önce ona kendi Resources sözlüğünü verin

PDF object model kodu için kontrol listesi

Dersler, Delphi'de ya da başka bir yerde, bir havuz ve işaretçi kapsayıcıları üzerine kurulu her PDF object modeline geneller:

  • ISO 32000-1 §7.7.3.4 uyarınca kalıtsal attribute'ları materyalleştirirken direkt değerleri derin kopyalayın ve indirect reference'leri aynı objecte yeni referanslar olarak tutun
  • Paylaşım kasıtlı ve belgelenmiş olmadıkça var olan bir instance'ı ikinci bir kapsayıcıya asla Add etmeyin; havuza ait olmak, çalışma zamanının hiçbir zaman şikâyet etmeyeceği anlamına gelir
  • Yalnızca geçerli düğümün direkt object olarak sahibi olduğu şeyi yerinde düzenleyin; indirect ya da kalıtsal değerleri taze bir direkt objectle değiştirin (copy-on-write)
  • Başka bir girdiden türeyen varsayılan değerler — bir MediaBox'tan gelen CropBox gibi — kendi instance'ına ihtiyaç duyar
  • Aliasing'ini, öteki tutucuda değiştir-sonra-incele dizileriyle test edin ve arada usulünce yazabilecek çağrıların sırasını denetleyin
  • Kaydedilen çıktıyı karşılaştırmak burada hiçbir şey kanıtlamaz: paylaşılan ve kopyalanan değerler ilk düzenlemeye dek aynı serileşir
  • PDFlibPas'ta MovePage, CollateDocumentsEx, BalancePageTree ya da CopyPageRanges çağırıp sonra sayfa kutularını düzenliyor ya da sayfalara çiziyorsanız v3.539.36 ve sonrasına yükseltin

PDFlibPas, page tree düzenlemeyi, belgeler arası kopyalamayı ve sayfa kutusu denetimini Delphi, C++Builder ve Free Pascal için tek bir TPDFlib sınıfıyla sunar. Sürümler, platformlar ve tam API referansı için PDFlibPas Delphi PDF library ürün sayfasına bakın