기술 문서

HotXLS로 Excel 셀 범위를 한 장의 이미지로 내보내기

때로 인도물은 문서가 아니라 표의 그림입니다. 상태 이메일 안의 요약 블록, 대시보드에 렌더링된 KPI 패널, 검색 결과 옆의 썸네일은 모두 셀을 원하고 어느 것도 종이를 원하지 않습니다. HotXLS의 TXLSCellImageExporter는 클래식 또는 XLSX 셀 사각형을 받아 페이지 크기도, 여백도, 머리글·바닥글도, 인쇄 제목도, 페이지 나누기도 없는 컴팩트한 PNG나 JPEG 한 장을 만듭니다. 해상도, 배율, 포맷, JPEG 품질은 설정 가능하고, 오브젝트, 눈금선, 셀 테두리에는 독립 스위치가 있으며, 배경은 색이나 투명일 수 있습니다. 그리고 파일 쓰기는 무언가 실패하면 기존 대상을 그대로 두는 같은 폴더 원자적 교체를 거칩니다

인쇄 경로에 플래그 하나가 아니라 자체 내보내기가 필요한 이유는 페이지 분할이 끌 수 있는 선택 계층이 아니기 때문입니다. 페이지 파이프라인이 존재하는 이유가 바로 그것입니다

범위를 인쇄 파이프라인으로 렌더링하지 않는 이유

인쇄 파이프라인은 여러분과 셀 사이에 페이지를 끼워 넣기 때문입니다. 용지 크기가 얼마나 들어가는지 정하고, 여백이 콘텐츠를 안쪽으로 밀고, 머리글과 바닥글이 요청하지 않은 띠를 차지하며, 인쇄 제목은 이미 갖고 있는 행을 반복하고, 페이지 나누기가 범위를 자릅니다. 하필 나누기에 걸친 요약 블록은 관심 행이 반으로 잘린 두 이미지로 나옵니다. 범위에 정확히 맞는 커스텀 페이지 크기를 설정해 보상할 수 있고 실제로 그러는 사람도 있지만, 그것은 범위가 바뀔 때마다 용지 지오메트리를 다시 계산한다는 뜻이고 머리글 띠와 인쇄 제목 로직은 여전히 경로에 남습니다

셀 내보내기는 사각형을 측정하고, 정확히 그 크기의 비트맵을 할당하고, 셀을 그 안에 그리고, 인코딩합니다. 페이지가 없으므로 설정으로 치울 것도 없습니다. 종이가 정말 필요한 경우에는 PDF 내보내기 경로가 올바른 도구이며, 워크시트 PDF 내보내기 문서에서 다룹니다

TXLSCellImageExporter가 셀 범위마다 한 이미지를 측정·그리기·인코딩하는 동안 인쇄 파이프라인은 페이지 나누기에서 범위를 자름
페이지 파이프라인은 여러분과 셀 사이에 용지 지오메트리를 끼우지만, 셀 내보내기의 경로 어디에도 페이지가 없습니다

렌더링 전에 측정합니다

Measure은 아무것도 인코딩하지 않고 현재 설정이 내놓을 픽셀 치수를 돌려줍니다. 두 가지 이유로 중요합니다. HTML이나 이메일 템플릿은 대개 이미지가 존재하기 전에 이미지 치수를 알아야 레이아웃 박스를 예약하고 layout shift를 피할 수 있습니다. 그리고 사용자가 고른 범위를 렌더링하는 서비스는 할당에 들어가기 전에 터무니없는 요청을 거부할 방법이 필요합니다

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;             // 레티나 밀도 출력
      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;

예산, 배율은 곱해지기 때문입니다

MaxPixelsMaxBytes는 방어적 장식이 아닙니다. 픽셀 수는 배율 인자의 제곱으로, 해상도 비율의 제곱으로 자라므로, 96 DPI에서 적절한 1200 곱하기 800인 범위는 600 DPI에서 대략 47 메가픽셀이 되고, 요약 블록 대신 전체 사용 범위를 고른 사용자는 그 위에 한 자릿수를 더 얹습니다. 상한이 없으면 실패 양상은 프로세스가 만족시킬 수 없는 할당이며, 그 프로세스가 하던 다른 모든 것을 끌어내립니다

상한이 있으면 요청이 실패하고 호출자가 고릅니다. 거부하거나, 배율을 줄이거나, 범위를 좁히거나입니다. 보고서 서버에는 훨씬 나은 위치이며, 상한 있는 EMF 및 WMF 디코더 문서에서 설명한 메타파일 디코더의 명시적 예산 뒤에도 같은 논리가 있습니다

HotXLS TXLSCellImageExporter의 예산 흐름: Measure가 픽셀 크기를 먼저 돌려주고 MaxPixels와 MaxBytes가 할당과 출력 크기를 묶음
거부는 할당 전에 일어나며, 바이트 예산 실패는 이전 이미지를 호출자에게 그대로 남깁니다

원자적 교체, 그리고 폴더가 중요한 이유

Save는 파일 이름에 대상에 직접 쓰지 않습니다. 같은 폴더에 임시 파일을 쓰고, 그 안에 인코딩한 뒤에야 대상을 교체합니다. 인코딩이 실패하거나, 중간에 예산을 넘기거나, 프로세스가 죽어도 이전 이미지는 여전히 그 자리에 유효하게 있습니다. 따라서 일정에 따라 타일을 재생성하는 대시보드는 잘린 PNG를 절대 보여 주지 않습니다. 대상을 열고 스트리밍을 시작하는 순진한 쓰기의 전형적 증상입니다

같은 폴더 세부는 우연이 아닙니다. 원자적 교체는 한 볼륨 안에서만 원자적입니다. 볼륨을 넘으면 운영체제가 복사한 뒤 지워야 하고, 이는 닫으려던 창을 다시 열어 놓습니다. 이 패턴의 어떤 구현이든 임시 파일을 시스템 temp 디렉터리에 두면, 출력이 다른 드라이브에 사는 기계에서는 원자적이지 않습니다

TXLSCellImageExporter Save는 같은 폴더의 임시 파일에 인코딩한 뒤 대상을 원자적으로 교체하며, 실패하면 이전 이미지가 유효하게 남음
임시 파일은 반드시 대상 옆에 있어야 합니다. 원자적 교체는 한 볼륨 안에서만 동작하기 때문입니다

페인트 이벤트는 실제 캔버스에 그립니다

범위 내보내기와 페이지 내보내기 모두 선행 및 후행 페인트 이벤트를 노출하며, 캔버스 핸들만이 아니라 완전한 읽기 전용 컨텍스트를 받습니다. 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는 블록 변환을 적용해 가는 고대비 선 주위에 눈에 띄는 링잉(ringing)을 만드는데, 셀 테두리와 작은 텍스트가 바로 그것이고, 사진은 완벽해 보이는 품질 설정에서도 인공물이 살아남습니다. JPEG가 자리를 찾는 곳은 범위가 내장 사진이 지배하고 파일 크기가 가장자리 충실도보다 중요할 때입니다. 투명 배경은 PNG를 요구합니다. JPEG에는 알파 채널이 없으므로, 색이 있는 표면 위에 앉을 타일은 여러분 대신 선택을 끝낸 셈입니다

범위에 병합 셀이 있다면 출력을 시트와 대조하십시오. 병합 영역은 컬럼 폭과 사람을 놀라게 하는 방식으로 상호작용하며, 레이아웃 규칙은 병합 셀과 보고서 템플릿 문서에서 다룹니다. HotXLS는 Excel 의존성 없이 Delphi와 C++Builder에서 XLS, XLSX, ODS, CSV를 읽고 쓰며, 전체 내보내기 표면은 HotXLS Delphi spreadsheet component 제품 페이지에 문서화되어 있습니다