하나의 평평한 회색 덩어리로 렌더링되는 해칭은 전형적인 타일링 패턴 실패 사례입니다. Delphi와 C++Builder용 네이티브 VCL PDF 컴포넌트인 HotPDF는 PatternType 1을 그릴 때, 현재 경로를 임시 클립으로 바꾸고 보이는 타일마다 패턴 콘텐츠 스트림을 한 번씩 재생하며, 패턴 선택은 그래픽 상태에 보관되어 q와 Q에 의해 복원됩니다
증상은 두 가지 형태로 나타나며, 원인을 알기 전까지는 서로 무관해 보입니다. CAD 도면이 단면 해칭을 잃고 단색 채우기로 되돌아오는 경우인데, 렌더러가 패턴을 평균 색으로 환원해서 그것을 칠했기 때문입니다. 또는 해칭이 새어 나가는 경우도 있습니다. 순백색이어야 할 타이틀 블록이 두 경로 앞쪽에 있던 상세도의 대각선 무늬를 이어받는 경우입니다. 둘 다 패턴 상태 문제이지만, 그중 하나만 타일을 그리는 것과 관련이 있습니다
타일링 패턴은 왜 다음 경로로 번져나가는가
선택된 패턴 이름은 그것을 사용한 연산자의 속성이 아니라 그래픽 상태의 일부이기 때문입니다. ISO 32000-1 §8.6.6.2는 Pattern 색공간을, 색상 값이 scn이나 SCN에 공급되는 패턴 이름인 색공간으로 정의하며, 색상 상태의 다른 모든 구성 요소는 q로 저장되고 Q로 복원됩니다. 패턴 이름도 같은 규칙을 따라야 합니다. HotPDF는 이를 상태 레코드 안에 FillPatternName과 StrokePatternName으로 채우기/스트로크 색공간 계열과 나란히 보관하므로, Q는 CTM을 되돌리는 것과 똑같이 이전 선택을 정확히 되돌립니다
대신 그 이름을 연산자 디스패처 안의 로컬 변수에 저장하면, 그 값은 스트림의 모든 Q를 살아남습니다. 그러면 실패는 예상치 못한 곳에서 나타납니다. 패턴이 적용된 경로 다음에 그려진 Form XObject가, 그 자신의 콘텐츠 스트림은 결코 만든 적 없는 패턴 선택을 물려받아서 해칭된 채로 채워집니다. 중첩된 form은 상황을 더 악화시키는데, 각 중첩 수준이 상태를 push/pop해도 잘못된 변수는 무시하기 때문입니다. cs나 CS로 패턴이 아닌 색공간을 설정하거나 단순한 g/rg/k를 발행할 때도 패턴 이름을 지워야 합니다. 그러지 않으면 낡은 선택이 그것에 의미를 부여했던 색공간보다 오래 살아남습니다
q
/Pattern cs % pattern colour space, ISO 32000-1 8.6.6.2
/P1 scn % coloured tiling pattern, PaintType 1
10 10 200 120 re f % this rectangle is hatched
Q
0 0 300 200 re f % must be black again, not hatched
q
/Cs2 cs % [/Pattern /DeviceCMYK] array
0 0.6 1 0 /P2 scn % uncoloured pattern plus its underlying colour
20 20 160 90 re f*
Q
패턴은 채우기가 아니라 클립을 통해서만 칠해진다
올바른 모델은 뺄셈식입니다. 그려질 형태로 디바이스 클립을 제한한 뒤 그 안에서 패턴 콘텐츠를 실행합니다. HotPDF는 절대 단색 근사치를 먼저 그리고 나서 덧칠하지 않는데, 그 중간 단계의 단색은 타일 사이 틈새로 비쳐 보이고 타일 콘텐츠 안의 투명도와 충돌하기 때문입니다. §8.7.3.2는 타일링 패턴을 고정된 수평·수직 간격으로 복제되는 콘텐츠 스트림이라고 설명하며, 복제는 이미 올바른 형태를 가진 클립을 상대로 해야만 의미가 있습니다. 채우기의 경우 변환은 직접적입니다. HPDFSelectFillPathClip은 f*, B*, b*에 대해 다각형 채우기 모드를 ALTERNATE로, 논제로 변형에는 WINDING으로 설정하고, GDI 경로를 만든 뒤 SelectClipPath로 클립에 교집합시킵니다. 이 한 줄이 짝수-홀수 패턴 채우기가 짝수-홀수 단색 채우기와 똑같은 구멍을 남기게 만드는데, 도넛 모양의 해칭된 영역에 정확히 필요한 동작입니다
스트로크는 잘못 처리하기 쉬운 부분입니다. 스트로크된 경로는 내부가 없으므로 경로 자체를 클립에 교집합시키면 빈 영역이 나오고 아무것도 칠해지지 않습니다. 그래서 HPDFSelectStrokePathClip은 먼저 현재 상태로부터 기하학적 펜을 만듭니다. J에서 온 끝 캡, j에서 온 접합, M에서 온 마이터 한계, 그리고 대시 배열이 활성화되어 있으면 PS_USERSTYLE과 함께 PS_GEOMETRIC을 사용한 다음, WidenPath를 호출해서 스트로크된 윤곽을 클리핑 전에 채울 수 있는 영역으로 변환합니다. 캡, 접합, 마이터, 대시 동작은 그렇게 해서 두 번째 구현이 아니라 구성에 의해 일반 스트로크와 일치하게 됩니다. 여기에는 정직한 한계가 두 가지 있습니다. 디바이스 단위 1 미만의 선 너비는 1픽셀로 클램프되고, 대시 배열은 ExtCreatePen이 허용하는 상한인 16개 항목에서 잘립니다
실제로 보이는 타일은 어느 것인가
보이는 범위는 변환을 거꾸로 실행해서 구합니다. 타일 배치는 패턴 공간에서 일어나지만, 페이지의 얼마만큼이 관련되는지 아는 유일한 것은 디바이스 공간에 있는 디바이스 클립 박스입니다. HotPDF는 BaseMatrix := CTM * PatternMatrix를 합성하고 그것을 역행렬로 만든 다음, GDI 클립 박스의 네 모서리를 그 역행렬을 통해 되돌려 매핑합니다. 매핑된 네 모서리의 축 정렬 경계가 실제로 커버될 수 있는 패턴 공간 사각형을 알려주고, 그 사각형을 패턴 BBox에 대해 XStep과 YStep으로 나누면 닫힌 인덱스 범위가 나옵니다. 각 셀은 그다음 CTM * PatternMatrix * Translate(i * XStep, j * YStep)이라는 CTM으로 렌더링되며, 자신의 변환된 BBox 다각형으로 두 번째 클리핑을 거칩니다. 이 두 번째 클립은 XStep이 경계 상자 너비보다 작을 때, 즉 겹치는 타일 디자인을 표현하는 방식에서 중요합니다. 이게 없으면 이웃한 셀들이 자신에게 선언된 범위 바깥에서 서로를 덮어씁니다. 셀별 클립이 NULLREGION으로 돌아오면 해당 셀은 토큰화도 실행도 없이 건너뜁니다
// Map the device clip box back into pattern space through the inverse of
// CTM * PatternMatrix, then convert those bounds into tile index ranges.
BaseMatrix := HPDFMatMul(FGSStack.State.CTM, PatternMatrix);
if not HPDFMatInvert(BaseMatrix, InverseMatrix) then Exit; // singular: refuse
if GetClipBox(FDC, ClipRect) = ERROR then Exit;
// MinX..MaxY are the axis-aligned bounds of the four mapped clip corners.
I0 := Floor((MinX - BBox[2]) / StepXAbs);
I1 := Ceil ((MaxX - BBox[0]) / StepXAbs);
J0 := Floor((MinY - BBox[3]) / StepYAbs);
J1 := Ceil ((MaxY - BBox[1]) / StepYAbs);
PlannedTiles := Int64(I1 - I0 + 1) * Int64(J1 - J0 + 1);
if (PlannedTiles <= 0) or (PlannedTiles > FPatternTilesRemaining) then Exit;
Dec(FPatternTilesRemaining, Integer(PlannedTiles));
색이 없는 패턴과 바깥에서 오는 색
PaintType 2 패턴은 형태만 가지고 색은 갖지 않으며, 색은 패턴 이름과 함께 도착합니다. §8.7.3.2는 색이 없는 패턴이 하위 공간을 선언하는 Pattern 색공간에서만 사용된다고 규정하므로, scn은 성분 값들을 먼저 받고 패턴 이름을 마지막에 받습니다. HotPDF는 패턴 색공간 항목에 저장된 하위 공간을 통해 그 성분들을 해석합니다. 이는 색 없는 해칭이 다른 어떤 채우기와 마찬가지로 Separation 잉크나 DeviceN 조합으로 색을 입힐 수 있다는 뜻이며, 그 해석의 메커니즘은 Separation과 DeviceN 별색 렌더링에서 다룹니다. 타일 내부에서는 두 페인트 타입이 크게 갈립니다. PaintType 2의 경우 렌더러는 타일이 진행되는 동안 색 연산자 억제 플래그를 설정하므로, 패턴 콘텐츠 안의 g, rg, k, scn은 모두 무시되고 모든 마크는 외부에서 공급된 색을 취합니다. PaintType 1은 반대입니다. 채우기와 스트로크 상태는 PDF 기본값인 DeviceGray 검정에 아이덴티티 색공간으로 리셋되고, 타일이 스스로 색을 입힙니다. 이 리셋을 건너뛰면 f 연산자 시점에 우연히 현재였던 색이 원래 자기서술적이어야 했던 패턴 안으로 새어 들어옵니다
그래픽 상태 스택 깊이는 왜 모든 타일마다 복원되어야 하는가
패턴 콘텐츠 스트림은 불균형해도 허용되며, 그 피해는 셀마다 누적되기 때문입니다. 스트림 안에 q 연산자 3개와 Q 연산자 2개를 담은 타일은 스택을 시작보다 한 프레임 더 깊게 남깁니다. 셀 사이에서 현재 상태 레코드만 복원하면 깊이는 계속 늘어나므로, 200번째 셀은 199번째 셀에 속한 스택 프레임에서 실행되며, 그 프레임이 가진 CTM과 클립이 무엇이든 그대로 이어받습니다. 그래서 HotPDF는 타일 루프 전에 상태 레코드와 스택 깊이를 스냅샷하고, 매 반복 맨 앞에서 RestoreSnapshot을 호출합니다. 이는 스택을 저장된 길이로 잘라내고 저장된 상태를 한 번에 재설치합니다. 페이지 Resources 딕셔너리와 색 연산자 억제 플래그도 같은 경계에서 복원되는데, 타일이 자기만의 리소스를 참조할 수 있고 그것을 이웃에게 넘겨서는 안 되기 때문입니다. GDI 클립 상태도 각 셀 주위에서 SaveDC/RestoreDC 쌍으로 같은 처리를 받아서, 자기만의 W n 클립을 설치한 타일이 다음 타일에서 사용 가능한 영역을 줄일 수 없게 합니다
예산, 거부, 그리고 렌더러가 그리지 않을 것들
타일링 패턴은 PDF 안에서 서비스 거부 파일을 작성하기 가장 쉬운 지점이므로, 한계는 휴리스틱이 아니라 정확한 숫자입니다. 패턴 중첩은 깊이 4로 제한되며, 이는 Form XObject 재귀에 쓰이는 것과 같은 가드로, 자신의 리소스 딕셔너리를 통해 스스로를 참조하는 패턴을 막습니다. 하나의 경로 페인트는 중첩된 패턴을 통틀어 최대 16,384개 타일을 실행할 수 있으며, 가장 바깥 패턴 페인트가 시작될 때만 리셋됩니다. 계획된 셀 수가 남은 예산을 넘어서는 타일 그리드는 셀을 하나도 실행하기 전에 즉시 거부됩니다
퇴화된 기하 형태는 근사되지 않고 거부됩니다. 누락되었거나 면적이 0인 BBox, 크기가 1e-6 미만인 XStep이나 YStep, 역행렬이 없는 CTM * PatternMatrix 곱, 1e9를 넘는 매핑된 클립 좌표, 또는 백만을 넘는 인덱스 크기는 모두 패턴 페인트를 아무것도 그리지 않고 반환하게 만듭니다. 그 결과는 렌더링 스레드 행업 대신 칠해지지 않은 영역이며, 배치 변환기에서 원하는 트레이드오프입니다. 성능은 하나의 결정에서 나옵니다. 패턴 스트림은 페인트 1회당 HPDFTokenizeContentStream으로 한 번만 토큰화되고, 그 토큰 배열은 보이는 모든 셀에 걸쳐 재사용되므로, 타일 수는 실행 비용을 곱하지만 렉싱 비용은 결코 곱하지 않습니다
Delphi에서 패턴이 적용된 페이지 렌더링하기
패턴 지원이 있어도 호출하는 코드는 아무것도 바뀌지 않습니다. 문서를 로드하고 페이지를 요청하면, 타일링 작업은 페이지-비트맵 렌더링이 이미 구동하는 콘텐츠 스트림 인터프리터 안에서 일어납니다. 같은 인터프리터가 비트맵, 메타파일, 프린터 디바이스 컨텍스트를 모두 담당하므로, 미리보기 썸네일에서 올바르게 보이는 해칭 도면은 같은 타일 기하로 인쇄됩니다. PatternType 2 셰이딩 패턴은 축형·방사형 셰이딩 렌더링에서 자세히 설명하는, 단순 sh 연산자와 평가 경로를 공유하는 다른 분기를 탑니다
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('assembly-drawing.pdf') > 0 then
begin
// Section hatching that previously flattened to a solid block now
// replays the tile content once per visible cell.
Bmp := Pdf.RenderLoadedPageToBitmap(0, 200);
if Assigned(Bmp) then
try
Bmp.SaveToFile('sheet1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
패턴이 적용된 영역이 여전히 이상해 보인다면 세 가지 실패 유형을 순서대로 확인하십시오. 완전히 비어 있는 영역은 대개 거부를 의미합니다. XStep, YStep, BBox에 퇴화된 값이 있는지 점검하거나, 16,384 상한에 대해 그리드가 필요로 할 타일 수를 세어보십시오. 단일한 평면 색으로 칠해진 영역은 패턴 이름이 페인트 연산자에 도달하지 못했다는 뜻이며, 스트림 안의 cs와 scn 순서를 가리킵니다. 있어서는 안 될 곳에 패턴이 나타난다면 상태 복원 문제이며, 그것을 물려받은 form이나 경로 주변의 q/Q 처리를 살펴봐야 합니다
타일링 패턴은 필요로 하는 파일이 여러분의 받은편지함에 도착하기 전까지는 보이지 않다가 일단 도착하면 그것이 일의 전부가 되는 종류의 PDF 기능입니다. Delphi나 C++Builder에서 도면 뷰어, 엔지니어링 문서 변환기, 리포트 렌더러를 만들고 있다면, 전체 컴포넌트와 렌더링 API는 HotPDF Delphi PDF 컴포넌트 페이지에 문서화되어 있습니다