200 sayfalık bir el kitabından yedi sayfayı çıkarın ve her yer imi yanlış bir yere düşer. Çözüm, anahatı düz bir başlık listesinden yeniden kurmak değildir. PDFiumPas TPdfOutlineEditor nesnesini sunar; bu, gerçek anahat ağacını yükler, öğeleri taşımanıza ve yeniden hedeflemenize izin verir, sonra her açık hedefi sayfa planınızdan geçirmek için ApplyPageMap işlevini çalıştırır
Sayfa silmek neden her yer imini bozar?
Çünkü bir anahat öğesi sayfa numarasını saklamaz. Bir sayfa sözlüğüne başvuru saklar ve sayfa nesneleri değiştiğinde başvuru ya taşınan bir sayfaya işaret eder ya da hiçbir şeye. ISO 32000-1 §12.3.2.2 açık hedefi, ilk öğesi bir sayfa sözlüğüne dolaylı başvuru olan ve ardından /Fit ya da /XYZ gibi bir sığdırma adı gelen bir dizi olarak tanımlar. Sayfayı silin ve elinizde boşta kalan bir başvuru kalır; sayfaları yeniden sıralayın ve başvuru hâlâ geçerlidir ama artık farklı bir bölümü anlatır. PDFiumPas bu diziyi yüklerken bir sayfa numarasına geri çözer; böylece TPdfOutlineItem.PageNumber size bir nesne numarası değil, herkese açık TPdf API siyle eşleşen bir tabanlı sayfa dizini verir. Soyutlamanın tüm amacı budur: yeniden eşleme mantığınız, belgeyi böldüğünüzde, yeniden sıraladığınızda ya da harmanladığınızda zaten kurduğunuz sayfa planıyla aynı koordinat sisteminde çalışır. O planı siz kuruyorsanız, aynı bir tabanlı uzlaşım PDF belgelerini birden çok dosyaya bölme ve n-up harmanlama ve sayfa yeniden sıralama boyunca da geçerlidir
Anahat bir liste değil, çift yönlü bağlı bir ağaçtır
Başlıklardan oluşan düz bir diziyi basitçe serileştirememenizin nedeni, ISO 32000-1 §12.3.3 ün her anahat öğesini beş ayrı bağlantıya bağlamasıdır: /Parent, /Prev, /Next, /First ve /Last. Tek bir alt ağacı taşımak bu yüzden eski üst öğeyi, yeni üst öğeyi, kesimin her iki yanındaki komşu kardeşleri, ekleme noktasını ve taşınan düğümün kendi üst göstericisini yeniden yazar. Bunlardan birini yanlış yapın ve uyumlu okuyucular kesik bir ağaç gösterir ya da döngüye girer. PDFiumPas düzenleme durumunu, kararlı tamsayı bir Id taşıyan TPdfOutlineItem kayıtlarından oluşan derinlik öncelikli bir dizi olarak tutar; böylece bir alt ağaç bitişik bir dilimdir ve kardeş zinciri hiçbir zaman elle bakımı yapılmaz, türetilir. TPdfOutlineEditor.Move o dilimi kaldırır, onu yeni üst öğenin altına istenen kardeş dizininde yeniden ekler ve yalnızca bloğun kökünü yeniden atar. Ayrıca grafiği bozacak iki hareketi de reddeder: bir öğeyi kendi alt ağacının içine taşımayı ve var olmayan bir üst öğeyi adlandırmayı
/Count neden imzalıdır?
Çünkü işaret boyutu değil, genişletilmiş durumu taşır. Pozitif bir /Count öğenin açık olduğu anlamına gelir ve sayı, o anda görünür durumda olan kaç torun olduğunu gösterir; negatif bir /Count öğenin daraltılmış olduğu anlamına gelir. PDFiumPas çocuğu olan her öğe için torun sayısını yazar ve IsOpen False olduğunda onu negatife çevirir; yüklerken durumu IsOpen := HasCount and (CountValue > 0) olarak geri okur. Bu, anahat yazıcılarında elle yapılan en yaygın tek hatadır: imzasız bir sayı yazmak ve tüm ağacı sessizce açılmaya zorlamak
var
Source, Dest: TMemoryStream;
Editor: TPdfOutlineEditor;
Options: TPdfOutlineEditOptions;
Report: TPdfOutlineValidationReport;
RootId, ChapterId: Integer;
begin
Source := TMemoryStream.Create;
Dest := TMemoryStream.Create;
Editor := nil;
try
Source.LoadFromFile('handbook.pdf');
Options := TPdfOutlineEditOptions.Default; // MaxItems 100000, MaxDepth 64
if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
raise Exception.Create(Report.ErrorMessage);
RootId := Editor[0].Id;
ChapterId := Editor[2].Id;
Editor.Move(ChapterId, RootId, 1); // kökün ikinci çocuğu olur
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // negatif bir /Count yazar
Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');
if not Editor.SaveIncremental(Source, Dest, Report) then
raise Exception.Create(Report.ErrorMessage);
Dest.SaveToFile('handbook-edited.pdf');
finally
Editor.Free;
Dest.Free;
Source.Free;
end;
end;
Retarget belirtimin izin verdiği iki biçimi de ele alır. DestinationInAction değerini False geçin; PDFiumPas doğrudan bir /Dest dizisi yazar; True geçin, ISO 32000-1 §12.6.4.2 uyarınca bir Git eylemi yazar: /A << /S /GoTo /D [ page ref suffix ] >>. Her iki durumda da önce öğedeki mevcut /Dest ve /A öğelerini kaldırır; böylece ikisi bir arada bulunup çelişemez. Sonek varsayılan olarak /Fit değeridir ve bir PDF adıyla başlamak zorundadır; işte bu yüzden boş ya da bozuk bir sonek, hiçbir okuyucunun ayrıştıramayacağı bir hedef dizisi üretmek yerine hemen istisna yükseltir
ApplyPageMap bir sayfa planını nasıl tüketir?
ApplyPageMap tam olarak sayfa planınızın zaten doğruladığı diziyi alır: NewPageNumbers; eski sayfa eksi bir ile dizinlenir, yeni bir tabanlı sayfa numarasını ya da o sayfa hayatta kalmadıysa sıfırı tutar. Öğe dizisini geriye doğru yürür; böylece bir alt ağacı silmek, henüz ziyaret etmediği bir dizini hiçbir zaman geçersiz kılmaz ve ne yaptığını RemappedDestinationCount ve RemovedDanglingItemCount aracılığıyla bildirir
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Özgün belgenin her sayfası için bir girdi
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == bu sayfa çıkarıldı
NewPageNumbers[0] := 1; // eski sayfa 1 -> yeni sayfa 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // eski sayfa 10 -> yeni sayfa 3
// True: boşta kalan tüm alt ağacı sil. False: öğeyi tut, hedefini kaldır
if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
raise Exception.Create(Report.ErrorMessage);
WriteLn(Format('%d remapped, %d dangling items removed',
[Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;
DeleteDangling bayrağı sıfıra eşlenen bir hedef için ilkeyi belirler ve her iki dal da bilinçlidir. True ile PDFiumPas öğeyi ve tüm alt ağacını siler; çünkü hedefi yok olan bir anahat düğümü genellikle onunla birlikte yok olan bir bölümün başındadır. False ile öğe, başlığı ve hiyerarşisi bozulmadan hayatta kalır ama /Dest ve /A öğeleri kaldırılır; bir insanın gözden geçirme sırasında onu yeniden hedefleyeceği durumda istediğiniz budur. Gerçekten bozuk girdi ise yamamak yerine yüksek sesle başarısız olur: negatif bir girdi ya da verilen haritanın sonunu aşan bir hedef, IssueKind poviInvalidPageMap olarak ayarlanmış biçimde False döndürür
Donuk girdiler ve dürüst ödünleşim
Her anahat öğesinin PDFiumPasin akıl yürütebileceği bir sayfa numarası yoktur. Üç tür dokunulmadan taşınır: adlandırılmış hedefler, /S /GoTo olmayan eylemler ve dosyayı üreten her neyse onun eklediği bilinmeyen sözlük anahtarları. Bunlar PageNumber sıfıra eşit biçimde yüklenir, öğedeki özgün baytlarını korur ve siz onları açıkça Retarget ile çağırmadıkça sözcüğü sözcüğüne geri yazılır
- Adlandırılmış bir hedef, belge ad ağacına açılan bir anahtardır; bu yüzden onu doğru biçimde yeniden eşlemek, anahat düzeyinde tahmin yürütmek değil, ağacı çözmek ve hedef girdisini yeniden yazmak demektir
- Bir
/URI,/Launchya da JavaScript eyleminin hiçbir sayfa anlamsallığı yoktur ve sessizce bir Git eylemine dönüştürülmemelidir - Üreticiye özgü anahtarlar ve yapı hedefleri korunur; çünkü anlamadığınız şeyi bırakmamak, gidiş dönüşlerin veri kaybetmesinin yoludur
Bedeli gerçektir ve açıkça söylemeye değer: ApplyPageMap bu öğeleri tamamen atlar; bu yüzden yer imlerinin tümü adlandırılmış hedefler kullanan bir belge, bir sayfa silme işleminden anahatı yapısal olarak geçerli ve anlamsal olarak bayat biçimde çıkar. Bu bilinçli tercihtir; bir gözden geçirenin yakalayabileceği bayat bir bağlantı, kimsenin fark etmediği kendinden emin yanlış olandan iyidir. Gelen dosyaları düzenlemeden önce ön incelemeden geçiriyorsanız, PDF giriş inceleme tezgahındaki bir envanter geçişi hangi belgelerin o kovaya düştüğünü size söyler
Kaydetme: önce artımlı düzeltim, sonra bağımsız bir yeniden yükleme
TPdfOutlineEditor.SaveIncremental dosyayı yeniden yazmak yerine seyrek bir artımlı düzeltim ekler. Yüklenen öğeler özgün dolaylı nesne başvurularını kesin nesil numarasıyla birlikte korur; böylece mevcut çapraz başvurular geçerli kalır; yalnızca sizin eklediğiniz öğeler, düzeltimin en yüksek nesne numarasının bir fazlasından ayrılan taze bir numara alır. Katalog aynı düzeltimde güncellenir ve kaynağın hiç anahatı yokken ona eksik bir /Outlines girdisi eklenir
Yazma işleminden sonra olan kısım, kopyalamaya değen kısımdır. PDFiumPas hedef akışını tamamen bağımsız bir düzenleyiciyle yeniden açar ve yeniden yüklenen ağacı bellek içindeki ağaçla karşılaştırır: öğe sayısı, başlıklar, sayfa numaraları, hedef sonekleri, eyleme karşı doğrudan hedef biçimi, stiller, genişletilmiş durum ve üst ilişkileri. Herhangi bir uyuşmazlık ya da herhangi bir yükleme hatası, hedef akışını temizler ve size inandırıcı görünen bir dosya vermek yerine poviVerificationFailure döndürür. Şifreli kaynaklar en başta poviEncryptedInput ile reddedilir; çünkü yeni başlıklar ve hedefler, /Encrypt römorkunu ileri kopyalayarak üretilemeyen dize içeriği yaratır
if not Editor.SaveIncremental(Source, Dest, Report) then
case Report.IssueKind of
poviEncryptedInput:
Log('Source is encrypted; outline editing needs an unprotected copy');
poviInvalidDestination:
Log(Format('Item %d %d targets a missing page',
[Report.ObjectNumber, Report.Generation]));
poviVerificationFailure:
Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
else
Log(Report.ErrorMessage);
end;
Anahatı olduğu şey gibi ele alın — kendi değişmezleri olan bağlı bir nesne grafiği — ve sayfa silme bir yer imi felaketi olmaktan çıkıp tek bir yöntem çağrısına verdiğiniz bir sayfa haritasına dönüşür. TPdfOutlineEditor, ApplyPageMap ve doğrulanan artımlı yazıcı, Delphi, C++Builder ve Lazarus için v3.98.0 sürümünden itibaren PDFiumPas içinde yer alır; tam API yi inceleyebilir ve bir deneme sürümünü PDFium Delphi Bileşeni ürün sayfasından indirebilirsiniz