Technical Article

PDFium을 사용한 자동화된 PDF 프리플라이트 및 위험 감사

엔터프라이즈 인쇄, 보관 및 문서 규정 준수 워크플로에서 PDF 파일은 단순히 "렌더링"할 수 없습니다. 감사를 받아야 합니다. PDF에는 기존 래스터 이미지 프로세서(RIP)를 손상시키는 병합되지 않은 투명도 그룹, 보안 위험을 초래하는 내장형 JavaScript, 300 DPI로 인쇄할 때 끔찍하게 보이는 저해상도 이미지가 포함되어 있을 수 있습니다.

PDF가 프로덕션 워크플로에 들어가기 전에 검사하는 이 과정을 프리플라이팅(Preflighting)이라고 합니다. 이 기사에서는 PDFium의 저수준 파싱 기능을 활용하여 Delphi에서 자동화된 프리플라이트 및 위험 감사 도구를 구축하는 방법을 알아봅니다.

주요 프리플라이트 검사 항목

표준 위험 감사는 일반적으로 PDF 내의 다음 요소를 확인합니다.

  • 대화형 요소: JavaScript 작업, 시작(Launch) 작업(외부 프로그램 실행) 및 AcroForm.
  • 리소스 지표: 저해상도 이미지 또는 누락된 포함 글꼴 확인.
  • 보안 상태: 문서 암호화, 암호 요구 사항 및 액세스 권한(예: 인쇄 비활성화).
  • 문서 표준: 문서 메타데이터에 있는 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 포인터 반복을 깔끔한 객체 지향 코드로 변환할 수 있습니다.

프리플라이트 단계를 자동화하면 나쁜 파일이 프로덕션 파이프라인을 망치는 것을 방지하여 시간과 렌더링 리소스를 모두 절약할 수 있습니다.

참고: 고급 PDF 파싱 및 프리플라이트 기능은 PDFium Component에서 완벽하게 지원됩니다.