Một yêu cầu rơi xuống bàn bạn: lấy một loạt sao kê đã được dựng sẵn, che đen số tài khoản, rồi xuất hai trang trên một tờ để tiết kiệm giấy. Cả hai nửa của công việc này đều là phẫu thuật content stream trên một PDF do bạn không tạo ra, nên không có canvas trang thân thiện để vẽ lên, cũng không có trình quản lý font nào để dựa vào. Bạn đang chỉnh trực tiếp object graph của một tài liệu đã nạp, bằng cách nối các toán tử vẽ thô vào một trang do công cụ khác dàn dựng. HotPDF cung cấp đúng hai điểm vào cho việc này, và cái nguy hiểm hơn lại là cái trông có vẻ vô hại
HotPDF là một component PDF VCL native cho Delphi và C++Builder. API cho tài liệu đã nạp ở round-nine đã thêm những phương thức đầu tiên có thể tạo nội dung mới trên một trang bạn mở từ đĩa thay vì trang bạn dựng từ đầu. Hai phương thức được nói tới ở đây là RedactLoadedRect, phương thức sơn một hình chữ nhật đặc lên một vùng, và StitchLoadedPage, phương thức co giãn một trang rồi vẽ nó lên trang khác. Cả hai đều hoạt động bằng cách ghi các toán tử content stream của ISO 32000-1 §8.5 vào luồng /Contents của trang. Hiểu những toán tử đó làm gì, và quan trọng không kém là chúng không làm gì, chính là ranh giới giữa một công cụ chạy được và một sự cố rò rỉ dữ liệu
Nối toán tử vào một trang đã nạp
Khi bạn dựng một trang bằng API HotPDF thông thường, component sở hữu content stream và tự tuần tự hóa các lệnh TextOut cùng các lệnh vector cho bạn. Một trang đã nạp thì khác: /Contents của nó là một stream object đã tồn tại, có thể được chia sẻ, có thể là một phần của content array, và bạn phải chèn vào đó mà không làm hỏng những gì đã có. Round nine đã giới thiệu ba helper nhỏ giúp việc này an toàn. NewIndirectStream cấp phát một THPDFStreamObject gián tiếp mới với buffer trống và mục /Length 0; ResolveLoadedStream đi theo tham chiếu gián tiếp xuống stream nền; và AppendLoadedStream ghi byte thô vào cuối stream rồi ghi lại /Length để object đã lưu vẫn hợp lệ
Mẫu mà cả hai phương thức public dùng đều giống nhau. Tìm /Contents của trang, resolve nó thành một stream, và nếu không có stream dùng được thì tạo mới rồi gắn vào. Sau đó nối các toán tử vào cuối. Vì các byte mới đi vào cuối stream, mô hình painter's model đảm bảo chúng vẽ đè lên mọi thứ mà bố cục gốc đã tạo. Thứ tự ấy chính là cơ chế đứng sau hình chữ nhật che mờ, và cũng là lý do hình chữ nhật đó không phải điều mà đa số mọi người vẫn nghĩ
RedactLoadedRect: che phủ mờ, không phải xóa
RedactLoadedRect nhận một chỉ số trang bắt đầu từ 0, bốn tọa độ user-space, và ba thành phần màu trong khoảng 0-1:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('statement.pdf') > 0 then
begin
// Cover the account-number band on page 1 with solid black.
// Coordinates are PDF user space: origin bottom-left, points.
Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
Pdf.SaveLoadedDocument('statement-covered.pdf');
end;
finally
Pdf.Free;
end;
end;
Về phía dưới, phương thức này phát ra ba toán tử vào content stream: thiết lập màu tô trong DeviceRGB (r g b rg), một đường path hình chữ nhật (x y w h re), rồi một lệnh tô (f). Chiều rộng và chiều cao được suy ra từ X2 - X1 và Y2 - Y1, nên bạn truyền hai góc đối diện và để phương thức tính kích thước. Truyền 0, 0, 0 cho màu sẽ cho một thanh đen; truyền 1, 1, 1 sẽ cho một thanh trắng khớp với trang trắng. Các tọa độ là user space riêng của trang đã nạp, nghĩa là gốc nằm ở góc dưới bên trái và đơn vị là point, đồng thời cũng có nghĩa là bạn cần /MediaBox của trang để đặt chính xác bất kỳ thứ gì; GetLoadedPageBox với pbMediaBox sẽ cho bạn điều đó
Đọc kỹ phần này hai lần: một hình chữ nhật tô kín chỉ che nội dung về mặt hiển thị, nó không xóa nội dung ấy. Văn bản, ảnh hoặc art vector bên dưới hình chữ nhật vẫn còn trong PDF, vẫn ở trong object graph, vẫn có thể trích xuất bởi bất kỳ ai sao chép trang, chạy trình trích xuất văn bản, hoặc đơn giản xóa hình chữ nhật của bạn khỏi content stream. Đây là che khuất trực quan, không phải redaction theo nghĩa pháp lý hay bảo mật. Nếu bạn đang giấu dữ liệu thật sự nhạy cảm, số tài khoản, hồ sơ y tế, danh tính, bất cứ thứ gì bị quản lý, thì chỉ phủ một ô đen rồi gửi file đi là đang mở một lỗ rò dữ liệu chờ bị phát hiện. Redaction thực sự đòi hỏi xóa object nội dung bên dưới, không phải sơn đè lên
Tên phương thức có chữ "Redact", và đó là một cảnh báo hữu ích về cách kết quả sẽ bị hiểu nhầm, chứ không phải lời hứa về thứ nó xóa. Chính phần comment của nó cũng nói rất thẳng: đây là "visual redaction primitive" và ghi chú rằng redaction xóa nội dung cần một content-stream interpreter đi qua rồi ghi lại các toán tử hiện có. Đường loaded-document của HotPDF không làm phần đó ở đây. Vì thế quy tắc an toàn rất hẹp: dùng RedactLoadedRect cho che phủ mang tính thẩm mỹ, không nhạy cảm, như giấu một watermark nháp, che một vùng trước khi chụp màn hình, hoặc phủ lên một logo cũ trên bản proof nội bộ. Ngay khi thứ ở dưới lớp phủ có thể gây hại nếu rò rỉ, phương thức này là sai công cụ, và cách đúng là dựng lại tài liệu không chứa dữ liệu đó hoặc dùng một pipeline xóa nội dung thật sự
StitchLoadedPage: co giãn, dịch, vẽ
Ghép N-up là bài toán dễ chịu hơn vì không có gì bị che, chỉ được sắp xếp lại. StitchLoadedPage nhận chỉ số trang đích, chỉ số trang nguồn, một độ lệch X/Y, và một hệ số scale, rồi vẽ trang nguồn lên trang đích ở vị trí và kích thước đó:
// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);
// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);
Chuỗi toán tử mà nó nối vào là một chuỗi transform-and-paint tiêu chuẩn: q để lưu graphics state, một ma trận cm mang scale trên đường chéo và độ lệch trong các vị trí tịnh tiến, /StitchSrc Do để gọi một external object, và Q để khôi phục trạng thái. Cặp q/Q rất quan trọng, vì nó cô lập phép biến đổi để trang được ghép không làm rò hệ tọa độ sang bất kỳ thứ gì nối tiếp sau đó. Phương thức này cũng chặn những lỗi rõ ràng, như chỉ số vượt phạm vi, đích trùng nguồn, hoặc scale không dương, trong trường hợp đó nó ép về 1.0 và thoát im lặng, nên hãy kiểm tra đầu vào của bạn vì một no-op im lặng trông y như thành công
StitchLoadedPageSideBySide là một tiện ích mỏng bọc quanh phương thức tổng quát. Nó đọc chiều rộng media box của trang đích, chia đôi, rồi gọi StitchLoadedPage với phần nửa chiều rộng đó làm độ lệch X và scale cố định 0.5, đặt trang nguồn ở nửa bên phải. Giá trị 0.5 cứng này giả định nguồn và đích có cùng chiều rộng; nếu không, trang nguồn sẽ không lấp kín nửa của nó một cách sạch sẽ, và bạn sẽ muốn dùng StitchLoadedPage tổng quát với scale bạn tự tính từ cả hai media box
Chiến lược XObject rút gọn và đánh đổi theo ISO
Đây là chỗ phần cài đặt chọn một lối tắt có chủ đích mà bạn cần biết trước khi tin vào đầu ra trên mọi trình xem. Một N-up imposition đúng chuẩn sẽ bọc nội dung trang nguồn trong một Form XObject, một object có thể vẽ độc lập mà ISO 32000-1 §8.10.1 yêu cầu phải có /Type /XObject, /Subtype /Form, và hộp cắt /BBox riêng. Bản stitch round-nine của HotPDF không xây wrapper đó. Thay vào đó, nó đăng ký trực tiếp chính page dictionary của trang nguồn dưới /Resources /XObject của trang đích với tên StitchSrc, rồi vẽ nó bằng Do. Một page dictionary và một Form XObject chia sẻ đủ mô hình nội dung, cả hai đều tham chiếu content stream và resource dictionary, nên nhiều trình đọc sẽ hiển thị kết quả
Nhưng đó không phải là một Form XObject tuân thủ chuẩn. Nó thiếu dấu /Subtype /Form và thiếu /BBox riêng, nghĩa là một bộ tiêu thụ nghiêm ngặt hoàn toàn có quyền bỏ qua Do hoặc cắt nó khác với điều bạn mong đợi. Phần TechnicalNotes cho round này nói rất rõ: cách làm này "renders under most readers" nhưng "not a strictly ISO-compliant Form XObject", và muốn tuân thủ đầy đủ thì phải tự sinh một Form XObject stream thật sự như một bước riêng. Vì vậy hãy coi đầu ra của stitch như bất kỳ cấu trúc không tuân thủ nào khác: kiểm tra nó trong đúng những trình xem khách hàng của bạn dùng, không chỉ trình trên máy bạn, và nếu bạn cần PDF để lưu trữ lâu dài hoặc sạch với trình xác thực nghiêm ngặt, đừng dựa vào đường này. Kỷ luật tương tự cũng áp dụng cho mọi thứ bạn xây trên object graph đã nạp, vì thế một bước preflight PDF trong Delphi xứng đáng có mặt trong pipeline phát hành mỗi khi bạn sửa đổi tài liệu bằng chương trình
Nó phù hợp ở đâu, và không phù hợp ở đâu
Cả hai phương thức đều là công cụ content stream, nên mental model là cùng kiểu với direct drawing. Nếu bạn đã từng dựng trang từ đầu bằng component, các toán tử vector và màu phía sau những lời gọi này sẽ trông quen từ vẽ canvas HotPDF trong Delphi; khác biệt chỉ là ở đây bạn đang nối thêm vào một stream do người khác viết ra thay vì một stream do chính bạn sở hữu. Hãy nhớ ba ranh giới sau:
- Redaction chỉ là che phủ trực quan.
RedactLoadedRectsơn đè lên nội dung và không bao giờ xóa nó. Với bất kỳ thứ gì nhạy cảm, hãy dựng lại nguồn hoặc dùng cơ chế xóa nội dung thật sự, một ô đen không phải là bảo mật - Stitch không tuân thủ chuẩn theo thiết kế. Trang nguồn được tham chiếu như một pseudo-XObject không có §8.10.1
/Subtype /Formvà/BBox, vì vậy hãy xác nhận kết quả render trong các trình xem mục tiêu của bạn và tránh dùng nó nơi cần xác thực nghiêm ngặt - Tọa độ là user space của trang. Gốc ở góc dưới bên trái, đơn vị là point, lấy theo media box của trang. Đọc box bằng
GetLoadedPageBoxtrước khi đặt bất kỳ thứ gì, vì trang bạn nạp có thể không có kích thước như bạn đã giả định
Nếu dùng trong đúng các giới hạn đó, cặp phương thức này đủ cho một workflow thực tế: sắp xếp lại trang để in, che vùng không mật, rồi ghi kết quả trở lại bằng SaveLoadedDocument, tất cả mà không cần render lại đầy đủ. API cho tài liệu đã nạp bao gồm các primitive stitch và mask này được cung cấp cùng HotPDF Component cho Delphi và C++Builder, bên cạnh các phương thức form field, annotation và FDF của cùng round