Teknik Makale

Delphi'de PDFium VCL ile PDF/A Arşiv Uyumluluğu

Her dosyayı PDF/A-1b diye işaretleyen bir dönüştürücü yayınlıyorsunuz, müşterinin kayıt sistemi bunları bir yıl boyunca içeri alıyor ve sonra bir denetim tüm partiyi veraPDF'den geçirince dosyaların üçte biri uyumsuz dönüyor. Hiçbir şey çökmemiş, istisna fırlatılmamış, dosyalar masanızdaki bütün görüntüleyicilerde gayet iyi açılmıştır. Sadece üzerlerine bastığınız standarda gerçekte uymuyorlardır. Arşiv PDF'lerinde olağan hata modu budur ve bu yüzden "bayrağı ayarladık" demek asla "doğrulandı" demekle aynı şey değildir

PDFium ve PDF/A hakkında anlaşılması gereken ilk şey, motorun bununla hiçbir ilgisi olmadığıdır. PDFium PDF oluşturur, ayrıştırır ve yazar; ancak açık yüzeyinde ConvertToPDFA yoktur, OutputIntent yazıcısı yoktur, XMP API'si yoktur. Arşiv uyumluluğunun bütün parçaları — XMP paketi, OutputIntent ve onun ICC profili, catalog işaretleri, doğrulama — PDFiumPas'ın kendisinde, kaydedilen baytları ayrıştırıp bunları incremental update ile yeniden yazan yaklaşık 2.000 satırlık saf Pascal biriminde (FPdfPdfa.pas) yaşar. İşin nerede yapıldığını bilmek, hataların nerede saklandığını da söyler; onlar PDFium'da saklanmaz

PDF/A gerçekte ne ister ve nerede sorun çıkarır

PDF/A tek bir biçim değildir. ISO 19005 üç bölüm tanımlar (PDF/A-1, -2, -3) ve her birinin içinde farklı sözler veren uyumluluk seviyeleri vardır. B seviyesi (basic), yalnızca görsel görünümün yeniden üretilebilir olduğunu garanti eder. A seviyesi (accessible), B'nin üzerine etiketli yapı ağacı ve Unicode eşlemesi ekler. Yalnızca 2. ve 3. bölümlerde bulunan U seviyesi ise bunların arasında yer alır: tam yapı ağacı olmadan güvenilir Unicode metin. ISO 19005-1'de U seviyesi yoktur ve kütüphane bu kısıtı doğrudan kodlar

Biçimin kuralları arasında pratikte gerçekten can yakan birkaç tanesi vardır. Şifreleme açıkça yasaktır (ISO 19005-1 §6.1.3 ve devamı): bir PDF/A dosyası /Encrypt sözlüğü taşıyamaz. Belge, geçerli bir ICC profiline işaret eden OutputIntent aracılığıyla bir çıktı işleme koşulu bildirmelidir (§6.2.3.2). Uygunluk iddiasının kendisi, PDF/A identification schema altındaki XMP metadata içinde görünmelidir. A seviyesi ayrıca belgeyi makinece okunabilir yapan §6.8 logical structure, yani tag tree gerektirir. Bunlardan herhangi biri eksikse, dosya kusursuz çizilse bile bir uyumluluk doğrulayıcısı dosyayı reddeder

Arşiv üreten tek çağrı

PDFiumPas, TPdf.SaveAsPdfA arkasındaki bütün hattı dışarı açar. Basit aşırı yükleme hedef uyumluluğu alır ve varsayılan olarak PDF/A-1b seçer; "bunu sonsuza kadar görüntülenebilir yap" biçimindeki yaygın kullanım için doğru varsayılan budur

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Arka planda bu iki aşamalı bir hamledir. SaveAsPdfA önce PDFium'dan belgeyi FPDF_SaveAsCopy ile serileştirmesini ister, ardından bu bayt akışını InjectPdfAMarkers yöntemine verir; o da XMP metadata'yı, gömülü ICC profiline sahip sRGB OutputIntent'i ve yeniden yazılmış catalog'u incremental update olarak ekler. Kaynak sıfır konumundan okunur, hedef sıfır konumundan yazılır; özgün nesne ağacı bozulmadan kalır ve işaretler mevcut %%EOF sonrasına eklenir. Dosya yerine baytları istiyorsanız, SaveAsPdfAToStream bir TStream ve aynı seçenekleri alır

Options record ile uyumluluk seçimi

Belirli bir bölüm ve seviyeyi hedeflemek için bir TPdfASaveOptions kaydı geçin. Bunun Conformance alanı bir TPdfAConformance değeri alır. Enum, geçerli bütün birleşimleri kapsar ve bunun dışında hiçbir şey kapsamaz: 1. bölüm için pac1b, pac1a; 2. bölüm için pac2b, pac2u, pac2a; 3. bölüm için pac3b, pac3u, pac3a; ayrıca doğrulama tarafı için pacUnknown ve pacNone. pac1u yoktur, çünkü standartta böyle bir seviye yoktur

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

Kaydın büyük kısmı boş bırakılabilir. Title, Author, Subject, Keywords, Creator ve Producer alanlarını boş bırakırsanız SaveAsPdfA bunları FPDF_GetMetaText aracılığıyla belgenin Info dictionary'sinden otomatik doldurur. CreationDate ile ModDate alanlarını boş bırakırsanız XMP tarihleri için mevcut UTC zamanını kullanır. DocumentId ile InstanceId alanlarını boş bırakırsanız, kütüphane bunları FPDF_GetFileIdentifier içinden önceden doldurur ve kaynak baytlardan türetilen deterministik bir ID'ye düşer. Bilinçli olarak geçersiz kılmak isteyebileceğiniz tek alan IccProfileData alanıdır: boş bırakılırsa paketlenmiş sRGB IEC61966-2.1 profili kullanılır, ancak CMYK ya da gri tonlamalı iş akışı kendi profilini vermelidir

A seviyesinin neden düşürüldüğü ve bunun neden dürüst seçim olduğu

Burada, bir bayrağın garanti anlamına gelmesini bekleyenleri şaşırtan incelik vardır. Etiket ağacı olmayan bir belge için pac1a isteyebilirsiniz, ancak PDF/A-1a §6.8 logical structure gerektirir ve kütüphane etiketsiz bir PDF'den yapı ağacı üretemez. SaveAsPdfA, A seviyesini iddia edip gerçekte başaramayan bir dosya üretmek yerine, gerçek bir etiketli yapı arar (/StructTreeRoot ile /MarkInfo ve /Marked true); bu yoksa iddiayı düşürür: pac1a, pac1b olur; pac2a, pac2b olur; aynı şey üç bölümün tamamında geçerlidir. İç yardımcılar PdfAIsLevelA ile PdfADowngradeToLevelB adlarını taşır

Gerekçeyi açıkça söylemeye değer: gerçekten karşıladığı seviyeyi dürüstçe ilan eden dosya, karşılamadığı bir seviyeyi yalan söyleyerek ilan eden dosyadan daha kullanışlıdır. U seviyesi farklı ele alınır. Gerçek Unicode kapsamını saptamak, meşru belgeleri gereksiz yere düşüren saf bir "/ToUnicode var mı" testini gerektirir (WinAnsi ve benzeri kodlamalar muaf tutulur); bu yüzden kaydetme tarafı U iddiasını çağıranın belirttiği gibi yazar ve olası uyuşmazlığın doğrulama tarafında işaretlenmesine izin verir. Garantili bir A seviyesi arşiv istiyorsanız, dönüştürmeden önce belgeyi etiketleyin; dönüştürücü orada olmayan yapıyı icat etmez

Yalnızca gerçek doğrulayıcının yakaladığı ICC tuzağı

Bu, en sert dersi veren arıza oldu; çünkü kütüphanenin kendi denetleyicisi bunu geçirirken, ISO 19005 referans doğrulayıcısı olan veraPDF geçirmedi. PDF/A, OutputIntent'in hedef profilinin geçerli bir ICCBased akışı olmasını ister ve §6.2.3.2, doğrulayıcının bu akışı renk uzayı olarak doğrulamasını zorunlu kılar. ICCBased bir akışın /N, yani renk bileşeni sayısını bildirmesi gerekir. Erken sürümlerden biri ICC akışı sözlüğünü yalnızca /Length ile yazıyor, /N alanını yazmıyordu ve veraPDF sonucu "The N entry (value null)... is missing" hatasıyla reddediyordu

Sinsi olan taraf, reddin yalnızca PDF/A-1b ve -1a için tetiklenmesiydi. 2. ve 3. bölüm uyumluluk modelleri hedef profil üzerinde o belirli denetimi çalıştırmıyordu; dolayısıyla aynı enjekte edilmiş yapı pac2b, pac3b ve pac2u altında doğrulanırken, pac1b altında yalnızca pdfaid:part değeri yüzünden başarısız oluyordu. Bir birim testi bunu asla göremezdi; çünkü kütüphanenin kendi ValidatePdfACompliance yordamı yalnızca /DestOutputProfile anahtarının varlığını denetliyor, akış sözlüğünün içeriğini denetlemiyordu. İç testler yeşil kalırken gerçek arşiv doğrulaması başarısız oluyordu

Düzeltme IccComponentCount yordamıdır; bu yordam ICC başlığının 16. ofsetindeki veri renk uzayı imzasını okur ve bunu bileşen sayısına eşler: GRAY için 1, RGB , Lab ve XYZ için 3, CMYK için 4; bilinmeyen profilde varsayılan 3 alınır. Bu sayı akış sözlüğüne /N olarak yazılır. 3'e sabit kodlanmış değildir; çünkü çağıran IccProfileData üzerinden CMYK veya gri tonlama profili verdiğinde de doğru değer üretilmelidir. Daha geniş ders yöntemseldir: kütüphane içi denetleyici ile otoriter doğrulayıcının her birinin kör noktaları vardır ve PDF/A çıktısı, öz denetimlere güvenmek yerine veraPDF gibi bir referans uygulamaya karşı uçtan uca test edilmelidir. Temiz arşivlerin arkasındaki aynı incremental-update disiplini, sıkıştırılmış nesne ve xref stream doğrulaması makalesinde anlatılır; bu önemlidir, çünkü enjektörün tükettiği modern PDF'ler çoğu zaman cross-reference stream üzerine kuruludur

Şifreleme, xref stream'ler ve diğer kenarlar

ISO 19005 şifrelemeyi yasakladığı için kaydetme yolu, yazmadan önce bunu kaldırır. SaveAsPdfA serileştirme sırasında FPDF_REMOVE_SECURITY uygular; böylece şifreli bir kaynak (parolasıyla yüklenmişse) arşive giderken şifresi çözülmüş olarak yazılır. Şifrelenmemiş bir belgede bu no-op olur ve hiçbir şeyi değiştirmez. Sonuç aynı kısıttır; HotPDF de bunu ters yönden uygular: tek bir dosya hem şifreli hem de PDF/A olamaz. Bir iş akışı ikisini de gerektiriyorsa cevap iki artefakttır: dağıtım için şifreli kopya ve arşiv için ayrı temiz kopya

Bir başka kenar da vurana kadar görünmez: saf cross-reference stream kullanan ve trailer anahtar sözcüğünü taşımayan PDF 1.5+ belgeleri. Enjektör, kaynak /Info değerini bulup incremental update'i eklemek için trailer'ı okur ve xref-stream biçimini kabul etmek zorundadır; aksi halde böyle bir belge, işaretler sessizce düşürülmüş biçimde kopyalanırdı. ISO 32000-1 §7.5.6 açıkça, klasik trailer incremental update'in xref-stream belgesinin arkasından gelmesine izin verir; /Prev xref-stream ofsetini işaret eder ve enjektör tam olarak bu yapıyı üretir. PDFium'un kendi FPDF_SaveAsCopy yordamı her zaman klasik trailer yazar; bu yüzden normal hatta enjektör saf xref-stream kaynakla karşılaşmaz, ancak dışarıdan gelen belgeler için okuma yolu bunu yine de destekler

İddiaya güvenmeden önce doğrulama

Kütüphane, TPdf.ValidatePdfA adlı bayt düzeyinde bir denetleyiciyle gelir; bu yordam bir TPdfAValidationResult döndürür. Bunun Conformance alanı algılanan seviyeyi bildirir ve Issues alanı bir TPdfAValidationIssue değer kümesidir; kolaylık yöntemi IsCompliant ise yalnızca gerçek bir seviye algılandığında ve sorun kümesi boş olduğunda true olur. Bunu bir batch içinde hızlı ilk kapı olarak çalıştırın

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

Bunun size ne kazandırdığı konusunda dürüst olun. Bayt düzeyindeki denetleyici yapısal sorunları — eksik OutputIntent, yasaklı eylem, mevcut bir /Encrypt, 1. bölümün yasakladığı şeffaflık — yüksek güvenle yakalar; yazı tipi gömme saptaması ise her glif kapsamını kovalamak yerine özellikle yüksek güven sinyali üreten bir sayım sezgisine dayanır. Yapmadığı şey içerik akışı operatör analizi yapmaktır; bu tam bir içerik ayrıştırıcısı gerektirirdi ve tasarım gereği kapsam dışıdır. Yayın kapısı için kütüphane içi denetleyiciyi veraPDF ile eşleyin: denetleyici anlıktır ve DLL olmadan her yerde çalışır, veraPDF ise otoriterdir. Bu eşleştirmeyi batch koşusuna bağlamak batch preflight report CLI konusudur; gerçek arşiv iş akışında bu doğrulamanın ait olduğu yer orasıdır

Burada gösterilen SaveAsPdfA, InjectPdfAMarkers ve ValidatePdfA API'leri, Delphi, C++Builder ve Lazarus/FPC için PDFium Component ile birlikte sunulur. Ürün sayfası, tam uyumluluk enum'u ve bu örneklerin arkasındaki options record da dahil olmak üzere eksiksiz API başvurusuna bağlantı verir