Teknik Makale

Delphi'de Boşta Kalan Başvuru Bırakmadan PDF Sayfa Silmek

HotPDF Delphi Component, yüklü bir PDF'ten THotPDF.DeletePage ile sayfa siler ve 2.751.0 sürümünden bu yana bu çağrı, o sayfayı hâlâ işaret eden her belge düzeyi başvuruyu da budar: /Names /Dests ağacındaki adlandırılmış hedefler, katalogdaki eski usul /Dests sözlüğü, yer imi /GoTo eylemleri, /StructTreeRoot altındaki yapı elemanları, ParentTree, ek açıklamalar için OBJR girdileri ve ayakta kalan sayfalardaki bağlantı ek açıklamaları. Sayfa ağacı en son, başka hiçbir şey silinen nesneye ulaşamadıktan sonra yeniden kurulur

Bunun önlediği arıza yeniden üretmesi kolay, tanılaması zordur. Etiketli bir raporun kapak sayfasını silin, kaydedin ve sonucu açın: Acrobat doğru sayfa sayısını gösterir, ama “Contents” yer imi artık hiçbir yere inmez, erişilebilirlik denetleyicisi sayfası olmayan bir yapı elemanı bildirir ve katı bir doğrulayıcı serbest bırakılmış bir nesneye başvuru listeler. Sayfa ağacında yanlış olan hiçbir şey yoktur. Sorun, bir PDF sayfasının yalnızca /Pages düğümünün bir yaprağı olmamasıdır; katalogun yarısının işaret ettiği bir hedeftir ve o yaprağı kaldırmak bu göstericilerin her birini boşta bırakır

Bir sayfayı /Kids içinden çıkarmak neden yeterli değil?

Çünkü ISO 32000-1, en az yedi bağımsız yapının bir sayfa nesnesine başvuru tutmasına izin verir ve bunlardan yalnızca biri sayfa ağacıdır. Sayfayı /Kids içinden atmak ve /Count değerini azaltmak §7.7.3'ü karşılar ve diğer her başvuru, ya xref'te serbest bırakılmış ya da yeniden yazılan dosyada hiç bulunmayan bir nesneye gösterici hâline gelir. Bu göstericilerden birini izleyen bir görüntüleyici null alır ve o null ile ne yapacağı görüntüleyiciye kalmıştır

  • /Names /Dests altındaki ad ağacı (§7.7.4, §12.3.2.3), adları ilk elemanı sayfa olan hedef dizilerine eşler
  • Katalogda doğrudan duran 1.2 öncesi /Dests sözlüğü, aynı türden dizileri adla anahtarlanmış olarak tutar
  • Ana hat öğeleri (§12.3.3) bir sayfaya ya satır içi bir /Dest ile ya da /S /GoTo ve bir /D dizisi taşıyan bir /A eylemiyle ulaşır
  • Yapı elemanları (§14.7.2), işaretli içeriklerinin hangi sayfada yaşadığını söyleyen bir /Pg anahtarı taşır ve /K çocukları, o sayfaya bağlı işaretli içerik başvuruları ile nesne başvuruları (§14.7.4.3) olabilir
  • ParentTree (§14.7.4.4), sayfa ve ek açıklama /StructParents numaralarını yapı elemanlarına geri eşler ve bir eleman kökten gelen /K zincirinde hiç görünmeden orada yaşayabilir
  • Diğer sayfalardaki bağlantı ek açıklamaları (§12.5.6.5), sayfayı hedefleyen bir /Dest ya da /GoTo eylemi taşır ve katalogdaki /OpenAction da aynısını yapabilir
Bir HotPDF sayfasını /Kids içinden çıkarmanın neden yetmediği: ISO 32000-1, /Names /Dests ad ağacının, eski usul katalog /Dests sözlüğünün, ana hat öğelerinin, /Pg taşıyan yapı elemanlarının, ParentTree'nin, bağlantı ek açıklamalarının ve /OpenAction'ın hepsinin aynı sayfa nesnesine başvuru tutmasına izin verir ve yalnızca sayfa ağacı yeniden kurulur
Bir PDF sayfası, katalogun yarısının işaret ettiği bir hedeftir: yaprağı düşürmek sayfa ağacını memnun ederken diğer her gösterici null'a çözülür; bu yüzden kırpılmış bir rapor Contents yer imini kaybeder ve erişilebilirlik kontrolünden kalır

THotPDF.DeletePage sayfa ağacına dokunmadan önce neyi temizler?

THotPDF.DeletePage(PageIndex) yüklü bir belgede önce başvuru süpürmesinin tamamını çalıştırır, sonra sayfa nesnesini DeleteObj ile silinmiş olarak işaretler, varsa widget ek açıklamalarını AcroForm alan ağacından ayırır, iç sayfa dizisini kaydırır ve son olarak /Kids, /Count ile ayakta kalan her sayfanın /Parent alanını yeniden yazmak için RebuildLoadedPageTree çağırır. Süpürme katalogu sabit bir sırayla ziyaret eder: /Names /Dests ad ağacı, eski usul /Dests sözlüğü, /OpenAction, ana hat ağacı, ParentTree ile birlikte /StructTreeRoot ve en son kalan her sayfanın /Annots dizileri. Her adım, bir başvurunun kaldırılacağına, yeniden hedefleneceğine ya da el değmemiş bırakılacağına, spesifikasyonun o yapıya sayfa olmadan ne yapmasına izin verdiğine göre karar verir. Bunlardan herhangi biri çalışmadan önce iki koruma devrededir: DeletePage aralık dışı bir indeks için Invalid page number hatasını verir ve son sayfayı kaldırmayı reddeder, çünkü sıfır çocuklu bir /Pages düğümü geçerli bir PDF değildir; DeletePages ise diğer yüklü belge sayfa işlemleriyle aynı 1 tabanlı "1,3-5,7-" gösterimini alır ve yazdığınız indeksler çalışırken geçerli kalsın diye en yüksek seçili indeksten aşağı doğru yineler

THotPDF.DeletePage sayfa ağacına dokunmadan önce çalıştırdığı sabit başvuru süpürmesi: korumalar aralık dışı bir indeksi ya da son sayfayı reddeder, sonra /Names /Dests ile eski usul /Dests budanır, /OpenAction düşürülür, ana hatlar NearestRetainedPage'e yeniden hedeflenir, StructTreeRoot ve ParentTree budanır, kalan sayfa bağlantıları kaldırılır ve en son RebuildLoadedPageTree çalışır
Her yapıya spesifikasyonun izin verdiği işlem uygulanır: adlar kaybolur, yer imleri en yakın kalan sayfaya iner, yapı elemanları /Pg'yi kaybeder ya da tamamen silinir ve /Kids yeniden yazımı ancak başka hiçbir şey silinen nesneye ulaşamadığında yapılır
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
    begin
      // Sıfır tabanlı: kapak sayfasını düşür. Ona işaret eden
      // adlandırılmış hedefler, yer imleri, yapı ağacı,
      // ParentTree ve bağlantı ek açıklamaları /Pages ağacı
      // yeniden kurulmadan önce budanır.
      Pdf.DeletePage(0);
      // Toplu işler için 1 tabanlı aralık sözdizimi; içeride en
      // yüksek indeks önce, böylece önceki indeksler geçerli kalır.
      Pdf.DeletePages('3-4,9');
      Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Adlandırılmış hedefler ile yer imleri neden farklı işlenir?

Adlandırılmış hedefler kaldırılır, yer imleri yeniden hedeflenir, çünkü artık var olmayan bir ad kabul edilebilir bir sonuçtur, hedefsiz bir yer imi ise görünür bir kusurdur. HotPDF /Names /Dests ağacında her düğümü gezer, her hedefi hem çıplak dizi biçiminde hem de /D anahtarlı sözlük biçiminde silinen sayfaya karşı sınar ve dizinin ilk elemanı o sayfa olduğunda ad/değer çiftini kaldırır. /Names ve /Kids alanları ikisi de boşalan bir düğüm silinmiş işaretlenir ve ebeveyninden koparılır, böylece ağaç hiçbir zaman içi boş yapraklar tutmaz. Aynı sınama eski usul katalog /Dests sözlüğü üzerinde de çalışır ve katalog /OpenAction alanı, silinen sayfa üzerinde açılıyorsa doğrudan düşürülür. Burada bir sınır var: bir ad ağacı düğümü girdi kaybettiğinde HotPDF yeni en düşük ve en yüksek anahtarları yeniden hesaplamak yerine o düğümün /Limits çiftini siler ve görüntüleyiciler adları onsuz da sorunsuz çözerken, ISO 32000-1 §7.9.6 okuyan katı bir uygunluk denetleyicisi kök olmayan bir düğümde eksik /Limits işaretleyebilir

Ana hat öğeleri ise ters yöne gider. RetargetOutlineDestinations, ana hat kökünden /First ve /Next boyunca ilerler; bozuk bir döngülü ağacın çağrıyı asmasını önlemek için bir görülenler listesi ve 128 derinlik sınırı kullanır ve sayfayı hedefleyen her /Dest dizisinde ya da /GoTo eyleminin /D dizisinde ilk elemanı NearestRetainedPage ile değiştirir: silinen sayfadan sonra gelen sayfa, silinen sayfa sonuncuysa ondan önceki sayfa. Sayfa başvurusundan sonraki görünüm parametreleri olduğu gibi bırakılır. Silinen bir bölüm açılışını hedefleyen bir yer imi bu yüzden kenar çubuğundan kaybolmak yerine kalan içeriğin ilk sayfasına iner ki gözden geçirenlerin kırpılmış bir belgeden beklediği davranış budur. Ancak hedef sınaması yalnızca açık dizilerle eşleşir: /Dest alanı eskiden silinen sayfaya çözülen bir ad dizesi olan bir ana hat öğesi yeniden hedeflenmez, çünkü ad ağacı girdisi yok olmuştur ve başvuru artık serbest bırakılmış bir nesneye değil hiçbir şeye çözülür, dolayısıyla görüntüleyici onu ölü bir yer imi sayar. Ana hat ağacının kendi mekaniği, /First, /Next ve pek açık olmayan /Count semantiği yüklü bir PDF'e yer imi ve adlandırılmış hedef ekleme kılavuzunda ele alınır

// Süpürmeye güvenmek yerine onu doğrula.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
  ShowMessage('Named destination "cover" was pruned');
// Kapağı hedefleyen bir yer imi artık onu izleyen sayfaya
// çözülür (silmeden sonra sıfır tabanlı indeks 0).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
  ShowMessage('Bookmark retargeted to the nearest retained page');

Yapı ağacına ve ParentTree'ye ne olur?

Yalnızca silinen sayfa yüzünden var olan yapı elemanları kaldırılır, birden fazla sayfaya yayılan elemanlar ise /Pg anahtarını kaybeder ama çocuklarını korur. PruneStructureElement, /StructTreeRoot altından /K zincirine 128 derinliğe kadar iner ve /K alanının §14.7.2'nin izin verdiği hem dizi hem de tek sözlük biçimini işler. Her eleman için önce çocukları budar, sonra elemanın kendisini değerlendirir: budama /K içeriğini boşalttıysa eleman silinmiş işaretlenir ve ebeveyni onu düşürür. Elemanın kendi /Pg alanı silinen sayfayı gösteriyor ve elemanın hâlâ çocukları ile bir /P ebeveyni varsa yalnızca /Pg kaldırılır, çünkü bir elemandaki /Pg, işaretli içerik çocuklarının varsayılan sayfasıdır ve o çocuklar başka sayfaları açıkça işaret edebilir. Yalnızca /Pg alanı silinen sayfa olan ve altında hiçbir şey kalmayan bir eleman tamamen kaldırılır

ParentTree de aynı işlemi görür ve sebebi geliştirme sırasında can yakan sebebin ta kendisidir: bir yapı elemanı ParentTree üzerinden erişilebilir olup başka hiçbir yerden erişilemeyebilir. Sayı ağacı /StructParents tamsayılarını ya tek bir elemana ya da bir eleman dizisine eşler; PruneParentTreeNode bulduğu her değer üzerinde PruneStructureElement çalıştırır, budanıp giden değerleri kaldırır, değer dizisi boşalan bir /Nums çiftini siler ve /Nums ile /Kids alanları ikisi de yok olan bir düğümü koparır. Yalnızca /K torunlarını budamak, o yetim elemanları /Pg üzerinden serbest bırakılmış bir sayfaya, /MCR çocukları üzerinden de serbest bırakılmış işaretli içerik başvurularına işaret eder hâlde bırakırdı. Metni yapı sırasında çıkarıyorsanız bu doğrudan önemlidir: yapı sırasında metin çıkarma tam olarak bu ağaçları gezer ve /Pg alanı null olan bir eleman, okuma sırasından sessizce düşen bir paragraftır

Ayakta kalan sayfalardaki hangi bağlantı ek açıklamaları kaldırılır?

Kalan bir sayfada /Dest dizisi ya da /GoTo eylemi silinen sayfayı hedefleyen her bağlantı ek açıklaması, yapı ağacındaki sahipliğiyle birlikte kaldırılır. RemoveRetainedPageDestinationAnnotations, hedef dışındaki her sayfanın /Annots dizisini gezer, ana hatlar için kullanılan aynı hedef sınamasını uygular, eşleşen ek açıklamayı silinmiş işaretler, diziden düşürür ve ardından PruneAnnotationReferencesInStructureTree çağırır; böylece /Obj alanı o ek açıklamayı gösteren OBJR sözlüğü yapı elemanından kaldırılır, OBJR elemanın tek çocuğuysa elemanın kendisi de kaldırılır. OBJR alanını yerinde bırakmak, /Obj alanının var olan bir nesneye başvurmasını şart koşan §14.7.4.3'ü ihlal eder ve bir PDF/UA denetiminde arkasında ek açıklama olmayan etiketli bir bağlantı olarak görünür. Yer imleriyle aradaki asimetriye dikkat edin: bağlantılar yeniden hedeflenmez, kaldırılır. Gövde metnindeki “sayfa 3'e bakın” diyen bir çapraz başvuru, sayfa 3 gittiğinde yanlıştır ve onu sayfa 4'e yöneltmek, bir yer iminin en yakın bölüme inmesinin olmadığı biçimde bir yalan olur; bu yüzden iş akışınız o bağlantıların korunmasını gerektiriyorsa DeletePage çağırmadan önce onları kendiniz yeniden hedefleyin

Kaldırılan bir /MCR ya da /OBJR neden asla serbest olarak kaydedilmemelidir?

Çünkü işaretli içerik başvuruları ile nesne başvuruları genellikle ebeveyn elemanın /K dizisinin içindeki doğrudan sözlüklerdir ve artımlı değişiklik kaydı, doğrudan bir nesneyi onu içeren en yakın dolaylı nesneye çözer. RemoveArrayItem bir /K dizisinden çocuk düşürdüğünde bellekteki nesneyi yalnızca bir THPDFLink ya da dolaylı olmayan bir değerse serbest bırakır ve MarkRemovedObject bir nesneyi serbest listeye yalnızca nesne numarası sıfırdan büyükse kaydeder. Bu süpürmenin ilk sürümü bu ayrımı yapmıyordu ve artımlı bir kayıtta etki, kaydın tasarım gereği yaptığı şeyin ta kendisiydi: RegisterIncrementalChange doğrudan /MCR nesnesinden grafik işlem köküne, yani o nesneye sahip olan kalan yapı elemanına kadar yürüdü ve o elemanı null olarak yazdı. Bir sayfa kaybeden bir belge, diğer sayfalardaki etiketli içeriği sessizce etiketsiz hâlde geri geldi. Doğrudan bir çocuk için tek doğru hamle, kabını TouchContainer üzerinden kirli işaretlemek ve böylece kabın yeniden yazılmasını sağlamak, serbest listeye ise hiç dokunmamaktır

HotPDF'te kaldırılan bir /MCR ya da OBJR çocuğunun neden asla serbest olarak kaydedilmemesi gerektiği: artımlı değişiklik kaydı doğrudan bir sözlüğü en yakın dolaylı kabına çözer, bu yüzden ilk sürüm kalan yapı elemanını null olarak yazıp ayakta kalan sayfaları sessizce etiketsizleştiriyordu; TouchContainer ise artık kabı yeniden yazar ve serbest listeye dokunmaz
Bellekteki çocuğu serbest bırakmak THPDFLink ya da dolaylı olmayan değerlerle sıfırdan büyük nesne numaralarına saklanmıştır, böylece artımlı bir kayıt eklenen bölüme yalnızca dokunulmuş kapları ve serbest bırakılan sayfa nesnesini yazar
// Artımlı güncelleme: eklenen bölüme yalnızca dokunulmuş
// kaplar ve serbest bırakılan sayfa nesnesi girer.
Pdf := THotPDF.Create(nil);
try
  Pdf.BeginIncrementalUpdate('tagged-report.pdf');
  Pdf.DeletePage(0);
  // /K alanı doğrudan bir /MCR kaybeden kalan yapı elemanları
  // yerinde yeniden yazılır, asla null olarak yazılmaz.
  Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
  Pdf.Free;
end;

Aynı ihtiyat, DeletePage yüklü bir belgede neyi bilerek serbest bırakmadığını da biçimlendirir. Silinen sayfanın içerik akışları, XObject'leri ve widget olmayan ek açıklamaları nesne olarak bırakılır, çünkü yüklü bir dosya bunlardan herhangi birini kalan bir sayfayla paylaşıyor olabilir ve silme anında aksini kanıtlamanın ucuz bir yolu yoktur. Doğruluk için sayfa ağacı başvurusunu kaldırmak yeterlidir; o nesnelerin hâlâ kapladığı baytlar ayrı bir sorudur ve nesne bağımlılık grafiği ile tutulan bayt analizi, kırpılmış bir belgenin hâlâ ne taşıdığını ölçmenin aracıdır

DeletePage mi DeleteLoadedPage mi: hangisini çağırmalısınız?

Kullanıcıya dönük her sayfa silme işlemi için DeletePage çağırın; DeleteLoadedPage fonksiyonunu ise tüm belgenin yeniden akıtıldığı ve belge düzeyinde hiçbir başvurunun korunmaya değer olmadığı duruma saklayın. 2.508.0 sürümünde eklenen THotPDF.DeleteLoadedPage(PageIndex) hafif varyanttır: iç sayfa dizisini kaydırır, /Kids ve /Count alanlarını yeniden yazmak için RebuildLoadedKidsArray çağırır, render edilmiş sayfa önbelleğini geçersiz kılar ve OnLoadedDocumentModified olayını tetikler. Ad ağacını, ana hatları, yapı ağacını ya da diğer sayfaların ek açıklamalarını gezmez ve sayfa nesnesini silinmiş olarak işaretlemez. N-up yerleştirmede doğru araç budur; orada HotPDF taze dizilmiş sayfaları ekler ve ardından her özgün sayfayı DeleteLoadedPage(0) ile düşürür: kaynak sayfalar tümüyle değiştirilmektedir ve sayfa içeriği sayfa nesnelerine değil onların kaynaklarına başvurur. Sıradan “bu sözleşmeden 7. sayfayı çıkar” işi için ise etiketli, yer imli ve çapraz bağlantılı bir belgeyi bir doğrulayıcıdan geçecek kadar tutarlı bırakan tek çağrı DeletePage olur; SaveLoadedDocument üzerinden tam yeniden yazmada da SaveIncrementalUpdate üzerinden artımlı güncellemede de. İki yöntem de Delphi ve C++Builder için HotPDF Delphi Component ile gelir ve harici bir görüntüleyici çalışma zamanı ya da bağımlılığı gerektirmez