PDF Library for Delphi v3.539.18 ve v3.539.20, hiçbir şeyi değiştirmeyen bir PDF kaydının belge metadatasını yine de bozabildiği iki durumu düzeltir: /CreationDate ile /ModDate aynı dize nesnesine başvurduğunda otomatik ModDate güncellemesi ikisini birden yeniden yazıyordu ve XMP nesnesi özgün /Metadata akışı okunmadan önce oluşturulduğunda varsayılan bir paket özgün olanın yerine geçiyordu. Düzeltmeler, 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
Bu senaryo bir PDF kütüphanesinin yaptığı en sıkıcı iştir: dosyayı yükle, yeni bir adla kaydet, arada hiçbir şeye dokunma. Sayfalar öncesinde ve sonrasında aynı render edildi. İçerik akışı hash'leri eşleşti. Dosya elimizdeki her kontrolü geçti ve yine de hiçbir render edicinin size gösteremeyeceği iki yerde yanlıştı. Her iki kusur da her gerçek düzenlemenin içinden geçtiği oku-değiştir-yaz yolunda oturuyordu; dolayısıyla herhangi bir kayıt onları tetiklemeye yetiyordu ve ikisi de ancak ikinci ve bağımsız bir ayrıştırıcı iki dosyanın görsel olmayan semantiğini karşılaştırdığında bulundu
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 kütüphane anahtarı değil nesneyi güncelliyordu. 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ğine dair hiçbir şey yoktur. Oluşturma anında aynı zaman damgasını iki kez yazan bir üretici, pekâlâ, iki anahtarı da tek bir 2728 0 R nesnesine işaret ettirebilir; yerel korpusumuzdaki bir CJK tasarım belgesinin yaptığı tam olarak buydu
Tetiği çeken şey otomatik değişiklik tarihidir. UserModDate set edilmediği sürece SaveToFile, yazmadan önce geçerli saatle SetInfo('ModDate', ...) çağırır; bu da SetRawInfo yordamına ulaşır. Eski SetRawInfo anahtarın altındaki nesneyi arıyor ve bir TPDFString bulursa üzerinde SetTo çağırıyordu. Bu, anahtarın o an çözüldüğü nesneye yerinde bir yazmadır ve o nesne paylaşılıyorsa /CreationDate artık kayıt saatini de bildirir. Belge yine açılır, yazdırılır ve eskisi gibi piksel piksel render edilir; dolayısıyla görsel bir regresyon paketi hiç duraksamadan 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 içindeki düzeltme küçüktür ve arkasındaki ilke geneldir: bir sözlük girdisini güncellemek o girdinin başvurusunu değiştirir, çözüldüğü nesneyi asla. Yeni kod mevcut TPDFStringMode değerini okur; böylece hex dize hex, literal dize literal kalır, ardından anahtarın altına FStructure.NewString(Value, StringMode) ile taze bir dize ekler. İki ayrıntı daha manşet değişikliği kadar önemlidir. Akış değerli girdiler için eski dal, akışı değiştirmeden önce SetTo('') ile temizliyordu; bu, o akışa hâlâ işaret eden diğer anahtarların değerini de boşaltırdı, dolayısıyla o temizleme kaldırıldı. Kenara itilen nesne de silinmez, çünkü yapının mülkiyetindedir ve diğer başvurular ona hâlâ ihtiyaç duyabilir
// Önce: anahtarın o an çözüldüğü nesneyi olduğu gibi değiştir
if Obj is TPDFString then
TPDFString(Obj).SetTo(Value);
// Sonra: gösterimi koru, yalnızca bu anahtarın başvurusunu değiştir
StringMode := smLiteral;
if Obj is TPDFString then
StringMode := TPDFString(Obj).StringMode;
ID.Add(Key, FStructure.NewString(Value, StringMode));
Tests\SharedInfoSemantics.inc içindeki regresyon, takma adı bir korpus dosyasına güvenmek yerine bilinçli olarak kurar: iki tarih anahtarından başvurulan tek bir hex dize, /Title ile /Subject tarafından paylaşılan tek bir doğrudan dize, /Author ile /Keywords tarafından paylaşılan tek bir akış. Her çiftin bir anahtarı güncellendikten sonra diğeri hâlâ özgün değerini okumalı ve güncellenen dize hâlâ hex olmalıdır. SetInformation için kamuya açık referans artık garantiyi tek cümleyle söylüyor: 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 hızlı bir yola sahiptir: XMP alanı zaten atanmışsa katalogdaki /Metadata akışını çözmek yerine XMP.SaveToString döndürür. Birkaç çağrı yeri XMP := TPDFlibXMP.Create; XMP.LoadFromString(GetMetadata); ile tembel başlatma yapıyordu; bu doğal okunur ve yanlıştır: GetMetadata çalıştığında XMP zaten atanmıştır, yani yüklenen "kaynak", bir satır önce oluşturulmuş bir nesnenin serileştirilmiş varsayılan paketidir. dc:creator değerini, özel ad alanlarını ve varsa standart tanımlamasını taşıyan özgün paket nesneye hiç ulaşmaz ve kayıt sırasında üzerine yazılır. Bunu tetiklemek için aynı otomatik değişiklik tarihi yeterlidir, çünkü SetInfo, xmp:ModifyDate değeri /ModDate ile aynı adımda kalsın diye XMP'yi Info sözlüğüne dokunmadan önce başlatır. Bu kusurun neyin arkasına saklandığına dikkat edin: ilk hatadaki Info sözlüğü karşılaştırması geçer, çünkü /Info içindeki /Author ve /Title el değmemiştir. 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);
Düzeltme iki şey yapar. TPDFDocument.EnsureXMP artık TPDFlibXMP.Create öncesinde Source := GetMetadata diye yakalar ve belgedeki her tembel başlatma onu çağırmakla değiştirilmiştir: SetInfo, SetXMPInformation, GetXMPInformation, PDF/A, PDF/X, PDF/E, PDF/VT, PDF/VCR ve PDF/UA kip ayarlayıcıları ile metadata onarım yolu. SetXMPProperty gibi kamuya açık giriş noktaları zaten EnsureXMP üzerinden geçiyordu ve GetXMPProperty da GetDocumentMetadata üzerinden okur, dolayısıyla tüm yüzey tek bir başlatma sırasını paylaşır. Üç satırlık bir dizinin tek bir doğru kopyası, bugün tesadüfen uyuşan on kopyadan daha değerlidir
Aynı yolda bulunan iki küçük tuzak
Windows'taki XMP serileştiricisi platformun XML yazıcısını kullanır ve bu yazıcı, paketin taşımaması gereken bir XML bildirimi üretir. Eski kod onu, <?xpacket ifadesine ulaşana kadar karakterleri silerek ayıklıyordu. ISO 16684-1 §7.3.2 xpacket sarmalayıcısını isteğe bağlı kılar ve yalnızca çıplak bir <x:xmpmeta> elemanı yazan bir üretici standart içindedir; dolayısıyla böyle bir pakette döngü baştan sona geçerli olan belgenin tamamını siliyordu. Serileştirici artık bildirimin kapanış ?> işaretini bulur ve yalnızca onu kaldırır. Tests\XMPRetentionSemantics.inc saklama kontrolünü iki kez çalıştırır; bir kez sarmalayıcı varken, bir kez de kesilip atılmışken; ve özel ad alanı işareti ile özgün yazarın SetInfo, GetMetadata, SaveToString ve bir yeniden yüklemeden sağ çıktığını doğrular. İkinci tuzak bir ön işlemci simgesiydi: SetInfo içindeki Info-XMP eşitlemesi, Free Pascal derlemeleri için set edilen NOVCL ile korunuyordu; oysa XMP arka ucu çerçeveyle değil işletim sistemiyle kapılanır, çünkü PDFlibXMP.pas NO_XMP simgesini yalnızca OS_WINDOWS yokken tanımlar. Dolayısıyla bir Windows Lazarus derlemesinde çalışan bir XMP nesnesi ile onu sessizce güncellemeyi atlayan bir SetInfo vardı. Koruma artık NO_XMP olduğundan bir Windows Free Pascal uygulaması da Delphi ile aynı eşitlemeyi alır
Doğrudan geçirme kaydında özgün ModDate nasıl korunur?
TPDFlibSaveOptions içinde KeepModDate 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ının girdisiyle aynı revizyonu tarif etmesi gereken doğrudan geçirme adımları için doğru seçimdir; içeriği gerçekten düzenleyen her şey için ise yanlış seçimdir, çünkü §14.3.3 /ModDate değerinin en son değişikliği yansıtmasını bekler. Ayrıca paylaşılan nesneleri değiştiren bir kütüphaneyi geriye dönük olarak düzeltmez; yalnızca kusuru açığa çıkaran tek yazmadan kaçınır. Sıradan bir kaydı güvenli kılan yukarıdaki iki düzeltmedir; seçenek ise bilinçli bir no-op'u dürüst kılar
Bir kaydın ModDate dışında hiçbir şeyi değiştirmediği nasıl doğrulanır?
Piksellerle değil ve akış hash'leriyle de değil, çünkü her iki kusur da her sayfayı ve her içerik akışını bayt bayt aynı bırakır. Onları yakalayan kontrol, test edilen kütüphaneyle hiçbir kod paylaşmayan bağımsız bir ayrıştırıcının kaynak dosyadan ve kaydedilmiş dosyadan aldığı görsel olmayan semantik anlık görüntü ve ardından yapılan yapısal bir karşılaştırmadır. Anlık görüntü, /ModDate hariç 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, ek dosya baytlarının hash'lerini ve metin olarak değil ağaç olarak ayrıştırılmış XMP paketini kapsar. Nesne numaraları bilinçli olarak bunun dışındadır, çünkü tam bir yeniden yazma her şeyi yeniden numaralar ve onlara dayanan bir karşılaştırma gürültü bildirir
Dışarıda bırakılanlar da 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 geçerli olduğu ya da belgenin PDF/UA veya herhangi bir PDF/A bölümünü karşıladığı anlamına gelmez. Bunlar ayrı araçlarla yanıtlanan ayrı sorulardır ve "metadata hayatta kaldı" ile "metadata uyumlu"yu birbirine karıştırmak, ilk hatanın bu kadar uzun süre saklanmasının sebebidir. Kütüphane tarafında iki regresyon artık Delphi Win32 ve Win64 ile Free Pascal Win32 ve Win64 üzerindeki her hedefli geçişte çalışır ve semantik karşılaştırma gerçek belge korpusu kıyaslaması için bir geçme koşuludur
Bu düzeltmelerin altındaki düzeyde çalışıyorsanız, bir kaydın nesneleri nasıl yeniden yazdığının mekaniği artımlı güncellemeler ve yalnızca ekleme yapan kayıt yazısında ele alınır; paylaşılan bir nesnenin olduğu yerde bırakıldığı tek kayıt kipi odur. Bayat ya da yeniden yazılmış bir tarihin okuyucuyu yanılttığı diğer yer ise değişiklik düzeyleri ve revizyon farkları yazısıdır. Aynı Info ve XMP çiftinin, iki yarının korunmakla kalmayıp birbiriyle uyuşturulduğu onarım tarafı görünümü PDF/A'ya dönüştürme ve metadatayı onarma 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