HotPDF converte i pacchetti XPS e OpenXPS in PDF all'interno di Delphi e C++Builder senza un driver di stampa, mappando ogni coordinata fixed-page a 96 DPI attraverso un'unica matrice di pagina in scala 0.75 con inversione dell'asse Y, pubblicando ogni VisualBrush come Form XObject condiviso e trasformando le modalità tile di ImageBrush in pattern di tiling PDF nativi invece di disegni di immagine ripetuti
Lo scenario che trascina la maggior parte delle aziende Windows in questa faccenda è banale e inevitabile. Qualcosa stampa già con la Microsoft XPS Document Writer — un report ERP datato, un modulo firmato, un lotto di estratti conto — e la policy di archiviazione dice PDF. XPS è un formato di acquisizione più che valido e terribile da consegnare a un sistema di record tra dieci anni. Quindi il file di spool deve diventare un PDF pagina per pagina, e nel momento in cui si inizia a scrivere quel convertitore si scopre che la parte interessante non è l'XML. È che XPS e PDF non sono d'accordo su dove si trovi l'origine, quanto valga un'unità e cosa a un pennello sia permesso essere
Dal pacchetto al PDF in un solo passaggio
Il punto di ingresso è il registro dei document handler, non una classe XPS speciale. THPDFDocumentHandlerRegistry.RegisterStandardHandlers installa i handler XPS, EPUB e CBZ; il riconoscimento si basa sul contenuto, quindi un pacchetto con [Content_Types].xml più almeno una parte .fpage ottiene un punteggio di 95 anche quando l'estensione del file mente, mentre una semplice estensione .xps o .oxps ottiene solo 10. Questo ordinamento conta quando si accettano upload, perché un malintenzionato che rinomina un EPUB in .xps non deve poter orientare la 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 è il punteggio onesto di questa conversione
finally
Output.Free;
Registry.Free;
end;
end;
Tutto ciò che riguarda la conversione viene preventivato prima di essere tentato. THPDFDocumentHandlerOptions.Default limita le voci di archivio a 10.000, i byte di archivio espansi a 1 GiB, il rapporto di compressione a 200, le risorse a 4.096 e le pagine a 10.000, e porta con sé un CancellationToken opzionale così che un job lato server possa essere interrotto a metà pacchetto. Leggere poi Info.UnsupportedFeatureCount e trattare un valore diverso da zero come un riscontro reale: HotPDF conta deliberatamente ciò che non ha potuto mappare invece di disegnare un'approssimazione e tacere su di essa
Perché una pagina XPS richiede una matrice invece di coordinate riscritte?
Perché riscrivere le coordinate fa perdere lo stack di trasformazioni. Un FixedPage XPS è specificato in unità a 96 DPI con l'origine in alto a sinistra e Y che cresce verso il basso; lo spazio utente PDF è a 72 DPI con l'origine in basso a sinistra e Y che cresce verso l'alto. La correzione ingenua è moltiplicare ogni numero per 0.75 e sottrarre ogni Y dall'altezza della pagina mentre lo si emette. Funziona per un unico percorso piatto e si sgretola nell'istante in cui entra un RenderTransform, un Canvas annidato o una matrice locale del pennello, perché quelle trasformazioni sono definite nello spazio XPS e la riscrittura per coordinata lo ha già abbandonato. HotPDF quindi conserva la proiezione come matrice e la compone. HPDFXPSPageMatrix restituisce le costanti fisse una volta per pagina, HPDFMultiplyXPSMatrix la concatena con la trasformazione di percorso accumulata, e il risultato viene emesso come un singolo operatore cm prima della geometria. I dati del percorso vengono poi scritti in numeri XPS non modificati, ed è anche per questo che la sintassi geometrica abbreviata può condividere lo stesso parser limitato usato per i path data SVG — solo il token fill-rule iniziale F0 o F1 è gestito dall'adattatore XPS. Se avete seguito lo stesso ragionamento per l'importazione vettoriale EMF e WMF, la forma dell'argomento è familiare: i formati di importazione si convertono per matrice, mai con aritmetica sulle coordinate foglia
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // unità XPS 96 DPI in punto PDF 72 DPI
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // Y XPS cresce verso il basso, Y PDF verso l'alto
Result.E := 0;
Result.F := PageHeight; // altezza della pagina PDF, in punti
end;
// Una CTM composta per visual, emessa prima di qualsiasi operatore di percorso
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
Come riutilizzare un VisualBrush senza disegnarlo due volte?
Un VisualBrush dipinge un albero visuale arbitrario — figli Canvas, Path e Glyphs — in una regione, eventualmente ripetuto su di essa. HotPDF compila quel visual una volta in un Form XObject PDF e poi lo posiziona, che è la stessa strategia di risorse descritta nell'importazione SVG tramite Form XObject. Due dettagli decidono se il meccanismo funziona. Primo, il visual va percorso come figli XML diretti: una scansione piatta alla ricerca di elementi degni di tile solleva i visual annidati fino al livello superiore della pagina e distrugge sia lo scoping delle risorse sia l'ordine di disegno. Secondo, il contenuto viene catturato con la matrice di pagina da XPS a PDF già applicata, quindi pubblicare il Form richiede di moltiplicare per l'inverso di quella matrice, altrimenti ogni posizionamento riapplica la scala 0.75 e l'inversione dell'asse Y. Il Form deve anche possedere le proprie risorse: HotPDF copia solo i font, gli XObject, i pattern, gli ExtGState e gli spazi colore a cui lo stream di contenuto catturato fa effettivamente riferimento; clonare l'intero dizionario di risorse di pagina trascinerebbe il Form in fase di registrazione nel proprio grafo di risorse e costruirebbe un ciclo. I font restano in un dizionario diretto sulle pagine ordinarie e vengono promossi a un dizionario indiretto condiviso solo quando il contenuto catturato contiene davvero un Tf, così un documento senza visual riutilizzabili non paga per questo macchinario. Notare un confine della specifica che vale la pena conoscere prima di aprire un bug report: la sezione 13.4 di ECMA-388 richiede che sia ViewboxUnits sia ViewportUnits su un VisualBrush siano Absolute, quindi le unità relative non sono una funzionalità mancante — sono input non conformi, e HotPDF rifiuta di inventare semantiche di coordinate per loro
Tiling di ImageBrush: quattro modalità, quattro dimensioni di cella
Le modalità tile di XPS si mappano sui pattern di tiling PDF della sezione 8.7.3 di ISO 32000-1 invece di essere espanse in posizionamenti di immagine ripetuti sull'area coperta, il che tiene la dimensione dell'output e il tempo di conversione indipendenti da quanta parte della pagina copre il pennello. La mappatura è meccanica una volta vista: la riflessione si esprime mettendo i posizionamenti specchiati dentro una singola cella del pattern e ingrandendo la cella di conseguenza
Tile— un posizionamento, la cella resta un viewport 1×1FlipX— due posizionamenti, cella allargata a 2×1FlipY— due posizionamenti, cella innalzata a 1×2FlipXY— quattro posizionamenti, cella espansa a 2×2
Ogni posizionamento porta il proprio rettangolo di clip, perché una mappatura Viewbox che oltrepassa la propria sotto-cella sconfinebbe nella riflessione vicina. Il /Matrix del pattern è la parte che prende in contropiede. Un pattern di tiling è ancorato allo spazio utente predefinito del suo content stream genitore, non allo stato grafico corrente al momento della selezione del pattern, quindi la matrice deve comporre esplicitamente tutti e tre i livelli — la proiezione fixed-page, la trasformazione Path e la Transform locale del pennello — invece di affidarsi a una CTM ambientale. HotPDF valida anche prima di allocare: RegisterImageTilingPattern limita un pattern a 1.024 posizionamenti e rifiuta clip degeneri, matrici non invertibili e indici di immagine non validi. Se volete il modello PDF generale alla base di tutto questo, i pattern di tiling e lo spazio colore Pattern coprono gli operatori sottostanti
// Proiezione fixed-page ripiegata nella matrice del pattern, poi quella locale del pennello
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);
Cosa succede quando un gradiente radiale non è un cerchio?
XPS definisce un RadialGradientBrush con GradientOrigin, Center, RadiusX e RadiusY, quindi il pennello è un'ellisse. Lo shading PDF di tipo 3, nella sezione 8.7.4.5.4 di ISO 32000-1, sfuma tra due cerchi e non ha modo di esprimere direttamente un'ellisse. Fare la media dei due raggi in un unico numero è la scorciatoia allettante ed è visibilmente sbagliata su qualsiasi pennello non quasi rotondo. HotPDF invece sposta il problema nel sistema di coordinate: scala Y di RadiusY / RadiusX, registra uno shading circolare onesto in quello spazio scalato, seleziona il pattern e emette subito la scala reciproca così che la geometria del percorso scritta subito dopo resta nello spazio utente XPS originale
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); // il pattern cattura la CTM proprio qui
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
L'ordine in quello snippet è tutto il trucco, e non è questione di stile. Un pattern di shading PDF cattura la matrice di trasformazione corrente nel momento in cui viene scelto come colore corrente, quindi la scala temporanea va emessa prima di SetFillPattern o SetStrokePattern, e la reciproca deve seguire la selezione ma precedere gli operatori di percorso. Sbagliare l'ordine in una direzione qualsiasi regala un gradiente che si renderizza correttamente sul primo percorso e deriva su ogni successivo. Un vincolo analogo vale per la modalità di coordinate relative: RadiusX e RadiusY vanno risolti separatamente contro larghezza e altezza del percorso, poiché scalarli entrambi con una singola lunghezza di lato cambia silenziosamente le proporzioni dell'ellisse su qualsiasi percorso non quadrato
Dove la conversione è onesta sui propri limiti
Alcuni costrutti XPS vengono convertiti in modo approssimato e altri non vengono convertiti affatto, e la scelta di progetto costante è contarli anziché fingere di convertirli. Le parti TIFF e JPEG XR vengono rasterizzate tramite WIC e non portano alcuna promessa sull'alpha preservato, mentre un PNG con canale alpha valido viene diviso in un'immagine base più una /SMask. La dimensione intrinseca dell'immagine deriva come pixel * 96 / DPI, leggendo prima la densità PNG pHYs o JPEG JFIF e ricadendo su 96 DPI, così un header di densità difettoso approda a una dimensione prevedibile anziché arbitraria. Risorse matrix irrisolte, trasformazioni relative non standard, ColorConvertedBitmap, modalità spread dei gradienti non supportate e geometrie malformate incrementano tutte UnsupportedFeatureCount, e l'input malformato fallisce in modo sicuro invece di degradare in un disegno silenziosamente diverso
Questo è l'atteggiamento utile per un convertitore archivistico: una conversione che approssima in silenzio è peggiore di una che dice quali quattro elementi non ha potuto rappresentare, perché solo la seconda dà qualcosa da verificare prima che il documento venga sigillato in un sistema di record. Se state valutando la conversione XPS e OpenXPS insieme al resto della pipeline documentale — composizione delle pagine, font, firma, output PDF/A — la pagina del componente PDF HotPDF per Delphi elenca il set completo di funzionalità e le versioni supportate di Delphi e C++Builder