Bài viết Kỹ thuật

Redaction PDF Thực tế trong Delphi: Xóa và Làm sạch

Redaction PDF thực tế trong Delphi nghĩa là xóa các byte cơ sở bên dưới, chứ không phải là vẽ đè lên chúng. losLab PDF Library vạch ra ranh giới đó bằng hai API: RedactRegion chỉnh sửa luồng nội dung trang để văn bản, vectơ và hình ảnh bị loại bỏ thực sự biến mất về mặt vật lý, và SanitizeDocument xóa sạch siêu dữ liệu (metadata), các kịch bản (script) cũng như hành động (action) nơi thông tin bị ẩn giấu vẫn có thể tồn tại. Nếu làm sai một trong hai phần, bạn sẽ phân phối một tệp trông có vẻ đã được kiểm duyệt trên màn hình nhưng lại giao lại bí mật cho bất kỳ ai sao chép văn bản hoặc mở Thuộc tính Tài liệu (Document Properties)

Chế độ lỗi này đã là một thực tế được ghi nhận công khai. Các tòa báo, tòa án và các cơ quan chính phủ đều từng phát hành các tệp PDF with a tệp PDF với một hình chữ nhật màu đen đè lên tên hoặc số, để rồi người đọc chỉ cần quét chọn văn bản bên dưới, sao chép nó, và dán chuỗi ký tự được cho là đã bị xóa ra trước bàn dân thiên hạ. Hình chữ nhật đó chỉ là một nét vẽ. Văn bản vẫn còn nguyên ở đó, nằm trong luồng nội dung thấp hơn một lớp so với nét vẽ, hoàn toàn nguyên vẹn và hoàn toàn có thể chọn được. losLab PDF Library, công cụ PDF cho Delphi và C++Builder được cung cấp thông qua lớp TPDFlib, coi sự khác biệt đó là mục đích cốt lõi của việc redaction chứ không phải là một ý nghĩ thoáng qua

Tại sao vẽ một hộp đen đè lên văn bản PDF không phải là redaction thực sự?

losLab PDF Library cung cấp MaskRect cho những trường hợp che phủ thực sự là điều bạn muốn: hình mờ (watermark), che tối trực quan, hoặc con dấu bằng chứng. MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) thêm một hình chữ nhật vào luồng nội dung trang và vẽ nó bằng toán tử xây dựng đường dẫn re tiếp nối bởi toán tử tô màu f (ISO 32000-1 §8.5). Mỗi byte đã có sẵn trên trang — các ký tự (glyph) của cái tên bạn đã che, các nét vẽ vectơ, hình ảnh được nhúng — vẫn giữ nguyên vị trí cũ, nằm ở một lệnh trước đó trong cùng luồng dữ liệu. Một trình xem chuẩn chỉnh sẽ vẽ hộp đen sau cùng, vì vậy nó chiến thắng về mặt trực quan. Việc che phủ không thay đổi bất kỳ thứ gì bên dưới

Bất kỳ ai cũng có thể lấy lại nội dung gốc bằng ba cách: quét chọn và sao chép trực tiếp văn bản xuyên qua hộp đục, chạy một bộ trích xuất văn bản trên trang, hoặc xóa hình chữ nhật che phủ khỏi luồng nội dung. MaskRect phản ánh trung thực điều này qua tên gọi của nó — nó che phủ chứ không loại bỏ. Kết quả giống như một miếng nhãn dán đè lên một từ mà bạn vẫn có thể đọc bằng cách bóc miếng nhãn đó ra

Cách RedactRegion xóa nội dung ở cấp độ lệnh

RedactRegion(Page, Left, Top, Width, Height, Options) hoạt động thấp hơn một lớp so với nét vẽ. losLab PDF Library phân tích cú pháp từng lớp nội dung của trang thành một TPDFContentProgram, duyệt qua danh sách lệnh bằng một bộ phân tích theo dõi CTM (FindInstructionsInRect trong mô hình nội dung), và đánh dấu mọi toán tử hiển thị văn bản, tô vẽ đường dẫn cũng như hình ảnh Do có vùng vẽ giao với hình chữ nhật của bạn. Các lệnh đó sẽ bị xóa bằng TPDFContentProgram.Delete, và lớp được chỉnh sửa sẽ được ghi lại bằng WritePageContentLayer. Các byte không trượt ra sau một tấm che — chúng rời khỏi luồng dữ liệu. Một bộ trích xuất văn bản chạy sau đó sẽ không tìm thấy gì, vì không còn gì để tìm

Có hai chi tiết triển khai rất đáng để bạn lưu ý. RedactRegion nhận một hình chữ nhật trên cùng bên trái theo điểm gốc và đơn vị hiện tại rồi chuyển đổi nội bộ sang không gian người dùng dưới cùng bên trái mà luồng nội dung sử dụng, nhờ đó bạn xác định vùng theo cách bạn nhìn thấy trên trang. Khi một hình chữ nhật bao phủ nhiều lệnh, RedactRegion sẽ xóa chúng theo thứ tự chỉ mục giảm dần, bởi vì việc xóa lệnh 4 trước lệnh 7 sẽ làm dịch chuyển mọi chỉ mục sau đó và làm hỏng phạm vi. Truyền Options = 0 và losLab PDF Library cũng sẽ đóng dấu một hộp màu đen đặc lên vùng đã xóa như một dấu hiệu hiển thị trực quan — nhưng dấu hiệu đó chỉ mang tính thẩm mỹ, được áp dụng sau khi nội dung thực tế đã biến mất

Trong thực tế, bạn hiếm khi mã hóa cứng hình chữ nhật. Bạn tìm kiếm trang để phát hiện chuỗi nhạy cảm và đọc lại hộp giới hạn (bounding box) của nó — cùng phương pháp tìm kiếm trang và liệt kê phần tử được đề cập trong việc định vị văn bản và phần tử trang trong Delphi — sau đó giao hình chữ nhật đó cho RedactRegion:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', '') = 1 then
    begin
      // Delete everything painted inside a 220 x 14 pt box on page 1.
      // Coordinates are top-left, in the current origin and units.
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);   // Options=0 stamps a black marker
      PDF.SaveToFile('contract-redacted.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Thông tin bị xóa vẫn còn ẩn giấu ở đâu trong tệp PDF?

Việc redaction chỉ dừng lại ở lớp hiển thị sẽ để lại dấu vết, bởi vì một tệp PDF lưu trữ thông tin ở những nơi mà người đọc không bao giờ nhìn thấy. losLab PDF Library nhóm các rò rỉ này thành các kênh đáng được gọi tên. Từ điển Thông tin Tài liệu (Document Information Dictionary) và luồng siêu dữ liệu XMP thường xuyên mang theo các chuỗi Author, Producer hoặc Keywords đặt tên cho chính người hoặc vụ việc mà bạn đã xóa khỏi phần thân. JavaScript cấp tài liệu và OpenAction của catalog có thể thực thi khi mở và tiếp cận dữ liệu bạn nghĩ là đã biến mất. Mỗi chú thích mang theo một hành động /A tùy chọn, và chính một chú thích — một ghi chú dán, một trường biểu mẫu cũ, một liên kết — có thể nằm ngay trên vị trí bị xóa với bản sao văn bản riêng của nó. Không có điều nào trong số này bị ảnh hưởng bởi mã hóa; một tệp được mã hóa với gói XMP dạng văn bản thuần túy vẫn làm rò rỉ tiêu đề của nó cho bất kỳ bộ lập chỉ mục nào, một lỗ hổng rất đáng để tự kiểm toán mã hóa và quyền hạn. Giải pháp làm sạch an toàn phải đi qua từng nơi ẩn giấu này, chứ không chỉ là trang giấy bạn đang xem

Làm sạch tài liệu bằng SanitizeDocument

losLab PDF Library giải quyết các rò rỉ đó bằng SanitizeDocument(Flags), một lệnh phối hợp chạy một tập hợp các tác vụ loại bỏ độc lập được chọn bởi một mặt nạ bit và trả về số lượng tác vụ đã áp dụng. Các cờ có thể kết hợp với nhau. SANITIZE_JAVASCRIPT loại bỏ mọi gói JavaScript cấp tài liệu và bất kỳ kịch bản OpenAction nào. SANITIZE_ACTIONS loại bỏ từ điển hành động /A khỏi mọi chú thích, thông qua phương thức mới TPDFAnnots.RemoveAnnotAction, và xóa OpenAction của catalog. SANITIZE_METADATA xóa trắng Author, Subject, Keywords, Creator và Producer trong cả Từ điển Thông tin Tài liệu (ISO 32000-1 §14.3.3) and và luồng siêu dữ liệu XMP (§14.3.2). SANITIZE_ANNOTATIONS xóa mọi chú thích trên toàn bộ tài liệu, và SANITIZE_OPEN_ACTION loại bỏ mục /OpenAction của catalog. SANITIZE_ALL là liên kết bitwise của cả năm cờ đó

Các quy ước chỉ mục bên trong các tác vụ này chính xác là những thứ dễ làm hỏng một vòng lặp tự viết, đó là lý do lớn tại sao SanitizeDocument tồn tại dưới dạng một lệnh gọi duy nhất. GlobalJavaScriptPackageName bắt đầu từ 0, và RemoveGlobalJavaScript thu hẹp danh sách khi thực hiện, do đó tác vụ JavaScript luôn đọc chỉ mục gói 0 và loại bỏ cho đến khi số lượng về không. Ngược lại, DeleteAnnotation bắt đầu từ 1. AnnotationCount báo cáo dựa trên trang hiện được chọn, vì thế tác vụ chú thích sẽ chọn lần lượt từng trang trước khi đếm. losLab PDF Library xử lý chính xác các ranh giới này từ bên trong, nhờ đó bạn chỉ cần truyền một bộ cờ và đọc lại một kết quả đếm duy nhất thay vì phải tái tạo năm quy tắc lặp khác nhau

var
  Applied: Integer;
begin
  // Strip metadata, scripts, and actions but keep the annotations:
  Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
    SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);

  // Or clear every hidden channel, annotations included:
  Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;

Quy trình redaction và làm sạch hoàn chỉnh

Kết hợp cả hai lại và quy trình đầy đủ sẽ rất ngắn gọn. losLab PDF Library loại bỏ nội dung hiển thị bằng RedactRegion, từng trang một, sau đó xóa sạch mọi kênh ẩn bằng SanitizeDocument(SANITIZE_ALL) trước khi tệp được ghi. Bởi vì redaction hầu như luôn chạy trên các tài liệu đến từ bên ngoài tầm kiểm soát của bạn, việc tải chúng qua một đường dẫn được tăng cường bảo mật là điều rất đáng giá — cùng tư thế phòng thủ được mô tả trong việc phân tích cú pháp các tệp PDF không đáng tin cậy một cách an toàn — để một đầu vào bị lỗi định dạng sẽ thất bại một cách êm đẹp thay vì bị lỗi giữa chừng trong lượt redaction

var
  PDF: TPDFlib;
  Applied: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('incoming.pdf', '') = 1 then
    begin
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);       // delete the sensitive line
      Applied := PDF.SanitizeDocument(SANITIZE_ALL);   // clear metadata, JS, actions, annots
      PDF.SaveToFile('published.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Nơi redaction luồng nội dung dừng lại

RedactRegion hoạt động trên luồng nội dung, đây chính xác là nơi văn bản số gốc (born-digital text) sinh sống và chính xác là nơi văn bản quét không tồn tại. Khi một trang là một hình ảnh quét, các từ là các điểm ảnh bên trong một XObject hình ảnh chứ không phải các toán tử văn bản, và losLab PDF Library không thể đẽo gọt một cái tên ra khỏi giữa một hình ảnh raster theo cách nó xóa một ký tự (glyph). Việc xử lý hình ảnh của nó được chủ ý thiết kế thận trọng: bất kỳ hình ảnh nào có vị trí giao với hình chữ nhật đều bị loại bỏ toàn bộ, do đó một hộp redaction trên một trang quét sẽ mang theo toàn bộ bản quét đó đi kèm thay vì chỉ một mảng của nó. Việc redaction một vùng bên trong hình ảnh đồng nghĩa với việc raster hóa, tô vẽ và nhúng lại ở cấp độ điểm ảnh, đây là một thao tác khác biệt so với việc xóa luồng nội dung. Sự thận trọng tương tự cũng áp dụng cho một lớp văn bản OCR đè lên một trang quét: RedactRegion loại bỏ các toán tử văn bản mà nó nhìn thấy, nhưng bức ảnh bên dưới vẫn nguyên vẹn trừ khi XObject hình ảnh của nó cũng giao với hộp chữ nhật

Hai điểm chi tiết giúp kết quả luôn trung thực. Thử nghiệm giao nhau ước tính chiều rộng của mỗi lượt chạy văn bản dựa trên số lượng ký tự và kích thước phông chữ của nó thay vì đo lường các chỉ số ký tự chính xác, do đó một từ rất ngắn được định dạng bằng một phông chữ tỷ lệ (proportional font) có thể lọt qua một hình chữ nhật được vẽ chặt chẽ — hãy cung cấp cho hộp một biên độ nhỏ. Việc xóa cũng có tính chất phân mảnh theo lệnh: RedactRegion loại bỏ toàn bộ các toán tử hiển thị văn bản, vì thế một hình chữ nhật cắt bớt chỉ một phần của lượt chạy văn bản có thể mang theo các ký tự liền kề vốn chia sẻ cùng toán tử đó. Cả hai giới hạn đều không phải là lý do để không tin tưởng cơ chế, mà chỉ là lý do để giới hạn hình chữ nhật một cách cẩn thận và kiểm tra kết quả

Quy tắc khép lại mọi quy trình redaction là xác minh chứ không phải tin tưởng. Hãy redaction vùng đó, làm sạch tài liệu, lưu vào một tệp mới, mở lại nó và thử trích xuất lại cả văn bản bạn đã loại bỏ lẫn siêu dữ liệu bạn đã làm sạch; khi cả hai đều trống rỗng, việc redaction đã thành công. Các API RedactRegionSanitizeDocument được hiển thị ở đây đi kèm với losLab PDF Library dành cho Delphi và C++Builder, bên cạnh tài liệu tham khảo hoàn chỉnh về mô hình nội dung và làm sạch