HotPDF konverterar XPS- och OpenXPS-paket till PDF inuti Delphi och C++Builder utan skrivardrivrutin, avbildar varje 96-DPI-koordinat för fasta sidor genom en enda sidmatris med 0,75-skala och Y-vändning, publicerar varje VisualBrush som en delad Form XObject och omvandlar ImageBrush-tilelägen till inbyggda PDF-tilemönster i stället för upprepade bildritningar
Scenariot som drar in de flesta Windows-butor i detta är tråkigt och oundvikligt. Något skriver redan ut till Microsoft XPS Document Writer — en äldre ERP-rapport, ett signerat formulär, en batch av avier — och arkivpolicyn säger PDF. XPS är ett fullständigt bra infångningsformat och ett fruktansvärt sådant att lämna till ett arkivsystem om ett decennium. Så spoolfilen måste bli en sida-för-sida-PDF, och i samma ögonblick som du börjar skriva den konverteraren upptäcker du att den intressanta delen inte är XML:en. Det är att XPS och PDF är oense om var origo ligger, vad en enhet är värd och vad en pensel får vara
Från paket till PDF i ett pass
Ingångspunkten är registerhanteraren för dokument, inte en särskild XPS-klass. THPDFDocumentHandlerRegistry.RegisterStandardHandlers installerar hanterarna för XPS, EPUB och CBZ; igenkänningen är innehållsbaserad, så ett paket som bär [Content_Types].xml plus minst en .fpage-del får 95 poäng även när filändelsen ljuger, medan en bar .xps- eller .oxps-ändelse bara får 10. Den ordningen spelar roll när du tar emot uppladdningar, eftersom en angripare som döper om en EPUB till .xps inte ska kunna styra 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 är det ärliga måttet för den här konverteringen
finally
Output.Free;
Registry.Free;
end;
end;
Allt i konverteringen budgeteras innan den försöks. THPDFDocumentHandlerOptions.Default begränsar arkivposter till 10 000, expanderade arkivbyte till 1 GiB, komprimeringsförhållandet till 200, resurser till 4 096 och sidor till 10 000, och den bär en valfri CancellationToken så att ett jobb på serversidan kan stoppas mitt i paketet. Läs Info.UnsupportedFeatureCount efteråt och behandla ett värde skilt från noll som ett riktigt fynd: HotPDF räknar avsiktligt det den inte kunde avbilda i stället för att rita en approximation och vara tyst om det
Varför behöver en XPS-sida en matris i stället för omskrivna koordinater?
Eftersom omskrivning av koordinater tappar transformstacken. En XPS FixedPage specificeras i 96-DPI-enheter med origo uppe till vänster och Y växande nedåt; PDF:s användarrymd är 72-DPI med origo nere till vänster och Y växande uppåt. Den naiva lösningen är att multiplicera varje tal med 0,75 och subtrahera varje Y från sidhöjden när du skriver ut det. Det fungerar för en platt bana och faller sönder i samma ögonblick som en RenderTransform, en nästlad Canvas eller en pensellokal matris kommer in, eftersom dessa transformeringar är definierade i XPS-rymden och din omskrivning per koordinat redan har lämnat den. HotPDF behåller därför projektionen som en matris och komponerar den. HPDFXPSPageMatrix returnerar de fasta konstanterna en gång per sida, HPDFMultiplyXPSMatrix konkatenerar den med den ackumulerade bantransformen, och resultatet skrivs ut som en enda cm-operator före geometrin. Bandata skrivs sedan i omodifierade XPS-tal, vilket också är därför den förkortade geometrisyntaxen kan dela samma avgränsade parser som används för SVG-bandata — bara den inledande F0- eller F1-fyllningsregeltoken hanteras av XPS-adaptern. Om du har följt samma resonemang för EMF- och WMF-vektorimport är argumentets form bekant: importformat konverteras via matris, aldrig via aritmetik på lövkoordinater
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // 96-DPI XPS-enhet till 72-DPI PDF-punkt
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // XPS Y växer nedåt, PDF Y växer uppåt
Result.E := 0;
Result.F := PageHeight; // PDF-sidhöjd, i punkter
end;
// En komponerad CTM per visuell, utskriven före varje banoperator
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
Hur återanvänder du en VisualBrush utan att rita den två gånger?
En VisualBrush målar ett godtyckligt visuellt träd — Canvas-, Path- och Glyphs-barn — in i en region, möjligen upprepat över den. HotPDF kompilerar den visuella en gång till en PDF Form XObject och placerar den sedan, vilket är samma resursstrategi som beskrivs i SVG-import via Form XObjects. Två detaljer avgör om det fungerar. För det första måste den visuella gås igenom som direkta XML-barn: en platt skanning efter tilevärdiga element drar upp nästlade visuella till sidans toppnivå och förstör både resursscopning och målningsordning. För det andra fångas innehållet med XPS-till-PDF-sidmatrisen redan applicerad, så att publicera Form:en kräver multiplikation med inversen av den matrisen, annars applicerar varje placering om 0,75-skalan och Y-vändningen. Form:en måste också äga sina resurser: HotPDF kopierar bara de teckensnitt, XObjects, mönster, ExtGStates och färgrymder som den fångade innehållsströmmen faktiskt refererar till; att klona hela sidans resursordlista skulle dra in Form:en som registreras i sin egen resursgraf och bygga en cykel. Teckensnitt ligger i en direkt ordlista på vanliga sidor och befordras till en delad indirekt ordlista först när fångat innehåll verkligen innehåller en Tf, så ett dokument utan återanvändbara visuella betalar inte för maskineriet. Notera en specifikationsgräns värd att känna till innan du rapporterar en bugg: ECMA-388 avsnitt 13.4 kräver att både ViewboxUnits och ViewportUnits på en VisualBrush är Absolute, så relativa enheter är inte en saknad funktion — de är icke-konform indata, och HotPDF vägrar att hitta på koordinatsemantik för dem
ImageBrush-tile: fyra lägen, fyra cellstorlekar
XPS-tilelägen avbildas på PDF-tilemönster från ISO 32000-1 avsnitt 8.7.3 i stället för att expanderas till upprepade bildplaceringar över det täckta området, vilket håller utdatastorlek och konverteringstid oberoende av hur mycket av sidan penseln täcker. Avbildningen är mekanisk när man väl ser den: spegling uttrycks genom att lägga speglade placeringar inuti en mönstercell och förstora cellen för att matcha
Tile— en placering, cellen förblir 1×1 viewportFlipX— två placeringar, cellen breddas till 2×1FlipY— två placeringar, cellen höjs till 1×2FlipXY— fyra placeringar, cellen expanderas till 2×2
Varje placering bär sin egen urklippsrektangel, eftersom en Viewbox-avbildning som överskrider sin undercell skulle blöda in i den intilliggande speglingen. Mönstrets /Matrix är den del som fångar folk. Ett tilemönster är förankrat i standardanvändarrymden för sin förälders innehållsström, inte i det grafikstillstånd som gäller när mönstret väljs, så matrisen måste komponera alla tre lagren explicit — den fasta sidprojektionen, Path-transformen och den pensellokala Transform — i stället för att lita på en omgivande CTM. HotPDF validerar också innan den allokerar: RegisterImageTilingPattern begränsar ett mönster till 1 024 placeringar och avvisar degenererade urklipp, icke-inverterbara matriser och ogiltiga bildindex. Om du vill ha den generella PDF-modellen bakom detta täcker tilemönster och Pattern-färgrymden de underliggande operatorerna
// Fast sidprojektion invikt i mönstermatrisen, sedan den pensellokala
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);
Vad händer när en radiell gradient inte är en cirkel?
XPS definierar en RadialGradientBrush med GradientOrigin, Center, RadiusX och RadiusY, så penseln är en ellips. PDF-toning typ 3, i ISO 32000-1 avsnitt 8.7.4.5.4, övergår mellan två cirklar och har inget sätt att uttrycka en ellips direkt. Att medelvärdesbilda de två radierna till ett tal är den frestande genvägen, och den är synbart fel på varje pensel som inte är nära rund. HotPDF flyttar i stället problemet in i koordinatsystemet: den skalar Y med RadiusY / RadiusX, registrerar en ärlig cirkulär toning i den skalade rymden, väljer mönstret och skriver omedelbart ut den reciproka skalan så att bangeometrin som skrivs härnäst fortfarande ligger i ursprunglig XPS-användarrymd
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önstret fångar CTM:n just här
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
Ordningen i det kodstycket är hela tricket, och den är inte stilistisk. Ett PDF-toningsmönster fångar den aktuella transformationsmatrisen i det ögonblick det väljs som aktuell färg, så den tillfälliga skalan måste skrivas ut före SetFillPattern eller SetStrokePattern, och den reciproka måste följa valet men föregå banoperatorerna. Få ordningen fel i någon riktning och du har en gradient som renderar korrekt på den första banan och driver på varje efterföljande. En besläktad begränsning gäller relativt koordinatläge: RadiusX och RadiusY måste lösas mot banans bredd och höjd separat, eftersom att skala båda med en enda kantlängd tyst ändrar ellipsens bildförhållande på varje icke-kvadratisk bana
Där konverteringen är ärlig om sina gränser
Vissa XPS-konstruktioner konverteras approximativt och vissa konverteras inte alls, och designvalet genomgående är att räkna dem i stället för att förfalska dem. TIFF- och JPEG XR-delar rastreras via WIC och bär inget löfte om bevarad alfa, medan PNG med en giltig alfakanal delas upp i en basbild plus en /SMask. Bildens inneboende storlek härleds som pixel * 96 / DPI, med PNG pHYs eller JPEG JFIF-densitet läst först och fallback till 96 DPI, så en dålig densitetsheader landar på en förutsägbar storlek i stället för en godtycklig. Olösta matrisresurser, icke-standardiserade relativa transformeringar, ColorConvertedBitmap, gradient-spread-lägen som inte stöds och felaktig geometri inkrementerar alla UnsupportedFeatureCount, och felaktig indata misslyckas stängt i stället för att degraderas till en tyst annorlunda ritning
Det är den användbara hållningen för en arkiveringskonverterare: en konvertering som tyst approximerar är sämre än en som talar om vilka fyra element den inte kunde representera, eftersom bara den andra ger dig något att kontrollera innan dokumentet förseglas in i ett arkivsystem. Om du utvärderar XPS- och OpenXPS-konvertering tillsammans med resten av dokumentpipelinen — sidkomposition, teckensnitt, signering, PDF/A-utdata — listar sidan för HotPDF Delphi PDF-komponent hela funktionsuppsättningen och de Delphi- och C++Builder-versioner som stöds