Technický článek

XPS a OpenXPS na PDF v Delphi: souřadnice a štětce

HotPDF převádí balíčky XPS a OpenXPS na PDF přímo v Delphi a C++Builderu bez ovladače tiskárny: každou souřadnici pevné stránky s 96 DPI mapuje jednou maticí stránky se škálou 0,75 a překlopením osy Y, každý VisualBrush publikuje jako sdílený Form XObject a režimy dlaždic ImageBrush mění na nativní PDF vzory pro dlaždice místo opakovaného vykreslování obrázků

Scénář, který do tohohle vtáhne většinu Windows dílen, je nudný a nevyhnutelný. Něco už teď tiskne do Microsoft XPS Document Writer — zastaralá sestava z ERP, podepsaný formulář, dávka výpisů — a archivační politika říká PDF. XPS je výborný formát pro zachycení tisku a hrozný dar pro systém správy záznamů o dekádu později. Soubor ze spoolu tedy musí skončit jako PDF stránku za stránku, a ve chvíli, kdy začnete psát ten převodník, zjistíte, že ta zajímavá část není XML. Je to, že se XPS a PDF neshodnou v tom, kde leží počátek, kolik stojí jednotka a co smí být štetec

Od balíčku k PDF jedním průchodem

Vstupním bodem je registr obslužných rutin dokumentů, ne žádná speciální třída pro XPS. THPDFDocumentHandlerRegistry.RegisterStandardHandlers instaluje obslužné rutiny XPS, EPUB a CBZ; rozpoznávání je založené na obsahu, takže balíček nesoucí [Content_Types].xml a aspoň jednu část .fpage dosáhne skóre 95, i když lže přípona souboru, zatímco samotná přípona .xps nebo .oxps dosáhne jen 10. To pořadí je důležité, když přijímáte uploady, protože útočník, který přejmenuje EPUB na .xps, nesmí řídit pipeline

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 je poctivé skóre tohoto převodu
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Všechno u převodu má rozpočet dřív, než se na to dojde. THPDFDocumentHandlerOptions.Default omezuje položky archivu na 10 000, rozbalené bajty archivu na 1 GiB, kompresní poměr na 200, zdroje na 4 096 a stránky na 10 000 a nese volitelný CancellationToken, takže úlohu na serveru lze zastavit v půlce balíčku. Poté si přečtěte Info.UnsupportedFeatureCount a berte nenulovou hodnotu jako reálný nález: HotPDF záměrně počítá, co nedokázalo namapovat, místo aby kreslilo přibližku a mlčelo o tom

Proč stránka XPS potřebuje matici místo přepočítaných souřadnic?

Protože přepis souřadnic ztrácí zásobník transformací. XPS FixedPage se udává v jednotkách 96 DPI s počátkem vlevo nahoře a osou Y rostoucí dolů; uživatelský prostor PDF je 72 DPI s počátkem vlevo dole a osou Y rostoucí nahoru. Naivní oprava je vynásobit každé číslo 0,75 a při výstupu odečíst každé Y od výšky stránky. To funguje na jednu plochou cestu a rozpadne se v okamžiku, kdy vstoupí RenderTransform, vnořený Canvas nebo matice lokální pro štetec, protože ty transformace jsou definované v prostoru XPS a váš přepis po souřadnicích ho už opustil. HotPDF proto drží projekci jako matici a skládá ji. HPDFXPSPageMatrix vrátí pevné konstanty jednou za stránku, HPDFMultiplyXPSMatrix zřetězí ji s akumulovanou transformací cesty a výsledek se vydá jako jediný operátor cm před geometrií. Data cest se pak píšou v nezměněných číslech XPS, což je i důvod, proč zkrácená geometrická syntaxe může sdílet téhož ohraničeného parsera používaného pro data cest SVG — jen úvodní token F0 nebo F1 pravidla vyplňování obsluhuje adaptér XPS. Kdo sledoval stejnou úvahu u importu vektorů EMF a WMF, zná tvar argumentu: importní formáty se převádějí maticí, nikdy aritmetikou na listových souřadnicích

HotPDF skládá pevnou matici stránky XPS s akumulovanou transformací cesty, takže souřadný systém XPS s 96 DPI a počátkem vlevo nahoře dorazí do uživatelského prostoru PDF s 72 DPI a počátkem vlevo dole, vydaný jako jediný operátor cm na každý vizuál
Projekce zůstává maticí a skládá se s každou vnořenou transformací, takže data cest lze psát v nezměněných číslech XPS
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // jednotka XPS 96 DPI na bod PDF 72 DPI
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // Y v XPS roste dolů, Y v PDF roste nahoru
  Result.E :=  0;
  Result.F := PageHeight;  // výška stránky PDF v bodech
end;

// jedna složená CTM na vizuál, vydávaná před jakýmkoli operátorem cesty
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Jak znovu použít VisualBrush bez dvojího vykreslení?

VisualBrush maluje libovolný vizuální strom — děti Canvas, Path a Glyphs — do oblasti, případně po ní opakovaně. HotPDF ten vizuál jednou zkompiluje do PDF Form XObject a pak ho umísťuje, což je tatáž strategie zdrojů popsaná v importu SVG přes Form XObjects. O úspěchu rozhodují dva detaily. Zaprvé musí být vizuál procházen jako přímé XML děti: plochý sken prvků hodných dlaždice vytáhne vnořené vizuály na vrchol stránky a zničí jak rozsah zdrojů, tak pořadí malování. Zadruhé je obsah zachycen s již aplikovanou maticí stránky z XPS do PDF, takže publikování Formu vyžaduje násobení inverzí té matice, jinak každé umístění znovu aplikuje škálu 0,75 a překlopení Y. Form si navíc musí přisvojit své zdroje: HotPDF kopíruje jen ta písma, XObjects, vzory, ExtGStates a barevné prostory, na které zachycený proud obsahu skutečně odkazuje; klonování celého slovníku zdrojů stránky by vtáhlo registrovaný Form do jeho vlastního grafu zdrojů a postavilo cyklus. Písma zůstávají v přímém slovníku na obyčejných stránkách a povýší na sdílený nepřímý slovník, jen když zachycený obsah skutečně obsahuje Tf, takže dokument bez opakovaně použitelných vizuálů za ten mechanismus neplatí. Vězte jednu hranici specifikace, než začnete podávat hlášení: ECMA-388 oddíl 13.4 vyžaduje, aby ViewboxUnits i ViewportUnits na VisualBrush byly Absolute, takže relativní jednotky nejsou chybějící funkce — jsou nekonformní vstup, a HotPDF odmítá vymýšlet souřadnou sémantiku pro ně

HotPDF zkompiluje vizuální strom XPS VisualBrush jednou do PDF Form XObject, publikuje ho inverzí pevné matice stránky, takže umístění znovu neaplikují škálu, a kopíruje jen zdroje, na které zachycený obsah skutečně odkazuje
Obsah je zachycen s již aplikovanou maticí stránky, takže Form se publikuje její inverzí a nese jen zdroje, na které odkazuje jeho vlastní proud obsahu

Dlaždicování ImageBrush: čtyři režimy, čtyři velikosti buněk

Režimy dlaždic XPS se mapují na PDF vzory pro dlaždice z ISO 32000-1 oddílu 8.7.3 místo rozvinutí do opakovaných umístění obrázků přes pokrytou plochu, takže velikost výstupu a čas převodu nezávisí na tom, kolik stránky štetec pokryje. Mapování je mechanické, jakmile ho uvidíte: odraz se vyjádří vložením zrcadlených umístění do jedné buňky vzoru a zvětšením buňky, aby seděla

  • Tile — jedno umístění, buňka zůstává výřez 1×1
  • FlipX — dvě umístění, buňka rozšířená na 2×1
  • FlipY — dvě umístění, buňka zvýšená na 1×2
  • FlipXY — čtyři umístění, buňka rozšířená na 2×2

Každé umístění nese vlastní obdélník ořezu, protože mapování Viewbox, které přesáhne svou dílčí buňku, by se rozlilo do vedlejšího odrazu. Tady lidi chytá /Matrix vzoru. Vzor pro dlaždice se kotví do výchozího uživatelského prostoru svého rodičovského proudu obsahu, ne do grafického stavu platného při výběru vzoru, takže matice musí explicitně složit všechny tři vrstvy — pevnou projekci stránky, transformaci Path a štětcem lokální Transform — namísto spoléhání na okolní CTM. HotPDF navíc validuje dřív, než alokuje: RegisterImageTilingPattern omezuje vzor na 1 024 umístění a odmítá degenerované ořezy, neinvertovatelné matice a neplatné indexy obrázků. Kdo chce obecný model z PDF strany, najde pod tím ležící operátory v vzorech pro dlaždice a barevném prostoru Pattern

// pevná projekce stránky složená do matice vzoru, pak ta lokální pro štetec
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);

Co se stane, když radiální přechod není kruh?

XPS definuje RadialGradientBrush s GradientOrigin, Center, RadiusX a RadiusY, takže štetec je elipsa. Stínování PDF typu 3, v ISO 32000-1 oddílu 8.7.4.5.4, míchá mezi dvěma kruhy a elipsu přímo vyjádřit neumí. Průměr dvou poloměrů do jednoho čísla je lákavá zkratka a na každém štectvi, které není skoro kulaté, viditelně selhává. HotPDF místo toho stěhuje problém do souřadného systému: škáluje Y poměrem RadiusY / RadiusX, registruje poctivé kruhové stínování v té škálované soustavě, vybere vzor a hned vydá převrácenou škálu, takže geometrie cesty psaná potom je stále v původním uživatelském prostoru 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);   // vzor zachytí CTM právě tady
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

Pořadí v tom úryvku je celý trik, a není to otázka stylu. PDF vzor stínování zachytí aktuální transformační matici v okamžiku, kdy je vybrán jako aktuální barva, takže dočasná škála se musí vydat před SetFillPattern nebo SetStrokePattern a převrácená škála musí následovat po výběru, ale předcházet operátorům cesty. Pokazte pořadí kterýmkoli směrem a máte přechod, který se správně vykreslí na první cestě a na každé další se plíží. Příbuzné omezení platí pro režim relativních souřadnic: RadiusX a RadiusY se musí rozkládat zvlášť proti šířce a výšce cesty, protože škálování obou jedinou délkou hrany tiše mění poměr stran elipsy na jakékoli nečtvercové cestě

HotPDF mapuje eliptický XPS RadialGradientBrush na stínování PDF typu 3 škálováním Y, registrací kruhového stínování ve škálované soustavě a vydáním převrácené škály až poté, co vzor zachytil aktuální transformační matici
Elipsu pohltí souřadný systém, ne stínování, a dočasná škála musí výběr vzoru ohraničit přesně v tomto pořadí

Kde je převod poctivý ke svým limitům

Některé konstrukce XPS se převádějí přibližně a některé vůbec, a volba návrhu je pořád stejná: spočítat je, ne předstírat je. Části TIFF a JPEG XR se rasterizují přes WIC a nenesou žádný slib o zachované alfě, zatímco PNG s platným alfa kanálem se rozdělí na základní obrázek a /SMask. Vnitřní velikost obrázku se odvozuje jako pixel * 96 / DPI, s čtením hustoty PNG pHYs nebo JPEG JFIF jako první a pádem zpět na 96 DPI, takže špatná hlavička hustoty dopadne na předvídatelnou velikost, ne na libovolnou. Nevyřešené maticové zdroje, nestandardní relativní transformace, ColorConvertedBitmap, nepodporované režimy rozprostření přechodu a znetvořená geometrie zvyšují UnsupportedFeatureCount a znetvořený vstup selhává uzavřeně, místo aby sklouzl k tiše jinému kreslení

To je užitečný postoj pro archivační převodník: převod, který tiše přibližuje, je horší než ten, který řekne, které čtyři prvky nedokázal zachytit, protože jen druhý vám dá co kontrolovat dřív, než je dokument zapečetěný do systému záznamů. Kdybyste hodnotili převod XPS a OpenXPS vedle zbytku dokumentní pipeline — skladba stránek, písma, podepisování, výstup PDF/A — stránka HotPDF Delphi PDF komponenty uvádí celou sadu funkcí a podporované verze Delphi a C++Builderu