Artículo técnico

Exportar un rango de Excel como una imagen con 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 dashboard, 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 único PNG o JPEG compacto sin tamaño de página, sin márgenes, sin cabeceras ni pies, sin títulos de impresión y sin saltos de página. Resolución, escala, formato y calidad JPEG son configurables, los objetos, las líneas de cuadrícula y los bordes de celda tienen interruptores independientes, el fondo puede ser de un color o transparente, y la escritura del fichero 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 en lugar de una bandera en la ruta de impresión es que la paginación no es una capa opcional que se pueda apagar. Es la razón de ser del 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 vosotros y las celdas. El tamaño del papel decide cuánto cabe, los márgenes empujan el contenido hacia dentro, las cabeceras y los pies ocupan bandas que no pedisteis, los títulos de impresión repiten filas que ya tenéis, 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 por la mitad. Podéis compensar todo eso montando un tamaño de página personalizado que coincida exactamente con el rango, y hay quien lo hace, pero eso significa recalcular la geometría del papel cada vez que cambia el rango y aún deja la banda de cabecera y la lógica de títulos de impresión en el camino

El exportador de celdas mide el rectángulo, aloca un bitmap de exactamente ese tamaño, dibuja las celdas en él y codifica. No hay página, así que no hay nada que configurar fuera. Para los casos en que sí queréis papel, la ruta de exportación a PDF es la herramienta correcta y se cubre en el artículo de exportación a 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 vosotros y las celdas; el exportador de celdas no tiene página alguna en la ruta

Medid antes de renderizar

Measure devuelve las dimensiones en píxeles que producirían los ajustes actuales sin codificar nada. Importa por dos motivos. Una plantilla HTML o de correo suele necesitar las dimensiones de la imagen antes de que exista, para poder reservar la caja y evitar saltos de disposición. Y un servicio que renderiza rangos elegidos por usuarios necesita una forma de rechazar una petición absurda antes de alocar por 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 los trazos finos nítidos
      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; reservad la caja de disposició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 adorno defensivo. El recuento de píxeles crece con el cuadrado del factor de escala y con el cuadrado del cociente de resolución, así que un rango de unos razonables 1200 por 800 a 96 DPI se convierte en 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 petición falla y quien llama puede elegir: rechazar, reducir la escala o estrechar el rango. Esa es una posición mucho mejor para un servidor de informes, 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 asignación y 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 fichero no escribe en el destino. Escribe un fichero temporal en la misma carpeta, codifica en él y solo entonces reemplaza el destino. Si la codificación falla, si el presupuesto se excede a mitad de camino o si matan al proceso, la imagen anterior sigue ahí y sigue válida. Un dashboard que regenera sus teselas por planificador nunca muestra, por tanto, un PNG truncado, que es el síntoma habitual 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 luego borrar, lo que reintroduce la ventana que intentabais cerrar. Toda implementación de este patrón que ponga su fichero temporal en el directorio temporal del sistema no es atómica en una máquina donde la salida vive en una unidad distinta

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

Los eventos de pintura dibujan en 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 en lugar de 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 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 cálculo de origen en sabores clásico y XLSX. Es suficiente para dibujar una marca de agua que escale correctamente, o un sello de página que sabe dónde está en la tirada

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 merecen fiarse. Los eventos se disparan exactamente una vez por fotograma renderizado, incluido cada fotograma de un TIFF multipágina, así que un contador incrementado en el manejador es de fiar. Permanecen en silencio durante la medición, así que un manejador con efecto secundario no corre dos veces para 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 vuestro propio código de dibujo no puede producir un fichero con sello a medias

Elegir el formato

PNG para todo lo cargado de texto. JPEG aplica una transformación por bloques que produce un llamativo visible alrededor de trazos finos de alto contraste, que es justo lo que son los bordes de celda y el texto pequeño, y los artefactos sobreviven a ajustes de calidad donde una fotografía se ve perfecta. JPEG gana su sitio cuando el rango está dominado por fotografías incrustadas y el tamaño del fichero importa más que la fidelidad de los bordes. Los fondos transparentes exigen PNG, ya que JPEG no tiene canal alfa, así que una tesela pensada para posarse sobre una superficie coloreada ya ha tomado la decisión por vosotros

Si vuestro rango contiene celdas combinadas, cotejad la salida con la hoja: las regiones combinadas interactúan con las anchuras de columna de formas que sorprenden, y las reglas de disposición se cubren en el artículo de celdas combinadas y plantillas de informe. 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 de producto de HotXLS Delphi spreadsheet component