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
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
// 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
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