Teknisk artikel

XPS og OpenXPS til PDF i Delphi: Koordinater og brushes

HotPDF konverterer XPS- og OpenXPS-pakker til PDF inde i Delphi og C++Builder uden en printerdriver, mapper hver 96-DPI fixed-page-koordinat gennem én Y-vendende sidematrice med 0.75-skalering, publicerer hver VisualBrush som en delt Form XObject og omsætter ImageBrush-tiletilstande til native PDF-tilemønstre i stedet for gentagne billedtegninger

Scenariet, der trækker de fleste Windows-værksteder ind i dette, er kedeligt og uundgåeligt. Noget udskriver allerede til Microsoft XPS Document Writer — en ældre ERP-rapport, en signeret formular, en bunke kontoudtog — og arkivpolitikken siger PDF. XPS er et glimrende capture-format og et forfærdeligt at overrække til et journaleringssystem om ti år. Så spoolfilen skal blive til en side-for-side-PDF, og i det øjeblik man begynder at skrive den konverter, opdager man, at det interessante ikke er XMLen. Det er, at XPS og PDF er uenige om, hvor origo ligger, hvad en enhed er værd, og hvad en brush må være

Fra pakke til PDF i ét gennemløb

Indgangspunktet er document handler-registret, ikke en speciel XPS-klasse. THPDFDocumentHandlerRegistry.RegisterStandardHandlers installerer XPS-, EPUB- og CBZ-handlerne; genkendelse er indholdsbaseret, så en pakke med [Content_Types].xml plus mindst én .fpage-part scorer 95, selv når filendelsen lyver, mens en bare .xps- eller .oxps-endelse kun scorer 10. Den rækkefølge betyder noget, når du accepterer uploads, for en angriber, der omdøber en EPUB til .xps, skal ikke styre pipelinen

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 er den ærlige score for denne konvertering
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Alt ved konverteringen er budgetteret, inden det forsøges. THPDFDocumentHandlerOptions.Default begrænser arkivindgange til 10.000, udvidede archive-bytes til 1 GiB, komprimeringsforholdet til 200, ressourcer til 4.096 og sider til 10.000, og den bærer en valgfri CancellationToken, så et serverjob kan stoppes midt i en pakke. Læs Info.UnsupportedFeatureCount bagefter og behandl en værdi forskellig fra nul som et reelt fund: HotPDF tæller bevidst, hvad den ikke kunne mappe, i stedet for at tegne en tilnærmelse og tie om det

Hvorfor behøver en XPS-side en matrice i stedet for omskrevne koordinater?

Fordi omskrivning af koordinater mister transformstakken. En XPS FixedPage er specificeret i 96-DPI-enheder med origo øverst til venstre og Y voksende nedad; PDF-brugerrum er 72-DPI med origo nederst til venstre og Y voksende opad. Den naive løsning er at gange hvert tal med 0.75 og trække hver Y fra sidehøjden, efterhånden som den emitteres. Det virker for én flad path og falder fra hinanden i det øjeblik en RenderTransform, en indlejret Canvas eller en brush-lokal matrice kommer til, fordi de transformerer er defineret i XPS-rummet, og din koordinatvise omskrivning allerede har forladt det. HotPDF beholder derfor projektionen som en matrice og komponerer den. HPDFXPSPageMatrix returnerer de faste konstanter én gang pr. side, HPDFMultiplyXPSMatrix sammenkæder den med den akkumulerede path-transform, og resultatet emitteres som en enkelt cm-operator før geometrien. Path-data skrives derefter i uændrede XPS-tal, hvilket også er grunden til, at den forkortede geometrisyntax kan dele den samme afgrænsede parser, der bruges til SVG path-data — kun det indledende F0- eller F1-fill-rule-token håndteres af XPS-adapteren. Hvis du har fulgt samme ræsonnement for EMF- og WMF-vektorimport, er argumentets form bekendt: importformater konverteres med matricer, aldrig med aritmetik på bladkoordinater

HotPDF komponerer den faste XPS-sidematrice med den akkumulerede path-transform, så et 96-DPI XPS-koordinatsystem med origo øverst til venstre når 72-DPI PDF-brugerrum med origo nederst til venstre, emitteret som én enkelt cm-operator pr. visual
Projektionen forbliver en matrice og komponeres med hver indlejret transform, så path-data kan skrives i uændrede XPS-tal
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // 96-DPI XPS-enhed til 72-DPI PDF-punkt
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // XPS Y vokser nedad, PDF Y vokser opad
  Result.E :=  0;
  Result.F := PageHeight;  // PDF-sidehøjde i punkter
end;

// Én komponeret CTM pr. visual, emitteret før en path-operator
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Hvordan genbruger du en VisualBrush uden at tegne den to gange?

En VisualBrush maler et vilkårligt visualtræ — Canvas-, Path- og Glyphs-børn — ind i et område, eventuelt gentaget på tværs af det. HotPDF kompilerer det visual én gang til et PDF Form XObject og placerer det derefter, hvilket er samme ressourcestrategi som beskrevet i SVG-import via Form XObjects. To detaljer afgør, om det virker. For det første skal visualtet gennemgås som direkte XML-børn: en flad scanning efter tile-værdige elementer trækker indlejrede visuals op til sidens topniveau og ødelægger både ressourcescoping og maleorden. For det andet fanges indholdet med XPS-til-PDF-sidematricen allerede anvendt, så publicering af Formen kræver gang med inversen af den matrice, ellers genanvender hver placering 0.75-skaleringen og Y-vendingen. Formen skal også eje sine egne ressourcer: HotPDF kopierer kun de fonte, XObjects, mønstre, ExtGStates og farverum, som den fangede indholdsstrøm faktisk refererer; at klone hele sideressourceordbogen ville trække den Form, der registreres, ind i sin egen ressourcegraf og bygge en cyklus. Fonte forbliver i en direkte ordbog på almindelige sider og opgraderes kun til en delt indirekte ordbog, når fanget indhold virkelig indeholder en Tf, så et dokument uden genanvendelige visuals ikke betaler for maskineriet. Bemærk én specifikationsgrænse, der er værd at kende, før du indrapporterer en fejl: ECMA-388 section 13.4 kræver, at både ViewboxUnits og ViewportUnits på en VisualBrush er Absolute, så relative enheder er ikke en manglende funktion — de er ikke-overholdende input, og HotPDF nægter at opfinde koordinatsemantik for dem

HotPDF kompilerer et XPS VisualBrush-visualtræ én gang til et PDF Form XObject, publicerer det gennem inversen af fixed-page-matricen, så placeringer ikke genanvender skaleringen, og kopierer kun de ressourcer, det fangede indhold faktisk refererer
Indhold fanges med sidematricen allerede anvendt, så Formen publiceres gennem sin invers og kun bærer de ressourcer, dens egen indholdsstrøm refererer

ImageBrush-tiling: fire tilstande, fire cellestørrelser

XPS-tiletilstande afbildes på PDF-tilemønstre fra ISO 32000-1 section 8.7.3 i stedet for at blive foldet ud i gentagne billedplaceringer på tværs af det dækkede område, hvilket holder outputstørrelse og konverteringstid uafhængig af, hvor stor en del af siden brushen dækker. Afbildningen er mekanisk, når du først ser den: refleksion udtrykkes ved at lægge spejlede placeringer inde i én mønstrecelle og forstørre cellen, så den matcher

  • Tile — én placering, cellen forbliver 1×1 viewport
  • FlipX — to placeringer, cellen udvidet til 2×1
  • FlipY — to placeringer, cellen forhøjet til 1×2
  • FlipXY — fire placeringer, cellen udvidet til 2×2

Hver placering bærer sit eget clip-rektangel, for en Viewbox-afbildning, der løber over sin subcelle, ville blø ud i den naborefleksion. Mønstrets /Matrix er delen, der fanger folk. Et tilemønster forankres i standardbrugerrummet for dets forælder-indholdsstrøm, ikke i den grafiktilstand, der var aktuel, da mønsteret blev valgt, så matricen skal eksplicit komponere alle tre lag — fixed-page-projektionen, Path-transformen og den brush-lokale Transform — i stedet for at stole på en omgivende CTM. HotPDF validerer også, før den allokerer: RegisterImageTilingPattern begrænser et mønster til 1.024 placeringer og afviser degenererede clips, ikke-invertible matricer og ugyldige billedindekser. Hvis du vil have den generelle PDF-side af modellen bag dette, dækker tilemønstre og Pattern-farverummet de underliggende operatorer

// Fixed-page-projektionen foldet ind i mønstermatricen, derefter den brush-lokale
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);

Hvad sker der, når en radial gradient ikke er en cirkel?

XPS definerer en RadialGradientBrush med GradientOrigin, Center, RadiusX og RadiusY, så brushen er en ellipse. PDF shading type 3, i ISO 32000-1 section 8.7.4.5.4, blander mellem to cirkler og har ingen måde at udtrykke en ellipse direkte på. At gennemsnitte de to radier til ét tal er den fristende genvej, og den er synligt forkert på enhver brush, der ikke er tæt på rund. HotPDF flytter i stedet problemet ind i koordinatsystemet: den skalerer Y med RadiusY / RadiusX, registrerer en ærlig cirkulær shading i det skalerede rum, vælger mønsteret og emitterer straks den reciprokke skalering, så path-geometrien, der skrives derefter, stadig er i det oprindelige XPS-brugerrum

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);   // mønsteret fanger CTM lige her
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

Rækkefølgen i det uddrag er hele tricket, og den er ikke stilistisk. Et PDF shading-mønster fanger den aktuelle transformationsmatrice i det øjeblik, det vælges som aktuel farve, så den midlertidige skalering skal emitteres inden SetFillPattern eller SetStrokePattern, og den reciprokke skal følge valget, men gå forud for path-operatorerne. Får du rækkefølgen forkert i én af retningerne, har du en gradient, der renderer korrekt på den første path og driver på hver efterfølgende. En relateret begrænsning gælder relativ koordinattilstand: RadiusX og RadiusY skal løses mod henholdsvis path-bredde og -højde, da skalering af begge med én kantlængde stille ændrer ellipsens aspektforhold på enhver ikke-kvadratisk path

HotPDF afbilder en elliptisk XPS RadialGradientBrush på PDF shading type 3 ved at skalere Y, registrere en cirkulær shading i det skalerede rum og emittere den reciprokke skalering først, efter at mønsteret har fanget den aktuelle transformationsmatrice
Ellipsen absorberes af koordinatsystemet frem for shadingen, og den midlertidige skalering skal indramme mønstervalget i præcis den rækkefølge

Hvor konverteringen er ærlig om sine grænser

Nogle XPS-konstrukter konverteres tilnærmelsesvis, og nogle konverteres slet ikke, og designvalget hele vejen igennem er at tælle dem frem for at forfalske dem. TIFF- og JPEG XR-parts rasteriseres gennem WIC og bærer intet løfte om bevaret alfa, mens PNG med en gyldig alfakanal opdeles i et basisbillede plus en /SMask. Billedets indre størrelse udledes som pixel * 96 / DPI, læser PNG pHYs eller JPEG JFIF-tæthed først og falder tilbage til 96 DPI, så en dårlig tæthedshoved lander på en forudsigelig størrelse frem for en vilkårlig. Uløste matriceressourcer, ikke-standard relative transformer, ColorConvertedBitmap, ikke-understøttede gradient-spread-tilstande og misdannet geometri øger alle UnsupportedFeatureCount, og misdannet input fejler lukket i stedet for at degradere til en stille anderledes tegning

Det er den nyttige holdning for en arkivkonverter: en konvertering, der stille tilnærmer, er værre end en, der fortæller dig, hvilke fire elementer den ikke kunne repræsentere, for kun den anden giver dig noget at kontrollere, inden dokumentet forsegles i et journaleringssystem. Hvis du evaluerer XPS- og OpenXPS-konvertering sammen med resten af dokumentpipelinen — sidesammensætning, fonte, signering, PDF/A-output — lister HotPDF Delphi PDF component-siden det fulde funktionssæt og de understøttede Delphi- og C++Builder-versioner