Teknisk artikkel

XPS og OpenXPS til PDF i Delphi: koordinater og pensler

HotPDF konverterer XPS- og OpenXPS-pakker til PDF inne i Delphi og C++Builder uten en utskriftsdriver, ved å mappe hver 96-DPI fastside-koordinat gjennom én 0.75-skala Y-vendende sidematrise, publisere hver VisualBrush som en delt Form XObject, og gjøre ImageBrush flis-moduser om til native PDF flisleggingsmønstre i stedet for gjentatte bildetegninger

Scenariet som drar de fleste Windows-butikker inn i dette er kjedelig og uunngåelig. Noe skriver allerede ut til Microsoft XPS Document Writer — en eldre ERP-rapport, et signert skjema, en bunke kontoutskrifter — og arkivpolicyen sier PDF. XPS er et helt grept fangstformat og et forferdelig et å gi et arkivsystem et tiår fra nå. Så spool-filen må bli en side-for-side PDF, og i det øyeblikket du begynner å skrive den konverteren oppdager du at den interessante delen ikke er XML-en. Det er at XPS og PDF er uenige om hvor origo er, hva en enhet er verdt, og hva en pensel får lov å være

Fra pakke til PDF i én gjennomgang

Inngangspunktet er dokumenthåndterer-registeret, ikke en spesiell XPS-klasse. THPDFDocumentHandlerRegistry.RegisterStandardHandlers installerer XPS-, EPUB- og CBZ-håndtererne; gjenkjenning er innholdsbasert, så en pakke som bærer [Content_Types].xml pluss minst én .fpage-del scorer 95 selv når filetternavnet lyver, mens et bart .xps- eller .oxps-etternavn bare scorer 10. Den ordningen betyr noe når du aksepterer opplastinger, for en angriper som omdøper 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 poengsummen for denne konverteringen
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Alt om konverteringen er budsjettert før det forsøkes. THPDFDocumentHandlerOptions.Default takler arkivoppføringer ved 10,000, utvidede arkivbyte ved 1 GiB, komprimeringsforholdet ved 200, ressurser ved 4,096, og sider ved 10,000, og den bærer en valgfri CancellationToken så en serversidejobb kan stoppes midt i pakken. Les Info.UnsupportedFeatureCount etterpå og behandle en ikke-null verdi som et reelt funn: HotPDF teller villet hva den ikke kunne mape i stedet for å tegne en tilnærming og tie om det

Hvorfor trenger en XPS-side en matrise i stedet for omskrevne koordinater?

Fordi å omskrive koordinater mister transformstakken. En XPS FixedPage er spesifisert i 96-DPI enheter med origo øverst til venstre og Y voksende nedover; PDF brukerrom er 72-DPI med origo nederst til venstre og Y voksende oppover. Den naive fiks er å multiplisere hvert tall med 0.75 og subtrahere hver Y fra sidehøyden etter hvert som du avgir den. Det virker for én flat bane og faller fra hverandre i det øyeblikket en RenderTransform, en nestet Canvas, eller en pensel-lokal matrise entrer, for de transformene er definert i XPS-rom og din per-koordinat omskriving har allerede forlatt det. HotPDF beholder derfor projeksjonen som en matrise og komponerer den. HPDFXPSPageMatrix returnerer de faste konstantene én gang per side, HPDFMultiplyXPSMatrix lenker den med den akkumulerte banetransformen, og resultatet avgis som en enkelt cm-operator før geometrien. Banedata skrives deretter i umodifiserte XPS-tall, noe som også er hvorfor den forkortede geometrisyntaksen kan dele den samme avgrensede parseren brukt for SVG-banedata — bare det ledende F0- eller F1-fyllesregel-symbolet håndteres av XPS-adapteren. Har du fulgt samme resonnement for EMF- og WMF-vektorimport, er argumentets form kjent: importformater konverteres ved matrise, aldri ved aritmetikk på bladkoordinater

HotPDF komponerer den faste XPS sidematrisen med den akkumulerte banetransformen slik at et 96-DPI øverst-venstre XPS koordinatsystem når 72-DPI PDF brukerrom med origo nederst til venstre, avgitt som en enkelt cm-operator per visuell
Projeksjonen forblir en matrise og komponeres med hver nestede transform, så banedata kan skrives i umodifiserte XPS-tall
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // 96-DPI XPS-enhet til 72-DPI PDF-punkt
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // XPS Y vokser ned, PDF Y vokser opp
  Result.E :=  0;
  Result.F := PageHeight;  // PDF-sidehøyde, i punkter
end;

// Én komponert CTM per visuell, avgitt før enhver baneoperator
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Hvordan gjenbruker du en VisualBrush uten å tegne den to ganger?

En VisualBrush maler et vilkårlig visuelt tre — Canvas-, Path- og Glyphs-barn — inn i en region, muligens gjentatt på tvers av den. HotPDF kompilerer den visuelle én gang til en PDF Form XObject og plasserer den så, noe som er samme ressursstrategi beskrevet i SVG-import via Form XObjects. To detaljer avgjør om det virker. For det første må det visuelle gås gjennom som direkte XML-barn: en flat skanning for flisverdige elementer trekker nestede visuelle opp til sidenivå topp og ødelegger både ressursskoping og maleorden. For det andre fanges innhold med XPS-til-PDF sidematrisen allerede anvendt, så å publisere Form-en krever multiplikasjon med inversen av den matrisen, ellers re-anvender hver plassering 0.75-skalaen og Y-vendingen. Form-en må også eie sine ressurser: HotPDF kopierer bare skrifttypene, XObjects, mønstrene, ExtGStates og fargerommene den fangede innholdsstrømmen faktisk refererer; å klone hele sideressursordboken ville dra Form-en som registreres inn i sin egen ressursgraf og bygge en syklus. Skrifttyper forblir i en direkte ordbok på vanlige sider og forfremmes til en delt indirekte ordbok bare når fanget innhold virkelig inneholder en Tf, så et dokument uten gjenbrukbare visuelle betaler ikke for maskineriet. Merk én spesifikasjonsgrense verdt å vite før du arkiverer en feil: ECMA-388 seksjon 13.4 krever at både ViewboxUnits og ViewportUnits på en VisualBrush er Absolute, så relative enheter er ikke en manglende funksjon — de er ikke-konformt inndata, og HotPDF nekter å finne opp koordinatsemantikk for dem

HotPDF kompilerer et XPS VisualBrush visuelt tre én gang til en PDF Form XObject, publiserer det gjennom inversen av fastside-matrisen slik at plasseringer ikke re-anvender skalaen, og kopierer bare ressursene det fangede innholdet faktisk refererer
Innhold fanges med sidematrisen allerede anvendt, så Form-en publiseres gjennom dens inverse og bærer bare ressursene dens egen innholdsstrøm refererer

ImageBrush flislegging: fire moduser, fire cellestørrelser

XPS flis-moduser mappes onto PDF flisleggingsmønstre fra ISO 32000-1 seksjon 8.7.3 i stedet for å utvides til gjentatte bildeplasseringer på tvers av det dekte området, noe som holder utdatastørrelse og konverteringstid uavhengig av hvor mye av siden penselen dekker. Mappingen er mekanisk når du ser den: refleksjon uttrykkes ved å legge speilede plasseringer inne i én mønstercelle og utvide cellen til å matche

  • Tile — én plassering, cellen forblir 1×1 viewport
  • FlipX — to plasseringer, cellen utvidet til 2×1
  • FlipY — to plasseringer, cellen forhøyet til 1×2
  • FlipXY — fire plasseringer, cellen ekspandert til 2×2

Hver plassering bærer sitt eget klipprektangel, for en Viewbox-mapping som overskrider sin sub-celle ville blø inn i den naborefleksjonen. Mønsterets /Matrix er delen som fanger folk. Et flisleggingsmønster er forankret til standard brukerrom til sin foreldreinnholdsstrøm, ikke til grafikktilstanden gjeldende når mønsteret velges, så matrisen må komponere alle tre lagene eksplisitt — fastside-projeksjonen, Path-transformen, og pensel-lokal Transform — i stedet for å stole på en omgivende CTM. HotPDF validerer også før den tildeler: RegisterImageTilingPattern begrenser et mønster til 1,024 plasseringer og avviser degenererte klipp, ikke-inverterbare matriser og ugyldige bildeindekser. Vil du ha den generelle PDF-side modellen bak dette, dekker flisleggingsmønstre og Pattern fargerommet de underliggende operatorene

// Fastside-projeksjon foldet inn i mønstermatrisen, så den pensel-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);

Hva skjer når en radial gradient ikke er en sirkel?

XPS definerer en RadialGradientBrush med GradientOrigin, Center, RadiusX og RadiusY, så penselen er en ellipse. PDF shading type 3, i ISO 32000-1 seksjon 8.7.4.5.4, blander mellom to sirkler og har ingen måte å uttrykke en ellipse direkte. Å gjennomsnittliggi de to radiene til ett tall er den fristende snarveien, og den er synlig galt på enhver pensel som ikke er nær rund. HotPDF flytter i stedet problemet inn i koordinatsystemet: den skalerer Y med RadiusY / RadiusX, registrerer en ærlig sirkulær shading i det skalerte rommet, velger mønsteret, og avgir umiddelbart den resiproke skalaen så banegeometrien skrevet neste fortsatt er i originalt XPS brukerrom

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-en akkurat her
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

Ordningen i det utdraget er hele trikset, og den er ikke stilistisk. Et PDF shading-mønster fanger den gjeldende transformasjonsmatrisen i øyeblikket den velges som gjeldende farge, så den midlertidige skalaen må avgis før SetFillPattern eller SetStrokePattern, og den resiproke må følge valget men gå foran baneoperatorene. Får du ordenen galt i noen retning har du en gradient som gjengis korrekt på den første banen og driver på hver etterfølgende. En relatert begrensning gjelder relativ koordinatmodus: RadiusX og RadiusY må løses mot banens bredde og høyde separat, siden å skalere begge med én enkelt kantlengde stille endrer ellipseaspektforholdet på enhver ikke-kvadratisk bane

HotPDF mapper en elliptisk XPS RadialGradientBrush onto PDF shading type 3 ved å skalere Y, registrere en sirkulær shading i det skalerte rommet, og avgir den resiproke skalaen først etter at mønsteret har fanget den gjeldende transformasjonsmatrisen
Ellipsen absorberes av koordinatsystemet snarere enn shadingen, og den midlertidige skalaen må innramme mønstervalget i nøyaktig den orden

Der konverteringen er ærlig om sine grenser

Noen XPS-konstruksjoner konverteres tilnærmet og noen konverteres ikke i det hele tatt, og designvalget gjennomgående er å telle dem snarere enn å forfalske dem. TIFF- og JPEG XR-deler rasteriseres gjennom WIC og bærer intet løfte om bevart alfa, mens PNG med en gyldig alfakanal splittes i et grunnbilde pluss en /SMask. Bilde inntrinnende størrelse deriveres som pixel * 96 / DPI, ved å lese PNG pHYs eller JPEG JFIF tetthet først og falle tilbake til 96 DPI, så en dårlig tetthet-header lander ved en forutsigbar størrelse snarere enn en vilkårlig. Uløste matriseressurser, ikke-standard relative transformer, ColorConvertedBitmap, ustøttede gradient spredningsmoduser og feilformet geometri øker alle UnsupportedFeatureCount, og feilformet inndata feiler lukket i stedet for å degraderes til en stille annerledes tegning

Det er den nyttige holdningen for en arkivkonverterer: en konvertering som stille tilnærmer er verre enn en som forteller deg hvilke fire elementer den ikke kunne representere, for bare den andre gir deg noe å sjekke før dokumentet forsegles inn i et arkivsystem. Evaluerer du XPS- og OpenXPS-konvertering sammen med resten av dokumentpipelinen — sidekomposisjon, skrifttyper, signering, PDF/A-utdata — lister HotPDF Delphi PDF component-siden det fullstendige funksjonssettet og de støttede Delphi- og C++Builder-versjonene