수년간 .xlsx만 내보내던 Delphi 보고 백엔드에 새로운 요구사항이 생겼다고 가정해 봅시다. 공공 부문 고객의 조달 규정이 OpenDocument Spreadsheet 출력을 의무화하고, 그 계정을 담당하는 분석가들은 LibreOffice에서 저장한 .ods 파일로 수정 사항을 돌려보냅니다. 이제 같은 코드가 ODS를 쓰고 읽어야 합니다. Delphi와 C++Builder를 위한 losLab의 네이티브 Object Pascal 스프레드시트 라이브러리인 HotXLS는 Excel도 LibreOffice도 어디에도 설치되지 않은 상태에서 두 방향 모두를 처리합니다. 하지만 이 라이브러리가 하지 않는 일이 있습니다. 바로 두 방향을 대칭으로 만드는 일입니다. 내보내기는 가져오기가 복원하는 것보다 훨씬 많은 것을 담고 있으며, 그렇지 않다고 가정하는 팀은 고객의 수정과 다음 보고서 사이 어딘가에서 수식과 서식이 증발하는 것을 지켜보게 되지만, 그것을 가리킬 어떤 오류도 나타나지 않습니다
ODS 지원은 XLS 파사드가 아니라 XLSX 파사드에 있습니다
HotXLS는 하나의 패키지 안에 두 개의 독립된 클래스 계층을 제공합니다. 바이너리 BIFF8 .xls 파일을 위한 lxHandle 유닛의 TXLSWorkbook과, OOXML .xlsx 패키지를 위한 lxHandleX 유닛의 TXLSXWorkbook입니다. OpenODS, SaveAsODS, GetODSSheetNames를 비롯한 모든 OpenDocument 진입점은 TXLSXWorkbook에 달려 있습니다. 이 배치는 임의적인 것이 아닙니다. OASIS ODF 1.3에 명시된 대로, ODS 패키지는 mimetype 멤버, 매니페스트, content.xml 본문을 담은 zip 아카이브이며, 이는 OOXML zip과 구조적으로 사촌 관계에 있습니다. 반면 BIFF8은 1990년대의 바이너리 레코드 스트림으로 이와는 공통점이 전혀 없습니다
이 배치에는 실용적인 함의가 있습니다. 레거시 .xls 워크북은 한 번의 호출로 .ods가 될 수 없습니다. 먼저 lxXlsxExport 유닛의 SaveXLSWorkbookAsXLSX로 BIFF 콘텐츠를 XLSX 모델로 다리 놓고, 결과를 TXLSXWorkbook으로 다시 연 다음, 거기서부터 내보내야 합니다. 이 브리지는 무손실이 아니며, 그 위에 쌓아 올리기 전에 빈틈을 알아둘 가치가 있습니다. 값, 수식, 숫자 서식, 글꼴, 채우기, 열 너비는 복사됩니다. 테두리, 병합된 범위, 코멘트, 차트, 조건부 서식은 버려집니다. 서식이 두꺼운 .xls 원본은 떠날 때보다 더 밋밋한 모습으로 ODS에 도착하며, 이는 ODS 라이터의 속성이 아니라 브리지의 속성입니다
가져오기 쪽의 감지는 자동입니다. 평범한 Open 메서드는 mimetype 멤버로 ODS 패키지를 인식하며, 그 멤버가 없으면 최상위 content.xml 확인으로 대체하므로, "사용자가 무엇을 업로드했든 연다"는 범용 코드 경로는 자체적인 확장자 스니핑이 필요 없습니다. 연 뒤에는 SourceFormat 속성이 어느 분기가 작동했는지 알려줍니다
TODSExportOptions로 ODS 내보내기
내보내기 호출 자체는 한 줄이지만, 그 주위의 옵션 객체는 나중에 리뷰어가 물어볼 결정 사항들을 담고 있습니다:
var
Book: TXLSXWorkbook;
Opts: TODSExportOptions;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly-report.xlsx');
Opts := TODSExportOptions.Create; // 호출자가 소유하고 해제함
try
Opts.Generator := 'ReportService 4.2'; // meta:generator를 덮어씀
Opts.IncludeCharts := True;
Opts.IncludeImages := True;
Book.SaveAsODS('quarterly-report.ods', Opts);
finally
Opts.Free;
end;
finally
Book.Free;
end;
end;
옵션 객체는 호출자가 소유합니다. HotXLS는 이를 해제하지 않으며, 이것이 안쪽의 try..finally가 선택 사항이 아니라 반드시 있어야 하는 이유입니다. 단순히 출력에 라벨을 붙이는 데 그치지 않고 실제로 출력을 바꾸는 두 속성은 더 자세히 살펴볼 가치가 있습니다. IncludeCharts := False를 설정하면 차트를 숨기는 것 이상의 일이 일어납니다. 차트 하위 문서와 그 매니페스트 항목을 패키지에서 완전히 제거하며, 소비자가 이를 만나면 걸려 넘어질 데이터 파이프라인일 때 정확히 원하는 동작입니다. Generator는 그렇지 않으면 HotXLS/<version>로 읽히는 ODF meta:generator 문자열을 덮어씁니다. 다운스트림 도구가 파일 생성자를 지문 삼아 지원 경로를 결정한다면 이를 재정의하십시오. 이 중 어느 것도 해당하지 않는다면 옵션 객체를 아예 생략하십시오. SaveAs(FileName, xlsxOpenDocumentSpreadsheet)를 호출하는 것은 기본값을 사용하는 SaveAsODS와 동일하며, 양쪽 모두의 스트림 오버로드를 사용하면 임시 파일 없이 패키지를 곧바로 HTTP 응답에 기록할 수 있습니다
가져오기 경로가 읽는 것과 의도적으로 건너뛰는 것
누군가에게 왕복 충실도를 약속하기 전에 이 부분을 주의 깊게 읽으십시오. HotXLS의 ODS 가져오기는 의도적으로 가벼운 경로입니다. 스칼라 셀 값과 저장 시점에 각 수식이 지니고 있던 캐시된 결과는 보존하며, 반복되는 행과 열은 그리드로 펼쳐 넣습니다. 스타일, ODS 수식 표현식, 도면은 가져오지 않습니다
가장 물릴 가능성이 높은 선택은 수식에 관한 것이며, 이는 의도적으로 내린 결정입니다. ODF 셀은 두 가지를 나란히 저장합니다. ODF 1.3 Part 4에 정의된 OpenFormula 방언으로 작성된 수식 표현식과, 생성 애플리케이션이 마지막으로 계산한 값입니다. OpenFormula를 Excel 수식 문법으로 번역하는 것은 그 자체로 함수 어휘, 참조 문법, 오류 모델을 둘러싼 진짜 경계 사례들이 있는 별도의 방언 변환 문제입니다. 대신 캐시된 값을 읽으면 이 조용한 오역이라는 문제 부류 전체를 우회할 수 있으므로, 여러분이 가져오는 숫자는 발신자가 마지막으로 본 숫자와 정확히 같습니다. 대가는 이 숫자들이 그것을 만들어낸 살아 있는 수식이 아니라 그냥 숫자로 도착한다는 점입니다
이로부터 곧바로 설계 시 고려해야 할 실패 양상이 따라옵니다. LibreOffice가 마지막으로 저장했을 때 합계가 정확했던 스프레드시트는 정확한 숫자로 가져와지지만, 이제 그 숫자들은 상수입니다. 입력 셀을 수정하고 재계산해도 아무것도 움직이지 않습니다. 수식이 사라졌고 최종 결과만 남아 있기 때문입니다. 워크플로에 가져오기 이후에도 살아 있는 수식이 필요하다면, XLSX 파사드에서는 등호 접두사 없이 표현식을 받는 Cell.Formula를 통해 여러분의 비즈니스 규칙으로부터 프로그래밍 방식으로 다시 세워야 합니다
비대칭적인 왕복을 고려한 설계
내보내기는 값, 스타일, 그리고 요청하면 차트와 이미지까지 포함해 메모리에 있는 워크북 모델 전체로부터 렌더링합니다. 가져오기는 값만 반환합니다. 따라서 .xlsx에서 .ods로 가는 구간은 충실도가 높지만, .ods에서 .xlsx로 돌아오는 구간은 값과 캐시된 결과만 가져올 뿐 스타일도 살아 있는 수식도 가져오지 않습니다. 이 둘을 연쇄하면 비대칭이 누적됩니다. .xlsx에서 .ods로, 다시 .xlsx로 완전히 한 바퀴 도는 사이클은 나갈 때는 모든 것을 충실하게 기록하지만 돌아올 때는 스타일과 수식을 잃습니다. 어느 단계에서도 잘못된 것은 없는데도 그렇습니다
Book := TXLSXWorkbook.Create;
try
Book.Open('vendor-revision.ods'); // 형식은 자동 감지됨
if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
begin
// ODS를 가져온 뒤에는 값과 캐시된 수식 결과는 존재하지만
// 스타일과 살아 있는 수식은 존재하지 않습니다. 저장하기 전에
// 다운스트림 파이프라인이 의존하는 것을 다시 만들어야 합니다.
Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
Book.SaveAs('vendor-revision.xlsx');
end;
finally
Book.Free;
end;
여기서 나오는 아키텍처 패턴은 이렇습니다. 들어오는 .ods 파일을 제자리에서 편집할 문서가 아니라 데이터 피드로 취급하십시오. 정본 워크북은 .xlsx로 유지하고, 고객의 수정본에서 값만 읽어내며, 정본 사본으로부터 필요할 때마다 새로운 ODS를 내보내십시오. 검증은 양쪽 진영 모두에 필요합니다. 내보낸 파일을 참조 ODF 소비자인 LibreOffice Calc에서도, 오랫동안 ODS를 읽어 왔지만 차트와 스타일 지원의 가장자리에서는 LibreOffice와 의견이 갈리는 Excel에서도 열어 보십시오. 시트 수, 몇 개의 핵심 셀, 차트 유무 확인이면 내보내기 프로필마다 충분한 스모크 체크가 됩니다
가져오기를 결정하기 전에 ODS 파일 선별하기
엔드포인트가 업로드를 받아들일 때, 시트 이름을 나열하는 것은 완전한 파싱보다 훨씬 저렴하며 구조적인 이상을 일찍 잡아냅니다:
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
raise Exception.Create('not a readable ODS package');
if Names.IndexOf('Data') < 0 then
raise Exception.Create('revision is missing the Data sheet');
finally
Book.Free;
Names.Free;
end;
반환값 관례가 사람들을 걸려 넘어지게 합니다. HotXLS 호출은 일반적으로 성공 시 양수 개수 또는 1을, 실패 시 -1을 반환하며 실패할 때 목록을 비우므로, 특정한 양수 하나와 비교하는 대신 <= 0으로 검사하십시오. GetODSSheetNames는 워크북 인스턴스를 초기화하지도 채우지도 않으므로, 단 하나의 프로브 객체로 들어오는 파일 디렉터리 전체를 검사할 수 있습니다. 이런 구조적 검사는 가장 흔한 실전 실패, 즉 분석가가 수정본을 돌려보내기 전에 시트 이름을 바꾸거나 삭제하는 경우를 세 계층 아래에서 nil 참조로 드러나는 대신, 오류 메시지가 여전히 파일 이름과 빠진 시트를 지목할 수 있는 관문에서 잡아냅니다
이를 중심으로 더 폭넓은 변환 파이프라인을 구축하고 있다면, 워크북 감사 및 변환 워크벤치 패턴은 목표 형식을 선택하기 전에 파일의 기능을 목록화하는 방법을 보여주며, 대규모 워크북 성능 가이드는 배치 내보내기를 합리적인 메모리 범위 안에 유지하는 방법을 다룹니다
HotXLS는 전체 소스 코드를 갖춘 네이티브 Delphi 및 C++Builder 스프레드시트 라이브러리입니다. 전체 기능 목록과 라이선스 세부사항은 HotXLS Delphi Component 제품 페이지에 있습니다