Technical Article

使用 PDFium 进行自动化的 PDF 预检和风险审计

在企业打印、归档和文档合规工作流程中,PDF 文件不能简单地“渲染”了事,它必须经过审计。PDF 可能包含会使旧版光栅图像处理器(RIP)崩溃的未拼合透明度组,可能包含构成安全风险的嵌入式 JavaScript,或者包含在以 300 DPI 打印时看起来很糟糕的低分辨率图像。

在 PDF 进入生产工作流程之前对其进行检查的过程称为预检(Preflighting)。在本文中,我们将探讨如何通过利用 PDFium 的底层解析功能,在 Delphi 中构建自动化的预检和风险审计工具。

关键的预检检查

标准的风险审计通常检查 PDF 中的以下元素:

  • 交互式元素: JavaScript 动作、启动动作(执行外部程序)和 AcroForms。
  • 资源指标: 存在低分辨率图像或缺失嵌入字体。
  • 安全状态: 文档加密、密码要求和访问权限(例如,禁用打印)。
  • 文档标准: 验证文档元数据中的 PDF/A 或 PDF/X 兼容性标记。

使用 PDFium 提取 PDF 元数据和注释

PDFium 提供了 Delphi 可以调用的稳健 C API。要执行预检审计,我们不仅仅是渲染页面,我们需要遍历 PDF 对象树。让我们看看如何遍历文档页面并检查注释,以发现具有潜在风险的 JavaScript 动作。

uses
  System.SysUtils, pdfium_lib;

procedure AuditPdfSecurity(const FileName: string);
var
  Doc: FPDF_DOCUMENT;
  PageCount, i, j: Integer;
  Page: FPDF_PAGE;
  AnnotCount: Integer;
  Annot: FPDF_ANNOTATION;
  AnnotSubType: FPDF_ANNOTATION_SUBTYPE;
  Action: FPDF_ACTION;
  ActionType: ULONG;
begin
  FPDF_InitLibrary();
  try
    // Load the document without a password
    Doc := FPDF_LoadDocument(PAnsiChar(AnsiString(FileName)), nil);
    if Doc = nil then
      raise Exception.Create('Failed to load document or password required.');
      
    try
      PageCount := FPDF_GetPageCount(Doc);
      Writeln(Format('Auditing %d pages...', [PageCount]));
      
      for i := 0 to PageCount - 1 do
      begin
        Page := FPDF_LoadPage(Doc, i);
        if Page <> nil then
        begin
          AnnotCount := FPDFPage_GetAnnotCount(Page);
          for j := 0 to AnnotCount - 1 do
          begin
            Annot := FPDFPage_GetAnnot(Page, j);
            if Annot <> nil then
            begin
              AnnotSubType := FPDFAnnot_GetSubtype(Annot);
              
              // Check for Link Annotations that might have malicious actions
              if AnnotSubType = FPDF_ANNOT_LINK then
              begin
                Action := FPDFAnnot_GetLinkAction(Annot);
                if Action <> nil then
                begin
                  ActionType := FPDFAction_GetType(Action);
                  // 2 represents PDFACTION_URI, 3 represents PDFACTION_SOUND, 4 represents PDFACTION_MOVIE
                  // We specifically look out for PDFACTION_LAUNCH or PDFACTION_JAVA_SCRIPT
                  if (ActionType = PDFACTION_JAVA_SCRIPT) or (ActionType = PDFACTION_LAUNCH) then
                  begin
                    Writeln(Format('WARNING: Malicious action detected on page %d', [i + 1]));
                  end;
                end;
              end;
              FPDFPage_CloseAnnot(Annot);
            end;
          end;
          FPDF_ClosePage(Page);
        end;
      end;
    finally
      FPDF_CloseDocument(Doc);
    end;
  finally
    FPDF_DestroyLibrary();
  end;
end;

检查图像分辨率

打印工作流程的另一个关键预检步骤是确保没有任何图像低于特定的 DPI 阈值。PDFium 允许您直接从页面流中提取图像对象。通过将提取图像的像素尺寸除以它在 PDF 页面上占据的物理尺寸(以磅为单位),您可以计算出有效的 DPI。

procedure AuditPageImages(Page: FPDF_PAGE);
var
  ObjCount, i: Integer;
  PageObj: FPDF_PAGEOBJECT;
  ImgWidth, ImgHeight: Integer;
begin
  ObjCount := FPDFPage_CountObjects(Page);
  for i := 0 to ObjCount - 1 do
  begin
    PageObj := FPDFPage_GetObject(Page, i);
    if FPDFPageObj_GetType(PageObj) = FPDF_PAGEOBJ_IMAGE then
    begin
      FPDFImageObj_GetBitmap(PageObj); // Returns an FPDF_BITMAP you can inspect
      // Retrieve metadata using FPDFImageObj_GetImageMetadata
      // Calculate effective DPI based on quad coordinates
    end;
  end;
end;

与 PDFium Component 集成

使用原始 PDFium C API 从头开始构建稳健的预检引擎需要大量的样板代码和对 PDF 规范的深入了解。使用包装器(wrapper)可以极大地简化这一过程。借助高级的 Delphi 包装器,您可以将复杂的 C 指针迭代转变为清晰的面向对象代码。

通过自动化预检阶段,您可以防止不良文件阻碍您的生产管道,从而节省时间和渲染资源。

注意:PDFium Component 完全支持高级的 PDF 解析和预检功能。