HotPDF convierte paquetes XPS y OpenXPS a PDF dentro de Delphi y C++Builder sin controlador de impresión, mapeando cada coordenada de página fija de 96 PPP a través de una única matriz de página con escala 0.75 e inversión de Y, publicando cada VisualBrush como un Form XObject compartido y convirtiendo los modos de mosaico de ImageBrush en patrones de teselado PDF nativos en lugar de dibujos de imagen repetidos
El escenario que arrastra a la mayoría de los equipos Windows a esto es sordo e inevitable. Algo ya imprime en Microsoft XPS Document Writer — un informe de un ERP heredado, un formulario firmado, un lote de extractos — y la política de archivo dice PDF. XPS es un formato de captura perfectamente bueno y uno terrible para entregar a un sistema de archivos dentro de una década. Así que el archivo de cola de impresión tiene que convertirse en un PDF página por página, y en el momento en que empiezas a escribir ese conversor descubres que la parte interesante no es el XML. Es que XPS y PDF discrepan sobre dónde está el origen, cuánto vale una unidad y qué puede ser un pincel
Del paquete al PDF en una pasada
El punto de entrada es el registro de manejadores de documentos, no una clase XPS especial. THPDFDocumentHandlerRegistry.RegisterStandardHandlers instala los manejadores de XPS, EPUB y CBZ; el reconocimiento se basa en el contenido, así que un paquete que lleva [Content_Types].xml más al menos una parte .fpage puntúa 95 incluso cuando la extensión del archivo miente, mientras que una extensión .xps u .oxps desnuda solo puntúa 10. Ese orden importa cuando aceptas subidas, porque un atacante que renombra un EPUB a .xps no debería dirigir la tubería
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 es la puntuación honesta de esta conversión
finally
Output.Free;
Registry.Free;
end;
end;
Todo en la conversión está presupuestado antes de intentarse. THPDFDocumentHandlerOptions.Default limita las entradas de archivo a 10.000, los bytes expandidos a 1 GiB, la relación de compresión a 200, los recursos a 4.096 y las páginas a 10.000, y lleva un CancellationToken opcional para poder detener un trabajo del lado del servidor a mitad de paquete. Lee Info.UnsupportedFeatureCount después y trata un valor distinto de cero como un hallazgo real: HotPDF cuenta deliberadamente lo que no pudo mapear en lugar de dibujar una aproximación y callárselo
¿Por qué una página XPS necesita una matriz en lugar de coordenadas reescritas?
Porque reescribir coordenadas pierde la pila de transformaciones. Una FixedPage XPS se especifica en unidades de 96 PPP con el origen arriba a la izquierda y Y creciendo hacia abajo; el espacio de usuario PDF es de 72 PPP con el origen abajo a la izquierda y Y creciendo hacia arriba. El arreglo ingenuo es multiplicar cada número por 0.75 y restar cada Y de la altura de página al emitirlo. Eso funciona para un trazado plano y se desmorona en cuanto entra en juego un RenderTransform, un Canvas anidado o una matriz local del pincel, porque esas transformaciones están definidas en espacio XPS y tu reescritura por coordenada ya lo abandonó. HotPDF por eso mantiene la proyección como una matriz y la compone. HPDFXPSPageMatrix devuelve las constantes fijas una vez por página, HPDFMultiplyXPSMatrix la concatena con la transformación acumulada del trazado, y el resultado se emite como un único operador cm antes de la geometría. Los datos del trazado se escriben entonces en números XPS sin modificar, que es también la razón por la que la sintaxis de geometría abreviada puede compartir el mismo parser acotado usado para datos de trazados SVG — solo el token de regla de relleno F0 o F1 inicial lo maneja el adaptador XPS. Si has seguido el mismo razonamiento para la importación vectorial EMF y WMF, la forma del argumento te resultará familiar: los formatos de importación se convierten con matrices, nunca con aritmética sobre coordenadas hoja
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // unidad XPS de 96 PPP a punto PDF de 72 PPP
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // la Y de XPS crece hacia abajo, la de PDF hacia arriba
Result.E := 0;
Result.F := PageHeight; // altura de página PDF, en puntos
end;
// Un CTM compuesto por visual, emitido antes de cualquier operador de trazado
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
¿Cómo reutilizas un VisualBrush sin dibujarlo dos veces?
Un VisualBrush pinta un árbol visual arbitrario — hijos Canvas, Path y Glyphs — en una región, posiblemente repetido por ella. HotPDF compila ese visual una vez en un Form XObject de PDF y luego lo coloca, que es la misma estrategia de recursos descrita en importación SVG mediante Form XObjects. Dos detalles deciden si funciona. Primero, el visual tiene que recorrerse como hijos XML directos: un escaneo plano de elementos dignos de mosaico sube los visuales anidados al nivel superior de la página y destruye tanto el ámbito de recursos como el orden de pintado. Segundo, el contenido se captura con la matriz de página XPS-a-PDF ya aplicada, así que publicar el Form exige multiplicar por la inversa de esa matriz; de lo contrario cada colocación reaplica la escala 0.75 y la inversión de Y. El Form también tiene que poseer sus recursos: HotPDF copia solo las fuentes, XObjects, patrones, ExtGStates y espacios de color que el flujo de contenido capturado referencia realmente; clonar el diccionario de recursos de toda la página arrastraría el Form que se está registrando a su propio grafo de recursos y construiría un ciclo. Las fuentes permanecen en un diccionario directo en páginas ordinarias y se promueven a un diccionario indirecto compartido solo cuando el contenido capturado contiene de verdad un Tf, así que un documento sin visuales reutilizables no paga la maquinaria. Apunta un límite de la especificación que conviene conocer antes de abrir un bug: ECMA-388 sección 13.4 exige que tanto ViewboxUnits como ViewportUnits de un VisualBrush sean Absolute, así que las unidades relativas no son una función ausente — son entrada no conforme, y HotPDF se niega a inventar semántica de coordenadas para ellas
Teselado de ImageBrush: cuatro modos, cuatro tamaños de celda
Los modos de mosaico XPS se mapean sobre patrones de teselado PDF de ISO 32000-1 sección 8.7.3 en lugar de expandirse en colocaciones de imagen repetidas por el área cubierta, lo que mantiene el tamaño de salida y el tiempo de conversión independientes de cuánto de la página cubra el pincel. El mapeo es mecánico una vez que lo ves: la reflexión se expresa poniendo colocaciones espejadas dentro de una celda de patrón y agrandando la celda para igualar
Tile— una colocación, la celda se queda en 1×1 del viewportFlipX— dos colocaciones, celda ensanchada a 2×1FlipY— dos colocaciones, celda elevada a 1×2FlipXY— cuatro colocaciones, celda expandida a 2×2
Cada colocación lleva su propio rectángulo de recorte, porque un mapeo de Viewbox que desborde su subcelda sangraría en la reflexión vecina. El /Matrix del patrón es la parte que caza a la gente. Un patrón de teselado se ancla al espacio de usuario por defecto de su flujo de contenido padre, no al estado gráfico vigente cuando se selecciona el patrón, así que la matriz tiene que componer explícitamente las tres capas — la proyección de página fija, la transformación del Path y el Transform local del pincel — en lugar de confiar en un CTM ambiental. HotPDF también valida antes de asignar: RegisterImageTilingPattern limita un patrón a 1.024 colocaciones y rechaza recortes degenerados, matrices no invertibles e índices de imagen inválidos. Si quieres el modelo general del lado PDF detrás de esto, patrones de teselado y el espacio de color Pattern cubre los operadores subyacentes
// Proyección de página fija plegada en la matriz del patrón, luego la local del pincel
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);
¿Qué pasa cuando un gradiente radial no es un círculo?
XPS define un RadialGradientBrush con GradientOrigin, Center, RadiusX y RadiusY, de modo que el pincel es una elipse. El sombreado tipo 3 de PDF, en ISO 32000-1 sección 8.7.4.5.4, interpola entre dos círculos y no tiene forma de expresar una elipse directamente. Promediar los dos radios en un solo número es el atajo tentador y es visiblemente incorrecto en cualquier pincel que no sea casi redondo. HotPDF en cambio mueve el problema al sistema de coordenadas: escala Y por RadiusY / RadiusX, registra un sombreado circular honesto en ese espacio escalado, selecciona el patrón y emite inmediatamente la escala recíproca para que la geometría del trazado que se escriba a continuación siga en el espacio de usuario XPS original
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); // el patrón captura el CTM justo aquí
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
El orden en ese fragmento es todo el truco, y no es estilístico. Un patrón de sombreado PDF captura la matriz de transformación actual en el momento en que se elige como color actual, así que la escala temporal debe emitirse antes de SetFillPattern o SetStrokePattern, y la recíproca debe ir después de la selección pero antes de los operadores de trazado. Equivocar el orden en cualquier dirección te da un gradiente que se renderiza bien en el primer trazado y deriva en cada uno posterior. Una restricción relacionada afecta al modo de coordenadas relativas: RadiusX y RadiusY deben resolverse contra el ancho y el alto del trazado por separado, porque escalar ambos por una única longitud de lado cambia silenciosamente la proporción de la elipse en cualquier trazado no cuadrado
Dónde la conversión es honesta sobre sus límites
Algunos constructos XPS se convierten de forma aproximada y otros no se convierten en absoluto, y la decisión de diseño en todo el componente es contarlos en lugar de fingirlos. Las partes TIFF y JPEG XR se rasterizan a través de WIC y no llevan ninguna promesa sobre alfa preservado, mientras que un PNG con canal alfa válido se divide en una imagen base más un /SMask. El tamaño intrínseco de la imagen se deriva como pixel * 96 / DPI, leyendo primero el pHYs del PNG o la densidad JFIF del JPEG y recurriendo a 96 PPP como respaldo, así que una cabecera de densidad mala aterriza en un tamaño predecible en lugar de uno arbitrario. Los recursos de matriz sin resolver, las transformaciones relativas no estándar, ColorConvertedBitmap, los modos de extensión de gradiente no soportados y la geometría malformada incrementan todos UnsupportedFeatureCount, y la entrada malformada falla de forma cerrada en lugar de degradarse a un dibujo silenciosamente distinto
Esa es la postura útil para un conversor de archivo: una conversión que aproxima en silencio es peor que una que te dice qué cuatro elementos no pudo representar, porque solo la segunda te da algo que comprobar antes de que el documento se selle en un sistema de archivos. Si estás evaluando la conversión de XPS y OpenXPS junto al resto de la tubería de documentos — composición de páginas, fuentes, firma, salida PDF/A — la página del componente PDF de HotPDF para Delphi lista el conjunto completo de funciones y las versiones soportadas de Delphi y C++Builder