Có người vẽ một hộp đen đè lên một cái tên, không flatten gì cả, đóng gói tệp, và người rà soát chọn hình chữ nhật rồi dán cái tên vào một email. PDFiumPas trả lời điều đó bằng redaction cấp toán tử: SaveAsRedacted chỉ xóa các Unicode scalar mà hộp ký tự của chúng chạm vào một hình chữ nhật xóa, dựng lại những người sống sót từ font, cỡ, ma trận, chế độ render và màu gốc, và cắt các path thẳng trục cùng ảnh thay vì vứt bỏ cả khối
Vì sao một hình chữ nhật vẽ lên không phải redaction
Một thao tác vẽ thêm lên trên một content stream không che giấu gì, vì các toán tử hiển thị văn bản bên dưới nó vẫn nằm trong stream và vẫn ánh xạ tới các code point. ISO 32000-1 §9.4 định nghĩa một đối tượng văn bản là một dãy các toán tử định vị và hiển thị nằm giữa BT và ET; một hình chữ nhật tô đặc vẽ sau đó đơn giản là một toán tử khác trong cùng stream. Trích xuất đi qua các toán tử, không đi qua pixel, nên chuỗi bị đè trở về nguyên vẹn. Redaction thật phải gỡ bỏ toán hạng, không phải che mờ đầu ra
Cách hiện thực an toàn hiển nhiên là tàn bạo: tìm mọi đối tượng trang có hộp bao cắt một hình chữ nhật xóa và xóa nguyên đối tượng. Đó là điều các bản PDFiumPas trước đây làm, đúng nhưng đắt. Một Tj đơn lẻ có thể mang cả một hàng bảng, nên che một số tài khoản kéo theo ngày, mô tả và số tiền. Một hình chữ nhật tô tình cờ là một dải bảng tràn cả chiều rộng biến mất trên cả trang. Logo hóa đơn biến mất vì redaction cắt một góc của nó. Phiên bản 3.101.0 dời quyết định xuống một tầng, từ đối tượng trang xuống toán hạng
Redaction cấp toán tử thực sự xóa gì?
PDFiumPas xóa các Unicode scalar, không phải các đối tượng văn bản. Trong lúc SaveAsRedacted chạy, component dựng một ánh xạ ký tự-đối-tượng-trang từ trang văn bản đã nạp, rồi với mọi ký tự thuộc đối tượng đang xét, nó đọc hộp ký tự và giao hộp đó với từng hình chữ nhật xóa. Các ký tự chạm một hình chữ nhật được đánh dấu để gỡ; phần còn lại được đánh dấu là người sống sót. Nếu không có gì giao nhau, đối tượng được để hoàn toàn yên. Nếu mọi ký tự đều giao nhau, đối tượng bị gỡ cả khối, y như trước đây. Chỉ trường hợp trộn lẫn mới kích hoạt một phép tách
Sau đó mỗi người sống sót được phát lại thành một đối tượng văn bản riêng, dựng từ font handle gốc, cỡ font gốc, ma trận văn bản theo từng ký tự, chế độ render văn bản gốc, và trạng thái tô lẫn nét của đối tượng cha gồm cả stroke width, line join, line cap và dash array. Dùng lại font handle thay vì phân giải một cái mới là điều giữ các glyph giống hệt về số đo, và dùng lại ma trận từng ký tự là điều giữ kerning và khoảng cách từ mà không cần chạy layout lại. Cái giá là số đối tượng: một ký tự giữ lại trở thành một đối tượng văn bản, vì thế TPdfRedactionOptions.MaxSplitObjects tồn tại như một trần cứng cho các mảnh được sinh ra
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // tệp đã mang sẵn các annotation /Redact
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // tách cấp toán tử (mặc định)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // trần cho các mảnh được sinh ra
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, đừng đóng gói
finally
Pdf.Free;
end;
end;
Hình chữ nhật thì cắt, hình học xoay thì không
Path chỉ được tách khi PDFiumPas chứng minh được path là một hình chữ nhật thẳng trục. Phép chứng minh được cố ý hẹp: ma trận đối tượng phải có cả hai hệ số shear dưới 0.0001, path phải gồm bốn đến sáu segment bắt đầu bằng MOVETO và tiếp tục chỉ với LINETO, và các điểm đã biến đổi phải rơi đúng bốn góc của hình bao đối tượng trong dung sai 0.01. Một path qua được phép kiểm tra đó bị thu nhỏ bằng phép trừ hình chữ nhật liên tiếp, mỗi hình chữ nhật xóa tách tập người sống sót thành các dải trái, phải, dưới và trên, và mọi dải kết quả được dựng lại với chế độ tô, cờ nét và trạng thái vẽ gốc. Đường cong, tam giác, hình bị clip và bất cứ thứ gì xoay đều trượt phép kiểm tra và cả đối tượng bị gỡ
Ảnh đi theo ISO 32000-1 §8.9, nơi các mẫu ảnh chiếm hình vuông đơn vị được ánh xạ qua ma trận biến đổi hiện hành. PDFiumPas đảo phép ánh xạ đó để biến từng mảnh không gian trang còn sống trở lại thành tọa độ ảnh đã chuẩn hóa, kẹp chúng vào khoảng đơn vị, rồi chuyển thành chỉ số pixel bằng cách làm tròn vào trong: các mép trái và trên đi qua Ceil, phải và dưới qua Floor. Chiều đó có ý nghĩa. Làm tròn ra ngoài sẽ cho phép một cột pixel nguồn một phần từ phía bị xóa sống sót tại mép mảnh. Các biên pixel nguyên sau đó được chuyển ngược thành tọa độ chuẩn hóa và dùng để suy ra ma trận mảnh, nên bitmap đã cắt đáp đúng trên biên pixel nơi nó bị cắt. Phép cắt là một bản sao hàng theo stride trên các định dạng Gray, BGR, BGRx và BGRA. Như với path, một ảnh xoay hay xiên, hay một ảnh có ma trận mang hệ số tỉ lệ suy biến, bị gỡ toàn bộ
// Sau một lời gọi SaveAsRedacted thành công
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
// không tách được gì: mọi đối tượng giao nhau bị vứt cả khối
LogWholeObjectFallback(SourcePdf);
Vì sao PDFiumPas fail closed với ký tự chưa ánh xạ?
Vì một glyph không có Unicode scalar tái lập được không thể được dựng lại một cách trung thực. Dựng lại một người sống sót nghĩa là gọi API đặt văn bản với một chuỗi, và điều đó đòi hỏi một code point ổn định cho mọi ký tự được giữ. Các font subset biểu tượng với dữ liệu ToUnicode hỏng hay thiếu có thể trả về một ánh xạ rỗng, và mã hóa lại bằng phỏng đoán sẽ tạo ra đầu ra trông đúng trên màn hình trong khi mang một ký tự khác bên dưới. PDFiumPas từ chối: phép kiểm tra ký tự giữ lại raise, ngoại lệ được bắt bên trong SaveAsRedacted, TPdfRedactionReport.Succeeded trở về False với thông điệp trong ErrorMessage, và hàm trả False. Cùng quy tắc áp cho ngân sách tách, thứ raise thay vì âm thầm cắt bớt tập mảnh. Khi một tài liệu có những font bạn không tin và bạn muốn hành vi cũ tất định, đặt Options.PreservePartialObjects := False và mọi đối tượng giao nhau biến mất cả khối
Cắt tỉa resource qua các phạm vi dùng chung
Việc tách đối tượng để lại các mồ côi, và tỉa chúng không đơn giản là diff từ điển /Resources cấp trang. ISO 32000-1 §7.8.3 cho phép cùng một từ điển resource được tham chiếu đồng thời bởi nhiều trang, bởi Form XObject, bởi pattern, và bởi appearance stream của annotation. Xóa một tên font vì một trang ngừng dùng nó sẽ làm gãy trang khác vẫn dùng. PruneUnusedPdfResources vì thế làm việc theo phạm vi: nó phân giải /Contents dù đó là mảng trực tiếp, một tham chiếu indirect tới mảng, hay một stream đơn, rồi thu thập việc dùng resource từ các toán tử thực sự gọi tên resource — Tf cho font, Do cho XObject, gs cho graphics state, CS, cs, SCN và scn cho không gian màu và pattern, sh cho shading, BDC và DP cho thuộc tính marked-content, cộng với mục /CS của ảnh inline. Khi một từ điển được vài phạm vi dùng chung, các tập tên đã dùng được hợp nhất theo từng hạng mục trước khi thứ gì bị gỡ
Chỉ những tên được xác nhận không được tham chiếu qua mọi phạm vi trỏ vào từ điển mới bị bỏ. Một phạm vi không parse nổi với độ tin cậy được để nguyên — đó là chiều bảo thủ: một tệp chưa tỉa chỉ là to hơn, một tệp tỉa sai là hỏng. Các từ điển sống sót được ghi trả như một bản cập nhật incremental thưa mang đúng các số generation, và một lượt viết lại tính chạm tới sau đó quét các đối tượng trở nên không chạm được một khi các tên biến mất. TPdfResourcePruneReport báo ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, các số byte, và một cờ Succeeded. SaveAsRedacted chạy bước này tự động trên đầu ra đã khử nhiễm, nên đường redaction đã gồm sẵn nó, nhưng hàm được xuất cấp stream cho những pipeline muốn nó một mình
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 giữ False: một bản viết lại incremental sẽ
// làm mất hiệu lực các khoảng byte mà chữ ký phủ
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;
Cắm nó vào một pipeline tài liệu
Đường redaction không bao giờ làm biến dạng tài liệu bạn đã nạp. SaveAsRedacted chụp một snapshot biệt lập, áp các annotation /Redact trên đó, gỡ attachment, chạy lượt khử nhiễm gỡ open action, catalog action, name tree, associated file, AcroForm và metadata, tỉa resource, và chỉ sau đó mới ghi stream đầu ra. Mở lại đầu ra đó như một tài liệu độc lập và trích xuất lại văn bản là bước kiểm chứng đáng giữ trong bộ test của chính bạn, vì đó là phép kiểm tra duy nhất trả lời câu hỏi ban đầu — một reader liệu còn lấy được chuỗi hay không. Một hệ quả cần tính trước: việc tách thay thế các đối tượng trang, nên bất kỳ handle FPDF_PAGEOBJECT nào bạn đang giữ đều chết sau đó — cùng cái bẫy vòng đời được nói trong handle đối tượng trang lỗi thời sau một transform
Hai mảnh kế cận làm trọn quy trình. Quyết định các hình chữ nhật xóa đặt ở đâu thường bắt đầu từ hình học đã trích xuất, và mô hình khối văn bản có cấu trúc cùng thứ tự đọc trong structured text blocks và reading order là nguồn hộp ứng viên tốt hơn các dãy ký tự thô. Việc phục vụ kết quả cho người rà soát thuộc về các quy tắc gia cố trong dựng một bản xem trước PDF an toàn, nơi điền biểu mẫu và JavaScript mặc định tắt. Cùng nhau chúng phủ vòng lặp mà đa số quy trình tuân thủ cần: định vị, xóa cấp toán tử, kiểm chứng bằng cách mở lại, xem trước an toàn. Bề mặt API đầy đủ, bản dùng thử và điều khoản licensing của component nằm trên trang sản phẩm PDFium Delphi Component