Artikel Teknis

Redaksi PDF Level Operator di Delphi dengan PDFiumPas

Seseorang melukis kotak hitam di atas sebuah nama, tidak meratakan apa pun, mengirim berkasnya, dan reviewer memilih persegi itu lalu menempelkan namanya ke email. PDFiumPas menjawabnya dengan redaksi level operator: SaveAsRedacted hanya menghapus skalar Unicode yang kotak karakternya menyentuh persegi redaksi, membangun ulang yang bertahan dari font, ukuran, matriks, mode render, dan warna aslinya, serta memotong path dan gambar sejajar sumbu alih-alih membuangnya utuh

Mengapa persegi yang dilukis bukanlah redaksi

Operasi gambar yang ditambahkan di atas content stream tidak menyembunyikan apa pun, karena operator text-showing di bawahnya masih ada di stream dan masih memetakan ke code point. ISO 32000-1 §9.4 mendefinisikan objek teks sebagai rangkaian operator positioning dan showing di dalam BT dan ET; persegi terisi yang digambar setelahnya hanyalah operator lain di stream yang sama. Ekstraksi menelusuri operator, bukan piksel, sehingga string yang tertutup kembali utuh. Redaksi yang sesungguhnya harus menghapus operannya, bukan mengaburkan outputnya

Implementasi aman yang paling jelas itu brutal: temukan setiap objek halaman yang bounding box-nya beririsan dengan persegi redaksi dan hapus seluruh objeknya. Persis itulah yang dilakukan rilis PDFiumPas sebelumnya, dan itu benar tetapi mahal. Satu Tj bisa membawa satu baris tabel penuh, sehingga menutup satu nomor rekening ikut menghapus tanggal, deskripsi, dan jumlahnya. Fill persegi yang kebetulan berupa pita tabel selebar halaman lenyap di seluruh halaman. Logo invoice menghilang karena redaksi memotong satu sudutnya. Versi 3.101.0 menurunkan keputusan satu level, dari objek halaman ke operan

Apa yang benar-benar dihapus redaksi level operator?

PDFiumPas menghapus skalar Unicode, bukan objek teks. Selama SaveAsRedacted, komponen membangun pemetaan karakter-ke-objek-halaman dari text page yang dimuat, lalu untuk setiap karakter milik objek yang diuji ia membaca kotak karakternya dan mengiris kotak itu terhadap setiap persegi redaksi. Karakter yang menyentuh sebuah persegi ditandai untuk dihapus; sisanya ditandai sebagai yang bertahan. Jika tidak ada yang beririsan, objek dibiarkan sepenuhnya sendiri. Jika setiap karakter beririsan, objek dibuang utuh, persis seperti sebelumnya. Hanya kasus campuran yang memicu pemecahan

Redaksi level operator PDFiumPas dibandingkan penghapusan seluruh objek di Delphi: jalur lama membuang satu objek teks utuh saat satu nomor rekening tertutup, sedangkan jalur pemecahan hanya menghapus karakter yang beririsan dan menulis ulang setiap yang bertahan sebagai objek teksnya sendiri
Hanya kasus campuran yang memicu pemecahan: tidak ada yang beririsan membiarkan objek sendirian, semuanya beririsan membuangnya utuh

Setiap yang bertahan kemudian ditulis ulang sebagai objek teksnya sendiri yang dibangun dari handle font asli, ukuran font asli, matriks teks per karakter, mode render teks asli, dan state fill serta stroke milik objek parent termasuk stroke width, line join, line cap, dan dash array. Memakai ulang handle font alih-alih me-resolve yang baru itulah yang menjaga glyph identik secara metrik, dan memakai ulang matriks per karakter itulah yang menjaga kerning dan spasi kata tetap di tempat tanpa menjalankan layout ulang. Biayanya jumlah objek: satu karakter yang dipertahankan menjadi satu objek teks, dan itulah alasan TPdfRedactionOptions.MaxSplitObjects ada sebagai plafon keras atas fragmen yang dihasilkan

procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
  Pdf: TPdf;
  Options: TPdfRedactionOptions;
  Report: TPdfRedactionReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourcePdf;   // berkas sudah membawa annotasi /Redact
    Pdf.Active := True;

    Options := TPdfRedactionOptions.Default;
    Options.PreservePartialObjects := True;    // pemecahan level operator (nilai default)
    Options.RemoveIntersectingAnnotations := True;
    Options.MaxSplitObjects := 20000;          // plafon fragmen yang dihasilkan

    if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
      raise Exception.Create(Report.ErrorMessage);   // gagal aman, jangan dikirim
  finally
    Pdf.Free;
  end;
end;

Persegi memotong, geometri rotasi tidak

Path hanya dipecah saat PDFiumPas bisa membuktikan path itu persegi sejajar sumbu. Buktinya sengaja dibuat sempit: matriks objek harus punya kedua suku shear di bawah 0.0001, path harus terdiri dari empat sampai enam segmen yang diawali MOVETO dan dilanjutkan LINETO saja, dan titik-titik tertransformasi harus mendarat di keempat sudut bounds objek dalam toleransi 0.01. Path yang lolos pemeriksaan itu direduksi lewat pengurangan persegi berturut-turut, setiap persegi redaksi memahat himpunan yang bertahan menjadi pita kiri, kanan, bawah, dan atas, dan setiap pita hasil diciptakan ulang dengan fill mode asli, flag stroke, dan paint state asli. Kurva, segitiga, bentuk terpotong, dan apa pun yang dirotasi gagal pemeriksaan dan seluruh objeknya dibuang

Gambar mengikuti ISO 32000-1 §8.9, di mana sampel gambar menempati persegi satuan yang dipetakan lewat current transformation matrix. PDFiumPas membalik pemetaan itu untuk mengubah setiap fragmen halaman yang bertahan kembali ke koordinat gambar ternormalisasi, men-clamp-nya ke interval satuan, lalu mengonversi ke indeks piksel dengan pembulatan ke dalam: tepi kiri dan atas melalui Ceil, kanan dan bawah melalui Floor. Arah itu penting. Pembulatan keluar akan membiarkan satu kolom parsial piksel sumber dari sisi yang direduksi bertahan di tepi fragmen. Bounds piksel integer lalu dikonversi balik ke koordinat ternormalisasi dan dipakai menurunkan matriks fragmen, sehingga bitmap hasil potong mendarat persis di batas piksel tempat ia dipotong. Potongan itu sendiri adalah penyalinan baris yang sadar stride lintas format Gray, BGR, BGRx, dan BGRA. Seperti path, gambar yang dirotasi atau miring, atau yang matriksnya punya suku skala degenerate, dibuang utuh

Cara PDFiumPas memotong gambar PDF yang terredaksi sebagian di Delphi: fragmen halaman yang bertahan dipetakan balik lewat CTM terbalik ke koordinat gambar ternormalisasi, di-clamp ke interval satuan, dan dibulatkan ke dalam sehingga tidak ada kolom piksel terredaksi yang bertahan
Ceil di kiri dan atas, Floor di kanan dan bawah, sehingga potongan mendarat di batas piksel utuh
// Setelah pemanggilan SaveAsRedacted yang berhasil
Writeln(Format('applied %d redaction(s) on %d page(s)',
  [Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
  [Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
  [Report.SplitTextObjectCount, Report.SplitPathObjectCount,
   Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
  [Report.ResourcePruneReport.RemovedNameCount,
   Report.ResourcePruneReport.RemovedObjectCount]));

if Report.PreservedFragmentCount = 0 then
  // tidak ada yang bisa dipecah: setiap objek beririsan dibuang utuh
  LogWholeObjectFallback(SourcePdf);

Mengapa PDFiumPas gagal aman pada karakter tak terpetakan?

Karena glyph tanpa skalar Unicode yang bisa direproduksi tidak bisa dibangun ulang secara jujur. Merekonstruksi yang bertahan berarti memanggil API text-setting dengan sebuah string, dan itu menuntut code point stabil untuk setiap karakter yang dipertahankan. Font subset simbolik dengan data ToUnicode rusak atau absen bisa menghasilkan pemetaan kosong, dan mengenkode ulang dengan menebak akan menghasilkan output yang tampak benar di layar sambil membawa karakter berbeda di baliknya. PDFiumPas menolak: pemeriksaan karakter-bertahan melempar, exception ditangkap di dalam SaveAsRedacted, TPdfRedactionReport.Succeeded kembali False dengan pesan di ErrorMessage, dan fungsi mengembalikan False. Aturan yang sama berlaku untuk anggaran pemecahan, yang melempar alih-alih diam-diam memotong himpunan fragmen. Saat sebuah dokumen punya font yang tak Anda percaya dan Anda ingin perilaku lama yang deterministik, setel Options.PreservePartialObjects := False dan setiap objek beririsan pergi utuh

Pemangkasan resource lintas scope bersama

Memecah objek meninggalkan yatim di belakangnya, dan memangkasnya tidak sesederhana membedakan kamus /Resources level halaman. ISO 32000-1 §7.8.3 mengizinkan kamus resource yang sama direferensikan oleh beberapa halaman, Form XObject, pattern, dan appearance stream annotasi sekaligus. Menghapus nama font karena satu halaman berhenti memakainya akan merusak halaman lain yang masih memakainya. PruneUnusedPdfResources karena itu bekerja per scope: ia me-resolve /Contents apakah itu array langsung, referensi indirect ke array, atau stream tunggal, lalu mengumpulkan pemakaian resource dari operator yang benar-benar menyebut resource — Tf untuk font, Do untuk XObject, gs untuk graphics state, CS, cs, SCN, dan scn untuk ruang warna serta pattern, sh untuk shading, BDC dan DP untuk properti marked-content, ditambah entri /CS gambar inline. Saat satu kamus dibagi beberapa scope, himpunan nama-terpakai di-union per kategori sebelum ada yang dihapus

Pemangkasan resource di PDFiumPas: tiga scope mereferensikan satu kamus resource bersama, himpunan nama-terpakai mereka di-union per kategori, dan hanya nama yang tak direferensikan scope mana pun yang dihapus sebelum objek tak terjangkau disapu
Satu kamus bisa melayani beberapa halaman, form XObject, dan appearance stream, sehingga PDFiumPas meng-union setiap himpunan nama-terpakai sebelum menjatuhkan satu nama pun

Hanya nama yang terkonfirmasi tak direferensikan oleh setiap scope yang menunjuk ke kamus itu yang dijatuhkan. Scope yang tidak bisa diparse dengan yakin dibiarkan utuh, dan itu arah konservatifnya: berkas yang tak dipangkas hanya lebih besar, berkas yang salah dipangkas korup. Kamus yang bertahan ditulis balik sebagai pembaruan inkremental sparse yang membawa nomor generasi persisnya, lalu rewrite reachability menyapu objek yang menjadi tak terjangkau setelah nama-nama itu hilang. TPdfResourcePruneReport melaporkan ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, jumlah byte, dan flag Succeeded. SaveAsRedacted menjalankan langkah ini otomatis pada output yang sudah disanitasi, sehingga jalur redaksi sudah menyertakannya, tetapi fungsi ini diekspor di level stream untuk pipeline yang ingin memakainya sendiri

uses
  FPdfCompress;

procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
  Source, Dest: TFileStream;
  Report: TPdfResourcePruneReport;
begin
  Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
  try
    Dest := TFileStream.Create(TargetPdf, fmCreate);
    try
      // AllowSignedDocument tetap False: penulisan ulang inkremental akan
      // membatalkan rentang byte yang dicakup signature
      PruneUnusedPdfResources(Source, Dest, Report);
      if not Report.Succeeded then
        raise Exception.Create(Report.ErrorMessage);
      Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
        [Report.RemovedNameCount, Report.UpdatedScopeCount,
         Report.SourceByteCount, Report.OutputByteCount]));
    finally
      Dest.Free;
    end;
  finally
    Source.Free;
  end;
end;

Menyambungkannya ke pipeline dokumen

Jalur redaksi tidak pernah memutasi dokumen yang Anda muat. SaveAsRedacted mengambil snapshot terisolasi, menerapkan annotasi /Redact di sana, membuang attachment, menjalankan pass sanitasi yang menghapus open action, catalog action, name tree, associated file, AcroForm, dan metadata, memangkas resource, dan baru kemudian menulis stream output. Membuka ulang output itu sebagai dokumen independen dan mengekstrak ulang teksnya adalah langkah verifikasi yang layak dipertahankan di test suite Anda sendiri, karena itulah satu-satunya pemeriksaan yang menjawab pertanyaan awalnya — mungkinkah pembaca masih mendapatkan stringnya. Satu konsekuensi yang perlu diantisipasi: pemecahan mengganti objek halaman, sehingga handle FPDF_PAGEOBJECT apa pun yang Anda pegang mati setelahnya, jebakan lifetime yang sama seperti dijelaskan di handle objek halaman yang basi setelah transformasi

Dua bagian tetangga melengkapi alur kerjanya. Memutuskan di mana persegi redaksi diletakkan lazimnya berangkat dari geometri hasil ekstraksi, dan model blok serta reading order di blok teks terstruktur dan reading order adalah sumber kotak kandidat yang lebih baik daripada deretan karakter mentah. Menyajikan hasilnya ke reviewer termasuk dalam aturan hardening di membangun pratinjau PDF yang aman, tempat pengisian formulir dan JavaScript tetap mati secara default. Bersama-sama mereka mencakup loop yang paling dibutuhkan alur kerja compliance: menemukan, meredaksi di level operator, memverifikasi dengan membuka ulang, mempratinjau dengan aman. Permukaan API lengkap, unduhan trial, dan ketentuan lisensi komponen ada di halaman produk PDFium Delphi Component