Technisch artikel

XPS en OpenXPS naar PDF in Delphi: coördinaten en brushes

HotPDF converteert XPS- en OpenXPS-pakketten naar PDF binnen Delphi en C++Builder zonder print driver, door elke 96-DPI fixed-page-coördinaat door één paginamatrix met schaal 0.75 en Y-flip te mappen, elke VisualBrush te publiceren als een gedeelde Form XObject, en ImageBrush-tile-modi om te zetten in native PDF tiling-patronen in plaats van herhaalde image-draws

Het scenario dat de meeste Windows-shops hiertoe brengt is saai en onvermijdelijk. Iets print al naar de Microsoft XPS Document Writer — een legacy ERP-rapport, een getekend formulier, een batch afschriften — en het archiefbeleid zegt PDF. XPS is een prima capture-formaat en een verschrikkelijk formaat om over tien jaar aan een recordsysteem te geven. Dus het spoelbestand moet een pagina-voor-pagina PDF worden, en op het moment dat u die converter begint te schrijven ontdekt u dat het interessante niet de XML is. Het is dat XPS en PDF het oneens zijn over waar de oorsprong ligt, wat een eenheid waard is, en wat een brush mag zijn

Van pakket naar PDF in één passage

Het entrypoint is het document-handlerregister, geen speciale XPS-klasse. THPDFDocumentHandlerRegistry.RegisterStandardHandlers installeert de XPS-, EPUB- en CBZ-handlers; herkenning is content-based, dus een pakket met [Content_Types].xml plus minstens één .fpage-onderdeel scoort 95 zelfs wanneer de bestandsextensie liegt, terwijl een blote .xps- of .oxps-extensie slechts 10 scoort. Die volgorde telt wanneer u uploads accepteert, want een aanvaller die een EPUB hernoemt naar .xps mag de pipeline niet sturen

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 is de eerlijke score voor deze conversie
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Alles rond de conversie wordt gebudgetteerd voordat het wordt geprobeerd. THPDFDocumentHandlerOptions.Default plafonneert archiefentries op 10.000, uitgepakte archive-bytes op 1 GiB, de compressieverhouding op 200, resources op 4.096, en pagina's op 10.000, en hij draagt een optionele CancellationToken zodat een server-side job halverwege het pakket kan worden gestopt. Lees daarna Info.UnsupportedFeatureCount en behandel een waarde ongelijk nul als een echte bevinding: HotPDF telt bewust wat hij niet kon mappen in plaats van een benadering te tekenen en erover te zwijgen

Waarom heeft een XPS-pagina een matrix nodig in plaats van herschreven coördinaten?

Omdat coördinaten herschrijven de transformstack verliest. Een XPS FixedPage is gespecificeerd in 96-DPI-eenheden met de oorsprong linksboven en Y omlaag groeiend; PDF user space is 72-DPI met de oorsprong linksonder en Y omhoog groeiend. De naïeve oplossing is elk getal met 0.75 vermenigvuldigen en elke Y van de paginahoogte aftrekken bij het emitteeren. Dat werkt voor één plat pad en valt uiteen op het moment dat een RenderTransform, een geneste Canvas, of een brush-lokale matrix binnenkomt, want die transforms zijn gedefinieerd in XPS-ruimte en uw herschrijving per coördinaat heeft die al verlaten. HotPDF houdt de projectie daarom als matrix vast en componeert hem. HPDFXPSPageMatrix geeft de vaste constanten één keer per pagina terug, HPDFMultiplyXPSMatrix concateneert hem met de opgetelde path-transform, en het resultaat wordt als één enkele cm-operator vóór de geometrie geëmitteerd. Path-data wordt daarna in ongewijzigde XPS-getallen geschreven, wat ook de reden is dat de afgekorte geometriesyntaxis dezelfde begrensde parser kan delen die voor SVG path-data wordt gebruikt — alleen het leidende F0- of F1-vulregeltoken wordt door de XPS-adapter afgehandeld. Als u dezelfde redenering hebt gevolgd bij EMF- en WMF-vectorimport, is de vorm van het argument vertrouwd: importformaten worden per matrix geconverteerd, nooit met rekenkunst op bladcoördinaten

HotPDF componeert de vaste XPS-paginamatrix met de opgetelde path-transform zodat een 96-DPI XPS-coördinatensysteem met oorsprong linksboven in 72-DPI PDF user space aankomt met zijn oorsprong linksonder, geëmitteerd als één enkele cm-operator per visual
De projectie blijft een matrix en wordt met elke geneste transform gecomponeerd, zodat path-data in ongewijzigde XPS-getallen kan worden geschreven
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // 96-DPI XPS-eenheid naar 72-DPI PDF-punt
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // XPS Y groeit naar beneden, PDF Y groeit naar boven
  Result.E :=  0;
  Result.F := PageHeight;  // PDF-paginahoogte, in punten
end;

// Eén samengestelde CTM per visual, geëmitteerd vóór elke path-operator
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Hoe hergebruikt u een VisualBrush zonder hem twee keer te tekenen?

Een VisualBrush verft een willekeurige visuele boom — Canvas-, Path- en Glyphs-kinderen — in een gebied, eventueel herhaald over dat gebied. HotPDF compileert die visual één keer tot een PDF Form XObject en plaatst hem daarna, wat dezelfde resourcestrategie is als beschreven in SVG-import via Form XObjects. Twee details bepalen of het werkt. Ten eerste moet de visual als directe XML-kinderen worden doorlopen: een platte scan naar tile-waardige elementen trekt geneste visuals omhoog naar het paginatopniveau en vernielt zowel resourcescoping als tekenvolgorde. Ten tweede wordt content vastgelegd met de XPS-naar-PDF-paginamatrix al toegepast, dus het publiceren van de Form vereist vermenigvuldigen met de inverse van die matrix, anders past elke plaatsing opnieuw de schaal 0.75 en de Y-flip toe. De Form moet ook zijn eigen resources bezitten: HotPDF kopieert alleen de fonts, XObjects, patronen, ExtGStates en kleurruimten die de vastgelegde contentstream werkelijk referentieert; de hele pagina-resource-dictionary klonen zou de Form die wordt geregistreerd in zijn eigen resourcegraph slepen en een cyclus bouwen. Fonts blijven in een directe dictionary op gewone pagina's en worden alleen gepromoveerd naar een gedeelde indirecte dictionary wanneer vastgelegde content werkelijk een Tf bevat, dus een document zonder herbruikbare visuals betaalt niet voor de machinerie. Merk één specificatiegrens op die de moeite waard is te weten voordat u een bug meldt: ECMA-388 sectie 13.4 vereist dat zowel ViewboxUnits als ViewportUnits op een VisualBrush Absolute is, dus relatieve eenheden zijn geen ontbrekende feature — het is niet-conforme input, en HotPDF weigert coördinaatsemantiek voor hen te verzinnen

HotPDF compileert een XPS VisualBrush visuele boom één keer tot een PDF Form XObject, publiceert hem via de inverse van de fixed-page-matrix zodat plaatsingen de schaal niet opnieuw toepassen, en kopieert alleen de resources die de vastgelegde content werkelijk referentieert
Content wordt vastgelegd met de paginamatrix al toegepast, dus de Form wordt via zijn inverse gepubliceerd en draagt alleen de resources waar zijn eigen contentstream naar refereert

ImageBrush tiling: vier modi, vier celgroottes

XPS-tile-modi worden gemapt op PDF tiling-patronen uit ISO 32000-1 sectie 8.7.3 in plaats van te worden uitgeklapt naar herhaalde image-plaatsingen over het gedekte gebied, wat de uitvoergrootte en conversietijd onafhankelijk houdt van hoeveel van de pagina de brush dekt. De mapping is mechanisch zodra u hem ziet: spiegelbeeld wordt uitgedrukt door gespiegelde plaatsingen binnen één patrooncel te zetten en de cel te vergroten om te matchen

  • Tile — één plaatsing, cel blijft 1×1 viewport
  • FlipX — twee plaatsingen, cel verbreed naar 2×1
  • FlipY — twee plaatsingen, cel verhoogd naar 1×2
  • FlipXY — vier plaatsingen, cel uitgebreid naar 2×2

Elke plaatsing draagt zijn eigen clip-rechthoek, want een Viewbox-mapping die zijn subcel overstroomt zou in de buurspiegeling bloeden. De patroon-/Matrix is het deel dat mensen vangt. Een tiling-patroon is geankerd aan de default user space van zijn ouder-contentstream, niet aan de grafische toestand die heerst wanneer het patroon wordt geselecteerd, dus de matrix moet alle drie de lagen expliciet componeren — de fixed-page-projectie, de Path-transform, en de brush-lokale Transform — in plaats van te leunen op een omgevings-CTM. HotPDF valideert ook voordat hij alloceert: RegisterImageTilingPattern beperkt een patroon tot 1.024 plaatsingen en wijst degenerische clips, niet-inverteerbare matrices en ongeldige image-indices af. Wilt u het algemene PDF-side model erachter, dan behandelt tiling-patronen en de Pattern-kleurruimte de onderliggende operatoren

// Fixed-page-projectie in de patroonmatrix gevouwen, daarna die van de brush zelf
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);

Wat gebeurt er wanneer een radiaal gradiënt geen cirkel is?

XPS definieert een RadialGradientBrush met GradientOrigin, Center, RadiusX en RadiusY, dus de brush is een ellips. PDF shading type 3, in ISO 32000-1 sectie 8.7.4.5.4, blendeert tussen twee cirkels en heeft geen manier om een ellips rechtstreeks uit te drukken. De twee stralen tot één getal middelen is de verleidelijke shortcut en hij is zichtbaar fout op elke brush die niet bij benadering rond is. HotPDF verplaatst het probleem in plaats daarvan naar het coördinatensysteem: hij schaalt Y met RadiusY / RadiusX, registreert een eerderlijke cirkelvormige shading in die geschaalde ruimte, selecteert het patroon, en emitteert onmiddellijk de reciproke schaal zodat de path-geometrie die erna wordt geschreven nog steeds in de oorspronkelijke XPS user space staat

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);   // het patroon vangt de CTM precies hier
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

De volgorde in dat fragment is de hele truc, en hij is niet stilistisch. Een PDF shading-patroon vangt de huidige transformatiematrix op het moment dat hij als huidige kleur wordt gekozen, dus de tijdelijke schaal moet vóór SetFillPattern of SetStrokePattern worden geëmitteerd, en de reciproke moet op de selectie volgen maar vóór de path-operators komen. Haal de volgorde in één van beide richtingen fout en u hebt een gradiënt die correct rendert op het eerste pad en afdwaalt op elk daaropvolgend. Een verwante beperking geldt voor relatieve coördinaatmodus: RadiusX en RadiusY moeten apart worden geresolveerd tegen de padbreedte en -hoogte, want beide met één zijlengte schalen verandert stilletjes de beeldverhouding van de ellips op elk niet-vierkant pad

HotPDF mapt een ellipsvormige XPS RadialGradientBrush op PDF shading type 3 door Y te schalen, een cirkelvormige shading in de geschaalde ruimte te registreren, en de reciproke schaal pas te emitteeren nadat het patroon de huidige transformatiematrix heeft gevangen
De ellips wordt door het coördinatensysteem geabsorbeerd in plaats van door de shading, en de tijdelijke schaal moet de patroonselectie omsluiten in precies die volgorde

Waar de conversie eerlijk is over zijn grenzen

Sommige XPS-constructen worden benaderd geconverteerd en sommige helemaal niet, en de ontwerpkeuze overal is ze te tellen in plaats van te faken. TIFF- en JPEG XR-onderdelen worden via WIC gerasterd en dragen geen belofte over bewaarde alpha, terwijl PNG met een geldig alphakanaal wordt gesplitst in een base-image plus een /SMask. De intrinsieke image-grootte wordt afgeleid als pixel * 96 / DPI, eerst PNG pHYs of JPEG JFIF-dichtheid lezend en terugvallend op 96 DPI, dus een slechte dichtheidsheader landt op een voorspelbare grootte in plaats van een willekeurige. Onopgeloste matrix-resources, niet-standaard relatieve transforms, ColorConvertedBitmap, niet-ondersteunde gradiënt-spread-modi en misvormde geometrie verhogen allemaal UnsupportedFeatureCount, en misvormde input faalt dicht in plaats van te degraderen naar stilletjes een andere tekening

Dat is de nuttige houding voor een archiefconverteerder: een conversie die stilletjes benadert is erger dan één die u vertelt welke vier elementen hij niet kon representeren, want alleen de tweede geeft u iets om te controleren voordat het document wordt verzegeld in een recordsysteem. Evalueert u XPS- en OpenXPS-conversie naast de rest van de documentpipeline — pagina-opbouw, fonts, signeren, PDF/A-uitvoer — dan zet de pagina HotPDF Delphi PDF component de volledige featureset en de ondersteunde Delphi- en C++Builder-versies op een rij