기술 문서

Delphi에서 XLSB 바이너리 워크북 읽고 쓰기

HotXLS는 XLSX에 쓰는 것과 같은 TXLSXWorkbook 퍼사드를 통해 Delphi에서 Excel Binary Workbook(XLSB) 파일을 읽고 씁니다: Open은 패키지 콘텐츠에서 바이너리 워크북을 감지하고, xlsxExcelBinaryWorkbook을 지정한 SaveAs나 SaveAsChecked가 하나를 기록합니다. 백엔드는 검증된 서브셋을 다루며, 목적지를 건드리기 전에 표현할 수 없는 것은 모두 거부합니다

마지막 절이 천천히 읽을 가치가 있는 부분입니다. XLSB는 XLSX의 단순 변형처럼 보이고, 패키징 관점에서는 실제로 그렇습니다. 문제는 두 포맷이 다르게 인코딩하는 디테일과, 한쪽을 다른 쪽으로 연결할 때 라이브러리가 조용히 가정하는 것에 삽니다. 이 백엔드의 가장 교훈적인 버그는 결코 형식 불량 레코드가 아니었습니다. 사라져 버린 기본 폰트, 그리고 값과 포맷 코드와 열 폭이 전부 옳은데도 Excel에서 ####로 변해 버린 날짜 열이었습니다

XLSB 파일이란 무엇이고 XLSX와 어떻게 다른가요?

XLSB 파일은 XLSX 워크북과 정확히 같은 파트 구조를 가진 OPC 패키지이되, 워크북·워크시트·스타일·공유 문자열 파트가 XML 대신 바이너리 레코드를 저장합니다. 포맷은 [MS-XLSB]에 규정되어 있고, 레코드 인코딩은 흔히 BIFF12라고 불립니다. [Content_Types].xml, 릴레이션십 파트, ZIP 컨테이너는 여전히 있습니다; 달라지는 것은 xl/workbook.bin이 xl/workbook.xml을 대체하고, xl/worksheets/sheet1.bin이 워크시트 XML을 대체하는 식입니다

각 BIFF12 레코드는 압축된 가변 길이 헤더로 시작합니다. 레코드 타입은 한두 바이트를 차지하고, 바이트당 일곱 비트에 최상위 비트가 연속 플래그이므로 레코드 타입은 14비트에 들어갑니다. 페이로드 크기는 같은 일곱 비트 방식으로 한네 바이트가 뒤따릅니다. 문자열은 32비트 문자 수와 그 뒤의 UTF-16 코드 유닛입니다. BIFF8과 비교하면 훨씬 깨끗한 레이아웃입니다: 8224바이트 CONTINUE 분할도 없고, 레코드 중간의 압축 대 와이드 문자열 플래그도 없습니다. XLSX와 비교하면 XML 토크나이징을 완전히 건너뛰는데, 큰 XLSB 파일이 디스크에서 더 작고 Excel이 여는 게 더 빠른 이유가 바로 그것입니다

패키징 디테일 하나가 감지 코드를 넘어뜨립니다. Excel은 매크로가 전혀 없는 워크북까지 포함해 XLSB 워크북 파트를 콘텐츠 타입 application/vnd.ms-excel.sheet.binary.macroEnabled.main으로 등록합니다. HotXLS는 [Content_Types].xml에서 그 등록을 먼저 확인하고 콘텐츠 타입이 없을 때만 .bin 파트 확장자로 폴백하며, 어느 쪽 경로든 엄격한 레코드 검증기를 통과해야 합니다. 파트 이름, 릴레이션십, 콘텐츠 타입에 관한 일반 규칙은 Excel이 정상적인 XLSX를 복구하는 이유와 그 뒤의 OPC 패키지 규칙에서 다룬 것들과 같습니다

XLSX 대신 XLSB는 언제 골라야 할까요?

하류 시스템이 요구하거나 열리는 시간을 신경 쓰는 Excel 사용자와 값 위주의 큰 워크북을 주고받을 때는 XLSB를 고르세요; 그 외에는 XLSX가 더 안전한 기본값으로 남습니다. XLSX는 폭넓은 도구 지원을 갖춘 ISO 표준(ISO/IEC 29500)이고, ZIP 도구와 텍스트 편집기로 들여다볼 수 있으며, 모든 스프레드시트 제품이 읽습니다. XLSB는 공개 사양이 있는 Microsoft 포맷이지만 생태계가 더 좁습니다

실전에서 요청은 두 방향에서 옵니다. 일부 리포팅 파이프라인과 아카이브는 몇 년 전부터 .xlsb로 표준화해서 다른 것은 거부합니다. 다른 팀들은 수십만 개의 숫자 셀을 가진 워크북을 만들다가 사용자가 바이너리 버전을 눈에 띄게 빨리 여는 걸 발견했습니다. 둘 다 정당한 이유입니다. "더 효율적일 것 같아서"는 아닙니다. HotXLS 바이너리 백엔드는 완전한 패키지를 메모리에 스테이징하므로, 유계 메모리 서버 익스포트용 도구가 아니기 때문입니다. 그 용도에는 스트리밍 XLSX 작성기가 올바른 길입니다

TXLSXWorkbook으로 XLSB 열고 저장하기

TXLSXWorkbook.Open에는 포맷 힌트가 필요 없습니다: 같은 호출이 XLSX, XLSM, XLSB를 모두 로드하고, 바이너리 로드에 성공한 뒤 TXLSXWorkbook.SourceFormat이 xlsxExcelBinaryWorkbook을 보고합니다. 쓰기는 명시적입니다. 포맷 없는 SaveAs(FileName)는 파일명이 .xlsb로 끝나더라도 언제나 Open XML 워크북을 만들므로, 바이너리 출력은 언제나 포맷을 이름으로 지정합니다

uses
  SysUtils, lxHandleX;

procedure ConvertToXlsb(const Source, Target: string);
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // Open은 패키지 자체에서 XLSX, XLSM 또는 XLSB를 감지합니다
    if Book.Open(Source) <> 1 then
      raise Exception.Create('Input is unsupported or invalid');

    // 확장자만으로는 바이너리 출력을 절대 고르지 않습니다
    if Book.SaveAsChecked(Target, xlsxExcelBinaryWorkbook) <> 1 then
      raise Exception.Create('XLSB output is unsupported or failed');
  finally
    Book.Free;
  end;
end;

스트림 출력은 SaveAs(Stream, xlsxExcelBinaryWorkbook)이나 SaveAsChecked의 스트림 오버로드로 갑니다. 내부적으로 둘 다 프라이빗 메서드 SaveAsXlsbStream과 SaveAsXlsbFile로 라우팅됩니다; 직접 호출하는 일은 없습니다. 파일 출력은 형제 임시 파일에 스테이징되어 최종 치환 한 번으로 커밋됩니다. 스트림 목적지는 읽기 가능하고 시크 가능해야 합니다. 작성기가 바이트, 크기, 위치를 스냅샷하고 최종 복사가 실패하면 복원하기 때문입니다. TXLSXWorkbook.LastSaveUsedStreaming은 바이너리 저장 후 False를 보고하는데, 정확합니다: 단일 바이트가 목적지에 도착하기 전에 패키지 전체가 메모리에 존재하니까요

완전한 충실도 대신 검증된 서브셋인 이유

HotXLS는 XLSB를 위한 두 번째 오브젝트 모델을 유지하지 않습니다. 열 때는 XlsbToOpenXml이 바이너리 패키지를 메모리 내 Open XML 패키지로 변환하고 평범한 XML 파서가 로드합니다; 저장할 때는 XML 작성기가 패키지를 만들고 OpenXmlToXlsb가 바이너리 레코드로 인코딩합니다. 따라서 모든 기능은 두 번의 번역을 살아남아야 하고, 브리지가 정확히 번역할 수 없는 기능은 근사되는 대신 거부됩니다

HotXLS 2.384.113 기준 지원 집합은 대부분의 데이터 워크북에 충분히 넓습니다: 타입이 지정된 값, 공유 및 인라인 문자열, 흔한 폰트, 채우기, 테두리, 숫자 포맷, 행 높이와 열 폭, 캐시된 결과를 가진 평범한 셀 수식, 정의된 이름, 병합 셀, 하이퍼링크, 틀 고정과 분할 창, 단순 커스텀 조건을 가진 AutoFilter 범위, 그리고 평범한 워크시트 테이블이 그 밖입니다. 밖에는 공유 및 배열 수식, 퓨처 및 사용자 정의 함수, 외부 워크북 참조, 차트와 드로잉, PivotTable과 슬라이서, VBA, 조건부 서식, 데이터 유효성 검사, 수동 계산이나 반복 같은 기본값이 아닌 계산 설정이 있습니다

거부는 강제적이지 권고가 아닙니다. TXLSXWorkbook.GetConversionReport(xlsConversionExcelBinaryWorkbook)는 저장 전에 미지원 기능을 나열하고, 그 항목들은 AAllowLossy = True든 검사 없는 SaveAs든 상관없이 저장을 실패시킵니다. 의도된 것입니다. 바이너리 사본에서 PivotTable을 조용히 떨어뜨리는 손실야말로 워크북이 고객 앞에 놓이기 전까지 아무도 눈치채지 못하는 종류입니다

uses
  Classes, lxHandleX, lxDiagnostics;

function CanSaveAsXlsb(Book: TXLSXWorkbook; Log: TStrings): Boolean;
var
  Report: TXLSConversionReport;
  I: Integer;
begin
  Report := Book.GetConversionReport(xlsConversionExcelBinaryWorkbook);
  try
    Result := True;
    for I := 0 to Report.Count - 1 do
      if Report[I].Fidelity = xlsConversionUnsupported then
      begin
        Result := False;
        Log.Add(Report[I].SheetName + ' ' + Report[I].Location + ': ' +
          Report[I].Feature + ' - ' + Report[I].Message);
      end;
  finally
    Report.Free;   // 리포트의 소유권은 호출자에게
  end;
end;

리포트는 살아 있는 모델을 다룹니다. 최종 패키지 검증은 여전히 모델이 결코 보지 못하는 것들, 예컨대 소스 파일의 형식 불량 레코드나 낯선 속성을 위한 마지막 관문입니다. 그래서 깨끗한 리포트 뒤에 실패한 저장이 올 수 있고, 여러분이 행동하는 근거는 저장 결과입니다

기본 폰트와 0.00 포맷은 왜 사라졌나요?

기본 폰트와 내장 숫자 포맷이 사라진 이유는 XML 스타일 리더가 두 가지 가정을 했기 때문입니다. HotXLS가 스스로 쓰는 파일에는 성립하지만 Excel이 쓰는 워크북에는 성립하지 않는 가정이었죠. 브리지는 두 리소스를 완벽히 보존했습니다; 손실은 한 층 위, 공개 Open 경로에서 일어났습니다. 그 간극이야말로 이 백엔드의 주교훈입니다: 올바른 포맷 브리지가 충실한 공개 왕복을 보장하지는 않습니다

첫 가정은 fonts[0]에 관한 것이었습니다. HotXLS가 XLSX를 저장할 때 암묵적 기본 폰트를 0번 슬롯에 쓰고, XML 리더는 그 슬롯을 건너뛰어 사용자에게 보이는 Fonts 컬렉션이 인덱스 0에서 시작하게 합니다. Excel이 만든 XLSB는 그 슬롯에 자기만의 명시적 기본값을 담고 있는데, 건너뛰는 순간 그 선택이 버려졌습니다. 둘째 가정은 숫자 포맷에 관한 것이었습니다. 리더는 커스텀 numFmtId 값(164 이상)만 NumberFormats로 매핑했습니다; 내장 id는 0으로 남았습니다. 내장 포맷 2, 즉 0.00을 달고 있는 스타일된 빈 셀은 General로 돌아왔습니다

수정은 전적으로 바이너리 임포트 경로에 삽니다. 전체 패키지가 프리플라이트를 통과한 뒤, XLSB 열기는 동기 XML 파싱에만 적용되는 프라이빗 스타일 모드를 설정합니다. 그 모드에서 리더는 0번 슬롯을 포함해 모든 폰트를 로드하고 각 fontId를 하나씩 밀며, 지원되는 각 내장 id를 모델 안의 명시적 포맷 코드로 변환하고, 날짜 감지에는 여전히 원래 numFmtId를 씁니다. 그래서 내장 날짜 포맷은 날짜 의미론을 유지합니다. finally 블록이 언제나 모드를 해제하고, 이후의 평범한 XLSX나 ODS Open은 보존된 기본 폰트 상태도 해제합니다

fonts[0]는 Excel 열 폭에 어떤 영향을 주나요?

Excel은 그 열의 셀에 적용된 폰트가 아니라 fonts[0]의 워크북 기본 폰트에서 열 폭을 계산합니다. 열 width는 기본 폰트의 최대 숫자 폭을 단위로 하는 문자 수로 저장되므로, fonts[0]을 바꾸면 셀이 전부 다른 폰트를 쓰는 열까지 포함해 모든 열의 픽셀 폭이 바뀝니다

이것이 조사 전체에서 가장 혼란스러운 증상을 낳았습니다. 다시 저장된 워크북은 셀 값도, 포맷 코드도, width 속성도 동일했고, 셀 폰트와 Normal 스타일이 모두 보존된 폰트를 가리켰는데도 Excel은 여전히 날짜 열을 ####로 그렸습니다. 기록된 fonts[0]이 Calibri 11로 되돌아가 최대 숫자 폭이 줄었고, 날짜가 더 이상 안 들어갔던 겁니다. HotXLS는 이제 소스 기본 폰트를 유지해 fonts[0]으로 다시 쓰고, 바이너리 소스가 한 번도 없었던 워크북에만 Calibri 11로 폴백합니다. 전체 폭 산술은 Delphi의 Excel 열 폭과 max digit width(MDW)에 있습니다

로캘 의존 내장 포맷은 추측하지 않고 거부합니다

명시적 포맷 코드 없는 내장 숫자 포맷 id는 의미가 고정된 경우에만 받아들여집니다; 로캘 의존 id는 패키지 프리플라이트에서 거부됩니다. HotXLS는 id 0에서 22, 37에서 40, 45에서 49를 0.00, 0%, mm:ss, @ 같은 알려진 코드로 매핑합니다. 164 미만의 나머지 id들은 렌더링이 Excel 설치의 지역 설정에 의존하는 포맷에 속하며, 임포트는 "A locale-dependent number format requires an explicit format code"라는 메시지로 그것들을 거부합니다

그 id들에게 코드를 발명해 주는 것은 실패보다 나쁩니다. 추측한 동아시아 날짜 패턴은 그럴듯해 보이고, 에러 없이 왕복하고, 사용자 절반에게는 틀린 것을 보여 줍니다. 타이밍도 중요합니다: 검사는 기존 워크북이 지워지기 전, 전체 패키지 프리플라이트에서 돕니다. 그래서 거부된 파일은 이전에 로드된 모델, 파일명, 소스 포맷을 그대로 둡니다. 내장 날짜 id가 1900과 1904 날짜 시스템과 어떻게 상호작용하는지는 Excel 날짜 시리얼과 numFmt 글이 다룹니다

procedure LoadBinaryWorkbook(Book: TXLSXWorkbook; const FileName: string);
var
  I: Integer;
begin
  // 바이너리 입력은 즉시 전체 로딩이 필요; 선택 모드는 선제 거부
  Book.MetadataOnly := False;
  Book.SelectedSheets.Clear;

  if Book.Open(FileName) <> 1 then
  begin
    for I := 0 to Book.Diagnostics.Count - 1 do
      Writeln(Book.Diagnostics[I].Message);
    Exit;   // 이전에 로드한 워크북은 여전히 온전합니다
  end;

  // 저장이 재계산하지 않는 한 수식 출력에는 캐시가 필요
  Book.RecalcBeforeSave := xrmAlways;
end;

프로덕션 코드에서의 XLSB 실용 규칙

바이너리 백엔드는 그 거부를 눌러 버릴 에러가 아니라 데이터로 다룰 때 믿고 쓸 만합니다. 위의 동작에서 나오는 규칙 몇 가지입니다

  • 바이너리 저장마다 포맷을 이름으로 지정하세요; .xlsb 확장자만으로는 여전히 XLSX가 만들어집니다
  • 사용자가 편집한 워크북을 저장하기 전에 GetConversionReport(xlsConversionExcelBinaryWorkbook)를 돌리고 미지원 기능을 사용자에게 노출하세요
  • 바이너리 입력에서는 MetadataOnly를 끄고 SelectedSheets를 비워 두세요. 지연 로딩은 변환된 아카이브를 계속 들고 있어야 하므로
  • 저장 전에 수식에 캐시된 결과를 주거나 재계산하세요. 캐시가 없으면 xrmManual 아래에서, 깨끗한 xrmIfDirty 아래에서도 거부됩니다
  • 완전한 변환 패키지에 바이너리 출력을 더한 만큼 메모리를 예산에 넣으세요; 메모리가 유계여야 하면 스트리밍 XLSX를 쓰세요

테스트할 때는 브리지 수준이 아니라 공개 API를 통해 Excel과 비교하세요. 기본 폰트와 0.00 회귀 둘 다 브리지 테스트를 통과했고, 둘 다 Open과 SaveAsChecked로 열고 다시 저장한 Excel제 워크북에서 즉시 나타났습니다

FAQ

HotXLS는 XLSB 파일을 자동으로 감지하나요?

예. TXLSXWorkbook.Open은 워크북 파트에 등록된 콘텐츠 타입을 살피고 바이너리 레코드를 검증합니다. 같은 호출이 XLSX와 XLSB를 모두 로드하고, SourceFormat이 어느 쪽을 읽었는지 보고합니다

.xlsb 파일명으로 SaveAs해도 XLSX가 쓰이는 이유는 무엇인가요?

HotXLS는 확장자에서 출력 포맷을 절대 추론하지 않습니다. 바이너리 워크북을 쓰려면 SaveAs나 SaveAsChecked에 xlsxExcelBinaryWorkbook을 넘기세요

워크북이 XLSB 서브셋 밖의 기능을 쓰면 어떻게 되나요?

손실 저장이 허용되었더라도 목적지가 바뀌기 전에 저장이 실패합니다. GetConversionReport가 막는 기능들을 미리 나열하고, Diagnostics가 나중에 실패를 설명합니다

XLSB 지원, 전환 리포트, 워크북 API의 나머지는 네이티브 Delphi와 C++Builder 스프레드시트 컴포넌트인 HotXLS에 들어 있습니다