Teknik Makale

Delphi'de PDF/A-3 İlişkili Dosyalar ve AFRelationship

Delphi'den bir PDF/A-3 belgesine kaynak dosya eklemek için PDFium Component, PDF 2.0 associated-file zinciri yazar: MIME /Subtype taşıyan gömülü bir dosya akışı, /AFRelationship taşıyan bir dosya belirtimi ve katalog ya da bir sayfaya asılı bir /AF dizisi. InjectAssociateFiles ile TPdf.SaveAsWithAssociateFiles o zinciri tek bir artımlı güncellemede kurar ve v3.121.2'den beri MIME türü tek, doğru kaçışlı bir PDF name olarak serileştirilir. Bu yazının kalanı doğrulayıcının neyi denetlediğini, text/plain'i kıran tek karakterlik hatayı ve eski sürümlerin sessizce istediğinizden başka bir şey yaptığı yerleri kapsar

Bir PDF/A-3 associated file aslında neye ihtiyaç duyar?

Bir PDF/A-3 eki, yalnızca üç nesne birbiriyle aynı fikirde olduğunda doğrulamayı geçer: gömülü dosya akışı /Type /EmbeddedFile artı bir MIME /Subtype bildirir, dosya belirtimi sözlüğü (ISO 32000-2 §7.11.3) /F, /UF, /EF ve /AFRelationship taşır ve belgedeki bir şey o dosya belirtimine bir /AF dizisi üzerinden referans verir (ISO 32000-2 §14.13). TPdf.CreateAttachment'ın yaptığı türden düz gömme, /Names /EmbeddedFiles ağacı üzerinden, ilişki alanlarını hiç kurmaz. PDFium Component'in kendi PDF/A-3b doğrulama test verisi bağımlılığı somutlaştırır: yalnızca /AFRelationship anahtarını yeniden adlandırın ve dosya ISO 19005-3'ün 6.8 maddesinde tam olarak tek kuralda düşer; yalnızca MIME /Subtype'ı atın ve farklı bir 6.8 kuralı düşer; aynı eki bir PDF/A-1b adayına koyun ve tümüyle reddedilir, çünkü PDF/A-1, üst veri ne kadar derli toplu olursa olsun gömülü dosyaları yasaklar

PDFium Component'te bir PDF A-3 associated file'ının üç nesneli zinciri: application xml gibi MIME Subtype taşıyan bir EmbeddedFile akışı; F, UF, EF ve Data'ya kurulmuş AFRelationship ile bir dosya belirtimi; katalog ya da bir sayfadan ona ait bir AF dizisi — ISO 19005-3 6.8 maddesi geçmeden önce doğrulayıcının denetlediği üç nesne
Akış, dosya belirtimi ve AF dizisi aynı fikirde olmalıdır; TPdf.CreateAttachment'ın düz name-tree gömmesi ilişki alanlarının hiçbirini kurmaz ve kurmayacaktır da

İlişki değeri, insanların tahmin etmeye eğilimli olduğu kısımdır. FPdfAssocFiles içindeki TPdfAFRelationship, enjektörün basabileceği her name tokenina bir enum üyesi eşler ve ilk beşi ISO 19005-3'ün tanıdığı alt kümeye aittir:

  • afSource → /Source: PDF'in üretildiği özgün belge; bir kelime işlemci dosyası ya da elektronik tablo gibi
  • afData → /Data: görünür içeriğin türetildiği ya da temsil ettiği makine okunur veri
  • afAlternative → /Alternative, afSupplement → /Supplement, afUnspecified → /Unspecified
  • afEncryptedPayload, afFormData, afTemplate: PDF/A-3 alt kümesinin dışına düşen PDF 2.0 ekleri; arşiv çıktısında bunları uzak tutun

/Subtype /text/plain doğrulamayı neden kırdı?

MIME hatası bir tokenization hatasıydı, uyumluluk boşluğu değil: v3.121.2 öncesinde enjektör, çağıranın dizesini doğrudan bir eğik çizginin ardına ekliyor ve /Subtype /text/plain üretiyordu. PDF sözdiziminde ikinci eğik çizgi yeni bir name nesnesi başlatır (ISO 32000-1 §7.3.5); akış sözlüğü bir anda /Subtype anahtarını, /text name'ini ve anahtar-değer çiftlerini dengesizleştiren başıboş bir /plain fazlasını taşıyordu. Bağımsız bir PDF/A doğrulayıcı dosyayı, daha bir PDF/A kuralına varmadan, EmbeddedFile sözlüğünü ayrıştırırken reddediyordu; başarısızlığın ek özelliği eksikmiş gibi değil dosya bozulması gibi görünmesinin nedeni buydu

Düzeltme MIME değerini EscapePdfName'den geçirir; bu, çözülen değeri text/plain olan tek bir name olan /text#2Fplain basar. Kaçış, eğik çizgiden bilinçli olarak geniştir. 32 ve altındaki her bayt (boşluk, tab, CR, LF), 127 ve üstündeki her bayt, ()<>[]{}/% ayraçları ve # kaçış karakterinin kendisi #XX olur. Yalnızca eğik çizgiyi kaçışlamak başka bir delik bırakırdı: >> içeren ya da boşluk taşıyan bir MIME dizesi sözlüğü erken kapatabilir ya da fazladan anahtarlar enjekte edebilirdi; bu yüzden regresyon testi, her ayraç artı tab, LF ve CR taşıyan düşmanca bir değeri besler ve tam kodlanmış çıktıyı denetler

MIME alt türü text slash plain, PDFium Component'te PDF A-3 ayrışmasını neden kırdı: değeri eğik çizgiden sonra bitiştirmek iki name nesnesi üretti; değer olarak /text artı EmbeddedFile sözlüğünü dengesizleştiren başıboş bir /plain; v3.121.2 düzeltmesi değeri EscapePdfName'den geçirir, böylece /text#2Fplain, text/plain'e çözülen tek name olur
Başarısızlık dosya bozulması gibi görünüyordu, çünkü ayrıştırıcıda, herhangi bir PDF/A kuralından önce oluyordu; kaçışlı name, çiftleri dengede tutar ve doğrulayıcıyı okur
// Enjektörün MIMEType = 'text/plain' için yazdığı
//   v3.121.2 öncesi:  /Type /EmbeddedFile /Subtype /text/plain     (iki name)
//   v3.121.2:         /Type /EmbeddedFile /Subtype /text#2Fplain   (tek name)
//
// Çağıranlar her zaman sıradan MIME değerini geçirir. Kendiniz önceden
// kaçışlarsanız '#' çifte kodlanır; 'text#2Fplain', 'text#232Fplain' olur
Options.Files[0].MIMEType := 'text/plain';

InjectAssociateFiles ile PDF/A-3 dosyası kurmak

PDF/A-3 çıktısı için uyumlu temel belgeyi TPdf.SaveAsPdfAToStream ile üretin ve sonra o akış üzerinde InjectAssociateFiles çağırın; iki adımlı bu hat, doğrulama test verisinin PDF/A-3b'yi geçmeden önce koşturduğu hattın ta kendisidir. TPdf.SaveAsWithAssociateFiles kolaylık sarmalayıcısıdır ama PDF/A yazıcısı üzerinden değil, saRemoveSecurity ile sıradan SaveAs yolu üzerinden kaydeder; dolayısıyla PDF/A'nın gerektirdiği XMP kimliğini ve çıkış niyetini eklemez. Kayıt türlerinin FPdfAssocFiles ile FPdfPdfa'da yaşadığına dikkat edin; her iki unit de uses maddenize girmelidir. v3.121.3'ten beri FileName ile Description'un düz ASCII olması gerekmez: /UF ile /Desc PDF metin dizeleri olarak yazılır, yazdırılabilir ASCII harfiyen ve geri kalan her şey bayt sırası işaretiyle UTF-16BE; eski /F name'i ise her zaman taşınabilir yazdırılabilir ASCII'dir ve öteki her karakter _ ile değiştirilir, böylece /F'i kendi kod sayfasıyla çözen okuyucular mojibake yerine alt çizgi gösterir. Daha eski derlemler üçünü de Delphi'de sistem ANSI kod sayfası üzerinden çeviriyor ya da Free Pascal'da ham UTF-8 baytları yazıyordu; eski derlemlerin aynı çıktıyı üretmesi gerekiyorsa adları yalnızca ASCII tutun

uses
  System.SysUtils, System.Classes, System.IOUtils,
  PDFium, FPdfPdfa, FPdfAssocFiles;

procedure SaveWithSourceData(Pdf: TPdf; const XmlPath, OutPath: string);
var
  PdfAOptions: TPdfASaveOptions;
  Options: TAssocFilesOptions;
  Base: TMemoryStream;
  Output: TFileStream;
begin
  PdfAOptions := TPdfASaveOptions.Default;
  PdfAOptions.Conformance := pac3b;

  Options := TAssocFilesOptions.Default;      // TargetPage = 0: katalog düzeyi /AF
  SetLength(Options.Files, 1);
  Options.Files[0].FileName := 'invoice-data.xml';
  Options.Files[0].Description := 'Structured invoice data';
  Options.Files[0].Content := TFile.ReadAllBytes(XmlPath);
  Options.Files[0].Relationship := afData;
  Options.Files[0].MIMEType := 'application/xml';  // /application#2Fxml olarak yazılır

  Base := TMemoryStream.Create;
  try
    if not Pdf.SaveAsPdfAToStream(Base, PdfAOptions) then
      raise Exception.Create('PDF/A-3 base save failed');
    Output := TFileStream.Create(OutPath, fmCreate);
    try
      InjectAssociateFiles(Base, Output, Options);  // Base'i geri sarar; başarısızlıkta EPdfAssocFilesError yükseltir
    finally
      Output.Free;
    end;
  finally
    Base.Free;
  end;
end;

Katalog mu sayfa mı: /AF dizisi nereye düşer?

TAssocFilesOptions.TargetPage, /AF dizisinin sahibine karar verir: 0, onu belge düzeyi bir ilişki olarak kataloğa ekler; 1..N ise o sayfa sözlüğüne ekler, 1 tabanlı. Enjektör her şeyi sabit bir yerleşimle tek bir artımlı güncelleme olarak ekler (önce gömülü akışlar, sonra dosya belirtimleri, sonra /AF dizisi, sonra yeniden yazılmış katalog ya da sayfa nesnesi); böylece var olan nesneler ofsetlerini korur ve hiçbir şey yeniden sıkıştırılmaz. Hedef sözlükteki önceki her /AF girdisi değiştirilir, birleştirilmez; bu, tekrarlanan kaydetmeyi idempotent kılar ama farklı bir dosya listesiyle yapılan ikinci çağrının kazandığı anlamına da gelir. İki davranış eskiden kendi kodunuzda bir koruma hak ediyordu ve ikisi de değişti. v3.122.0 öncesinde aralık dışı bir TargetPage başarısız olmuyordu; kataloğa dönüyordu, böylece bir yazım hatası hiç sinyal olmaksızın sayfa düzeyi bir ilişkiyi belge düzeyine çeviriyordu. v3.122.0'dan beri SaveAsWithAssociateFiles ile SaveAsWithAssociateFilesToStream, TargetPage 0..PageCount dışındayken EPdfError yükseltir ve InjectAssociateFiles, negatif bir TargetPage ya da var olmayan bir sayfayı adlandıran biri için yeni EPdfAssocFilesError'ı yükseltir ve hedef akışı değiştirilmemiş bırakır. v3.121.4 öncesinde sayfa araması kaydedilen baytları dosya sırasında /Type /Page sözlükleri için tarıyordu; sayfa nesneleri göründükleri sıradan başka bir düzende saklandığında — sayfalar yeniden sıralandığında ya da eklendiğinde örneğin — dosyayı farklı bir sayfaya ekleyebiliyordu; v3.121.4'ten beri TargetPage, belge sayfa sırasında o konumdaki sayfayı adlandırır

AF dizisi PDFium Component'te nereye düşer: TargetPage 0 onu kataloğa ekler, 1-N sayfaları onu sayfa sözlüğüne ekler ve aralık dışı bir değer — v3.122.0 öncesinde sessizce kataloğa dönüyordu — artık istisna yükseltir; enjektör her şeyi, var olan ofsetleri koruyan ve önceki her AF girdisini değiştiren sabit bir yerleşimle tek artımlı güncelleme olarak ekler
v3.122.0 öncesinde aralık dışı bir TargetPage sessizce belge düzeyi ilişkiye dönüşüyordu; güncel sürümler onun yerine yükseltir ve farklı dosya listeli ikinci çağrı hâlâ kazanır
procedure AttachChartSource(Pdf: TPdf; PageNumber: Integer;
  const CsvBytes: TBytes; const OutPath: string);
var
  Options: TAssocFilesOptions;
begin
  // v3.122.0'dan beri aralık dışı bir TargetPage EPdfError yükseltir (eski
  // derlemler sessizce katalog düzeyi /AF'ye dönerdi); önce denetlemek sayfayı adlandırır
  if (PageNumber < 1) or (PageNumber > Pdf.PageCount) then
    raise EArgumentOutOfRangeException.CreateFmt('No page %d', [PageNumber]);

  Options := TAssocFilesOptions.Default;
  Options.TargetPage := PageNumber;
  SetLength(Options.Files, 1);
  Options.Files[0].FileName := 'chart-data.csv';
  Options.Files[0].Content := CsvBytes;
  Options.Files[0].Relationship := afSource;
  Options.Files[0].MIMEType := 'text/csv';

  if not Pdf.SaveAsWithAssociateFiles(OutPath, Options) then
    raise Exception.Create('Associated-file save failed');
end;

AFRelationship'i güvenilir biçimde geri nasıl okursunuz?

TPdf.AttachmentRelationship[Index], bir ekin /AFRelationship name'ini yerli FPDFAttachment_GetAFRelationship ihracı üzerinden döndürür; ama boş dizenin iki olası anlamı vardır, bu yüzden önce AttachmentRelationshipFeaturesAvailable çağırın. Bağlama toleranslı yüklenir: PDFium DLL bu ihracı taşımıyorsa her ilişki boş okunur; bu, basitçe /AFRelationship'i olmayan bir dosya belirtiminden ayırt edilemez. Özellik indeksini AttachmentCount ile paylaşır; bu, /Names /EmbeddedFiles ağacındaki girdileri sayar. Enjektör yalnızca /AF zincirini yazar ve name-tree girdisi eklemez; dolayısıyla InjectAssociateFiles üzerinden eklenen bir dosya o indeksin dışındadır; enjekte edilen zinciri teyit için kaydedilen baytları inceleyin ya da bir PDF/A doğrulayıcı koşturun. O name ağacının iç işleyişi PDFium Component ile Delphi'de PDF ekleriyle çalışma makalesinde kapsanıyor

procedure ReportRelationships(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Rel: string;
begin
  if not AttachmentRelationshipFeaturesAvailable then
  begin
    Writeln('This PDFium build cannot report /AFRelationship');
    Exit;  // boş yanıt belirsiz olurdu, o yüzden sorma
  end;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    for I := 0 to Pdf.AttachmentCount - 1 do
    begin
      Rel := Pdf.AttachmentRelationship[I];
      if Rel = '' then
        Rel := '(no /AFRelationship)';
      Writeln(Pdf.AttachmentName[I], ': ', Rel);
    end;
  finally
    Pdf.Free;
  end;
end;

SaveAsWithAssociateFiles neyi garanti etmez?

TPdf.SaveAsWithAssociateFiles, dosya biçimi zarfını ve istenen dosyaların enjekte edildiğini garanti eder; uyumluluğu değil. Enjeksiyon kısmı yenidir: v3.122.0 öncesinde, kaydedilen baytlarda okunabilir bir trailer yokken ya da katalog sözlüğü bulunamadığında InjectAssociateFiles girdiyi değiştirmeden kopyalıyor ve yöntem yine de True döndürüyordu. v3.122.0'dan beri InjectAssociateFiles bu durumlarda hiçbir şey yazmadan EPdfAssocFilesError yükseltir, SaveAsWithAssociateFiles False döndürür ve hedefi açmadan önce tam çıktıyı bir save store'da kurduğu için reddedilen ya da başarısız bir kaydetme artık var olan bir dosyayı kesmiyor. Boş bir Files dizisi, tasarımla, belgeyi değiştirmeden kopyalamaya devam eder. Yükün içeriği de sizin sorumluluğunuzdadır: enjektör, bir XML dosyasının biçimli olduğunu, MIME türünün baytlarla eşleştiğini ya da temel belgenin gerçekten PDF/A olduğunu denetlemez. Son dosyayı, bir doğrulayıcı onu görene dek doğrulanmamış sayın; PDFium Component ve PDF/A arşiv uyumluluğu makalesinde anlatılan disiplin de budur. Gelen sözlükleri de kendiniz ayrıştırıyorsanız aynı #XX name kuralları tersten uygulanır; konu PDF sözlüklerini ayrıştırırken name token tuzakları makalesinde ele alınıyor

Associated files, PDF/A çıktısı, ek üst verisi ve doğrulama aynı bileşende gelir; böylece yukarıdaki hat, derlemede ikinci bir PDF kütüphanesi olmaksızın koşar. API referansı, deneme indirmesi ve lisans seçenekleri PDFium Component ürün sayfasındadır