HotPDF는 Normal이 아닌 블렌드 모드로 그려진 PDF 투명도 그룹을, 그룹이 자기 블렌드를 직접 계산하게 하는 방식으로 렌더링합니다: Cb(1 − α) + α·B(Cb, Cs). 추적 중인 그룹 알파를 쓰거나 검은 패스와 흰 패스에서 복원합니다. v2.770.0부터는 THotPDF.RenderColorimetricLuminosityMasks 속성이 러미노시티 소프트 마스크가 색을 흔한 뷰어처럼 가중 RGB로 읽을지, ISO 32000-1이 기술하듯 CIE Y로 읽을지도 선택합니다
두 변경은 같은 부류의 파일에서 나왔습니다: 디자인 도구에서 내보낸 페이지로, 제품 사진이 부드러운 드롭 섀도 위에 놓여 있는 페이지입니다. 섀도는 /BM /Multiply를 단 투명도 그룹으로 그려진 Form XObject이고, 페이드아웃은 러미노시티 소프트 마스크입니다. 다른 뷰어에서는 섀도가 밝은 중성 회색입니다. 구버전 HotPDF 빌드에서는 거의 검게 나오고 배경색으로 기울었습니다. 별개의 버그 둘이 겹쳐 있었던 것인데, "불투명 오브젝트 우선" 설계에서 자라난 렌더러라면 둘 다 부딪히게 되니 뜯어 볼 가치가 있습니다
Multiply 드롭 섀도는 왜 너무 어둡게 렌더링될까요?
Multiply 섀도가 너무 어둡게 나오는 경우는 렌더러가 그룹 고유의 색과 알파가 아니라 그룹의 합성 결과를 블렌드할 때입니다. 그 결과의 부분 투명 픽셀에는 이미 배경이 들어 있으므로, 그것을 배경과 블렌드하면 배경에 배경을 곱하는 셈이 됩니다. 불투명도를 곁들인 Normal 블렌딩은 선형이라 이런 실수를 해도 눈에 띄지 않고 살아남습니다; Multiply, Screen, Darken 등 비선형 모드는 그렇지 않습니다
HotPDF 렌더링 파이프라인은 원래 간단하고 빠른 모델로 그려진 오브젝트별로 블렌드 모드를 처리했습니다. 오브젝트를 그리기 전에 렌더러는 영향받는 픽셀을 스냅샷하고; 그린 후에는 "이전"과 "이후" 비트맵을 비교해 "이후" 색을 불투명 소스 Cs로 취급하고, B(Cb, Cs)를 계산해 현재 불투명도와 합성합니다. 채워진 패스, 글리프, 불투명 이미지라면 정확히 맞습니다: 소스는 형태 안에서 실제로 불투명하고, 픽셀이 바뀐 곳도 그 형태이니까요
투명도 그룹은 그 가정을 깨뜨립니다. ISO 32000-1 §11.4는 그룹을, 자기 색과 자기 알파로 먼저 자기 결과에 합성된 다음에야 단일 오브젝트로 배경과 블렌드되는 것으로 정의합니다. 섀도가 30퍼센트 불투명한 지점에서 "이후" 비트맵은 대략 0.3의 섀도 색에 0.7의 그 아래 페이지를 담고 있습니다. 그것을 불투명 소스로 Multiply에 밀어 넣으면 공식은 배경 × (이미 배경을 담고 있는 값)이 됩니다: 섀도는 어두워지고 배경의 색조를 띱니다. 아무것도 크래시하지 않고 진단도 울지 않으며, 페이지가 그저 잘못 보입니다. 렌더러가 실패하는 방식 중 최악이죠
HotPDF가 투명도 그룹의 자체 블렌딩을 허용하는 방식
v2.769.3부터 HotPDF는 Normal이 아닌 블렌드 모드로 그려진 그룹의 블렌드를 범용 오브젝트별 경로에서 그룹 렌더러 자체로 옮깁니다. 그룹은 자기 색, 자기 알파, Do 연산자를 위해 찍은 배경 스냅샷에서 Cb(1 − α) + α·B(Cb, Cs)를 직접 계산하고, 결과에 이미 블렌드되었음을 표시해서 감싸는 Do가 두 번 블렌드하는 대신 현재 불투명도와 합성만 하게 합니다. 일반 오브젝트는 빠른 이전-이후 모델을 유지하고, Normal 블렌딩 하의 그룹은 선형 경우가 한 번도 틀린 적이 없으므로 영향받지 않습니다
어려운 부분은 알파입니다. 아이솔레이티드 그룹(/I true)은 완전히 투명한 배경에서 시작하므로, 렌더러는 이미 별도의 알파 평면을 추적하고 있어 α를 직접 읽을 수 있습니다. 논아이솔레이티드 그룹은 페이지에 이미 있는 것 위에 합성되며 자체 알파 평면이 없습니다. HotPDF는 그룹을 두 번 그려서 복원합니다. 한 번은 검은 위에, 한 번은 흰 위에:
- 검은 배경에서는 각 픽셀이
α·Cs로 끝납니다 - 흰 배경에서는 같은 픽셀이
α·Cs + (1 − α)로 끝납니다 - 빼면
α = 1 − (white − black)이 나오고, 이어서Cs = black / α입니다
이것이 통하는 이유는 렌더러가 수행하는 모든 Normal 합성, 소프트 마스크와 앤티앨리어싱된 가장자리를 가진 이미지를 위한 GDI AlphaBlend까지도 배경에 대해 선형이기 때문입니다. 다만 이 트릭에는 전제가 있습니다: 그룹 콘텐츠 자체가 정상적으로 블렌드되어야 합니다. 그룹 자신의 ExtGState 리소스가 Normal이 아닌 /BM을 설정하면 두 패스는 더 이상 깔끔한 선형항으로 갈라지지 않으므로, HotPDF는 그룹을 분할하지 않고 이전 동작으로 폴백합니다. 이 검사는 중첩된 폼이 아니라 그룹 자신의 리소스만 뒤집니다. 비용도 실재합니다: 그룹을 두 번 그리므로 그런 페이지는 다소 느리게 렌더링됩니다. 그룹 안에서 만들어진 소프트 마스크는 두 패스가 끝날 때까지 유지되므로, 두 번째 패스는 다시 만드는 대신 마스크 캐시를 맞힙니다
// 채널별 알파 복원의 개념도 (값 0..1).
// Black = 검은 배경 위에 그린 그룹, White = 흰 배경 위에 그린 그룹.
procedure RecoverGroupPixel(Black, White: Double;
out Alpha, Source: Double);
begin
Alpha := 1.0 - (White - Black);
if Alpha < 0.0 then Alpha := 0.0;
if Alpha > 1.0 then Alpha := 1.0;
if Alpha > 0.0 then
Source := Black / Alpha // un-premultiply
else
Source := 0.0; // 완전 투명: 색은 무관
end;
// Multiply 블렌드, 이어서 그룹 합성 Cb(1 - a) + a * B(Cb, Cs)
function CompositeMultiply(Backdrop, Source, Alpha: Double): Double;
begin
Result := Backdrop * (1.0 - Alpha) + Alpha * (Backdrop * Source);
end;
위 코드는 수식의 개념 설명이지 HotPDF API가 아닙니다; 렌더러는 THotPDF.RenderLoadedPageToBitmap 안에서 채널별, 픽셀별로 같은 일을 합니다. 실전에서는 두 8비트 패스에서 복원한 알파가 0에 가까워질수록 정밀도를 잃어, 아주 옅은 가장자리에는 양자화 노이즈가 좀 남습니다. 그래서 나눗셈은 알파가 분명히 0보다 위인 곳에서만 의미가 있습니다
그룹 안에서는 블렌드 모드가 Normal로 초기화됩니다
블렌드 모드는 상수 알파나 소프트 마스크처럼 투명도 그룹 전체에 적용되지 그 안의 각 오브젝트에 적용되지 않으므로, 그룹 콘텐츠는 Normal 블렌딩에서 시작해야 합니다(ISO 32000-1 §11.6.6). HotPDF는 그룹 진입 시 불투명도와 소프트 마스크는 리셋했지만 블렌드 모드는 그대로 뒀었습니다. 아이솔레이티드 그룹 안에서의 결과는 극적이었습니다: 콘텐츠가 그룹의 텅 빈 초기 배경, 즉 검은색에 스스로를 곱해 버렸고, 그룹 전체가 검게 그려졌습니다
소프트 마스크가 만들어질 때도 같은 리셋이 적용됩니다. 하나의 ExtGState가 /BM /Multiply와 /SMask를 동시에 설정하면, 마스크 그룹은 Normal 블렌딩으로 그려져야 합니다; 그렇지 않으면 흰 마스크 그룹이 검게 변해서 마스크가 드러내려던 것을 정확히 가려 버립니다. 두 리셋 모두 v2.769.3의 일부이고, 단순한 문서에서는 둘 다 보이지 않습니다. 그렇게 오래 살아남았던 이유죠
중간 회색에서 러미노시티 소프트 마스크 값은 얼마가 되어야 할까요?
ISO 32000-1에 따르면 그룹이 CIE 기반 블렌딩 공간을 쓰는 러미노시티 소프트 마스크는 CIE 1931 Y를 취해야 하고, 0.5의 sRGB 중간 회색에 대한 Y는 Y가 선형 광이므로 겨우 0.21 정도입니다. 흔한 PDF 뷰어는 대신 그룹 색을 RGB로 해석하고 인코딩된 값에 대략 0.30 / 0.59 / 0.11의 가중치를 주는데, 같은 회색에서 약 0.5가 나옵니다. v2.770.0부터 HotPDF는 뷰어 호환 가중치를 기본으로 하고, 사양대로 읽기를 원할 때 THotPDF.RenderColorimetricLuminosityMasks를 제공합니다
간극은 학문적이지 않습니다. 디자인 도구 익스포트의 페이드는 보통 ICCBased 또는 캘리브레이티드 그룹의 그라디언트이고, 중간 톤이야말로 선형 Y와 감마 인코딩된 회색이 가장 크게 갈라지는 곳입니다. CIE Y로는 중간 회색 마스크가 절반이 아니라 섀도의 약 5분의 1만 통과시키므로, 드롭 섀도와 비네트는 고객이 이 파일을 여는 다른 모든 뷰어보다 눈에 띄게 밝게 나옵니다. 표준 문자 그대로 맞고 모두의 눈에는 틀린 기본값은 나쁜 거래이므로, 기본값은 뷰어를 따르고 엄격 모드는 속성 하나 거리에 있습니다
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
begin
// 기본값(False): 러미노시티 마스크는 해석된 색에 DeviceRGB와
// 같은 가중치를 적용해, 흔한 뷰어가 보여 주는 것과 일치시킵니다.
// True: 마스크 그룹이 CIE 기반 색 공간이면 CIE Y를 사용합니다.
Pdf.RenderColorimetricLuminosityMasks := False;
Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
if Bmp <> nil then
try
Bmp.SaveToFile('catalog-page-viewer.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
이 속성은 마스크 그룹의 블렌딩 공간이 CIE 기반일 때만 의미가 있습니다: CalGray, CalRGB, Lab, ICCBased가 그 대상입니다. 그룹이 DeviceGray, DeviceRGB, DeviceCMYK를 쓰는 마스크는 어느 설정에서든 이전과 정확히 같게 렌더링됩니다. 디바이스 공간의 회색 변환은 두 모드에서 같으니까요
함정들: /BC, /CS, 그리고 렌더 캐시
러미노시티 소프트 마스크는 배경만큼 일관됩니다: 그룹 바깥의 마스크 영역을 채우는 /BC 색은 그룹 안의 페인트와 같은 규칙으로 변환되어야 합니다. HotPDF는 v2.770.0에서 둘을 함께 바꿨습니다. 페인트는 가중 RGB로, 배경은 CIE Y로 변환하면 그룹의 바운딩 박스가 끝나는 곳에 보이는 심이 생깁니다. 배경이 콘텐츠와 다른 헬퍼를 거칠 때 들이기 쉬운 버그죠
그룹 속성들도 이 모든 일이 일어나기 전에 유효해야 합니다. HotPDF는 블렌딩 공간을 그룹의 /CS 항목에서 읽고, /BC가 그 공간의 컴포넌트 수와 정확히 같은 개수의 숫자를 담을 것을 요구합니다: RGB 기반 공간이면 셋, CMYK면 넷, 회색이면 하나. /CS가 없거나 개수가 틀린 /BC는 그룹이 실행되기 전에 마스크를 실패시키고, 페인트는 추측한 배경으로 마스크되는 대신 마스크 없이 그려집니다. Separation, DeviceN, NChannel은 합법적인 블렌딩 공간이 아니므로, 마스크 그룹 안의 스팟 컬러 페인트는 러미노시티가 계산되기 전에 틴트 변환을 거쳐 대체 공간으로 갑니다
마지막으로, 이 설정은 픽셀을 바꾸므로 캐시 키의 일부입니다. RenderColorimetricLuminosityMasks를 켠 비트맵과 끈 비트맵은 메모리 렌더 캐시와 디스크 렌더 캐시 양쪽에서 따로 캐시되고, THotPDF.RenderLoadedPageToBitmapCached 호출 사이에서 속성을 바꿔도 다른 모드의 비트맵이 돌아오는 일은 없습니다. 운영 관련 메모 하나: v2.769.3은 자체 소프트 마스크를 설정하지 않은 투명도 그룹이 있는 페이지가 렌더링에 실패하는 Win64 전용 회귀를 낳았습니다; v2.770.1에서 고쳐졌으므로 64비트 애플리케이션은 그 릴리스 이상을 써야 합니다
// 나란히 교정: 같은 페이지를 두 모드로 렌더링합니다.
// 각 모드는 자체 키로 캐시되므로 반복 호출이 쌉니다.
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);
Pdf.RenderColorimetricLuminosityMasks := True;
StrictBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);
// ... 둘 다 표시한 뒤 해제: 캐시 호출은 호출자 소유 사본을 반환
ViewerBmp.Free;
StrictBmp.Free;
DPI 선택, 페이지 캐시, 렌더러가 아직 근사하는 것들을 포함한 더 넓은 렌더링 API는 HotPDF로 PDF 페이지를 Delphi TBitmap으로 렌더링하기를 참고하세요. 문제가 반대 방향, 즉 그리는 것이 아니라 인쇄나 아카이빙을 위해 파일에서 투명도를 제거하는 것이라면 PDFlibPas의 소프트 마스크와 투명도 평탄화가 그쪽을 다룹니다
FAQ
HotPDF는 투명도 그룹 안의 PDF 블렌드 모드를 올바르게 렌더링하나요?
예, v2.769.3부터 그렇습니다. Multiply나 다른 Normal이 아닌 블렌드 모드로 그려진 그룹은 자기 색과 알파를 배경과 블렌드하고, 콘텐츠는 ISO 32000-1 §11.6.6이 요구하듯 Normal 블렌딩에서 시작합니다
HotPDF는 논아이솔레이티드 그룹을 왜 두 번 렌더링하나요?
논아이솔레이티드 그룹에는 자체 알파 평면이 없습니다. HotPDF는 그룹을 검은 위와 흰 위로 그려 알파를 1 − (white − black)으로 복원하는데, 속도를 좀 희생하지만 Multiply 섀도를 올바르게 만듭니다
RenderColorimetricLuminosityMasks를 True로 설정해야 할까요?
흔한 PDF 뷰어가 보여 주는 것과 맞추려면 False로 두세요. True는 CIE 기반 마스크 그룹에 대해 ISO 32000-1의 CIE Y 해석이 필요할 때, 예컨대 준수성 비교에서만 설정하세요
블렌드 모드 투명도 그룹, 러미노시티 소프트 마스크, 그리고 이들을 그리는 페이지 렌더러는 HotPDF Delphi Component의 일부입니다. 외부 DLL 없이 PDF 문서를 생성, 편집, 렌더링하는 Delphi와 C++Builder용 네이티브 VCL 라이브러리죠