有時交付物不是文件,而是一張表格的圖片。狀態郵件裡的摘要區塊、儀表板上的 KPI 面板、搜尋結果旁的縮圖:它們要的是儲存格,沒有一個要紙。TXLSCellImageExporter 在 HotXLS 中接受傳統或 XLSX 儲存格矩形,產出一張緊湊的 PNG 或 JPEG:沒有頁面尺寸、沒有邊界、沒有頁首頁尾、沒有列印標題、沒有分頁符。解析度、縮放、格式與 JPEG 品質可設定,物件、格線與儲存格框線有獨立開關,背景可以是色彩或透明,而檔案寫入經過同資料夾的原子式取代,任何失敗都讓既有目標原封不動
這需要自己的匯出器,而不是列印路徑上的一個旗標,原因在於分頁不是可以關掉的選用層;它正是頁面管線存在的目的
為什麼不透過列印管線渲染範圍?
因為列印管線在您與儲存格之間插了一頁。紙張尺寸決定裝得下多少,邊界把內容向內推,頁首頁尾佔走您沒要求的帶狀區域,列印標題重複您已經有的列,分頁符把範圍切開。一個碰巧跨過分頁的摘要區塊,出來就是兩張圖,有趣的那一列被切成兩半。您可以透過設定一個與範圍完全相符的自訂紙張尺寸來補償這一切,人們確實這麼做,但那意味著範圍每次改變都要重算紙張幾何,而且頁首帶與列印標題邏輯仍在路徑上
儲存格匯出器量測矩形、配置剛好那個大小的點陣圖、把儲存格畫進去、然後編碼。沒有頁面,所以沒有什麼需要設定掉。在您確實要紙的場景,PDF 匯出路徑才是合適工具,見工作表 PDF 匯出文章
渲染之前先量測
Measure 回傳目前設定會產出的像素尺寸,不編碼任何東西。這有兩個理由。HTML 或郵件範本通常在圖片存在之前就需要圖片尺寸,以便保留方塊、避免版面偏移。而渲染使用者所選範圍的服務,需要一種在為荒謬請求配置資源之前就拒絕它的方式
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 讓細線條保持清晰
Exporter.DPI := 96;
Exporter.Scale := 2.0; // 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 與 H 現在已知;在編碼前先保留版面方塊
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;
預算,因為縮放會相乘
MaxPixels 與 MaxBytes 不是防禦性裝飾。像素數以縮放係數的平方增長,也以解析度比的平方增長,一個 96 DPI 下合理的 1200 乘 800 範圍,到 600 DPI 變成約 47 百萬像素;而選取整個已使用範圍而不是摘要區塊的使用者,又在此之上再添一個數量級。沒有上限時,失敗模式是行程無法滿足的配置,把該行程正在做的其他一切一起拖垮
有上限時,請求失敗,呼叫端可以選擇:拒絕、調降縮放,或縮小範圍。對報表伺服器來說這是好得多的處境,而這與有界限 EMF 與 WMF 解碼器文章所述中繼檔解碼器裡明確預算背後的推理相同
原子式取代,以及資料夾為什麼重要
Save 到檔案名稱並不直接寫入目標。它在同一資料夾寫一個暫存檔,編碼進去,然後才取代目標。如果編碼失敗、中途超過預算,或行程被殺,前一張圖片仍在且仍有效。按排程重新產生磚塊圖的儀表板,因此永遠不會顯示被截斷的 PNG——那是天真寫法打開目的地就開始串流的常見症狀
同一資料夾這個細節不是偶然。原子式取代只在同一個磁碟區內是原子的,跨磁碟區時作業系統必須先複製再刪除,重新引入您想關掉的那扇窗。任何把暫存檔放進系統暫存目錄的此模式實作,在輸出位於不同磁碟機的機器上都不是原子的
繪圖事件在真實畫布上作畫
範圍匯出器與頁面匯出器都暴露前置與後置繪圖事件,而且它們收到的是完整唯讀內容,而不只是一個畫布 handle。TXLSPagePaintContext 攜帶活著的畫布、像素界限、以點計的頁面尺寸、實際使用的解析度與縮放、文件頁碼、表內頁碼、總頁數、工作表名稱,以及傳統與 XLSX 兩種形式的來源工作表。這足以畫出一個正確縮放的浮水印,或一枚知道自己身在整批中哪個位置的頁面戳記
procedure TReportJob.StampDraft(Sender: TObject;
const AContext: TXLSPagePaintContext);
begin
// 具縮放感知,戳記在 1x 與 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;
三個行為值得依靠。事件對每個渲染影格正好觸發一次,包括多頁 TIFF 的每一影格,所以在處理器裡遞增的計數器可信。它們在量測期間保持沉默,所以有副作用的處理器不會為一次輸出跑兩次。而如果前置事件拋出例外,後置事件不觸發,也不會寫出任何部分圖片位元組,所以您自己繪圖程式碼裡的例外,不可能產出蓋了一半戳記的檔案
選擇格式
文字密集的內容用 PNG。JPEG 套用的區塊轉換會在細的高對比線條周圍產生可見的振鈴,而儲存格框線與小字正是這種線條,而且在照片看來完美的品質設定下,這些瑕疵依然存在。當範圍以內嵌照片為主、檔案大小比邊緣保真更重要時,JPEG 才有它的位置。透明背景需要 PNG,因為 JPEG 沒有 Alpha 色版,所以要坐在彩色表面上的磚塊圖已經替您做了選擇
如果您的範圍含合併儲存格,把輸出對照工作表檢查:合併區域與欄寬的互動常出人意料,版面規則在合併儲存格與報表範本文章中涵蓋。HotXLS 從 Delphi 與 C++Builder 讀寫 XLS、XLSX、ODS 與 CSV,不依賴 Excel,完整匯出器介面記錄於 HotXLS Delphi spreadsheet component 產品頁