HotPDF는 HPDFResolveColorSpace를 통해 색상 공간을 해석하고, HPDFEvalTintTransform으로 틴트 변환(tint-transform) 함수를 평가하며, 대체 색상 공간을 통해 결과를 스크린용 RGB로 변환함으로써 로드된 PDF 페이지에서 Separation 및 DeviceN 별색(spot colors)을 렌더링합니다. v2.375.0 이후 이 파이프라인은 Type 4 PostScript 계산기를 포함한 4가지 PDF 함수 유형을 모두 평가하므로, Pantone 잉크가 지정된 인쇄용 파일이 자리 표시자 대신 실제 색상을 표시합니다. 본 기사에서는 이 파이프라인의 작동 방식과 개발 과정에서 겪었던 시행착오들을 공유합니다
오류가 발생하는 케이스는 항상 동일합니다. 고객이 인쇄소로부터 PDF를 받습니다: 헤드라인은 이름이 지정된 별색 잉크로 설정되어 있고, 포장 아트는 두 개의 잉크가 혼합된 DeviceN 블렌드를 사용하며, 본문 텍스트는 일반 검은색입니다. Acrobat에서는 완벽하게 보습니다. 하지만 여러분의 Delphi 애플리케이션에서는 헤드라인이 검은색으로 렌더링되거나, 심지어 전혀 렌더링되지 않아 고객이 파일이 아닌 여러분의 소프트웨어에 버그 리포트를 접수하게 됩니다. 파일 자체는 정상이며, 단지 렌더러가 파일이 사용하는 색상 공간을 해석하지 못할 뿐입니다
PDF 뷰어에서 별색이 검은색으로 렌더링되는 이유는 무엇입니까?
렌더러가 디바이스 색상 연산자(rg, g, k)만 구현하고 범용(generic) 연산자를 무시할 때 별색은 검은색으로 렌더링되거나 사라집니다. ISO 32000-1 §8.6은 세 가지 색상 공간 그룹을 정의합니다: 디바이스 공간(DeviceGray, DeviceRGB, DeviceCMYK), CIE 기반 공간(CalGray, CalRGB, Lab, ICCBased), 그리고 특수 공간(Indexed, Separation, DeviceN, Pattern)입니다. 디바이스 그룹 외부의 모든 것은 범용 연산자로 선택됩니다: cs 및 CS는 페이지 리소스 사전에서 이름으로 색상 공간을 선택하고, sc, SC, scn, SCN은 컴포넌트 값을 제공합니다. 이러한 연산자를 무시하는 렌더러는 마지막으로 설정된 색상을 고수하므로, 별색 헤드라인으로 시작하는 페이지에서는 초기 DeviceGray 블랙이 그대로 적용됩니다
HotPDF는 v2.333.0에서 페이지 렌더러에 범용 연산자 세트를 추가하고 통합된 해석 경로를 도입했습니다: 단순 이름, 인라인 배열 또는 간접 참조 여부와 관계없이 모든 /ColorSpace 리소스 항목은 하나의 THPDFColorSpace 레코드로 파싱되며, 모든 채우기 또는 선 색상 요청은 단일 HPDFResolveColor 호출을 거치게 됩니다. 패밀리 열거형을 통해 지원 범위를 한눈에 볼 수 있습니다
type
THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
csfIndexed, csfCalGray, csfCalRGB, csfLab,
csfICCBased, csfSeparation, csfDeviceN,
csfUnsupported);
function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;
function HPDFResolveColor(const CS: THPDFColorSpace;
const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;
한 가지 유용한 설계 방식이 있습니다: csfUnsupported는 오류가 아닌 퍼스트 클래스 패밀리로 처리됩니다. 렌더러가 해석할 수 없는 색상 공간은 페이지 렌더링을 중단하는 대신 정의된 대체(fallback) 공간으로 백다운되며, 이는 주류 뷰어의 동작 방식과 일치하여 단 하나의 특이한 채우기 요소로 인해 문서 전체가 빈 페이지로 나오는 현상을 방지합니다
틴트 변환을 통해 하나의 잉크 값을 실제 색상으로 변환하는 방법
Separation 공간은 잉크 이름, 대체 색상 공간 및 틴트 변환 함수의 세 가지 정보를 전달합니다. [/Separation /PANTONE485 /DeviceCMYK f] 배열은 다음을 의미합니다: 콘텐츠 스트림이 0.8 scn을 작성할 때 틴트 값 0.8을 함수 f에 전달하고 그 결과물인 CMYK 4개 성분을 칠합니다. DeviceN은 이를 N개 입력 함수가 있는 N개 잉크로 일반화합니다. 잉크 이름 자체는 스크린 상에서 권장 사양일 뿐이며, 틴트 변환이 렌더링 의미 구조의 핵심이므로 색상 공간만 파싱하고 함수를 무시하는 렌더러는 제 역할을 수행하지 못하는 셈입니다
HPDFEvalTintTransform은 이 단계를 구동하는 함수 엔진입니다. v2.334.0에 도입되어 Type 2 지수 함수(Domain 및 Range 클리핑이 적용된 C0 + x^N * (C1 - C0)), Type 3 스티칭 함수(Encode 재매핑이 적용된 경계 선택적 하위 함수 재귀), 8/16/32비트 샘플을 사용하는 Type 0 샘플링 함수를 평가합니다. Type 0은 Type 0 색상 LUT 빌드 기사에서 작성 측면에서 다루었던 것과 동일한 룩업 테이블 구조이며, 렌더링 측에서는 디코딩된 샘플 바이트로부터 성분 값으로 역방향 탐색을 진행합니다
Type 4 PostScript 계산기 함수는 가장 늦게 구현된 영역입니다. v2.375.0 전까지는 중립적인 자리 표시자로 대체 처리되었으나, v2.375.0 이후 HPDFEvalPostScriptCalculator가 제한된 피연산자 스택 상에서 연산, 비교, 부울 및 비트 연산자, roll을 포함한 스택 조작, if/ifelse 조건문을 포함하는 완전한 ISO 32000-1 §7.10.5 연산자 세트를 실행합니다. 예외적인 세부 동작들은 생각보다 엄격합니다. PostScript의 round는 반올림 시 더 큰 값 방향으로 처리되므로 Delphi의 은행원 반올림 방식인 Round를 사용할 수 없습니다; 삼각 함수 연산자는 도(degrees) 단위로 작동하며 atan은 [0, 360) 범위의 값을 반환합니다; 또한 exp는 자연 지수가 아닌 두 피연산자 거듭제곱입니다. 동일한 평가 엔진이 함수 기반 그라데이션 채우기도 구동하므로, 동일한 릴리스에서 축 및 방사형 셰이딩 렌더링이 계산기 구동 램프를 채택하게 되었습니다
// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
const Inputs: THPDFColorComps; InputCount: Integer;
out AltComps: THPDFColorComps): Boolean;
// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
const Inputs: THPDFColorComps; InputCount: Integer;
var Outputs: THPDFColorComps; OutCount: Integer): Boolean;
정밀도에 관한 명확한 설명: 배정밀도(double precision)로 평가되는 소프트웨어 계산기는 RIP와 비트 단위로 일치하지 않을 수 있으며, Range 경계에서의 클리핑은 다른 구현과 최하위 비트 수준에서 미세한 차이를 보일 수 있습니다. 화면 표시 및 회귀 렌더링 테스트에서는 차이가 미미하지만, 색상 관리 증명(proofing) 시스템을 개발 중이라면 틴트 변환은 첫 번째 단계일 뿐이며 다운스트림 단계에서 실제 CMM 엔진을 결합해야 합니다
ICC 엔진이 배제된 CalGray, CalRGB, Lab 및 ICCBased 처리
CIE 기반 패밀리는 동일한 리졸버의 다른 분기를 거칩니다. HotPDF는 역전달 함수에서 6/29 임계점 큐브를 포함하여 표준 Lab-XYZ-sRGB 체인을 거쳐 Lab 값을 변환하며, 채널별 감마 및 3x3 선형 행렬이 결합된 CalRGB와 단일 감마를 지닌 CalGray를 처리합니다. 성능 관련 세부 사항은 화이트 포인트 처리입니다: XYZ-sRGB 변환 행렬은 색상 공간에 선언된 화이트 포인트로 브래드포드(Bradford) 적응 처리되어 해석된 THPDFColorSpace 레코드에 캐싱되므로, 선언된 광원이 아무리 독특하더라도 픽셀당 작업은 단일 3x3 행렬 곱셈으로 고정되어 유지됩니다
ICCBased 공간은 고도로 실용적으로 처리됩니다. PDF 사양은 색상 관리 엔진이 없는 뷰어도 정상적으로 그릴 수 있도록 ICCBased 스트림에 /Alternate 공간을 선언하거나 컴포넌트 수 /N을 통해 이를 보완하도록 요구합니다. HotPDF는 대체 색상 공간을 통해 ICCBased를 해석하며, 항목이 없는 경우 /N이 1, 3 또는 4인 것에 근거해 DeviceGray, DeviceRGB 또는 DeviceCMYK로 유추하고 프로필 바이트를 직접 파싱하지는 않습니다. 이는 lcms 종속성 및 프로필 조회 비용이 전혀 없음을 의미하는 동시에, 색상 표현 정밀도 측면에서는 손실이 발생할 수 있습니다: 즉, 프로필 정보가 대체 공간과 크게 상이한 ICCBased 공간은 대체 공간 기반의 드로잉 결과를 나타냅니다. 화면 표시 및 섬네일 생성의 경우 이는 가벼운 뷰어가 채택하는 전형적인 트레이드오프 방식이며, 제품 사양 문서에 명확히 명시해 둘 부분입니다
모든 명명된 색상 공간 조회를 실패하게 만들었던 버그
v2.333.0 연산자 구성은 42번의 릴리스 동안 숨겨져 있던 결함을 포함한 채 배포되었습니다: cs 및 CS 핸들러가 맨 앞 슬래시가 포함된 피연산자(/CS0)를 슬래시 없이 저장된 리소스 사전 키(CS0)와 비교하여 조회했던 것입니다. 이에 따라 명명된 색상 공간 조회가 항상 실패하였고, 코드는 자동으로 기본 DeviceGray로 대체 작동했습니다. 발생한 오류 현상은 알아차리기 매우 어려운 형태였습니다: Separation 채우기 값 1 scn이 DeviceGray 1.0(흰색)이 되었고, 흰색 페이지 위에 흰색 잉크를 칠하는 오류는 스크린샷으로 버그가 신고될 가능성이 낮기 때문입니다. v2.375.0에서의 수정 방식은 모든 피연산자-리소스 조회 단계에 공통 이름 정규화(name-normalization) 도우미를 적용하는 것이었습니다
동일한 조사 과정에서 연관된 두 가지 결함이 더 발견되었습니다. 첫째, 배열 값 객체에 대한 간접 참조가 해석되지 않은 채 반환되었습니다: 렌더러의 문서 접근 모듈이 스트림 및 사전에 대해서만 전용 리졸버를 제공하여, 독립형 [/Separation ...] 배열을 가리키는 /CS0 5 0 R이 원시 참조 형태로 반환되어 색상 공간이 지원되지 않는 것으로 해석되었습니다. 둘째, HPDFReadNumericArray가 엄격한 길이 규칙을 고수하여 PDF 배열 크기가 전달된 버퍼보다 최소한 같거나 길 것을 요구했습니다. 이로 인해 Type 2 함수의 /C0 및 /C1을 4성분 버퍼로 읽어 들일 때 1성분 및 3성분 대체 공간의 읽기가 실패하여 두 배열이 모두 0으로 비워졌고, v2.376.0에서 유연한 HPDFReadNumericArrayUpTo 판독기를 도입하기 전까지 CMYK가 아닌 모든 지수 기반 틴트 색상이 검은색으로 렌더링되는 결과가 발생했습니다. 여기서 얻은 교훈은 명확합니다: 가변 길이 수치 배열을 포함하는 것으로 명세된 PDF 키는 버퍼 크기를 엄격하게 일치시키는 대신 존재하는 데이터만 채우는 방식으로 읽어야 합니다. 고정 크기 버퍼와 엄격한 검증 방식을 쓰면 유효한 파일 데이터가 0으로 지워지기 때문입니다
실제 작동하는 별색 렌더링 테스트를 구축하는 방법
한 가지 의문점은 왜 이 결함들이 존재함에도 기존 테스트 제품군이 성공(green) 판정을 내렸는가 하는 점입니다. 기존 회귀 테스트인 SeparationRendersDistinguishable은 단지 렌더링된 비트맵이 완전히 검은색이 아닌지만 단정(assert)했습니다. 모든 별색을 DeviceGray로 떨어뜨린 파이프라인은 회색과 흰색 출력물을 생성하였고, 이는 완전히 검은색이 아니므로 전체 기능이 작동하지 않았음에도 테스트 통과 판정이 내려진 것입니다. "공백이 아님", "완전히 검은색이 아님", "다이제스트 값이 0이 아님"과 같은 약한 검증 방식은 거의 모든 오류 상황에서도 일부 픽셀이 생성되기 때문에 올바른 렌더러와 오류 상태의 렌더러를 구분하지 못합니다
이러한 오류를 잡아내는 효과적인 검증 방식은 기대되는 색상을 고정하는 것입니다: Separation 잉크가 알려진 색상으로 해석되는 직접 빌드한 최소 크기의 PDF를 렌더링한 후, 해당 색상 성분이 우세한 픽셀의 개수를 집계하는 것입니다. 렌더링 비트맵 검사는 페이지-비트맵 렌더링 가이드에서 설명한 것과 동일한 RenderLoadedPageToBitmap 진입점을 사용합니다
var
Pdf: THotPDF;
Bmp: TBitmap;
X, Y, RedHits: Integer;
Px: TColor;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
begin
Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
try
RedHits := 0;
for Y := 0 to Bmp.Height - 1 do
for X := 0 to Bmp.Width - 1 do
begin
Px := Bmp.Canvas.Pixels[X, Y];
if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
(GetBValue(Px) < 100) then
Inc(RedHits);
end;
// Lock the expected ink: demand a real area of red-dominant
// pixels, never settle for "not all black".
Assert(RedHits > 500);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
테스트 검증만큼 테스트 픽스처(fixture)도 중요합니다. 단 하나의 Separation 채우기만 포함하는 수백 바이트 크기의 직접 빌드한 PDF는 예상 출력물에 대한 모호성을 없애 줍니다; 복잡한 실제 파일은 많은 코드를 실행하지만 어떤 지점에서 결함이 발생했는지 추적하기 어렵습니다. 우리는 이제 기대 색상 픽셀 카운팅을 모든 렌더링 스모크 테스트의 최저 기준으로 삼고 있습니다. 이 방식만이 결함들을 명확히 드러나게 만들었기 때문입니다
별색 및 CIE 색상 공간 렌더링은 위에 소개된 함수 엔진, 셰이딩 렌더링 및 비트맵 내보내기 경로와 함께 Delphi 및 C++Builder용 HotPDF Component의 로드된 문서 처리 파이프라인의 일부로 제공됩니다