Техническа статия

XPS и OpenXPS към PDF в Delphi: координати и четки

HotPDF преобразува пакети XPS и OpenXPS към PDF в Delphi и C++Builder без print driver, като пропуска всяка fixed-page координата от 96 DPI през една page матрица с мащаб 0.75 и обръщане по Y, публикува всеки VisualBrush като споделен Form XObject и превръща tile режимите на ImageBrush в нативни PDF tiling шаблони, вместо повторни image рисувания

Сценарият, който дръпва повечето Windows магазини в това, е скучен и неминуем. Нещо вече печата към Microsoft XPS Document Writer — наследен ERP отчет, подписана форма, партида извлечения — а архивната политика казва PDF. XPS е съвсем приличен формат за улавяне и ужасен, за да се предаде на система за записи след десетилетие. Затова spool файлът трябва да стане PDF страница за страница, а в момента, в който започнете да пишете този конвертор, откривате, че интересната част не е XML. Това е, че XPS и PDF не са съгласни къде е началото, колко струва една единица и какво е позволено да бъде една четка

От пакет към PDF в един проход

Входната точка е регистърът на document handler, а не специален XPS клас. THPDFDocumentHandlerRegistry.RegisterStandardHandlers инсталира handler-ите за 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, ресурсите на 4096, а страниците на 10 000, и носи опционален CancellationToken, така че сървърна задача може да бъде спряна по средата на пакета. Прочетете после Info.UnsupportedFeatureCount и третирайте ненулева стойност като реална находка: HotPDF умишлено брои това, което не е успяло да съпостави, вместо да рисува приближение и да мълчи за него

Защо XPS страница има нужда от матрица, вместо от пренаписани координати?

Защото пренаписването на координати губи стека от трансформации. XPS FixedPage е зададен в единици 96 DPI с начало горе вляво и Y, растящ надолу; PDF user space е 72 DPI с начало долу вляво и Y, растящ нагоре. Наивната поправка е да умножите всяко число по 0.75 и да изваждате всяко Y от височината на страницата, докато го извеждате. Това работи за един плосък path и се разпада в момента, в който влезе RenderTransform, вложен Canvas или матрица на ниво четка, защото тези трансформации са дефинирани в XPS пространството, а вашето пренаписване по координата вече е излязло от него. Затова HotPDF пази проекцията като матрица и я композира. HPDFXPSPageMatrix връща фиксираните константи веднъж на страница, HPDFMultiplyXPSMatrix конкатенира с натрупаната transform на path, а резултатът се извежда като един-единствен оператор cm преди геометрията. Path данните после се записват в немодифицирани XPS числа, което е и причината съкратеният geometry синтаксис да може да споделя същия ограничен parser, използван за SVG path данни — само водещият токен за fill rule F0 или F1 се обработва от XPS адаптера. Ако сте следвали същото разсъждение за векторен импорт на EMF и WMF, формата на аргумента е позната: импортните формати се преобразуват с матрица, никога с аритметика по крайните координати

HotPDF композира фиксираната XPS page матрица с натрупаната transform на path, така че координатна система XPS 96 DPI с начало горе вляво стига до PDF user space 72 DPI с начало долу вляво, изведена като един cm оператор на visual
Проекцията остава матрица и се композира с всяка вложена трансформация, така че path данните могат да се запишат в немодифицирани 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 расте надолу, Y на PDF расте нагоре
  Result.E :=  0;
  Result.F := PageHeight;  // височина на PDF страницата, в точки
end;

// Една композита CTM на visual, извеждана преди всеки path оператор
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Как преизползвате VisualBrush, без да го рисувате два пъти?

VisualBrush рисува произволно visual дърво — деца Canvas, Path и Glyphs — в област, евентуално повторено по нея. HotPDF компилира този visual веднъж в PDF Form XObject и после го поставя, което е същата ресурсна стратегия, описана в статията за SVG импорт чрез Form XObjects. Две подробности решават дали ще работи. Първо, visual-ът трябва да бъде обходен като директни XML деца: плоско сканиране за елементи, достойни за tile, издърпва вложени visual-и до най-горното ниво на страницата и унищожава и resource scoping, и реда на рисуване. Второ, съдържанието се улавя с вече приложената page матрица от XPS към PDF, така че публикуването на Form изисква умножаване по обратната на тази матрица, иначе всяко поставяне прилага отново мащаба 0.75 и обръщането по Y. Form трябва също да притежава своите ресурси: HotPDF копира само шрифтовете, XObjects, pattern-ите, ExtGStates и цветовите пространства, които уловеният content stream реално референцира; клонирането на целия page resource речник би дръпнало Form-а, който се регистрира, в собствената му ресурсна графика и би изградило цикъл. Шрифтовете остават в директен речник на обикновените страници и се повишават до споделен индиректен речник само когато уловеното съдържание реално съдържа Tf, така че документ без преизползваеми visual-и не плаща за механизма. Забележете една граница на спецификацията, добре да се знае, преди да вдигнете бъг: ECMA-388 раздел 13.4 изисква и ViewboxUnits, и ViewportUnits на VisualBrush да са Absolute, така че относителните единици не са липсваща възможност — те са несъответстващ вход, а HotPDF отказва да измисля координатна семантика за тях

HotPDF компилира visual дърво на XPS VisualBrush веднъж в PDF Form XObject, публикува го през обратната на fixed-page матрицата, така че поставянията не прилагат отново мащаба, и копира само ресурсите, които уловеното съдържание реално референцира
Съдържанието се улавя с вече приложена page матрица, така че Form се публикува през обратната ѝ и носи само ресурсите, които собственият му content stream референцира

ImageBrush tiling: четири режима, четири размера на клетка

Tile режимите на XPS се съпоставят на PDF tiling шаблони от ISO 32000-1 раздел 8.7.3, вместо да бъдат разширени в повтарящи се image поставяния по покритата област, което държи размера на изхода и времето за конверсия независими от това колко от страницата покрива четката. Съпоставката е механична, щом я видите: огледалното отражение се изразява, като огледалните поставяния влязат вътре в една pattern клетка, а клетката се разшири, за да съвпадне

  • Tile — една поставка, клетката остава 1×1 viewport
  • FlipX — две поставяния, клетката разширена до 2×1
  • FlipY — две поставяния, клетката издължена до 1×2
  • FlipXY — четири поставяния, клетката разширена до 2×2

Всяко поставяне носи собствен clip правоъгълник, защото съпоставка Viewbox, която прелива своя под-клетка, би протекла в съседното отражение. /Matrix на pattern е частта, която хваща хората. Tiling pattern се закотва към user space по подразбиране на родителския content stream, а не към graphics state, актуален в момента на избора на pattern, така че матрицата трябва да композира изрично и трите слоя — fixed-page проекцията, transform на Path и локалната за четката Transform — вместо да разчита на околен CTM. HotPDF също валидира, преди да задели: RegisterImageTilingPattern ограничава pattern до 1024 поставяния и отхвърля дегенерирали clip-ове, необратими матрици и невалидни image индекси. Ако искате общия модел от страната на PDF, статията за tiling шаблони и цветното пространство Pattern покрива подлежащите оператори

// Fixed-page проекцията сгъната в 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, така че четката е елипса. PDF shading тип 3, в ISO 32000-1 раздел 8.7.4.5.4, смесва между две окръжности и няма как да изрази елипса директно. Осредняването на двата радиуса в едно число е примамливият shortcut и е видимо грешно на всяка четка, която не е близо до кръг. HotPDF вместо това мести проблема в координатната система: мащабира Y с RadiusY / RadiusX, регистрира честно кръгово shading в това мащабирано пространство, избира pattern-а и веднага извежда реципрочния мащаб, така че path геометрията, записана след това, е все още в оригиналното XPS user space

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);   // pattern-ът улавя CTM точно тук
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

Подредбата в този фрагмент е целият трик и не е стилистична. PDF shading pattern улавя текущата матрица на трансформация в момента, в който е избран като текущ цвят, така че временният мащаб трябва да бъде изведен преди SetFillPattern или SetStrokePattern, а реципрочният трябва да след избора, но преди path операторите. Объркате ли реда в някоя от двете посоки, получавате градиент, който рендерира вярно на първия path и се отклонява на всеки следващ. Свързано ограничение важи за режима с относителни координати: RadiusX и RadiusY трябва да се разрешат поотделно спрямо ширината и височината на path, защото мащабирането и на двата с една дължина на ръб тихо променя съотношението на страните на елипсата на всеки несъответно квадратен path

HotPDF съпоставя елиптичен XPS RadialGradientBrush на PDF shading тип 3, като мащабира Y, регистрира кръгово shading в мащабираното пространство и извежда реципрочния мащаб само след като pattern-ът е уловил текущата матрица на трансформация
Елипсата се поема от координатната система, а не от shading-а, а временният мащаб трябва да обгърне избора на pattern точно в този ред

Къде конверсията е честна за границите си

Някои XPS конструкции се преобразуват приблизително, а други изобщо не се преобразуват, и проектният избор навсякъде е да ги изброят, вместо да ги изфалшифицират. Частите TIFF и JPEG XR се растеризират през WIC и не носят обещание за запазен alpha, докато PNG с валиден alpha канал се разделя на базово изображение плюс /SMask. Вътрешният размер на изображение се извежда като pixel * 96 / DPI, четейки първо плътността на PNG pHYs или JPEG JFIF, а иначе 96 DPI, така че лош header на плътност се приземява на предвидим размер, а не на произволен. Неразрешени matrix ресурси, нестандартни относителни transforms, ColorConvertedBitmap, неподдържани spread режими на градиент и лошо оформена геометрия всички увеличават UnsupportedFeatureCount, а лошо оформленият вход се проваля затворено, вместо да се влоши до тихо различно рисуване

Това е полезната поза за конвертор за архиви: конверсия, която тихо приближава, е по-лоша от такава, която ви казва кои четири елемента не е успяла да представи, защото само втората ви дава какво да проверите, преди документът да бъде запечатан в система за записи. Ако оценявате конверсията XPS и OpenXPS заедно с останалата част от документния конвейер — композиция на страници, шрифтове, подписване, изход PDF/A — страницата на HotPDF Delphi PDF component изброява пълния набор възможности и поддържаните версии Delphi и C++Builder