기술 문서

PDFium 컴포넌트 CLI를 사용한 Delphi에서의 일괄 PDF 프리플라이트 보고서

일괄(batch) 프리플라이트 도구는 창이 없는 콘솔 프로그램으로, PDF가 있는 폴더를 지정받아 지정한 적합성 표준에 따라 각각을 검증하고 발견된 사항에 대해 기계가 읽을 수 있는 증거를 남깁니다. 아무도 앉아서 그것을 지켜보지 않습니다. 이 도구는 새벽 2시에 cron이나 Windows 작업 스케줄러(Windows Task Scheduler)에서, 또는 CI 파이프라인의 게이트로 실행되며, 그 출력에 신경 쓰는 다음 사람은 종료 코드를 읽는 스케줄러이거나 몇 주 후에 보고서를 여는 감사자입니다. 이는 "올바르다"는 것의 의미를 바꿉니다. Delphi, C++Builder 및 Lazarus용 소스 코드 PDF 라이브러리인 PDFium Component의 프리플라이트 엔진은 검증 호출 자체를 거의 아주 간단하게 만듭니다. 도구가 제 역할을 다하는지 결정하는 작업은 이러한 호출 주변에 있습니다. 즉, 어떤 프로필을 확인했는지, 종료 코드가 스케줄러에게 무엇을 알렸는지, 그리고 누군가 찾아볼 때 실수를 잡아냈을 보고서가 여전히 존재하는지 등입니다

계약: 스케줄러가 실제로 볼 수 있는 것

CI 러너나 Windows Task Scheduler는 도구에서 정확히 두 가지, 즉 종료 코드(exit code)와 도구가 남긴 파일만을 봅니다. 로그 줄, 콘솔 색상, 진행 상황 출력 등은 모두 실시간으로 지켜보는 사람을 위한 것이며, 새벽 2시에는 아무도 지켜보지 않습니다. 따라서 API를 건드리기 전에 종료 코드의 어휘를 고정하고 단순하게 유지하십시오:

  • 0: 모든 파일이 요청된 모든 프로필을 준수함
  • 1: 최소 한 파일에서 검증 발견 사항(findings)이 발생함
  • 2: 최소 한 파일에서 도구 자체가 실패함 (손상된 입력, 잠금, 크래시)

코드 1과 2의 차이는 팀들이 건너뛰었다가 나중에 후회하는 부분입니다. 열리지 않는 손상된 PDF는 검증 실패가 아닙니다. 이를 코드 1에 포함시키면 대량의 손상된 스캔본이 대시보드에 갑작스러운 적합성 붕괴로 나타나게 되고, 실제로는 업스트림의 고장 난 스캐너가 원인임에도 누군가는 일어나지도 않은 표준 회귀(regression)를 쫓게 됩니다

계약에는 두 가지 항목이 더 포함되어야 합니다. 첫 번째는 파일당 타임아웃입니다. 깊게 중첩된 객체 구조를 가진 수천 페이지의 병적인 PDF는 단일 검증 과정을 몇 분 동안 붙잡아둘 수 있으며, 야간 작업 시간에는 이를 기다릴 여유가 없습니다. 기한이 되면 해당 파일의 작업을 중단하고, 이를 도구 실패로 간주한 다음 일괄 처리를 계속 진행하십시오. 두 번째는 격리 디렉터리입니다. 타임아웃되거나 열 수 없는 모든 입력을 제자리에 두지 말고 따로 옮기십시오. 몇 달이 지나면 그 디렉터리에는 실제 고객이 보내는 최악의 문서들이 조용히 쌓이게 되며, 이 코퍼스(corpus)는 여러분이 직접 작성할 수 있는 어떤 합성 샘플보다 릴리스 테스트에 훨씬 더 가치 있습니다

표준 선택, 그리고 적합성 수준이 중요한 이유

TPdfPreflightStandard 열거형은 실무에서 등장하는 제품군을 다룹니다. ISO 19005 아카이브 적합성을 위한 ppsPdfA, ISO 14289 접근성을 위한 ppsPdfUa, 인쇄 교환을 위한 ppsPdfX, 그리고 엔지니어링, 래스터 및 가변 데이터 작업을 위한 ppsPdfE, ppsPdfR, ppsPdfVT가 있습니다. 제품군 내에서 엔진은 문서가 주장하는 적합성 수준을 읽고 이를 결과의 ConformanceName에 표준별로 보고합니다. 수준(level)이야말로 실제 차이가 존재하는 곳이기 때문에 제품군을 지정하는 것만으로는 거의 충분하지 않습니다. PDF/A-2b는 시각적 재현성만을 보장할 뿐 그 이상은 아닙니다. PDF/A-3a는 논리적 구조 태깅(tagging)에 대한 요구 사항을 추가하고 소스 파일 포함을 허용하는데, 이는 태그 트리가 전혀 없는 스캔 자료가 통과하기에는 훨씬 더 까다로운 기준입니다. 이 두 방향 중 어느 쪽이든 잘못 설정하면 일괄 처리는 여러분을 속이게 됩니다. 보존 정책에서 실제로 PDF/A-2b를 원하는데 구조 태그 누락으로 파일을 실패시키면, 보고서는 아무도 수정하지 않을 발견 사항으로 채워집니다. 수준을 확인하지 않고 모든 PDF/A 레이블을 허용하면 약속한 것보다 약한 기준을 충족하는 문서를 승인하게 됩니다. 정부 구매자의 접근성 의무 사항은 점점 더 이 모든 것 위에 PDF/UA를 겹쳐 놓지만, BuildPdfPreflightReport(FPdfPreflightReport 유닛 제공)는 표준 집합을 취하므로 실행 비용이 추가되지 않습니다:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

한 번의 호출로 두 표준을 모두 평가하고 단일 통합 보고서 레코드를 반환합니다

빈 발견 사항 목록이 통과를 의미하지 않는 이유

보고서는 표준별로 발견 사항을 열거하며, 이슈 목록이 비어 있다는 것은 오직 "실제로 실행된 표준에서 문제가 발견되지 않았음"을 의미합니다. 이는 "파일이 여러분이 관심 있는 표준을 준수함"이라는 주장보다 좁은 의미이며, 이 둘 사이의 간극에서 일괄 프리플라이트가 조용히 썩어갑니다. 집합에서 ppsPdfA를 누락하는 구성 오타는 진정으로 깨끗한 파일과 정확히 동일한 빈 이슈 목록을 생성합니다. 따라서 침묵을 의심하십시오. Report.Results를 순회하며 확인하려 했던 모든 표준에 대해 두 가지, 즉 해당 결과 항목이 존재하는지, 그리고 Status = pfsPass가 뒷받침하는 IsCompliant 플래그가 참인지 확인하십시오. 어떤 표준이 평가되었는지 확인하지도 않은 채 "발견 사항 없음"을 "아카이브 준비 완료"와 동일시하는 야간 작업은 수개월 동안 부적합 파일 폴더를 무사히 통과시키는 고전적인 방법이며, 결국 외부 감사자가 veraPDF로 파일을 열고 나서야 전체 아카이브에 의문이 제기됩니다

두 번째 함정은 발견 사항 자체가 무엇인지에 숨어 있습니다. 각 TPdfPreflightIssueCode, Category, DescriptionRecommendation을 전달하며, 페이지나 객체가 아니라 위반된 규칙의 이름을 지정합니다. 이는 피드백 루프에 결과를 초래하는 설계상의 선택입니다. 보고서는 파일 제작 팀에게 임베딩되지 않은 글꼴이나 누락된 XMP 식별자 등 어떤 종류의 결함이 존재하는지 알려주며, 특정 위반 객체를 찾는 것은 검증자가 아니라 하위 수정 도구의 역할입니다. 보고서 소비자는 안정적인 Code 값을 기준으로 구축해야 하며, 경고 없이 릴리스 간에 문구가 변경될 수 있는 사람이 읽을 수 있는 설명 텍스트를 기준으로 구축해서는 절대 안 됩니다

기계와 온콜 담당자를 위한 보고서 파일

보고서 레코드는 동일한 발견 사항을 SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile, SaveMarkdownToFile의 다섯 가지 형식으로 작성하며, 디스크가 아닌 메모리에서 문자열을 원할 때를 위해 각각 일치하는 ToJson 스타일의 함수를 제공합니다. 하나만 선택하려는 충동을 억제하십시오. 파이프라인을 위해 JSON을 작성하여 CI가 텍스트를 스크래핑하지 않고도 작업 레코드에 이를 첨부하고 이슈 코드와 표준별 상태를 파싱할 수 있게 하십시오. 호출(page)을 받는 사람을 위해 HTML을 작성하여 아무런 도구 없이도 모든 브라우저에서 열 수 있게 하십시오. 이 두 가지를 함께 사용하면 파일당 코드 한 줄이 더 들지만, 온콜 엔지니어가 새벽 2시에 원시 JSON 블롭을 리버스 엔지니어링하여 어떤 파일이 고장 났는지 알아내는 일괄 처리 최악의 작업을 면하게 해줍니다. 형식 선택보다 더 중요한 규칙이 하나 있습니다. 각 보고서 이름은 항상 입력 파일 이름에서 파생시켜야 하며 절대 타임스탬프에서 파생시켜서는 안 됩니다. 그렇지 않으면 두 개의 병렬 실행이 겹쳐 입력과 더 이상 일치시킬 수 없는 보고서가 생성됩니다

심각도 임계값은 코드가 아닌 구성(configuration)에 속해야 합니다. 대체 설명이 없는 주석은 PDF/UA 제출 포털에서는 엄격한 실패 사유이지만 내부 아카이브에서는 무시할 수 있는 참고 사항이며, 두 경우 모두 동일한 발견 사항입니다. 정책이 재컴파일 없이 변경될 수 있도록 프로필당 실패 임계수준(fail-on level)을 노출하고, 시행 중이던 수준을 작업 요약 자체에 기록하십시오. 다음 분기에는 아무도 지난 10월의 일괄 처리가 어떤 임계값 아래에서 실행되었는지 기억하지 못할 것이며, 요약본은 그 기억이 살아남는 유일한 장소입니다

하나의 잘못된 PDF가 일괄 처리를 망치지 않도록 파일 격리하기

procedure RunPreflightBatch(const InputDir, ReportDir: string;
  out FilesWithFindings, ToolFailures: Integer);
var
  SR: TSearchRec;
  Pdf: TPdf;
  Report: TPdfPreflightReport;
begin
  FilesWithFindings := 0;
  ToolFailures := 0;
  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
  try
    repeat
      Pdf := TPdf.Create(nil);   // fresh instance per file: no state bleed
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // load failures are silent, not raised
            raise EPdfError.Create('Cannot open ' + SR.Name);
          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
          if Report.TotalIssueCount > 0 then
            Inc(FilesWithFindings);
        except
          on E: Exception do
          begin
            Inc(ToolFailures);   // exit-code-2 territory, not a validation verdict
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

그 루프에는 세 가지 의도적인 선택이 들어 있습니다. 파일당 새로운 TPdf를 사용하면 엔진 상태를 손상시키는 하나의 문서가 그 뒤를 잇는 파일들을 망치지 않도록 보장합니다. 명시적인 Active 확인은 그만한 가치가 있습니다. Active := True는 로드 오류를 발생시키는 대신 삼켜버리기 때문입니다. 가드를 제거하면 잘린 파일이 검증 호출로 흘러들어가 하위 어딘가에서 오해를 불러일으키는 메시지와 함께 실패하게 됩니다. 내부의 try..except는 의도적으로 파일별 스코프 내에 존재하므로, 단일 예외가 발생하더라도 실패 카운터를 올리고 루프는 계속 진행됩니다. 5,000번째 파일이 심하게 손상되었을 때조차 4,999개의 정상적인 파일에 대한 깨끗한 보고서를 원할 것입니다. 그리고 두 보고서 형식 모두 판정이 집계되기 전에 디스크에 기록되므로 나중에 요약 로직의 버그로 인해 계산이 잘못되더라도 증거는 살아남습니다

그런 다음 종료 코드 매핑은 프로젝트 파일에서 몇 줄로 요약됩니다:

begin
  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
  if Failures > 0 then
    Halt(2)
  else if Findings > 0 then
    Halt(1);
  // falling through exits with 0: every file conformed
end.

프리플라이트가 해주지 않는 것

엔진은 감지할 뿐 복구하지 않습니다. 임베딩되지 않은 글꼴이나 장치 종속적 색상 공간에 대한 발견 사항은 파일을 생성하는 사람을 위한 작업 지시서이며, 검증자에게는 이를 그 자리에서 패치할 방법이 없습니다. 따라서 피드백 루프를 신중하게 계획하십시오. 보고서는 파일 제작 팀이 실제로 읽는 곳에 도달해야 합니다. 그렇지 않으면 누군가 적합성 비율이 개선되지 않는 이유를 마침내 물어볼 때까지 동일한 발견 사항이 매일 밤 다시 나타날 것입니다. 외부 감사자가 대신 교차 확인을 하기 전에, veraPDF(PDF/A의 경우)나 Acrobat의 프리플라이트(PDF/X의 경우)와 같은 독립적인 검증자를 통해 판정 샘플을 교차 확인하는 것도 가치가 있습니다. 실제 고객 파일에 대해 두 엔진이 의견을 달리한다면, 그 문서는 성가신 것이 아니라 릴리스 테스트에서 누락되었던 바로 그 회귀 케이스(regression case)입니다. 그것을 보관하고, 이름을 지정하고, 모든 빌드에서 실행하십시오

알아둘 만한 또 다른 조합이 있습니다. 동일한 검증 엔진이 리뷰 UI의 대화형 검사를 구동하므로, 이 헤드리스 CLI와 분석가 대상의 PDF 수집 리뷰 워크벤치(PDF intake review workbench)는 시간이 지남에 따라 분리되지 않고 단일 검증 어휘를 공유할 수 있습니다. 그리고 [ppsPdfA, ppsPdfUa]는 동일한 패스에서 접근성을 평가하므로 일괄 처리의 PDF/UA 측면은 Delphi에서 접근 가능한 PDF 리더 구축과 같은 뷰어 측 작업과 깔끔하게 일치합니다. 프로필, 보고서 형식 및 전체 프리플라이트 API는 PDFium Component 제품 페이지에 문서화되어 있습니다