기술 문서

HotPDF PDF/X-1a 색상 검사: RGB 콘텐츠 잡아내기

PDFXCompliance를 'X-1a'로 설정하면 HotPDF는 작업 맨 끝에서 콘텐츠 색을 검증한다. EndDoc이 THotPDF.EnsurePDFX1aContentColorSpaces를 호출하고, 이것은 어떤 페이지의 어디에서도 DeviceRGB, CalRGB, ICCBased RGB 콘텐츠를 허용하지 않으며 문제의 리소스를 이름으로 지목하는 예외를 던진다. 인쇄소에서 튕겨 나올 PDF/X-1a 문서는 아예 기록되지 않는다

마지막 부분이 핵심이다. RGB 로고가 하나 섞인 PDF/X-1a 파일은 화면에서 완벽해 보이고 대충 훑어도 통과하다가, 인쇄 슬롯 이틀 전에 프린터 프리플라이트에서 실패한다. 어떤 페이지의 어떤 이미지인지 알려 주는 메시지와 함께 Delphi 빌드 안에서 잡는 편이 짜증 나는 인쇄소 담당자에게 들어 듣는 것보다 훨씬 싸다

PDF/X-1a는 왜 RGB 콘텐츠를 거부하는가?

PDF/X-1a가 RGB를 거부하는 이유는 이 포맷이 모든 색이 하나의 특정 CMYK 인쇄 조건에 대해 이미 확정됐다고 약속하기 때문이다. ISO 15930-1:2001 §6.2.3은 콘텐츠 색을 DeviceCMYK, DeviceGray, 스팟 컬러랜트로 제한한다. 즉 alternate가 CMYK나 Gray인 Separation과 DeviceN 색 공간만 된다는 뜻이다. 파일에 실리는 GTS_PDFX OutputIntent가 그 인쇄 조건, 이를테면 FOGRA39 coated offset을 지명하고, 받는 워크플로는 색 변환 없이 수치를 판에 곧바로 보내도 된다

RGB는 이 계약을 깬다. RGB 값은 잉크 조리법이 아니라 디스플레이나 작업 색 공간에 상대적인 색을 묘사하므로, 하류의 누군가는 그것을 어떻게 분판할지 결정해야 한다. X-1a가 존재하는 이유가 정확히 그 결정을 수신자에게서 빼앗는 것이다. ISO 32000-1 §8.6은 PDF 색 공간을 device, CIE 기반, special 계열로 묶는데, RGB 구성원은 한 계열을 넘어 나타난다. DeviceRGB는 device 공간이고, 3컴포넌트 CalRGB와 ICCBased는 CIE 기반이며, Indexed나 Separation 공간은 RGB base나 alternate를 밑에 숨길 수 있다. rg 연산자만 찾는 검사기는 이들 대부분을 놓친다. 후속 판인 PDF/X-1a:2003(ISO 15930-4)도 같은 색 규칙을 유지한다. 유의할 점은 HotPDF의 'X-1a' 값이 XMP 패킷에 PDF/X-1a:2001 식별을 기록한다는 것이다

콘텐츠가 실제로 RGB이고 그래야 한다면 X-1a가 잘못된 목표다. HotPDF는 이 검사를 'X-1a'에만 적용하므로 ICC 기반과 캘리브레이션된 색을 허용하는 'X-3'과 'X-4'는 영향받지 않는다

EnsurePDFX1aContentColorSpaces가 실제로 검사하는 것

EnsurePDFX1aContentColorSpaces는 draw 시점 플래그와 모든 페이지 리소스의 재귀 순회를 결합하며, PDF/X 게이트에서 마지막으로 돌아간다. EndDoc은 먼저 일반 PDF/X 요건을 강제한다. Title이 설정돼야 하고, Trapped는 'True', 'False', 'Unknown' 중 하나여야 하고, GTS_PDFX OutputIntent가 존재해야 하고(EnsurePDFXOutputIntent), 모든 페이지에 TrimBox나 ArtBox가 필요하다(EnsurePDFXPageBoxes). 이것들이 통과한 뒤에야 색 검사가 돌아간다

Delphi에서 HotPDF EndDoc 안의 PDF/X 게이트 순서: 메타데이터, OutputIntent, 페이지 박스가 먼저이고 그다음에 RGB 콘텐츠 색 검사가 온다
색 검사는 X-1a 게이트에서 마지막으로 돌고, draw 시점 플래그 덕분에 compliance가 지정되기 전에 그린 RGB도 여전히 잡힌다

첫 경로는 문서 전체 플래그다. HotPDF의 RGB 색 setter, 비트맵 이미지 임베딩, 어노테이션 색 항목은 그리는 동안 DeviceRGB 사용을 기록하는데, 리소스 사전에 나타나지 않는 인라인 rg와 RG 연산자까지 커버된다. 두 번째 경로는 각 페이지의 /Resources를 순회하며 다음을 들여다본다

  • /ColorSpace 안의 이름 있는 색 공간, DeviceRGB의 동의어인 인라인 이미지 축약 RGB 포함
  • 모든 Image XObject의 /ColorSpace
  • Form XObject와 tiling Pattern, 각자의 중첩 /Resources로 재귀해 들어간다
  • Shading Pattern과 페이지 /Shading 사전의 항목, shading의 /ColorSpace를 통해 본다
  • Indexed base 공간, Separation과 DeviceN alternate, Pattern base 공간, 중첩 깊이 8까지 따라 내려간다

거부되는 것은 셋이다. DeviceRGB, CalRGB, 그리고 프로파일 스트림이 N = 3이라고 선언하는 ICCBased 공간. Form XObject와 pattern이 공유하는 리소스 사전은 한 번만 방문되므로 서로 참조하는 form 집합이 순회를 무한 루프에 빠뜨리지 않는다. 이 메서드는 THotPDF에서 public이지만 평범한 사용에서는 직접 호출할 일이 없다. EndDoc이 대신 호출해 주기 때문이다

RGB가 PDF 페이지 리소스에 숨는 곳: 중첩 form, pattern, shading, alternate를 따라가는 Delphi의 HotPDF 순회가 DeviceRGB, CalRGB, ICCBased RGB에 도달한다
순회는 중첩 리소스를 깊이 8까지 따라가며 DeviceRGB, CalRGB, N = 3인 ICCBased 공간을 어디서 표면화하든 거부한다

검사를 통과하는 X-1a 페이지를 Delphi에서 그리기

적합한 HotPDF X-1a 문서에는 CMYK OutputIntent, 페이지마다 TrimBox, 그리고 CMYK, Gray, 스팟 API를 통해서만 설정되는 색이 필요하다. 아래 골격은 FOGRA39 프로파일을 쓴다. AddPDFXOutputIntent는 출력 조건 식별자, 정보 문자열, ICC 프로파일 스트림, 컴포넌트 수, alternate 공간을 받는다

var
  Pdf: THotPDF;
  Profile: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure-x1a.pdf';
    Pdf.Title := 'Spring brochure, press edition';
    Pdf.Trapped := 'False';
    Pdf.PDFXCompliance := 'X-1a';
    Pdf.BeginDoc;

    Profile := TFileStream.Create('CoatedFOGRA39.icc', fmOpenRead or fmShareDenyWrite);
    try
      Pdf.AddPDFXOutputIntent('FOGRA39 (ISO 12647-2:2004)',
        'Coated FOGRA39 offset', Profile, 4, 'DeviceCMYK');
    finally
      Profile.Free;
    end;
    Pdf.CurrentPage.SetTrimBox(0, 0, 612, 792);   // 모든 페이지에 필수

    Pdf.CurrentPage.SetCMYKFillColor(0, 0.5, 0.8, 0.1);
    Pdf.CurrentPage.Rectangle(72, 600, 468, 120);
    Pdf.CurrentPage.Fill;

    Pdf.CurrentPage.SetGrayFillColor(0.2);         // DeviceGray 텍스트
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    Pdf.CurrentPage.TextOut(72, 560, 0, 'Spring catalog, press edition');
    Pdf.EndDoc;                                    // 여기서 색 검사가 돈다
  finally
    Pdf.Free;
  end;
end;

이 맥락에서 TColor 기반 호출은 모두 함정이다. SetRGBFillColor, SetRGBStrokeColor, SetRGBColor는 전부 GDI 색을 받아 DeviceRGB를 내보내므로, 수년 전 화면 지향 리포트를 위해 작성된 헬퍼 루틴이 재사용되는 순간 인쇄 작업을 오염시킨다. 0부터 1 범위의 컴포넌트로 SetCMYKFillColor, SetCMYKStrokeColor, SetCMYKColor를 쓰거나 Gray 대응물을 사용하라

스팟 컬러와 CMYK 그라디언트

스팟 컬러랜트는 alternate가 CMYK나 Gray이기만 하면 X-1a에서 허용된다. RegisterSeparation이 Separation 공간을 만들고 그 리소스 이름을 돌려주며, RegisterAxialGradient는 넘겨준 컴포넌트 수에서 shading 색 공간을 고른다. 네 컴포넌트면 DeviceCMYK다

var
  SpotCS, Ramp: AnsiString;
begin
  // alternate로 근사 CMYK 조합을 지닌 스팟 잉크
  SpotCS := Pdf.RegisterSeparation('Corporate Red', 'DeviceCMYK',
    [0, 1, 0.8, 0.05]);
  Pdf.CurrentPage.SetFillColorSpace(SpotCS);
  Pdf.CurrentPage.SetFillColor([1.0]);           // 100% 틴트
  Pdf.CurrentPage.Rectangle(72, 420, 200, 80);
  Pdf.CurrentPage.Fill;

  // 정거장마다 4컴포넌트: shading은 DeviceRGB가 아니라 DeviceCMYK다
  Ramp := Pdf.RegisterAxialGradient(72, 300, 540, 300,
    [0, 0, 0, 0], [1, 0.6, 0, 0.2]);
  Pdf.CurrentPage.SetFillPattern(Ramp);
  Pdf.CurrentPage.Rectangle(72, 260, 468, 80);
  Pdf.CurrentPage.Fill;
end;

같은 gradient 호출에 세 컴포넌트를 넘기면 DeviceRGB shading이 되고, 리소스 순회는 shading pattern 이름으로 이를 거부한다. 'DeviceRGB'를 alternate로 등록한 Separation도 마찬가지다. 스팟 이름은 무해해 보이지만 순회는 alternate를 따라가 밑에 있는 RGB를 발견한다. 뷰어나 RIP가 alternate로 폴백할 때 그런 스팟 잉크가 어떻게 합성되는지는 Delphi에서 Separation과 DeviceN 스팟 컬러 렌더링하기 글이 다룬다

래스터 이미지도 같은 규율이 필요하다. HotPDF는 Flate 압축 비트맵을 DeviceRGB로 고정하는 반면, icJpeg로 임베딩한 JPEG는 자기 컴포넌트 수를 유지한다. 그래서 4컴포넌트 CMYK JPEG나 그레이스케일 JPEG는 괜찮고 평범한 RGB JPEG는 아니다. 아트워크는 PDF에 닿기 전에 이미징 파이프라인에서 인쇄 프로파일로 변환하라

/DefaultRGB는 왜 PDF/X-1a 파일을 구하지 못하는가?

/DefaultRGB가 X-1a에 도움이 되지 않는 이유는 규칙이 RGB가 특성화됐는지가 아니라 인쇄기가 무엇을 받는지에 관한 것이기 때문이다. PDF/A에서는 HotPDF가 RegisterDefaultColorSpace('DefaultRGB', ...)로 DeviceRGB 사용을 흡수하게 한다. Default 색 공간이 DeviceRGB를 ICC 프로파일에 매핑하는 순간 콘텐츠는 디바이스 독립적이 되고 PDF/A 일관성 검사가 이를 받아들인다. 그 워크플로를 겪어 본 개발자는 여기서 같은 처방을 시도하는 경향이 있다

EnsurePDFX1aContentColorSpaces는 그 매핑을 일부러 무시한다. 특성화된 RGB 값도 여전히 RGB이고, X-1a OutputIntent는 CMYK 인쇄 조건이므로 누군가는 여전히 변환해야 한다. 두 아카이브·인쇄 프로파일의 차이를 하나만 기억한다면 이것으로 하라. PDF/A는 색이 모호하지 않은지 묻고, PDF/X-1a는 색이 이미 잉크인지 묻는다

그리기 순서, 그리고 세지 않는 페이지 셋업

검사는 이미 기록된 콘텐츠를 대상으로 EndDoc에서 돌기 때문에, 셋업 순서는 무관하다. PDFXCompliance가 지정되기 전에 그린 RGB도, OutputIntent가 등록되기 전에 그린 RGB도 여전히 잡힌다. 공용 루틴으로 페이지를 만들고 출력 프로파일을 늦게, 가끔은 문서 절반이 존재한 뒤 고른 사용자 설정으로 결정하는 코드에서 이것이 중요하다

Pdf.BeginDoc;
// ... OutputIntent와 TrimBox는 앞서와 같이 ...
Pdf.CurrentPage.SetRGBFillColor(clRed);   // 레거시 헬퍼, DeviceRGB를 내보낸다
Pdf.CurrentPage.Rectangle(72, 72, 200, 100);
Pdf.CurrentPage.Fill;

Pdf.PDFXCompliance := 'X-1a';             // 그린 뒤에 결정됨
Pdf.EndDoc;                               // 여전히 예외: DeviceRGB가 기록됨

draw 시점 플래그에는 비슷한 검사를 직접 만들 때 알아 둘 함정이 하나 있다. 모든 새 페이지는 SetRGBColor(0)를 통한 검정 그래픽 상태로 초기화되는데, 이것이 0 0 0 rg와 0 0 0 RG 쌍을 내보낸다. 이 페이지 셋업 보일러플레이트가 DeviceRGB 플래그를 설정하게 놔두면 모든 문서가 RGB 오염으로 보인다. 이 검사에도, PDF/A 색 일관성 검사에도. HotPDF는 그 초기화 주변에서 플래그를 저장하고 복원하므로 실제로 그린 색만 세어진다

EndDoc이 예외를 던졌을 때 RGB 콘텐츠는 어떻게 찾나?

예외 메시지 맨 앞의 위치를 읽어라. 두 경로 중 어느 쪽이 문제를 발견했는지 알려 주기 때문이다. 메시지는 PDFXCompliance 'X-1a': <where> uses <what> 형태이고, 뒤이어 ISO 15930-1 참조와 CMYK나 Gray로 변환하거나 X-3, X-4로 갈아타라는 조언이 붙는다. 검증기는 첫 위반에서 평범한 Exception을 던지므로 예외 클래스가 아니라 텍스트로 대조하라

Delphi에서 HotPDF EndDoc RGB 예외의 해부: draw 시점 플래그는 문서 전체 사용을 보고하고, 리소스 순회는 정확한 페이지와 키를 지명한다
메시지 맨 앞의 위치를 읽어라 — 문서 전체 보고라면 그리기 코드로, 페이지와 리소스 이름이라면 하나의 키로 향한다
  • document content uses DeviceRGB (rg/RG operators, RGB images or annotation colours recorded during drawing)라면 draw 시점 플래그가 발화한 것이다. 플래그는 문서 전체이고 순회 전에 검사되므로 페이지 번호는 없다. 그리기 코드에서 SetRGB* 호출, Flate 압축 비트맵, RGB JPEG, TColor 어노테이션 색을 찾아라
  • page 3 image XObject /Im2 uses DeviceRGB content나 page 1 shading Pattern /P1 uses DeviceRGB content는 리소스 순회에서 나오며 정확한 리소스 키를 가리킨다
  • ColorSpace /... 위치 뒤의 uses an ICCBased RGB colour space (N=3)은 RegisterICCProfile로 sRGB 프로파일을 등록하고 SetFillColorSpace로 선택한 전형적 결과로, draw 시점 플래그가 보지 못하는 경로다
  • 중첩 위치는 사슬로 이어진다. 이를테면 page 2 Form XObject /Fm1 ColorSpace /CS1처럼, 페이지에서 RGB 공간을 지닌 리소스까지 경로를 따라갈 수 있다
try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    if Pos('X-1a', E.Message) > 0 then
      LogPrepressFailure(Pdf.FileName, E.Message);  // 여러분 고유의 로거
    raise;
  end;
end;

첫 위반만 보고되므로 RGB 발원지가 여럿인 작업의 수정은 반복이다. 고치고, 다시 빌드하고, 다음 메시지를 읽는다. 배치 작업이라면 파일마다 전체 메시지를 로그에 남기고, TColor를 여전히 받아들이는 차트 헬퍼나 로고 비트맵 같은 알려진 범인 목록을 짧게 유지하는 편이 값진다

프리플라이트에서 RGB 게이트의 자리

EnsurePDFX1aContentColorSpaces는 빌드 안에서 도는 표적형 RGB 게이트이며, 전체 PDF/X-1a 프리플라이트를 대체하는 것이 아니라 짝을 이룬다. 각 페이지의 /Resources에서 시작하는 리소스 순회와 HotPDF의 색 setter가 인라인 rg와 RG 연산자를 위해 유지하는 draw 시점 플래그를 통해 RGB 계열을 커버하고, 중첩 색 공간은 잘못된 순환에 대한 방어로 깊이 8까지 따라가며, 첫 위반이 EndDoc마다 보고된다. Lab이나 CalGray 같은 다른 색 계열은 전체 프리플라이트의 몫이므로, 완성된 파일을 빌드의 일부로 외부 검증기에 통과시켜 완전한 적합성 판정을 받아라. 적합성 출력의 더 넓은 그림은 HotPDF로 PDF/A, PDF/X, PDF/UA 출력하기 가이드에 있고, 무인 파이프라인에 검증기를 물리는 것은 Delphi에서 PDF 프리플라이트 검사 자동화하기가 다룬다

HotPDF는 PDF/X, PDF/A, PDF/UA 출력, CMYK와 스팟 컬러 API, ICC OutputIntent 지원을 갖춘 Delphi와 C++Builder용 네이티브 VCL PDF 컴포넌트다. 세부 사항, 라이선스, 체험판 내려받기는 HotPDF Delphi PDF 컴포넌트 제품 페이지에 있다