HotPDF конвертує пакети XPS та OpenXPS у PDF всередині Delphi і C++Builder без драйвера друку: проєктує кожну координату FixedPage зі 96 DPI через одну сторінкову матрицю з масштабом 0.75 і перевертанням Y, публікує кожен VisualBrush як спільний Form XObject і перетворює плиткові режими ImageBrush на нативні плиткові патерни PDF замість повторних малювань зображення
Сценарій, що затягує більшість Windows-майстерень у це, нудний і неминучий. Щось уже друкується в Microsoft XPS Document Writer — звіт зі старого ERP, підписана форма, пачка виписок — а політика архіву каже PDF. XPS — цілком пристойний формат захоплення і жахливий, щоб віддати його системі обліку через десятиліття. Тож спул-файл мусить стати PDF зі сторінка-в-сторінку, і щойно ви сідаєте писати той конвертер, виявляється, що цікава частина — не XML. Це те, що XPS і PDF розходяться в тому, де початок координат, чого вартує одиниця і чим дозволено бути пензлю
Від пакета до 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 ГіБ, коефіцієнт стиснення 200, ресурси 4 096 і сторінки 10 000, а також несе необовʼязковий CancellationToken, щоб серверне завдання можна було зупинити посеред пакета. Після цього прочитайте Info.UnsupportedFeatureCount і сприймайте ненульове значення як реальну знахідку: HotPDF свідомо рахує те, що не змогла відобразити, замість малювати наближення й мовчати про це
Чому сторінці XPS потрібна матриця, а не переписані координати?
Бо переписування координат втрачає стек трансформацій. XPS FixedPage специфікований у одиницях 96 DPI з початком у верхньому лівому куті та Y, що росте вниз; простір користувача PDF — 72 DPI з початком у нижньому лівому куті та Y, що росте вгору. Наївне виправлення — множити кожне число на 0.75 і віднімати кожен Y від висоти сторінки під час видачі. Це працює для однієї пласкої фігури й розвалюється щойно зʼявляються RenderTransform, вкладений Canvas або матриця на рівні пензля, бо ті трансформації визначені в просторі XPS, а ваше покоординатне переписування вже його покинуло. Тому HotPDF тримає проєкцію як матрицю і композиціонує її. HPDFXPSPageMatrix повертає фіксовані константи один раз на сторінку, HPDFMultiplyXPSMatrix конкатенує її з накопиченою трансформацією фігури, і результат видається одним оператором cm перед геометрією. Дані фігур потім записуються немодифікованими числами XPS, — саме тому скорочений синтаксис геометрії може ділити той самий обмежений парсер, що й для SVG path data; XPS-адаптер обробляє лише початковий токен правила заповнення F0 чи F1. Якщо ви проводили таке саме міркування для імпорту векторів EMF і WMF, форма аргументу знайома: формати імпорту конвертуються матрицею, ніколи арифметикою над кінцевими координатами
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // одиниця XPS 96 DPI в точку PDF 72 DPI
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // Y у XPS росте вниз, у PDF — вгору
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 і потім його розміщує — та сама ресурсна стратегія, що описана в імпорті SVG через Form XObjects. Чи це працює, вирішують дві деталі. Перше: візуал треба обходити як прямих дітей XML — пласке сканування на предмет плиткових елементів витягне вкладені візуали на верхній рівень сторінки й знищить і області видимості ресурсів, і порядок малювання. Друге: вміст захоплюється з уже застосованою матрицею сторінки XPS→PDF, тож публікація Form вимагає множення на обернену до тієї матриці, інакше кожне розміщення знову застосує масштаб 0.75 і перевертання Y. Form також мусить володіти своїми ресурсами: HotPDF копіює лише ті шрифти, XObjects, патерни, ExtGStates і колірні простори, на які потік захопленого вмісту справді посилається; клонування цілого словника ресурсів сторінки втягло б реєстровану Form у власний ресурсний граф і збудувало цикл. Шрифти лишаються в прямому словнику на звичайних сторінках і підвищуються до спільного непрямого словника лише коли захоплений вміст справді містить Tf, тож документ без повторно використовуваних візуалів не платить за цей механізм. Зауважте одну межу специфікації, варту знання до подання багрепорта: ECMA-388, розділ 13.4, вимагає, щоб і ViewboxUnits, і ViewportUnits на VisualBrush були Absolute, тож відносні одиниці — не бракуюча можливість, це некоректний ввід, і HotPDF відмовляється вигадувати для них семантику координат
Плитки ImageBrush: чотири режими, чотири розміри комірки
Плиткові режими XPS відображаються на плиткові патерни PDF з ISO 32000-1, розділ 8.7.3, а не розгортаються в повторні розміщення зображень по вкритій ділянці, що тримає розмір виводу й час конвертації незалежними від того, скільки сторінки вкриває пензель. Відображення механічне, щойно його побачиш: відбиття виражається дзеркальними розміщеннями всередині однієї комірки патерна та збільшенням комірки відповідно
Tile— одне розміщення, комірка лишається вʼюпортом 1×1FlipX— два розміщення, комірка розширюється до 2×1FlipY— два розміщення, комірка подовжується до 1×2FlipXY— чотири розміщення, комірка розростається до 2×2
Кожне розміщення несе власний прямокутник відсікання, бо відображення Viewbox, що виходить за свою підкомірку, затече в сусіднє дзеркало. /Matrix патерна — частина, що підловлює людей. Плитковий патерн якориться до простору користувача за замовчуванням батьківського потоку вмісту, а не до графічного стану, чинного на момент вибору патерна, тож матриця мусить явно скомпозиціювати всі три шари — фіксовану сторінкову проєкцію, трансформацію Path і Transform на рівні пензля — замість покладатися на навколишню CTM. 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 визначає RadialGradientBrush з GradientOrigin, Center, RadiusX і RadiusY, тож пензель — це еліпс. Затінення типу 3 у PDF, ISO 32000-1 розділ 8.7.4.5.4, змішує між двома колами і не має способу виразити еліпс напряму. Усереднити два радіуси в одне число — спокусливий ярлик, і він явно хибний на будь-якому пензлі, не близькому до круглого. 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, а пошкоджений ввід зазнає відмови в закритому стані замість деградації до мовчки іншого малюнка
Це корисна позиція для архівного конвертера: конвертація, що тихо наближає, гірша за ту, яка каже, які чотири елементи вона не змогла подати, бо лише друга дає вам щось перевірити до того, як документ запечатають у систему обліку. Якщо ви оцінюєте конвертацію XPS та OpenXPS поруч з іншою частиною документного конвеєра — композицією сторінок, шрифтами, підписами, виводом PDF/A — сторінка HotPDF Delphi PDF component перелічує повний набір можливостей і підтримувані версії Delphi та C++Builder