Технічна стаття

Конвертація XPS/OpenXPS у PDF в Delphi: координати й пензлі

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, форма аргументу знайома: формати імпорту конвертуються матрицею, ніколи арифметикою над кінцевими координатами

HotPDF композиціонує фіксовану сторінкову матрицю XPS із накопиченою трансформацією фігури, тож координатна система XPS зі 96 DPI і початком зверху-зліва досягає простору користувача PDF 72 DPI з початком знизу-зліва, видаючись одним оператором cm на візуал
Проєкція лишається матрицею і композиціонується з кожною вкладеною трансформацією, тож дані фігур можна писати немодифікованими числами XPS
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 відмовляється вигадувати для них семантику координат

HotPDF компілює візуальне дерево VisualBrush з XPS один раз у PDF Form XObject, публікує його через обернену фіксовану сторінкову матрицю, тож розміщення не застосовують масштаб повторно, і копіює лише ресурси, на які захоплений вміст справді посилається
Вміст захоплюється з уже застосованою матрицею сторінки, тож Form публікується через обернену до неї і несе лише ресурси, на які посилається її власний потік вмісту

Плитки ImageBrush: чотири режими, чотири розміри комірки

Плиткові режими XPS відображаються на плиткові патерни PDF з ISO 32000-1, розділ 8.7.3, а не розгортаються в повторні розміщення зображень по вкритій ділянці, що тримає розмір виводу й час конвертації незалежними від того, скільки сторінки вкриває пензель. Відображення механічне, щойно його побачиш: відбиття виражається дзеркальними розміщеннями всередині однієї комірки патерна та збільшенням комірки відповідно

  • Tile — одне розміщення, комірка лишається вʼюпортом 1×1
  • FlipX — два розміщення, комірка розширюється до 2×1
  • FlipY — два розміщення, комірка подовжується до 1×2
  • FlipXY — чотири розміщення, комірка розростається до 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 мусять розвʼязуватися щодо ширини та висоти фігури окремо, бо масштабування обох однією довжиною ребра мовчки змінює співвідношення сторін еліпса на будь-якій неквадратній фігурі

HotPDF відображає еліптичний XPS RadialGradientBrush на затінення типу 3 у PDF, масштабуючи Y, реєструючи кругле затінення в масштабованому просторі й видаючи обернений масштаб лише після того, як патерн захопив поточну матрицю трансформації
Еліпс поглинається координатною системою, а не затіненням, і тимчасовий масштаб мусить охоплювати вибір патерна саме в такому порядку

Де конвертація чесна щодо своїх меж

Деякі конструкції 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