HotPDF는 프린트 드라이버 없이 델파이와 C++Builder 안에서 XPS와 OpenXPS 패키지를 PDF로 변환합니다. 모든 96-DPI 고정 페이지 좌표를 하나의 0.75 배율 Y 뒤집기 페이지 매트릭스로 매핑하고, 각 VisualBrush를 공유 Form XObject로 발행하며, ImageBrush 타일 모드를 반복 이미지 드로우 대신 네이티브 PDF 타일링 패턴으로 바꿉니다
대부분의 윈도우 개발팀을 여기로 끌어오는 시나리오는 지루하고 피할 수 없습니다. 무언가가 이미 Microsoft XPS Document Writer로 인쇄되고 있습니다. 레거시 ERP 보고서, 서명된 양식, 명세서 한 묶음. 그리고 아카이브 정책은 PDF를 요구합니다. XPS는 캡처 포맷으로는 완벽히 훌륭하지만 십 년 뒤 기록 시스템에 넘겨 주기엔 끔찍합니다. 그래서 스풀 파일은 페이지 대 페이지 PDF가 되어야 하고, 그 변환기를 쓰기 시작하는 순간 흥미로운 부분은 XML이 아니라는 걸 알게 됩니다. XPS와 PDF는 원점이 어디인지, 단위가 얼마의 가치가 있는지, 브러시가 무엇이 될 수 있는지에 대해 서로 disagree합니다
패키지에서 PDF로 한 번의 패스로
진입점은 특별한 XPS 클래스가 아니라 문서 핸들러 레지스트리입니다. THPDFDocumentHandlerRegistry.RegisterStandardHandlers가 XPS, EPUB, CBZ 핸들러를 설치합니다. 인식은 내용 기반이므로, [Content_Types].xml과 최소 하나의 .fpage 파트를 실은 패키지는 파일 확장자가 거짓말해도 95점을 받고, 맨 확장자 .xps나 .oxps만으로는 10점밖에 받지 못합니다. 업로드를 받을 때 그 순서가 중요합니다. EPUB을 .xps로 이름 바꾼 공격자가 파이프라인을 끌고 가면 안 되기 때문입니다
var
Handled: IHPDFHandledDocument;
Info: THPDFDocumentHandlerInfo;
Options: THPDFDocumentHandlerOptions;
Registry: THPDFDocumentHandlerRegistry;
Output: TFileStream;
begin
Registry := THPDFDocumentHandlerRegistry.Create;
Output := TFileStream.Create('spool.pdf', fmCreate);
try
Registry.RegisterStandardHandlers;
Options := THPDFDocumentHandlerOptions.Default;
if not Registry.OpenFromFile('spool.xps', '', Options, Handled, Info) then
raise Exception.Create('no registered handler recognised the package');
if Handled.Format = hdfXPS then
Handled.WritePDF(Output, Options, Info);
// Info.UnsupportedFeatureCount가 이 변환의 정직한 점수다
finally
Output.Free;
Registry.Free;
end;
end;
변환에 관한 모든 것은 시도되기 전에 예산이 잡힙니다. THPDFDocumentHandlerOptions.Default는 아카이브 항목을 10,000, 확장된 아카이브 바이트를 1 GiB, 압축률을 200, 리소스를 4,096, 페이지를 10,000으로 제한하고, 선택적 CancellationToken을 실어 서버 측 작업이 패키지 중간에 멈출 수 있게 합니다. 끝난 뒤 Info.UnsupportedFeatureCount를 읽고 0이 아닌 값을 진짜 발견으로 다루십시오. HotPDF는 근사를 그려 놓고 조용히 있기보다 매핑하지 못한 것을 일부러 셉니다
XPS 페이지는 왜 좌표를 다시 쓰는 대신 매트릭스가 필요한가
좌표를 다시 쓰면 변환 스택이 사라지기 때문입니다. XPS FixedPage는 원점이 왼쪽 위에 있고 Y가 아래로 자라는 96-DPI 단위로 지정됩니다. PDF 사용자 공간은 원점이 왼쪽 아래에 있고 Y가 위로 자라는 72-DPI입니다. 순진한 고침은 모든 숫자에 0.75를 곱하고 내보낼 때마다 각 Y를 페이지 높이에서 빼는 것입니다. 한 개의 평평한 경로에서는 통하지만 RenderTransform, 중첩된 Canvas, 브러시 로컬 매트릭스가 들어오는 순간 무너집니다. 그 변환들은 XPS 공간에서 정의되는데 좌표별 재작성은 이미 그 공간을 떠났기 때문입니다. 그래서 HotPDF는 투영을 매트릭스로 유지하고 합성합니다. HPDFXPSPageMatrix가 페이지마다 한 번 고정 상수를 돌려주고, HPDFMultiplyXPSMatrix가 그것을 누적된 경로 변환과 연결하며, 결과는 기하 전에 단일 cm 연산자로 내보내집니다. 경로 데이터는 수정되지 않은 XPS 숫자로 기록되고, 이것이 약식 기하 문법이 SVG 경로 데이터용으로 쓰이는 것과 같은 경계가 있는 파서를 공유할 수 있는 이유이기도 합니다. 선행하는 F0 또는 F1 fill-rule 토큰만 XPS 어댑터가 처리합니다. EMF와 WMF 벡터 임포트에서 같은 논리를 따라왔다면 논거의 형태가 낯익을 것입니다. 임포트 포맷은 매트릭스로 변환되지, 리프 좌표의 산술로 변환되지 않습니다
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // 96-DPI XPS 단위를 72-DPI PDF 포인트로
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // XPS Y는 아래로, PDF Y는 위로 자란다
Result.E := 0;
Result.F := PageHeight; // PDF 페이지 높이, 포인트 단위
end;
// 비주얼마다 하나의 합성된 CTM, 어떤 경로 연산자보다 먼저 내보낸다
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
VisualBrush를 두 번 그리지 않고 어떻게 재사용하는가
VisualBrush는 임의의 비주얼 트리 — Canvas, Path, Glyphs 자식 — 를 영역에, 영역 전체에 반복되기도 하면서 그립니다. HotPDF는 그 비주얼을 한 번 PDF Form XObject로 컴파일한 다음 배치하는데, 이는 Form XObject를 통한 SVG 임포트에서 기술한 것과 같은 리소스 전략입니다. 두 세부가 성패를 가릅니다. 첫째, 비주얼은 직접 XML 자식으로 걸어야 합니다. 타일로 쓸 만한 요소를 평평하게 훑으면 중첩된 비주얼이 페이지 최상위로 끌어 올려져 리소스 스코핑과 그리기 순서가 모두 파괴됩니다. 둘째, 콘텐츠는 XPS-to-PDF 페이지 매트릭스가 이미 적용된 채로 캡처되므로, Form을 발행하려면 그 매트릭스의 역수를 곱해야 합니다. 그렇지 않으면 모든 배치가 0.75 배율과 Y 뒤집기를 다시 적용합니다. Form은 자기 리소스도 소유해야 합니다. HotPDF는 캡처된 콘텐츠 스트림이 실제 참조하는 폰트, XObject, 패턴, ExtGState, 색 공간만 복사합니다. 페이지 리소스 사전 전체를 복제하면 등록 중인 Form이 자기 자신의 리소스 그래프로 끌려 들어가 사이클이 만들어집니다. 폰트는 평범한 페이지에서 direct 사전에 머물다 캡처된 콘텐츠가 실제로 Tf를 담을 때만 공유 indirect 사전으로 승격되므로, 재사용 가능한 비주얼이 없는 문서는 그 기계 장치 비용을 내지 않습니다. 버그를 등록하기 전에 알아둘 가치가 있는 명세 경계 하나. ECMA-388 13.4절은 VisualBrush의 ViewboxUnits와 ViewportUnits 둘 다 Absolute를 요구하므로, 상대 단위는 빠진 기능이 아니라 규격에 맞지 않는 입력이고 HotPDF는 그들을 위한 좌표 의미론을 발명하기를 거부합니다
ImageBrush 타일링: 네 모드, 네 셀 크기
XPS 타일 모드는 커버된 영역 전체에 반복 이미지 배치로 펼쳐지는 대신 ISO 32000-1 8.7.3절의 PDF 타일링 패턴으로 매핑되는데, 그래야 출력 크기와 변환 시간이 브러시가 페이지의 얼마를 덮는지와 무관해집니다. 매핑은 일단 보면 기계적입니다. 반사는 거울상 배치를 하나의 패턴 셀 안에 넣고 그에 맞게 셀을 키워서 표현됩니다
Tile— 배치 하나, 셀은 1×1 뷰포트 유지FlipX— 배치 둘, 셀이 2×1로 넓어짐FlipY— 배치 둘, 셀이 1×2로 높아짐FlipXY— 배치 넷, 셀이 2×2로 확장
각 배치는 자기 클립 사각형을 싣습니다. 서브 셀을 넘어서는 Viewbox 매핑은 옆 반사로 번지기 때문입니다. 사람을 붙잡는 부분은 패턴 /Matrix입니다. 타일링 패턴은 패턴이 선택될 때의 그래픽 상태가 아니라 부모 콘텐츠 스트림의 기본 사용자 공간에 앵커되므로, 매트릭스는 주변 CTM에 의존하는 대신 세 층을 모두 명시적으로 합성해야 합니다. 고정 페이지 투영, Path 변환, 브러시 로컬 Transform이 그 세 층입니다. HotPDF는 할당 전에 검증도 합니다. RegisterImageTilingPattern은 패턴을 1,024 배치로 제한하고 퇴화한 클립, 역변환 불가능 매트릭스, 잘못된 이미지 인덱스를 거부합니다. 이 뒤에 있는 일반적인 PDF 쪽 모델이 필요하면 타일링 패턴과 Pattern 색 공간이 기반 연산자를 다룹니다
// 고정 페이지 투영을 패턴 매트릭스에 접어 넣고, 그 다음 브러시 로컬 것
PatternMatrix := HPDFMatFromOps( 0.75 * PathMatrix.A, -0.75 * PathMatrix.B,
0.75 * PathMatrix.C, -0.75 * PathMatrix.D,
0.75 * PathMatrix.E,
PageHeight - 0.75 * PathMatrix.F);
if Brush.HasTransform then
PatternMatrix := HPDFMatMul(PatternMatrix, BrushMatrix);
PatternName := Document.RegisterImageTilingPattern(Resource.ImageIndex,
Brush.Viewport.Left, Brush.Viewport.Top,
Brush.Viewport.Left + CellWidth, Brush.Viewport.Top + CellHeight,
CellWidth, CellHeight, Placements, pttNoDistortion, PatternMatrix);
방사형 그러데이션이 원이 아니면 무슨 일이 벌어지는가
XPS는 GradientOrigin, Center, RadiusX, RadiusY를 가진 RadialGradientBrush를 정의하므로 브러시는 타원입니다. ISO 32000-1 8.7.4.5.4절의 PDF 셰이딩 타입 3은 두 원 사이를 블렌드하고 타원을 직접 표현할 방법이 없습니다. 두 반지름을 한 숫자로 평균 내는 것이 유혹적인 지름길이고, 원에 가깝지 않은 어떤 브러시에서도 눈에 띄게 틀립니다. HotPDF는 대신 문제를 좌표계로 옮깁니다. Y를 RadiusY / RadiusX로 스케일하고, 그 스케일된 공간에서 정직한 원형 셰이딩을 등록하고, 패턴을 선택하고, 즉시 역수 스케일을 내보내서 다음에 기록될 경로 기하가 여전히 원래 XPS 사용자 공간에 있게 합니다
ScaleY := Brush.RadiusY / Brush.RadiusX;
PatternName := Document.RegisterMultiStopRadialGradient(
Brush.StartX, Brush.StartY / ScaleY, 0,
Brush.EndX, Brush.EndY / ScaleY, Brush.RadiusX,
StopPositions, StopColours, 3);
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(ScaleY) + ' 0 0 cm'#10);
Page.SetFillPattern(PatternName); // 패턴이 바로 여기서 CTM을 포착한다
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
그 스니펫의 순서가 온 통트럭이고 취향의 문제가 아닙니다. PDF 셰이딩 패턴은 현재 색으로 선택되는 순간의 현재 변환 매트릭스를 포착하므로, 임시 스케일은 SetFillPattern이나 SetStrokePattern 전에 내보내야 하고 역수는 선택 뒤, 경로 연산자 앞에 와야 합니다. 어느 쪽으로든 순서를 틀리면 첫 경로에서는 올바르게 렌더링되고 그 뒤의 모든 경로에서 흘러가는 그러데이션을 얻습니다. 관련 제약이 상대 좌표 모드에도 적용됩니다. RadiusX와 RadiusY는 경로 너비와 높이에 대해 따로따로 해석되어야 합니다. 하나의 모서리 길이로 둘 다 스케일하면 정사각형이 아닌 경로에서 타원의 가로세로 비가 조용히 바뀝니다
변환이 자기 한계에 대해 정직한 지점
일부 XPS 구성물은 근사 변환되고 일부는 전혀 변환되지 않으며, 처음부터 끝까지의 설계 선택은 그들을 흉내 내는 대신 세는 것입니다. TIFF와 JPEG XR 파트는 WIC로 래스터화되고 보존된 알파에 관한 약속이 없는 반면, 유효한 알파 채널을 가진 PNG는 베이스 이미지와 /SMask로 분할됩니다. 이미지 고유 크기는 pixel * 96 / DPI로 유도되는데, 먼저 PNG pHYs나 JPEG JFIF 밀도를 읽고 96 DPI로 폴백하므로 나쁜 밀도 헤더는 임의의 크기가 아니라 예측 가능한 크기에 착지합니다. 해석되지 않은 매트릭스 리소스, 비표준 상대 변환, ColorConvertedBitmap, 지원되지 않는 그러데이션 스프레드 모드, 기형적인 기하는 모두 UnsupportedFeatureCount를 증가시키고, 기형적인 입력은 조용히 다른 그림으로 저하하는 대신 fail close합니다
아카이브 변환기에게 유용한 자세가 그것입니다. 조용히 근사하는 변환은 표현하지 못한 네 개의 요소가 무엇인지 말해 주는 변환보다 나쁩니다. 문서가 기록 시스템에 봉인되기 전에 검사할 뭔가를 주는 것은 후자뿐이기 때문입니다. XPS와 OpenXPS 변환을 문서 파이프라인의 나머지 — 페이지 구성, 폰트, 서명, PDF/A 출력 — 와 함께 평가하고 있다면 HotPDF Delphi PDF component 페이지가 전체 기능 집합과 지원되는 델파이, C++Builder 버전을 나열합니다