Artikel Teknis

Cangkok Field AcroForm Antar PDF di Delphi dengan PDFiumPas

Memindahkan sekumpulan field formulir dari templat tahun lalu ke tata letak tahun ini adalah titik di mana bolak-balik FDF dan XFDF berhenti memadai: nilainya tiba, tetapi appearance stream, aksi kalkulasi, dan resource default tidak. PDFiumPas menjawab kasus itu dengan GraftPdfAcroForm, yang mengkloning seluruh graf objek field dari satu PDF dan menuliskannya ke PDF lain

Alasan ekspor level data tidak bisa melakukan ini bersifat struktural. Sebuah field bukanlah satu rekaman, ia adalah subgraf. ISO 32000-1 §12.7 mendefinisikan kamus formulir interaktif yang menampung /Fields, /CO, /DR, dan /DA, §12.7.3 mendefinisikan kamus field yang menggantung di bawahnya, dan §12.5.6.19 mendefinisikan annotasi widget yang memberi field tersebut kotak yang terlihat di halaman. XFDF hanya membawa daun-daun struktur itu. Mencangkok membawa strukturnya sendiri

Mengapa menyalin array /Fields tak pernah cukup

Menyalin /Fields dari satu dokumen ke dokumen lain menghasilkan formulir yang rusak dengan cara-cara paling menarik sekaligus, karena array itu hanya berisi referensi indirect dan tidak ada lagi. ISO 32000-1 §7.3.10 membuat sebuah objek dapat dialamatkan lewat nomor objek ditambah generasi, dan nomor-nomor itu hanya bermakna di dalam berkas asalnya. Tempelkan array itu ke berkas lain, maka setiap referensi di dalamnya entah menggantung, atau lebih buruk, diam-diam menunjuk ke objek tak terkait yang kebetulan menempati slot tersebut di dokumen tujuan. Di bawah setiap referensi terdapat graf yang sekaligus bersama dan bersiklus. Kamus field menunjuk ke anak-anaknya, setiap anak menunjuk balik ke /Parent-nya, sebuah widget menunjuk ke appearance stream-nya dan ke halaman yang membawanya lewat /P, appearance stream menunjuk ke font di kamus resource default formulir, dan kamus additional-action di bawah /AA menunjuk ke objek lain lagi. Dua widget di halaman berbeda lazimnya berbagi satu font dan satu appearance XObject. Karena itu, graft yang benar harus menelusuri graf itu, mengkloning setiap objek yang terjangkau tepat satu kali, mengarahkan ulang /P milik setiap widget ke halaman tujuan hasil pemetaan, dan menambahkan widget hasil kloning ke array /Annots halaman itu — jika tidak, field itu ada di dalam formulir tetapi tak terlihat di halaman. Jika Anda pernah mengejar perbedaan antara field, widget-nya, dan annotasi halaman yang menampilkannya, catatan kami tentang indeks widget versus indeks annotasi membahas persis pembagian itu

Graf objek di balik satu field formulir PDF saat PDFiumPas mencangkoknya di Delphi: kamus formulir, field, annotasi widget, array annotasi halaman tujuan, serta appearance stream dan font yang dibagi kedua widget, ditambah referensi balik /Parent yang menutup siklus
Sebuah field adalah subgraf bersama yang bersiklus, itulah mengapa menyalin array /Fields antar dokumen membiarkan setiap referensi menggantung

Apa saja yang dibutuhkan GraftPdfAcroForm dari Anda?

Ia membutuhkan tiga stream yang terpisah dan satu pemetaan halaman yang eksplisit. GraftPdfAcroForm menerima Source, Destination, dan Output sebagai instance TStream terpisah, sebuah array TPdfGraftPageMappings, record TPdfAcroFormGraftOptions, sebuah TPdfCrossDocumentGraftMap opsional, dan out parameter TPdfAcroFormGraftReport. Ia mengembalikan Boolean alih-alih melempar exception, dan saat gagal report membawa alasannya di ErrorMessage. Pemetaan halaman berbasis satu pada kedua sisi dan tidak disimpulkan: setiap halaman sumber yang membawa widget yang hendak Anda cangkok harus muncul di dalamnya. Memberi nil untuk peta graft adalah hal yang sah — fungsi lalu membuat dan membebaskan satu peta privat selama pemanggilan berlangsung — dan TPdfAcroFormGraftOptions.Default memberi Anda CollisionPolicy berisi pagcpReject, RenamePrefix berisi Imported_, MaxObjects 100000, MaxDepth 128, dan AllowSignedDestination bernilai False. Tiga nilai terakhir itu adalah anggaran, dan anggaran itu ada karena graf objek yang hendak Anda telusuri berasal dari berkas yang bukan tulisan Anda

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;

Bagaimana peta graft menghindari kloning font bersama dua kali?

TPdfCrossDocumentGraftMap menampung tabel referensi sumber-ke-tujuan yang kuncinya membawa nomor objek sekaligus generasi, dan cloner rekursif memeriksanya sebelum turun ke anak. Urutan operasinya itulah yang membuat siklus aman: cloner mengalokasikan nomor objek tujuan dan mendaftarkan pemetaan lebih dulu, baru kemudian menelusuri referensi anak dari objek sumber. Sebuah parent yang menjangkau anak yang menunjuk balik ke parent-nya akan menemukan parent itu sudah terdaftar dan mengembalikan referensi tujuan yang sudah ada, bukan berekursi lagi. Pemeriksaan yang sama itulah yang membuat sebuah font, appearance stream, atau aksi yang dipakai enam widget dikloning satu kali dan direferensikan enam kali. Peta terikat ke dokumen sumber lewat hash SHA-256 dari byte sumbernya, diekspos sebagai SourceIdentity. Jika Anda memberikan GraftPdfAcroForm sebuah peta yang identitasnya tidak cocok dengan sumber yang Anda berikan, ia menolak pemanggilan itu alih-alih memakai ulang referensi yang tidak pernah valid untuk berkas ini. Pemetaan halaman disemai ke peta yang sama sebelum kloning dimulai, dan justru itulah cara /P milik sebuah widget berakhir menunjuk ke halaman tujuan: objek halaman sumber sudah ter-resolve ke objek halaman tujuan hasil pemetaan, sehingga proses penulisan ulang referensi biasa menanganinya tanpa kasus khusus

Peta graft lintas dokumen PDFiumPas di Delphi mengunci setiap referensi sumber dengan nomor objek dan generasi, mendaftarkan pemetaan tujuan sebelum turun sehingga referensi balik parent berhenti, dan mengembalikan entri yang sudah ada sehingga font bersama hanya dikloning satu kali
Mendaftarkan pemetaan sebelum menelusuri anak-anak itulah yang membuat graf bersiklus aman dan objek bersama terkloning tepat satu kali
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
      // Entri yang ditambahkan pemanggilan ini sudah di-rollback;
      // apa pun yang terdaftar sebelumnya tetap utuh.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Rollback itulah inti dari memiliki peta sendiri. PDFiumPas memperlakukan peta yang diberikan pemanggil secara transaksional: graft yang gagal membuang entri yang ditambahkan pemanggilan itu dan mempertahankan setiap pemetaan yang sudah ada sebelumnya, sehingga satu penolakan tidak pernah meninggalkan cache referensi ke objek yang tidak pernah ditulis. Namun pegang satu peta per dokumen tujuan — sisi tujuan dari setiap entri adalah nomor objek di berkas tertentu itu, dan tidak bermakna apa-apa di berkas lain

Tabrakan nama field: tolak atau ubah nama

Nama field yang terkualifikasi penuh harus tetap unik di dalam satu formulir, dan PDFiumPas tidak akan menebak maksud Anda saat keduanya berbenturan. TPdfAcroFormCollisionPolicy menawarkan tepat dua jawaban. Di bawah pagcpReject, nilai default, field sumber pertama yang judulnya sudah ada di tujuan membatalkan seluruh graft dengan error dan membiarkan stream output kosong. Di bawah pagcpRename, field sumber yang berbenturan diganti nama dengan prefiks RenamePrefix dan graft berlanjut, dengan Report.RenamedFieldCount memberi tahu Anda seberapa sering hal itu terjadi

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);

Mengganti nama bukan tanpa biaya, dan keputusan itu sebaiknya diambil dengan sengaja, bukan sekadar demi membuat sebuah error menghilang. Field yang diganti nama adalah field yang berbeda: JavaScript apa pun di tujuan yang menyebutnya berdasarkan nama, entri kalkulasi di /CO yang ditulis manusia berdasarkan nama lama, dan konsumen hilir apa pun yang mengunci pada nama field semuanya perlu tahu soal prefiks itu. Jika kedua dokumen memang mendeskripsikan field yang sama, perbaikan yang jujur biasanya direkonsiliasi di hulu, bukan saat mencangkok. Setelah graft mendarat, menelusuri formulir gabungan untuk memastikan apa yang benar-benar Anda dapatkan adalah langkah berikutnya yang wajar, dan navigasi field formulir di PDFiumPas membahas penelusuran itu

Di mana graft sengaja berperilaku fail closed

Setiap kondisi ambigu adalah error, bukan hasil sebisanya, dan itu keputusan desain yang layak dipahami sebelum mengejutkan Anda di production. GraftPdfAcroForm mengembalikan False, mengosongkan ulang stream output, dan melaporkan alasannya saat mengenai salah satu kondisi berikut

  • Formulir sumber membawa entri /XFA — paket XFA adalah model formulir paralel dan tidak bisa direduksi menjadi kamus field AcroForm
  • Ada widget yang berada di halaman sumber tanpa entri di pemetaan halaman, yang tanpa itu akan diam-diam membuang field tersebut atau menempelkannya ke halaman yang salah
  • Pemetaan halaman berada di luar rentang, atau dua pemetaan memakai halaman sumber atau halaman tujuan yang sama
  • Kedua formulir mendefinisikan kamus resource default /DR, karena menggabungkan dua ruang nama resource berisiko mengarahkan ulang sebuah nama yang sudah ada ke font yang berbeda
  • Graf objek melebihi MaxObjects atau rekursi melebihi MaxDepth
  • Tujuan mengandung signature dan AllowSignedDestination bernilai False
  • Peta graft yang diberikan milik dokumen sumber yang berbeda, atau sebuah referensi sumber menggantung

Jalur penulisannya sama konservatifnya. PDFiumPas menulis hasilnya sebagai revisi inkremental sparse yang ditambahkan ke tujuan, lalu me-materialisasi ulang output yang ditulis dan membaca ulang formulirnya: jika jumlah field pada hasil tidak sama dengan jumlah field asli tujuan ditambah milik sumber, seluruh graft ditolak dan output dikosongkan. Anda tidak akan pernah mendapat berkas yang tercangkok setengah jadi. Biaya kebijakan itu nyata — benturan /DR atau tujuan yang ber-signature menghentikan Anda langsung, dan Anda harus menyelesaikannya sendiri alih-alih menerima gabungan yang mendekati — tetapi alternatifnya adalah formulir yang terbuka baik-baik saja namun menghitung dengan salah

Cara GraftPdfAcroForm PDFiumPas gagal aman di Delphi: revisi yang ditulis dibaca ulang dan jumlah fieldnya diverifikasi, kondisi ambigu seperti XFA atau halaman tak terpetakan menolak pemanggilan, dan penolakan hanya membuang entri peta yang ditambahkan pemanggilan itu
Jalur tulis terverifikasi dan peta transaksional itulah alasan graft yang ditolak tidak pernah meninggalkan berkas yang tergabung setengah jadi

Kapan mencangkok bukan alat yang tepat

Grafting memindahkan struktur, jadi pakailah saat struktur itulah yang kurang. Jika kedua dokumen sudah membawa set field yang sama dan Anda hanya perlu memindahkan nilai serta annotasi antar keduanya, jalur ekspor-impor di artikel data formulir XFDF lebih ringan, standar, dan dapat dibalik. Gunakan GraftPdfAcroForm ketika tujuan tidak punya field sama sekali, atau set fieldnya berbeda, dan Anda butuh widget, appearance stream, aksi, serta urutan kalkulasi ikut berpindah utuh. Satu catatan praktis terakhir soal identitas: karena peta graft mengunci pada nomor objek ditambah generasi dan terikat ke SHA-256 dari byte sumber, menyimpan ulang atau mengoptimasi sumber antar eksekusi menghasilkan identitas berbeda dan peta yang tidak berlaku lagi. Snapshot sumber yang Anda cangkok dan jaga tetap stabil untuk satu batch; perlakukan ia sebagai artefak input, bukan sesuatu yang bebas ditulis ulang oleh job harian

GraftPdfAcroForm, TPdfCrossDocumentGraftMap, dan perangkat PDF toolkit level stream di sekitarnya disertakan dalam PDFiumPas Delphi PDFium Component untuk Delphi, C++Builder, dan Lazarus, tempat halaman produk membawa referensi API lengkap untuk opsi graft, field report, dan sisa permukaan pengeditan dokumen