Teknik Makale

Delphi ile PDFiumPas: PDFler Arası AcroForm Alanı Aşılama

Bir form alanı bloğunu geçen yılın şablonundan bu yılın yerleşimine taşımak, FDF ve XFDF gidiş dönüşlerinin artık yeterli olmadığı noktadır: değerler gelir ama görünüm akışları, hesaplama eylemleri ve varsayılan kaynaklar gelmez. PDFiumPas bu duruma, tüm alan nesne grafiğini bir PDFden klonlayıp başka bir PDFe yazan GraftPdfAcroForm ile yanıt verir

Veri düzeyinde bir dışa aktarımın bunu yapamamasının nedeni yapısaldır. Bir alan bir kayıt değildir, bir alt graftır. ISO 32000-1 §12.7, /Fields, /CO, /DR ve /DA girdilerini tutan etkileşimli form sözlüğünü tanımlar; §12.7.3 onun altında asılı duran alan sözlüklerini tanımlar ve §12.5.6.19, bu alanlara bir sayfa üzerinde görünür bir kutu veren widget açıklamalarını tanımlar. XFDF o yapının yapraklarını taşır. Aşılama ise yapının kendisini taşır

/Fields dizisini kopyalamak neden asla yeterli olmaz?

/Fields dizisini bir belgeden başka birine kopyalamak, her ilginç yönden bozuk bir form üretir; çünkü dizi dolaylı başvurulardan başka bir şey tutmaz. ISO 32000-1 §7.3.10, bir dolaylı nesneyi nesne numarası artı nesil ile adreslenebilir kılar ve bu numaralar yalnızca geldikleri dosyanın içinde anlamlıdır. Diziyi karşı tarafa yapıştırdığınızda içindeki her başvuru ya sarkır ya da daha da kötüsü, hedefte o yuvayı işgal etmekte olan ilgisiz bir nesneye sessizce çözülür. Her başvurunun altında hem paylaşılan hem döngüsel bir grafik bulunur. Bir alan sözlüğü çocuklarına işaret eder, her çocuk /Parent ile ebeveynine geri işaret eder, bir widget görünüm akışlarına ve /P ile onu taşıyan sayfaya işaret eder, görünüm akışları formun varsayılan kaynak sözlüğündeki yazı tiplerine işaret eder ve /AA altındaki ek eylem sözlükleri daha da fazla nesneye işaret eder. Farklı sayfalardaki iki widget sıklıkla tek bir yazı tipini ve tek bir görünüm XObjectini paylaşır. Bu yüzden doğru bir aşılama o grafiği yürümeli, erişilebilir her nesneyi tam olarak bir kez klonlamalı, her widgetın /P başvurusunu eşlenen hedef sayfaya yönlendirmeli ve klonlanan widgetı o sayfanın /Annots dizisine eklemelidir — aksi takdirde alan formda var olur ve sayfada görünmez. Bir alan, widgetı ve onu görüntüleyen sayfa açıklaması arasındaki ayrımın peşindeyseniz, widget dizinine karşı açıklama dizini notumuz tam olarak bu ayrımı kapsar

PDFiumPasin Delphi içinde aşıladığı haliyle tek bir PDF form alanının ardındaki nesne grafiği: form sözlüğü, alan, widget açıklamaları, hedef sayfa açıklama dizileri ve her iki widgetın paylaştığı görünüm akışı ile yazı tipi; ayrıca döngüyü kapatan ebeveyn geri başvurusu
Bir alan paylaşılan döngüsel bir alt graftır; /Fields dizisini belgeler arasında kopyalamanın her başvuruyu sarkık bırakmasının nedeni budur

GraftPdfAcroForm sizden ne ister?

Üç ayrı akış ve açık bir sayfa eşlemesi ister. GraftPdfAcroForm Source, Destination ve Output akışlarını ayrı TStream örnekleri olarak, bir TPdfGraftPageMappings dizisini, bir TPdfAcroFormGraftOptions kaydını, isteğe bağlı bir TPdfCrossDocumentGraftMap nesnesini ve bir çıktı TPdfAcroFormGraftReport alır. Özel durum yükseltmek yerine Boolean döndürür ve başarısızlıkta rapor nedeni ErrorMessage içinde taşır. Sayfa eşlemesi her iki tarafta da bir tabanlıdır ve çıkarım yapılmaz: aşılayacağınız bir widget taşıyan her kaynak sayfa içinde yer almalıdır. Aşılama haritası için nil geçmek meşrudur — işlev o zaman çağrı süresince kendine ait bir tane oluşturup serbest bırakır — ve TPdfAcroFormGraftOptions.Default size CollisionPolicy için pagcpReject, RenamePrefix için Imported_, 10000 MaxObjects, 128 MaxDepth ve AllowSignedDestination için False değerlerini verir. Bu son üçü bütçelerdir ve orada olmalarının nedeni, yürümek üzere olduğunuz nesne grafiğinin yazmadığınız bir dosyadan gelmesidir

uses
  Classes, SysUtils, FPdfCompress;

var
  Source, Destination, Output: TMemoryStream;
  Options: TPdfAcroFormGraftOptions;
  Mappings: TPdfGraftPageMappings;
  Report: TPdfAcroFormGraftReport;
begin
  Source := TMemoryStream.Create;
  Destination := TMemoryStream.Create;
  Output := TMemoryStream.Create;
  try
    Source.LoadFromFile('claim-template-2025.pdf');
    Destination.LoadFromFile('claim-layout-2026.pdf');
    Source.Position := 0;
    Destination.Position := 0;

    Options := TPdfAcroFormGraftOptions.Default;

    SetLength(Mappings, 2);
    Mappings[0].SourcePageNumber := 1;
    Mappings[0].DestinationPageNumber := 1;
    Mappings[1].SourcePageNumber := 2;
    Mappings[1].DestinationPageNumber := 3;

    if GraftPdfAcroForm(Source, Destination, Output, Mappings,
      Options, nil, Report) then
      Output.SaveToFile('claim-2026-with-fields.pdf')
    else
      raise Exception.Create(Report.ErrorMessage);
  finally
    Output.Free;
    Destination.Free;
    Source.Free;
  end;
end;

Aşılama haritası paylaşılan bir yazı tipinin iki kez klonlanmasını nasıl önler?

TPdfCrossDocumentGraftMap, anahtarları hem nesne numarasını hem nesli taşıyan kaynaktan hedefe bir başvuru tablosu tutar ve özyinelemeli klonlayıcı derinleşmeden önce ona danışır. İşlem sırası, döngüleri güvenli kılan şeydir: klonlayıcı hedef nesne numarasını ayırır ve eşlemeyi önce kaydeder, sonra kaynak nesnenin çocuk başvurularını yürür. Ebeveynine geri işaret eden bir çocuğa ulaşan bir ebeveyn, ebeveynin zaten kayıtlı olduğunu bulur ve özyineleme yerine var olan hedef başvurusunu döndürür. Aynı arama, altı widget tarafından paylaşılan bir yazı tipinin, görünüm akışının ya da eylemin bir kez klonlanıp altı kez başvurulmasını sağlayan şeydir. Harita, kaynak baytlarının SHA-256 karmasıyla kaynak belgeye bağlanır ve SourceIdentity olarak sunulur. GraftPdfAcroForm işlevine, kimliği geçtiğiniz kaynakla eşleşmeyen bir harita verirseniz, bu dosya için hiçbir zaman geçerli olmamış başvuruları yeniden kullanmak yerine çağrıyı reddeder. Sayfa eşlemeleri, klonlama başlamadan önce aynı haritaya tohum olarak eklenir; bir widgetın /P başvurusunun hedef sayfayı göstermesinin yolu tam olarak budur: kaynak sayfa nesnesi zaten eşlenen hedef sayfa nesnesine çözülür, böylece sıradan başvuru yeniden yazma geçişi onu özel bir durum olmadan halleder

Delphi içindeki PDFiumPas belgeler arası aşılama haritası; her kaynak başvuruyu nesne numarası ve nesille anahtarlar, derinleşmeden önce hedef eşlemeyi kaydeder, böylece bir ebeveyn geri başvurusu sonlanır ve var olan girdiyi döndürür; paylaşılan bir yazı tipi yalnızca bir kez klonlanır
Eşlemeyi çocukları yürümeden önce kaydetmek, döngüsel bir grafiği güvenli ve paylaşılan bir nesneyi tam olarak bir kez klonlanmış kılan şeydir
uses
  Classes, SysUtils, FPdfCompress, FPdfSha256;

var
  GraftMap: TPdfCrossDocumentGraftMap;
  SourceBytes: TBytes;
  EntriesBefore: Integer;
begin
  SetLength(SourceBytes, Source.Size);
  Source.Position := 0;
  if Length(SourceBytes) > 0 then
    Source.ReadBuffer(SourceBytes[0], Length(SourceBytes));

  GraftMap := TPdfCrossDocumentGraftMap.Create(
    AnsiString(SHA256Hex(SHA256Bytes(SourceBytes))));
  try
    EntriesBefore := GraftMap.Count;
    Source.Position := 0;
    if not GraftPdfAcroForm(Source, Destination, Output, Mappings,
      Options, GraftMap, Report) then
    begin
      // Bu çağrının eklediği girdiler geri alındı;
      // ondan önce kaydedilen her şey hâlâ sağlam.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Geri alım, haritaya kendinizin sahip olmasının noktasıdır. PDFiumPas, çağıran tarafından sağlanan bir haritaya işlemsel davranır: başarısız bir aşılama o çağrının eklediği girdileri atar ve önceden var olan her eşlemeyi korur; böylece tek bir reddetme, hiç yazılmamış nesnelere yönelik bir başvuru önbelleğini asla ardında bırakmaz. Yine de hedef belge başına bir harita tutun — her girdinin hedef tarafı o belirli dosyadaki bir nesne numarasıdır ve farklı bir dosyada hiçbir anlam ifade etmez

Alan adı çakışmaları: reddet ya da yeniden adlandır

Tam nitelikli alan adları bir formun içinde benzersiz kalmalıdır ve PDFiumPas, çakıştıklarında ne kastettiğinizi tahmin etmez. TPdfAcroFormCollisionPolicy tam olarak iki yanıt sunar. Varsayılan olan pagcpReject altında, başlığı hedefte zaten var olan ilk kaynak alanı tüm aşılamayı bir hatayla durdurur ve çıktı akışını boş bırakır. pagcpRename altında, çakışan kaynak alanı RenamePrefix önekiyle yeniden adlandırılır ve aşılama sürer; Report.RenamedFieldCount bunun ne kadar sık gerçekleştiğini söyler

Options := TPdfAcroFormGraftOptions.Default;
Options.CollisionPolicy := pagcpRename;
Options.RenamePrefix := 'Y2025_';
Options.MaxObjects := 20000;
Options.MaxDepth := 64;

if GraftPdfAcroForm(Source, Destination, Output, Mappings,
  Options, nil, Report) then
begin
  WriteLn('source fields  : ', Report.SourceFieldCount);
  WriteLn('existing fields: ', Report.DestinationFieldCount);
  WriteLn('grafted fields : ', Report.GraftedFieldCount);
  WriteLn('renamed fields : ', Report.RenamedFieldCount);
  WriteLn('cloned objects : ', Report.GraftedObjectCount);
  WriteLn('reused objects : ', Report.ReusedObjectCount);
  WriteLn('mapped pages   : ', Report.MappedPageCount);
  WriteLn('output bytes   : ', Report.OutputByteCount);
end
else
  WriteLn('graft refused  : ', Report.ErrorMessage);

Yeniden adlandırma bedelsiz değildir ve bir hatayı yok etmek için ona uzanmak yerine bilinçli olarak karar vermelisiniz. Yeniden adlandırılan alan farklı bir alandır: hedefteki ona adla adres veren herhangi bir JavaScript, bir insanın eski ada karşı yazdığı /CO içindeki her hesaplama girdisi ve alan adına göre anahtarlanan her aşağı akım tüketicisi önekten haberdar olmalıdır. İki belge gerçekten aynı alanı tanımlıyorsa, dürüst onarım genellikle adları aşılama anında değil yukarı akışta mutabık kılmaktır. Aşılama gerçekleştikten sonra, gerçekte ne elde ettiğinizi doğrulamak için birleştirilmiş formu yürümek doğal sonraki adımdır ve PDFiumPas ile form alanı gezinimi bu gezinmeyi kapsar

Aşılamanın bilinçli olarak kapalı başarısız olduğu yerler

Her belirsiz koşul bir hatadır, asla elden gelen en iyi çaba sonucu değildir ve bu, üretimde sizi şaşırtmadan önce anlaşılmaya değer bir tasarım kararıdır. GraftPdfAcroForm şu durumlardan herhangi biriyle karşılaştığında False döndürür, çıktı akışını sıfırlar ve nedeni bildirir

  • Kaynak form bir /XFA girdisi taşıyorsa — XFA paketleri paralel bir form modelidir ve AcroForm alan sözlüklerine indirgenemez
  • Bir widget, sayfa eşlemesinde girdisi olmayan bir kaynak sayfada yaşıyorsa; aksi takdirde alan sessizce düşer ya da yanlış sayfaya eklenirdi
  • Sayfa eşlemeleri aralık dışıysa ya da iki eşleme aynı kaynak ya da hedef sayfayı yeniden kullanıyorsa
  • Her iki form da bir varsayılan kaynak sözlüğü /DR tanımlıyorsa; iki kaynak ad alanını birleştirmek var olan bir adı farklı bir yazı tipine yeniden yönlendirme riski taşır
  • Nesne grafiği MaxObjects sınırını ya da özyineleme MaxDepth sınırını aşarsa
  • Hedef bir imza içeriyorsa ve AllowSignedDestination False ise
  • Sağlanan aşılama haritası farklı bir kaynak belgeye aitse ya da bir kaynak başvuru sarkıyorsa

Yazma yolu aynı ölçüde tutucudur. PDFiumPas sonucu hedefe eklenen seyrek bir artımlı düzeltme olarak yayar, sonra yazılan çıktıyı yeniden maddeselleştirir ve formunu yeniden okur: sonucun alan sayısı hedefin özgün alan sayısı artı kaynağınkine eşit değilse, tüm aşılama reddedilir ve çıktı temizlenir. Kısmen aşılanmış bir dosya asla elde etmezsiniz. Bu politikanın maliyeti gerçektir — bir /DR çakışması ya da imzalı bir hedef sizi doğrudan durdurur ve birleştirilmiş bir yaklaşımı kabul etmek yerine bunu kendiniz çözmeniz gerekir — ama alternatifi, sorunsuz açılan ama yanlış hesaplayan bir formdur

PDFiumPas GraftPdfAcroForm işlevinin Delphi içinde nasıl kapalı başarısız olduğu: yazılan düzeltme yeniden okunur ve alan sayısı doğrulanır, XFA ya da eşlenmemiş sayfa gibi her belirsiz koşul çağrıyı reddeder ve bir reddetme yalnızca o çağrının eklediği harita girdilerini atar
Doğrulanan yazma yolu ve işlemsel harita, reddedilen bir aşılamanın asla kısmen birleştirilmiş bir dosya bırakmamasının nedenidir

Aşılamanın yanlış araç olduğu durum

Aşılama yapıyı taşır; bu yüzden eksikliğini çektiğiniz şey yapı olduğunda kullanın. Her iki belge de zaten aynı alan kümesini taşıyorsa ve yalnızca değerleri ve açıklamaları aralarında taşımanız gerekiyorsa, XFDF form verisi makalesindeki dışa ve içe aktarım yolu daha hafif, standart ve tersine çevrilebilirdir. Hedefte hiç alan yoksa ya da farklı bir küme varsa ve widgetların, görünüm akışlarının, eylemlerin ve hesaplama sırasının bozulmadan karşıya geçmesini gerektiriyorsanız GraftPdfAcroForm işlevine uzanın. Kimlik konusunda son bir pratik not: aşılama haritası nesne numarası artı nesille anahtarlandığı ve kaynak baytlarının bir SHA-256 karmasına bağlandığı için, kaynağı çalıştırmalar arasında yeniden kaydetmek ya da iyileştirmek farklı bir kimlik ve artık uygulanamayan bir harita üretir. Aşıladığınız kaynağın anlık görüntüsünü alın ve toplu iş boyunca sabit tutun; onu bir girdi yapıtı olarak ele alın, gece işinin özgürce yeniden yazabileceği bir şey olarak değil

GraftPdfAcroForm, TPdfCrossDocumentGraftMap ve onu çevreleyen akış düzeyi PDF araç takımı, Delphi, C++Builder ve Lazarus için PDFiumPas Delphi PDFium Component ürünüyle birlikte sunulur; ürün sayfası aşılama seçeneklerinin, rapor alanlarının ve belge düzenleme yüzeyinin geri kalanının tam API başvurusunu taşır