Artículo técnico

Exportar un rango de celdas como imagen en HotXLS

A veces el entregable no es un documento, es una imagen de una tabla. Un bloque de resumen en un correo de estado, un panel de KPI renderizado en un tablero, una miniatura junto a un resultado de búsqueda: todos quieren las celdas y ninguno quiere papel. TXLSCellImageExporter en HotXLS toma un rectángulo de celdas clásico o XLSX y produce un PNG o JPEG compacto sin tamaño de página, ni márgenes, ni encabezados ni pies, ni títulos de impresión ni saltos de página. La resolución, la escala, el formato y la calidad JPEG son configurables, los objetos, las líneas de cuadrícula y los bordes de celda tienen interruptores independientes, el fondo puede ser un color o transparente, y la escritura del archivo pasa por un reemplazo atómico en la misma carpeta que deja un destino existente intacto si algo falla

La razón de que esto necesite su propio exportador y no una bandera en la vía de impresión es que la paginación no es una capa opcional que puedan apagar. Es la cosa para la que existe el pipeline de páginas

Por qué no renderizar el rango por el pipeline de impresión

Porque el pipeline de impresión inserta una página entre ustedes y las celdas. El tamaño de papel decide cuánto cabe, los márgenes empujan el contenido hacia adentro, los encabezados y pies ocupan bandas que no pidieron, los títulos de impresión repiten filas que ya tienen, y los saltos de página parten el rango. Un bloque de resumen que casualmente cruza un salto sale como dos imágenes con la fila interesante cortada a la mitad. Pueden compensar todo eso configurando un tamaño de página personalizado que coincida exactamente con el rango, y la gente lo hace, pero eso significa recalcular la geometría del papel cada vez que el rango cambia, y aun así deja la banda de encabezado y la lógica de títulos de impresión en el camino

El exportador de celdas mide el rectángulo, asigna un bitmap de exactamente ese tamaño, dibuja las celdas dentro y codifica. No hay página, así que no hay nada que configurar fuera. Para los casos donde sí quieren papel, la vía de exportación PDF es la herramienta correcta y se cubre en el artículo de exportación PDF de hojas

TXLSCellImageExporter mide, dibuja y codifica una imagen por rango de celdas mientras el pipeline de impresión parte el rango en los saltos de página
El pipeline de páginas inserta geometría de papel entre ustedes y las celdas; el exportador de celdas no tiene página en ningún punto de la vía

Medir antes de renderizar

Measure devuelve las dimensiones en píxeles que produciría la configuración actual sin codificar nada. Eso importa por dos razones. Una plantilla de HTML o de correo normalmente necesita las dimensiones de la imagen antes de que exista, para poder reservar la caja y evitar desplazamientos de maquetación. Y un servicio que renderiza rangos elegidos por usuarios necesita una forma de rechazar una solicitud absurda antes de asignar para ella

uses
  lxHandleX, lxPagination;

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Exporter: TXLSCellImageExporter;
  Summary: TXLSXRange;
  W, H, Bytes: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarter.xlsx');
    Sheet := Book.Sheets.ByPos[0];
    Summary := Sheet.Range['A1:F20'];

    Exporter := TXLSCellImageExporter.Create;
    try
      Exporter.ImageFormat := xpifPng;   // PNG mantiene nítidos los trazos finos
      Exporter.DPI := 96;
      Exporter.Scale := 2.0;             // salida con densidad retina
      Exporter.IncludeGridlines := False;
      Exporter.IncludeCellBorders := True;
      Exporter.TransparentBackground := True;
      Exporter.MaxPixels := 40 * 1000 * 1000;
      Exporter.MaxBytes := 8 * 1024 * 1024;

      if not Exporter.Measure(Summary, W, H) then
        raise Exception.Create('range exceeds the configured budget');
      // W y H ya se conocen; reservar la caja de maquetación antes de codificar
      Bytes := Exporter.Save(Summary, 'summary.png');
      if Bytes <= 0 then
        raise Exception.Create('image export failed, previous file kept');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

Presupuestos, porque la escala multiplica

MaxPixels y MaxBytes no son decoración defensiva. El recuento de píxeles crece con el cuadrado del factor de escala y con el cuadrado de la razón de resolución, así que un rango que es un razonable 1200 por 800 a 96 DPI se vuelve de unos 47 megapíxeles a 600 DPI, y un usuario que selecciona todo el rango usado en lugar de un bloque de resumen añade otro orden de magnitud encima. Sin tope el modo de fallo es una asignación que el proceso no puede satisfacer, lo que tumba todo lo demás que ese proceso estuviera haciendo

Con un tope la solicitud falla y quien llama elige: rechazar, reducir la escala o estrechar el rango. Es una posición mucho mejor para un servidor de reportes, y es el mismo razonamiento detrás de los presupuestos explícitos del decodificador de metarchivos descrito en el artículo del decodificador acotado de EMF y WMF

Flujo de presupuesto de TXLSCellImageExporter en HotXLS: Measure devuelve primero el tamaño en píxeles, luego MaxPixels y MaxBytes acotan la asignación y el tamaño de salida
El rechazo ocurre antes de la asignación, y un fallo del presupuesto de bytes deja la imagen anterior intacta para quien llama

Reemplazo atómico y por qué importa la carpeta

Save a un nombre de archivo no escribe sobre el destino. Escribe un archivo temporal en la misma carpeta, codifica dentro de él y solo entonces reemplaza el destino. Si la codificación falla, si el presupuesto se excede a mitad de camino, o si el proceso muere, la imagen anterior sigue ahí y sigue siendo válida. Un tablero que regenera sus mosaicos en un horario por lo tanto nunca muestra un PNG truncado, que es el síntoma usual de una escritura ingenua que abre el destino y empieza a verter

El detalle de la misma carpeta no es casual. Un reemplazo atómico solo es atómico dentro de un volumen, porque entre volúmenes el sistema operativo tiene que copiar y después borrar, lo que reintroduce la ventana que intentaban cerrar. Cualquier implementación de este patrón que ponga su archivo temporal en el directorio temporal del sistema no es atómica en una máquina donde la salida vive en otra unidad

El Save de TXLSCellImageExporter codifica en un archivo temporal en la misma carpeta y luego reemplaza el destino atómicamente; los fallos dejan la imagen anterior válida
El archivo temporal debe vivir junto al destino porque un reemplazo atómico solo funciona dentro de un volumen

Los eventos de pintura dibujan sobre el canvas real

Tanto el exportador de rangos como el exportador de páginas exponen eventos de pintura al inicio y al final, y reciben un contexto completo de solo lectura y no solo un handle de canvas. TXLSPagePaintContext lleva el canvas vivo, los límites en píxeles, el tamaño de página en puntos, la resolución y la escala realmente en uso, el número de página del documento, el número de página dentro de la hoja, el recuento total de páginas, el nombre de la hoja y la hoja de origen en las variantes clásica y XLSX. Es suficiente para dibujar una marca de agua que escale correctamente, o un sello de página que sepa dónde está en la corrida

procedure TReportJob.StampDraft(Sender: TObject;
  const AContext: TXLSPagePaintContext);
begin
  // Consciente de la escala, así el sello se ve igual a 1x y a 3x
  AContext.Canvas.Font.Height := Round(-48 * AContext.Scale);
  AContext.Canvas.Font.Color := clSilver;
  AContext.Canvas.Brush.Style := bsClear;
  AContext.Canvas.TextOut(AContext.Bounds.Left + Round(24 * AContext.Scale),
    AContext.Bounds.Top + Round(24 * AContext.Scale), 'DRAFT');
end;

Exporter.AfterPaint := Job.StampDraft;

Tres comportamientos valen la pena apalancar. Los eventos se disparan exactamente una vez por cuadro renderizado, incluido cada cuadro de un TIFF multipágina, así que un contador incrementado en el manejador es confiable. Se mantienen en silencio durante la medición, así que un manejador con un efecto secundario no corre dos veces por una salida. Y si el evento inicial lanza una excepción, el evento final no se dispara y no se escriben bytes parciales de imagen, así que una excepción en su propio código de dibujo no puede producir un archivo medio sellado

Elegir el formato

PNG para todo lo que tenga mucho texto. JPEG aplica una transformación por bloques que produce un halo visible alrededor de trazos finos de alto contraste, que es exactamente lo que son los bordes de celda y el texto pequeño, y los artefactos sobreviven en configuraciones de calidad donde una fotografía se ve perfecta. JPEG gana su lugar cuando el rango está dominado por fotografías incrustadas y el tamaño del archivo importa más que la fidelidad de los bordes. Los fondos transparentes exigen PNG, ya que JPEG no tiene canal alfa, así que un mosaico destinado a posarse sobre una superficie de color ya tomó la decisión por ustedes

Si su rango contiene celdas combinadas, cotejen la salida contra la hoja: las regiones combinadas interactúan con los anchos de columna de maneras que sorprenden a la gente, y las reglas de maquetación se cubren en el artículo de celdas combinadas y plantillas de reportes. HotXLS lee y escribe XLS, XLSX, ODS y CSV desde Delphi y C++Builder sin dependencia de Excel, y toda la superficie del exportador está documentada en la página del producto HotXLS Delphi spreadsheet component