Artículo técnico

XPS y OpenXPS a PDF en Delphi: coordenadas y pinceles

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 DPI a través de una matriz de página de escala 0.75 con volteo 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 de Windows a esto es aburrido e inevitable. Algo ya imprime a la Microsoft XPS Document Writer — un reporte de un ERP antiguo, un formulario firmado, un lote de estados de cuenta — 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 archivo documental dentro de una década. Así que el archivo de spool tiene que convertirse en un PDF página por página, y en cuanto empieza a escribir ese conversor descubre 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é se permite que sea un pincel

Del paquete al PDF en una sola 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 o .oxps desnuda solo puntúa 10. Ese orden importa cuando acepta cargas, porque un atacante que renombra un EPUB a .xps no debe dirigir 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 es la puntuación honesta de esta conversión
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Todo lo relacionado con la conversión tiene presupuesto antes de intentarse. THPDFDocumentHandlerOptions.Default limita las entradas de archivo a 10,000, los bytes de archivo expandido a 1 GiB, la razón de compresión a 200, los recursos a 4,096 y las páginas a 10,000, y lleva un CancellationToken opcional para que un trabajo del lado del servidor pueda detenerse a mitad del paquete. Lea después Info.UnsupportedFeatureCount y trate 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 callarlo

¿Por qué una página XPS necesita una matriz en lugar de coordenadas reescritas?

Porque reescribir coordenadas pierde la pila de transformaciones. Una FixedPage de XPS se especifica en unidades de 96 DPI con el origen en la esquina superior izquierda y la Y creciendo hacia abajo; el espacio de usuario PDF es de 72 DPI con el origen en la esquina inferior izquierda y la Y creciendo hacia arriba. La corrección ingenua es multiplicar cada número por 0.75 y restar cada Y de la altura de la página al emitirla. Eso funciona para una ruta plana y se derrumba en cuanto entra un RenderTransform, un Canvas anidado o una matriz local del pincel, porque esas transformaciones están definidas en el espacio XPS y su reescritura por coordenada ya lo abandonó. HotPDF mantiene por eso la proyección como matriz y la compone. HPDFXPSPageMatrix devuelve las constantes fijas una vez por página, HPDFMultiplyXPSMatrix la concatena con la transformación de ruta acumulada, y el resultado se emite como un único operador cm antes de la geometría. Los datos de ruta 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 ruta SVG — solo el token inicial de regla de relleno F0 o F1 lo maneja el adaptador XPS. Si siguió el mismo razonamiento para la importación vectorial de EMF y WMF, la forma del argumento le resultará familiar: los formatos de importación se convierten por matriz, nunca por aritmética sobre coordenadas hoja

HotPDF compone la matriz fija de página XPS con la transformación de ruta acumulada, de modo que un sistema de coordenadas XPS de 96 DPI con origen superior izquierdo llega al espacio de usuario PDF de 72 DPI con su origen en la esquina inferior izquierda, emitido como un único operador cm por visual
La proyección permanece como matriz y se compone con cada transformación anidada, así que los datos de ruta pueden escribirse en números XPS sin modificar
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // unidad XPS de 96 DPI a punto PDF de 72 DPI
  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 ruta
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

¿Cómo reutiliza un VisualBrush sin dibujarlo dos veces?

Un VisualBrush pinta un árbol visual arbitrario — hijos Canvas, Path y Glyphs — dentro de 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 la importación de SVG mediante Form XObjects. Dos detalles deciden si funciona. Primero, el visual debe recorrerse como hijos XML directos: un barrido 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 de 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 el volteo de Y. El Form también debe ser dueño de sus recursos: HotPDF copia solo las fuentes, XObjects, patrones, ExtGStates y espacios de color que el flujo de contenido capturado realmente referencia; clonar todo el diccionario de recursos de la página arrastraría el Form que se registra a su propio grafo de recursos y construiría un ciclo. Las fuentes permanecen en un diccionario directo en las páginas ordinarias y se promueven a un diccionario indirecto compartido solo cuando el contenido capturado realmente contiene un Tf, así que un documento sin visuales reutilizables no paga por la maquinaria. Anote un límite de la especificación que conviene conocer antes de reportar un bug: la sección 13.4 de ECMA-388 exige que tanto ViewboxUnits como ViewportUnits de un VisualBrush sean Absolute, así que las unidades relativas no son una característica faltante — son entrada no conforme, y HotPDF se niega a inventar semántica de coordenadas para ellas

HotPDF compila un árbol visual de VisualBrush de XPS una vez en un Form XObject de PDF, lo publica a través de la inversa de la matriz fija de página para que las colocaciones no reapliquen la escala, y copia solo los recursos que el contenido capturado realmente referencia
El contenido se captura con la matriz de página ya aplicada, así que el Form se publica a través de su inversa y lleva solo los recursos que su propio flujo de contenido referencia

Teselado de ImageBrush: cuatro modos, cuatro tamaños de celda

Los modos de mosaico de XPS se mapean sobre patrones de teselado PDF de la sección 8.7.3 de ISO 32000-1 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 en cuanto lo ve: el reflejo se expresa poniendo colocaciones espejadas dentro de una celda de patrón y agrandando la celda para igualar

  • Tile — una colocación, la celda permanece como viewport de 1×1
  • FlipX — dos colocaciones, celda ensanchada a 2×1
  • FlipY — dos colocaciones, celda elevada a 1×2
  • FlipXY — cuatro colocaciones, celda expandida a 2×2

Cada colocación lleva su propio rectángulo de recorte, porque un mapeo Viewbox que desborde su subcelda sangraría en el reflejo vecino. El /Matrix del patrón es la parte que atrapa 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 de gráficos vigente cuando se selecciona el patrón, así que la matriz debe componer explícitamente las tres capas — la proyección fija de página, la transformación de la Path y la Transform local del pincel — en lugar de confiar en un CTM ambiente. 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 quiere el modelo general del lado PDF detrás de esto, los patrones de teselado y el espacio de color Pattern cubre los operadores subyacentes

// Proyección fija de página integrada 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, así que el pincel es una elipse. El sombreado tipo 3 de PDF, en la sección 8.7.4.5.4 de ISO 32000-1, 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 esté cerca de ser redondo. HotPDF mueve en cambio el problema al sistema de coordenadas: escala la 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 de ruta que se escribe 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 de ese fragmento es todo el truco, y no es cuestión de estilo. 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 ruta. Equivoque el orden en cualquier dirección y tendrá un gradiente que se renderiza correctamente en la primera ruta y se desvía en cada una de las siguientes. Una restricción relacionada aplica al modo de coordenadas relativas: RadiusX y RadiusY deben resolverse contra el ancho y el alto de la ruta por separado, porque escalar ambos por una única longitud de borde cambia silenciosamente la relación de aspecto de la elipse en cualquier ruta no cuadrada

HotPDF mapea un RadialGradientBrush elíptico de XPS sobre el sombreado tipo 3 de PDF escalando la Y, registrando un sombreado circular en el espacio escalado, y emitiendo la escala recíproca solo después de que el patrón haya capturado la matriz de transformación actual
La elipse la absorbe el sistema de coordenadas y no el sombreado, y la escala temporal debe rodear la selección del patrón en exactamente ese orden

Dónde la conversión es honesta sobre sus límites

Algunas construcciones XPS se convierten de forma aproximada y otras no se convierten en absoluto, y la decisión de diseño a lo largo de todo el proceso es contarlas en lugar de fingirlas. Las partes TIFF y JPEG XR se rasterizan a través de WIC y no llevan promesa alguna 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 la densidad pHYs del PNG o JFIF del JPEG y recurriendo a 96 DPI como respaldo, así que un encabezado de densidad erróneo cae en un tamaño predecible y no en 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 le dice qué cuatro elementos no pudo representar, porque solo la segunda le da algo que verificar antes de que el documento se selle en un sistema de archivo documental. Si está evaluando la conversión de XPS y OpenXPS junto con el resto de la pipeline de documentos — composición de páginas, fuentes, firma, salida PDF/A — la página del componente PDF HotPDF para Delphi lista el conjunto completo de características y las versiones soportadas de Delphi y C++Builder