Hiçbir şeyi değiştirmeyen bir PDF kaydı, belge metadatasını iki şekilde bozabilir: /CreationDate ile /ModDate aynı dize nesnesine başvurduğunda, o nesneye yazan otomatik bir ModDate güncellemesi ikisini birden yeniden yazar; XMP nesnesi, özgün /Metadata akışı okunmadan önce oluşturulduğunda da varsayılan bir paket özgün olanın yerine geçer. PDF Library for Delphi, paylaşılan nesneleri değiştirmek yerine sözlük başvurularını değiştirir ve mevcut paketi tembel XMP başlatmasından önce yakalar
Ayarlamak, bir PDF kütüphanesinin yaptığı en sıkıcı işlemdir: dosyayı yükle, yeni bir adla kaydet, arada hiçbir şeye dokunma. Sayfalar öncesiyle sonrasında aynı şekilde işlenir ve içerik akışı hash'leri eşleşir. Bir dosya bu kontrollerin hepsini geçebilir ve yine de hiçbir işleyicinin size göstermeyeceği iki yerde yanlış olabilir. Her iki tuzak da her gerçek düzenlemenin içinden geçtiği oku-değiştir-yaz yolunda durur; dolayısıyla herhangi bir kaydetme onları tetiklemeye yeter ve ikisini de ancak iki dosyanın görsel olmayan semantiğini karşılaştıran ikinci, bağımsız bir ayrıştırıcı ortaya çıkarır
Bir PDF'i kaydetmek CreationDate'ini neden değiştirir?
Çünkü belge bilgi sözlüğünün iki anahtardan tek bir dolaylı dize nesnesine başvurmasına izin verilir ve anahtar yerine nesneyi güncelleyen bir yazar ikisini birden değiştirir. ISO 32000-1 §7.3.10 herhangi bir sözlük değerinin dolaylı başvuru olmasına izin verir ve §14.3.3 Tablo 317'de /CreationDate altındaki değerin /ModDate altındaki değerden farklı bir nesne olması gerektiğini söyleyen hiçbir şey yoktur. Oluşturma anında aynı zaman damgasını iki kez yazan bir üretici, tamamen yasal olarak, iki anahtarı da tek bir 2728 0 R nesnesine yönlendirebilir; gerçek dünyadaki dosyalar tam olarak bunu yapar
Tetiği çeken şey otomatik değişiklik tarihidir. UserModDate ayarlanmadığı sürece SaveToFile, yazmadan önce geçerli saatle SetInfo('ModDate', ...) çağırır; bu da SetRawInfo'a ulaşır. Saf bir SetRawInfo anahtarın altındaki nesneye bakar ve bir TPDFString bulursa üzerinde SetTo çağırır. Bu, anahtarın o an çözüldüğü nesneye yerinde bir yazmadır ve o nesne paylaşıldığında /CreationDate de kaydetme saatini raporlar. Belge yine açılır, yazdırılır ve eskisi gibi piksel piksel işlenir; dolayısıyla görsel bir regresyon paketi göz kırpmadan geçer
var
Lib: TPDFlib;
Before, After: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('design.pdf', '');
Before := Lib.GetInformation(7); // 7 = CreationDate, 8 = ModDate
Lib.SaveToFile('design-resaved.pdf');
Lib.LoadFromFile('design-resaved.pdf', '');
After := Lib.GetInformation(7);
if Before <> After then
Log('a save that changed nothing rewrote CreationDate');
finally
Lib.Free;
end;
end;
TPDFDocument.SetRawInfo'daki kural küçüktür ve arkasındaki ilke geneldir: bir sözlük girdisini güncellemek, o girdinin başvurusunu değiştirir; tesadüfen çözüldüğü nesneyi asla. Kod, mevcut TPDFStringMode değerini okur — böylece hex dize hex, literal dize literal kalır — sonra anahtarın altına FStructure.NewString(Value, StringMode) ile taze bir dize ekler. İki ayrıntı daha en az o kadar önemlidir. Akış değerli bir girdi, önce eski akış temizlenmeden değiştirilir, çünkü üzerine SetTo('') uygulamak, o akışa hâlâ işaret eden diğer tüm anahtarların değerini boşaltırdı. Ve yerini alan nesne silinmez, çünkü yapı onun sahibidir ve diğer başvurular ona hâlâ ihtiyaç duyabilir
// Naive: anahtarın o an çözüldüğü nesneyi olduğu gibi değiştirmek
if Obj is TPDFString then
TPDFString(Obj).SetTo(Value);
// Doğru: gösterimi korumak, yalnızca bu anahtarın başvurusunu değiştirmek
StringMode := smLiteral;
if Obj is TPDFString then
StringMode := TPDFString(Obj).StringMode;
ID.Add(Key, FStructure.NewString(Value, StringMode));
Bunun için bir test, tesadüfen nesne paylaşan bir dosyaya güvenmek yerine takma adı bilinçli kurmalıdır: iki tarih anahtarından başvurulan bir hex dize, /Title ile /Subject tarafından paylaşılan bir doğrudan dize, /Author ile /Keywords tarafından paylaşılan bir akış. Her çiftin bir anahtarını güncelledikten sonra diğeri özgün değerini okumalı ve güncellenen dize hâlâ hex olmalıdır. SetInformation için kamuya açık referans garantisi tek cümleyle verir: bir Info alanını güncellemek yalnızca o alanı değiştirir, diğer alanlar aynı nesneye başvursa bile
Mevcut bir XMP paketi neden varsayılanlarla değiştirilir?
İki satırın sırası yüzünden. TPDFDocument.GetMetadata'nın bir hızlı yolu vardır: XMP alanı zaten atanmışsa XMP.SaveToString döndürür ve katalogdaki /Metadata akışının çözümü atlanır. XMP := TPDFlibXMP.Create; XMP.LoadFromString(GetMetadata); yazılmış bir tembel başlatma doğal okunur ve yanlıştır: GetMetadata çalıştığında XMP zaten atanmıştır, dolayısıyla yüklenen "kaynak", bir satır önce oluşturulmuş bir nesnenin serileştirilmiş varsayılan paketidir. dc:creator'u, özel ad alanlarını ve varsa standart tanımlamasını taşıyan özgün paket nesneye hiç ulaşmaz ve kaydetmede üzerine yazılır. Aynı otomatik değişiklik tarihi bunu tetiklemeye yeter, çünkü SetInfo, xmp:ModifyDate değeri /ModDate ile adım adım kalsın diye XMP'yi Info sözlüğüne dokunmadan önce başlatır. Bu tuzakların neyin arkasına saklandığına dikkat edin: ilk tuzağın Info sözlüğü karşılaştırması geçer, çünkü /Author ile /Title'a, /Info içinde, dokunulmamıştır. Yalnızca XMP ağacı değişmiştir ve bunu ancak o ağacı ayrıştırıp karşılaştıran bir kontrol fark eder
// Yanlış: GetMetadata artık bir önceki satırda oluşturulan nesneyi serileştirir
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(GetMetadata);
// Doğru: önce /Metadata akışını yakala, sonra oluştur ve yükle
Source := GetMetadata;
XMP := TPDFlibXMP.Create;
XMP.LoadFromString(Source);
PDF Library for Delphi bu sırayı tek bir yerde tutar. TPDFDocument.EnsureXMP, Source := GetMetadata atamasını TPDFlibXMP.Create'den önce yakalar ve belgedeki her tembel başlatma ondan geçer: SetInfo, SetXMPInformation, GetXMPInformation, PDF/A, PDF/X, PDF/E, PDF/VT, PDF/VCR ve PDF/UA kip ayarlayıcıları ve metadata onarım yolu. SetXMPProperty gibi kamuya açık giriş noktaları da EnsureXMP'den geçer, GetXMPProperty ise GetDocumentMetadata üzerinden okur; böylece bütün yüzey tek bir başlatma sırası paylaşır. Üç satırlık bir dizinin tek doğru kopyası, bugün tesadüfen uyuşan on kopyadan daha değerlidir
Aynı yoldaki iki küçük tuzak
Windows'taki XMP serileştiricisi platformun XML yazıcısını kullanır; bu yazıcı, paketin taşımaması gereken bir XML bildirimi üretir. O bildirimi ayıklamanın açık yolu — <?xpacket'e ulaşana dek karakter silmek — geçerli girdi üzerinde başarısız olur: ISO 16684-1 §7.3.2 xpacket sarmalayıcısını isteğe bağlı kılar ve çıplak bir <x:xmpmeta> öğesi yazan üretici standart içindedir; dolayısıyla böyle bir pakette o döngü, geçerli belgenin tamamını siler. Serileştirici, bildirimin kapanış ?> işaretini bulur ve yalnızca onu kaldırır. Kopyalamaya değer bir saklama kontrolü iki kez çalışır, bir kez sarmalayıcıyla bir kez onu kesip attıktan sonra; ve özel ad alanı işaretiyle özgün yazarın SetInfo, GetMetadata, SaveToString ve bir yeniden yüklemeyi atlattığını doğrular. İkinci tuzak bir ön işlemci simgesidir. NOVCL Free Pascal derlemeleri için tanımlanır, ama XMP arka ucu çerçeveyle değil işletim sistemiyle koşullanır; çünkü PDFlibXMP.pas, NO_XMP'yi yalnızca OS_WINDOWS yokken tanımlar. SetInfo'daki Info-XMP eşitlemesini NOVCL ile korumak, bir Windows Lazarus derlemesinde çalışan bir XMP nesnesi ile onu güncellemeyi sessizce atlayan bir SetInfo bırakırdı. Koruma NO_XMP'dir; böylece bir Windows Free Pascal uygulaması, Delphi ile aynı eşitlemeyi alır
Doğrudan geçirme kaydında özgün ModDate nasıl korunur?
KeepModDate'i TPDFlibSaveOptions içinde ayarlayın ve SaveToFileOptions üzerinden kaydedin. Bu seçenek çağrı süresince UserModDate değerini ayarlar ve SaveToFile ardından otomatik zaman damgasını atlar; XMP nesnesini tembel olarak başlatan adım da budur. Metadatasına hiç dokunmadığınız ve hiçbir uyumluluk kipinin etkinleştirilmediği bir belge, hem Info sözlüğünü hem de /Metadata akışını yüklendiği gibi korur. SetInformation(8, ...) çağırmak kalıcı olarak aynı etkiyi yapar, çünkü değişiklik tarihini kendiniz ayarlamak onu kullanıcı denetimli olarak işaretler
var
Options: TPDFlibSaveOptions;
begin
FillChar(Options, SizeOf(Options), 0);
Options.OptimizeContentStreams := True;
Options.PackObjectStreams := True;
Options.KeepModDate := True; // otomatik /ModDate yok, tembel XMP başlatması yok
if Lib.SaveToFileOptions('design-resaved.pdf', Options) <> 1 then
Log(Format('save failed, LastErrorCode=%d', [Lib.LastErrorCode]));
end;
Bunun size ne kazandırdığı konusunda dürüst olun. KeepModDate, çıktısı girdisiyle aynı revizyonu anlatması gereken bir doğrudan geçirme adımı için doğru seçimdir; içeriği gerçekten düzenleyen her şey için yanlış seçimdir, çünkü §14.3.3 /ModDate'in en son değişikliği yansıtmasını bekler. Doğru başvuru işlemenin yerine de geçmez; yalnızca otomatik güncellemeyi tetikleyen tek yazmadan kaçınır. Paylaşılan nesneleri değiştirmek yerine başvuruları değiştirmek ve paketi başlatmadan önce yakalamak, sıradan bir kaydetmeyi güvenli kılan şeydir; seçenek ise bilinçli bir no-op'u dürüst kılan şeydir
Bir kaydın ModDate dışında hiçbir şeyi değiştirmediği nasıl doğrulanır?
Piksellerle değil, akış hash'leriyle de değil; çünkü her iki tuzak da her sayfayı ve her içerik akışını bayt bayt aynı bırakır. Onları yakalayan kontrol, test edilen kütüphaneyle hiç kod paylaşmayan bağımsız bir ayrıştırıcının kaynak dosyadan ve kaydedilen dosyadan aldığı görsel olmayan bir semantik anlık görüntü ve ardından gelen yapısal karşılaştırmadır. Anlık görüntü, /ModDate dışarıda bırakılmış Info sözlüğünü, her yer iminin nesne numarası yerine sayfa numarasına çözüldüğü ana hat ağacını, aynı şekilde çözülen adlandırılmış hedefleri ve bağlantı hedeflerini, form alanı değerlerini, hash olarak ek dosya baytlarını ve metin olarak karşılaştırılmak yerine ağaç olarak ayrıştırılan XMP paketini kapsar. Nesne numaraları bilinçli olarak dışarıdadır; tam bir yeniden yazım her şeyi yeniden numaralar ve onlara bağlanan bir karşılaştırma gürültü rapor ederdi
Dışarıda bırakılanlar, içeriye alınanlar kadar önemlidir. /ModDate, xmp:ModifyDate ve xmp:MetadataDate değerlerinin değişmesi beklenir ve karşılaştırmadan önce düşürülür; kaynağında hiç XMP taşımayan bir dosya, bir paket kazandığı için cezalandırılmaz. Kontrolün iddia etmediği şey de aynı ölçüde açıktır: mevcut bir paketi korumak, o paketin şema açısından geçerli olduğu ya da belgenin PDF/UA'yı veya herhangi bir PDF/A bölümünü karşıladığı hakkında hiçbir şey söylemez. Bunlar ayrı araçlarla yanıtlanan ayrı sorulardır ve "metadata hayatta kaldı" ile "metadata uyumlu" söylemlerini birbirine karıştırmak, paylaşılan tarihli bir yeniden yazımın uzun süre saklanabilmesinin yoludur
Bu kuralların bir alt düzeyinde çalışıyorsanız, bir kaydetmenin nesneleri nasıl yeniden yazdığının mekaniği artımlı güncellemeler ve yalnızca ekleme yapan kaydetme yazısında — paylaşılan bir nesnenin olduğu yerde bırakıldığı tek kaydetme kipi odur — ve değişiklik düzeyleri ve revizyon farkları yazısında — bayat ya da yeniden yazılmış bir tarihin okuyucuyu yanılttığı diğer yer odur — ele alınır. Aynı Info ve XMP çiftinin, iki yarının yalnızca korunmakla kalmayıp birbirine uydurulduğu onarım tarafı görünümü ise PDF/A'ya dönüştürme ve metadata onarımı yazısındadır
PDF Library for Delphi, Delphi, C++Builder ve Lazarus için yerel bir Pascal PDF kütüphanesidir ve burada anlatılan oku-değiştir-yaz yolu, kendi sürecinizdeki her düzenlemenin içinden geçtiği aynı yoldur; dolayısıyla yukarıdaki garantiler günde bir kez de kaydetseniz bin kez de kaydetseniz geçerlidir — desteklenen derleyiciler ve platformlar için PDF Library for Delphi ürün sayfasına bakın