Delphi와 C++Builder용 네이티브 VCL PDF 컴포넌트인 HotPDF는 샘플 그리드가 아니라 수식으로 구성되는 세 가지 PDF 함수 유형 — ISO 32000-1 §7.10.3, §7.10.4, §7.10.5에 대응하는 Type 2 지수 보간, Type 3 스티칭, Type 4 PostScript 계산기 함수 — 를 평가한다. Type 2는 곡선을 따라 두 출력 벡터 사이를 혼합하고, Type 3는 하나의 입력 도메인에 걸쳐 여러 하위 함수를 연결하며, Type 4는 콘텐츠 스트림이 입력값으로부터 필요로 하는 거의 모든 것을 분기하고 비교하고 계산할 수 있는 제한된 PostScript 프로그램을 실행한다. 이 셋 중 무엇이든 조금이라도 잘못되면 그 실패는 결코 버그라고 스스로 알리지 않는다 — 그저 완전히 평평한 밴드가 있는 그라디언트, 순수한 검정으로 렌더링되는 별색, 또는 테스트 스위트가 우연히 시도하지 않은 입력값에서 정확히 1만큼 어긋나는 계산기 함수로 나타날 뿐이다
이 셋은 수식 대신 샘플링된 그리드를 저장하는 네 번째 유형인 Type 0과 나란히 존재하며, Type 0은 Type 0 색상 룩업 테이블에 관한 자매 글에서 별도로 다룬다. 두 계열 모두 입력을 출력에 매핑한다는 같은 문제를 해결하지만, Type 0은 한 번 계산되어 파일에 구워 넣어지는 데이터인 반면 Type 2, 3, 4는 호출할 때마다 리더가 평가하는 코드다. 이 네 가지 모두는 함수 딕셔너리의 /FunctionType 항목을 기준으로 HotPDF 렌더러 안의 단일 디스패치 지점을 공유하므로, 셰이딩이나 틴트 변환, 하프톤 스팟 함수는 색상을 요청하기 전에 자신이 넷 중 어느 것을 받았는지 알 필요가 전혀 없다
PDF Type 2 지수 함수는 어떻게 동작하는가?
PDF Type 2 함수는 단일 수식 하나를 계산한다 — y = C0 + x^N × (C1 − C0)를 성분별로 적용하며, 여기서 x는 함수의 유일한 입력값으로 수식이 실행되기 전에 /Domain을 기준으로 정규화된다(ISO 32000-1 §7.10.3). /C0과 /C1은 그 범위의 양 끝에서의 출력 벡터로 출력 성분당 숫자 하나씩이며, /N은 그 사이 곡선의 모양을 결정하는 지수다: N = 1은 대부분의 그라디언트 스톱과 듀오톤 변환 뒤에 있는 직선 램프를 만들고, N이 1보다 크면 곡선이 C0 쪽으로 당겨지며, N이 0과 1 사이면 곡선이 C1 쪽으로 밀린다. RegisterExponentialFunction은 이 다섯 인자로부터 그 딕셔너리를 만들어 셰이딩, 하프톤 스팟 함수, 또는 스펙이 /Function 키를 받아들이는 다른 어느 곳에든 꽂을 수 있는 함수 객체를 돌려준다
C0과 C1 사이의 성분 개수 관계는 두 번 중요하다: Type 2 함수를 작성할 때 한 번, 그리고 HotPDF가 자신이 만들지 않은 함수를 렌더링해야 할 때 또 한 번이다. 작성하는 쪽에서는 RegisterExponentialFunction이 C0과 C1을 서로 비교해 불일치하면 예외를 일으키므로, BeginDoc에 도달하는 호출은 이미 자기 일관적인 함수 객체를 갖는다. 하지만 렌더링하는 쪽에서는 평가기가 소스 파일이 실제로 선언한 /C0과 /C1 배열이 무엇이든 그것을 신뢰해야 한다 — 예를 들어 미리보기용으로 열린 인쇄소 파일이나 사용자에게 다시 표시되는 서명된 문서 같은 것 — 그리고 2.376.0 이전 버전은 이 배열들을 CMYK 경우인 4개 성분 크기의 버퍼로 읽어 들였다. 1개나 3개 요소짜리 /C0과 /C1을 가진 DeviceGray나 DeviceRGB 지수 틴트는 이 읽기가 조용히 실패해 두 배열 모두 0으로 남았고, 그래서 틴트는 의도한 색 대신 평평한 검정으로 칠해졌다. 버전 2.376.0은 고정 버퍼 대신 함수가 실제로 선언한 출력 개수에 맞춰 리더 크기를 조정했다 — 이는 기존 테스트 스위트가 항상 CMYK로만 실행되어 4개를 4개 안에 넣는 것이 늘 들어맞았기 때문에, 정확히 CMYK가 아닌 테스트 케이스만이 드러낼 수 있는 종류의 버그였다
var
EaseIn: THPDFDictionaryObject;
begin
// Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
EaseIn := Pdf.RegisterExponentialFunction(
[0,1], // Domain: single input, clamped to [0,1]
[0, 0, 0], // C0: output at x = 0
[0.8, 0, 0], // C1: output at x = 1
3, // N: exponent, 1 = linear, > 1 eases toward C0
[]); // Range omitted: defaults to a [0,1] clamp per output
end;
Type 3 스티칭: Bounds 배열로 하위 함수 연결하기
PDF Type 3 함수는 k개의 하위 함수를 하나의 입력 /Domain에 걸친 하나의 구간별 매핑으로 이어 붙이며, 이를 가능하게 하는 두 배열이 /Bounds와 /Encode다(ISO 32000-1 §7.10.4). /Bounds는 /Domain을 k개의 연속 구간으로 잘라내는 k−1개의 내부 분할점을 담는다. 평가기는 입력값을 넘어서는 상한을 가진 첫 번째 구간, 또는 입력값이 마지막 경계에 도달하면 마지막 구간을 골라 그 구간의 하위 함수로 넘긴다. 그다음 /Encode는 그 구간 안에서의 입력 위치를 선택된 하위 함수 자체가 기대하는 입력 범위 — 하위 함수가 지수 구간 하나 더인 경우 보통 [0, 1] — 로 다시 매핑한 뒤, 그 하위 함수 자체의 /Domain과 /Range 안으로 한 단계 더 깊이 들어가 평가를 이어간다
HotPDF의 스티칭 평가기는 예전에는 정확히 두 개의 하위 함수만 처리했고, /Bounds 리더는 8개 요소 전체 배열을 요구했다. 그래서 2구간 그라디언트가 실제로 필요로 하는 단 하나의 분할점 — /Bounds 안의 숫자 하나 — 은 항상 파싱에 실패했고 함수는 아무것도 반환하지 않았다. /Encode는 아예 적용되지 않았다. 버전 2.376.0은 이 선택 로직을 스펙이 설명하는 일반적인 k-하위함수 검색으로 다시 작성하고 /Bounds를 실제 선언된 길이에 맞춰 읽기 시작했으므로, 그만큼의 지수 구간으로 이어 붙인 3단, 4단, 5단 그라디언트도 이제는 2구간 그라디언트가 늘 그렇다고 주장해 온 방식대로 정확히 해석된다. 아래 예제는 검정에서 빨강을 거쳐 흰색으로 가는 2구간 램프를 만드는데, 이는 하나의 지수 곡선이 디자인이 요구하는 모든 색상 스톱을 감당하지 못할 때 축 방향 또는 방사형 셰이딩이 손을 뻗는 형태다
var
ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
// Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
ToRed := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0], [0.8, 0, 0], 1, []);
ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1], 1, []);
Ramp := Pdf.RegisterStitchingFunction(
[0,1], // Domain: the stitched function's own input range
[ToRed, ToWhite], // Functions: k = 2 sub-functions
[0.5], // Bounds: k - 1 = 1 split point
[0,1, 0,1], // Encode: 2 numbers per sub-function
[]); // Range omitted: inherited from each sub-function
end;
Type 4 PostScript 계산기 함수가 Type 2와 3이 할 수 없는 무엇을 할 수 있는가?
PDF Type 4 함수는 의도적으로 제한되어 있긴 하지만 진짜 프로그램을 실행한다: 입력값을 피연산자 스택에 밀어 넣고, 산술·비교·스택 조작·불리언 연산자와 if/ifelse 조건문을 실행한 뒤, 완료되면 출력값을 스택에 남기는 PostScript 계산기다(ISO 32000-1 §7.10.5, Table 42). 루프 구문도, 이름 붙은 변수 저장소도 없이 오직 스택만 있어 규격을 따르는 프로그램은 추론하기 쉽지만, 그 제한된 연산자 집합 안에서도 Type 4는 Type 2와 Type 3가 표현할 수 없는 것 — DeviceN 분판을 위한 진짜 다중 잉크 혼합 공식이나 조건부 임계값을 가진 하프톤 스팟 함수 같은 것 — 을 표현할 수 있다. HotPDF의 평가기인 HPDFEvalPostScriptCalculator는 숫자, 연산자, { } 프로시저 블록으로 프로그램을 한 번 토큰화한 뒤, ISO 32000-1 §7.10.5가 요구하는 깊이인 100개 항목짜리 피연산자 스택을 순회하며, 병적이거나 손으로 작성된 프로그램에 대한 방어적 안전망으로 평가된 연산자 50,000개라는 강한 상한선을 둔다
roll 연산자: 방향이 거꾸로 되기 쉽다
roll은 처음 시도할 때 방향이 거꾸로 나오기 가장 쉬운 연산자인데, 그 인자 순서와 회전 방향 모두 영어로 서술하는 방식과 반대로 동작하기 때문이다. n j roll은 개수 n과 회전량 j를 팝한 뒤, 상위 n개의 스택 항목을 j 위치만큼 순환 이동시키며, 한쪽 끝에서 밀려난 항목은 다른 쪽 끝으로 돌아온다. 스펙에 그대로 나오는 대표 예시는 a b c 3 1 roll이 c a b를 만든다는 것이다 — 최상위 항목이 그룹의 맨 아래로 이동하는 것이 아니라, 그 반대로 최상위 항목이 아래로 가고 나머지 모든 항목이 한 자리씩 위로 밀려 자리를 만든다. HotPDF의 평가기는 스택 항목 i의 새 위치를 (i + j) mod n으로 계산하며 이는 그 예시와 정확히 일치하지만, 회전 방향이 뒤집힌 채로도 똑같이 쉽게 쓸 수 있는 두 줄짜리 루프다. 그리고 방향이 뒤집힌 roll도 그럴듯해 보이는 색을 만들어낸다 — 다만 그것이 파일 작성자가 요청한 색이 아닐 뿐이다
const
Prog = '{ 3 1 roll }'; // (a b c) -> (c a b): the third input moves to the front
var
Reorder: THPDFStreamObject;
begin
// Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
Reorder := Pdf.RegisterPostScriptFunction(
[0,1, 0,1, 0,1], // Domain: 2 numbers per input
[0,1, 0,1, 0,1], // Range: 2 numbers per output (required for Type 4)
Prog);
end;
round는 Delphi의 Round가 아니다: 반올림 대 은행원 반올림
PostScript의 round 연산자는 .5의 애매한 경우를 항상 더 큰 정수 쪽으로 반올림하는데, Delphi의 내장 Round 함수는 그렇지 않다: 이 함수는 반복 반올림이 편향을 누적하지 않도록 .5의 애매한 경우가 어느 쪽으로 떨어질지 번갈아 정하는 은행원 반올림 규칙인 half-to-even 방식으로 반올림한다. 이 둘은 거의 모든 곳에서 일치하지만 바로 여기서 중요한 경계에서만 정확히 어긋난다 — Delphi의 Round(0.5)는 0을, Round(2.5)는 2를 반환하는 반면, PDF 스펙의 round는 같은 입력에 대해 1과 3을 원한다 — 그래서 이 불일치는 가벼운 테스트로는 드러나지 않고, 계산기 프로그램의 중간 연산 결과가 정확히 반정수에 놓이는 곳마다 일관되게 1만큼 어긋나는 형태로 재현된다. ISO 32000-1 §7.10.5 Table 42는 round가 소수 .5를 더 큰 정수 쪽으로 미룬다고 명시하므로, HotPDF는 Delphi의 Round를 호출하는 대신 이 연산자를 Floor(x + 0.5)로 구현하며, Type 4 프로그램의 연산을 손으로 재구현하거나 검산하는 코드는 모두 동일한 대체가 필요하다
function PostScriptRound(const X: Double): Double;
begin
// ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
// integer. Delphi's Round() is banker's rounding and disagrees here:
// Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
Result := Floor(X + 0.5);
end;
등록 시점의 검증이 잘못된 계산기 프로그램을 일찍 잡아낸다
잘못된 형식의 Type 4 프로그램은 작성 시점에 잡아내면 저렴하지만 다른 어느 곳에서 잡아내든 비싸므로, RegisterPostScriptFunction은 소스 텍스트를 그저 저장만 하지 않는다: 함수 객체가 문서에 기록되기 전에 선언된 /Domain의 중간점에서 프로그램을 한 번 시험 평가한다. 짝이 맞지 않는 { } 블록, 인식되지 않는 연산자, 스택 언더플로, /Range와 맞지 않는 출력 개수 모두 이 시험 실행에서 실패해 즉시 예외를 일으키며, 이때 호출 스택은 이미 출고된 파일에서 QA 중 발견된 렌더링 결함이 아니라 RegisterPostScriptFunction 호출 자체를 가리킨다. 이 중간점 시험은 프로그램이 전체 /Domain에 걸쳐 올바르다는 것을 증명하지는 않는다 — 입력 범위의 한쪽 가장자리 근처에서만 오작동하는 조건 분기는 단일 샘플 지점을 여전히 통과할 수 있다 — 하지만 한쪽 구석에서만 잘못된 것이 아니라 구조적으로 완전히 망가진 프로그램 전체 부류는 걸러낸다
그라디언트와 별색이 이 함수들을 실제로 어디에 쓰는가
Type 2, 3, 4는 실제 PDF에서 단독으로 나타나는 경우가 드물다. 이들은 스펙이 /Function 키를 받아들이는 곳이라면 어디든 나타나며, 가장 흔한 소비처 둘은 셰이딩과 별색 틴트 변환이다. 축 방향 또는 방사형 그라디언트의 sh 연산자(ISO 32000-1 §8.7.4.5)는 그라디언트 축을 따라 위치마다 /Function을 한 번씩 평가하는데, 이는 정확히 Type 3 스티칭이 존재하는 이유인 다중 스톱 상황이다. Separation이나 DeviceN 색상 공간의 틴트 변환은 이 세 유형이 자주 자리 잡는 또 다른 곳이며, 여기서 Type 4가 진가를 발휘한다: 단일 별색 잉크는 보통 Type 2나 Type 0 곡선으로 환원되지만, 진짜 트래핑과 중복 인쇄 동작을 가진 여러 잉크의 DeviceN 블렌드는 흔히 PostScript 계산기만이 표현할 수 있는 조건부 로직을 필요로 하며, 이는 Separation과 DeviceN 별색 렌더링에 관한 글에서 다루는 경우다. RegisterSeparationFunc는 작성 쪽의 짝이 되는 호출로, 색료 이름, 대체 색상 공간, Register*Function 계열이 반환하는 어떤 객체든 받아 그 틴트 변환을 페이지의 나머지 부분이 scn/SCN으로 선택할 수 있는 Separation 색상 공간 리소스에 연결한다
Type 0의 샘플링된 그리드와 여기 다룬 수식 기반의 세 유형을 합치면 PDF가 선언할 수 있는 모든 /Function을 아우르며, 올바른 유형을 고르는 것은 대체로 이미 무엇을 가지고 있는지의 문제다: 다른 곳에서 계산된 룩업 테이블은 Type 0이 되고, 두 끝점 사이의 블렌드는 Type 2가 되며, 하나의 도메인에 걸쳐 이어 붙인 여러 블렌드는 Type 3가 되고, 진짜 조건부 로직이 있는 것은 무엇이든 Type 4가 된다. RegisterExponentialFunction, RegisterStitchingFunction, RegisterPostScriptFunction은 나머지 ISO 32000-1 함수·셰이딩 API와 함께 Delphi와 C++Builder용 HotPDF 컴포넌트 표준판에 포함되어 있다