odfjs/scripts/createOdsFile.js

252 lines
11 KiB
JavaScript
Raw Normal View History

import { ZipWriter, BlobWriter, TextReader } from '@zip.js/zip.js';
import {serializeToString, createDocument} from './DOMUtils.js'
/** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */
const stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
2026-02-05 01:10:10 -07:00
<office:document-styles
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
office:version="1.2">
2026-02-05 01:10:10 -07:00
<office:styles>
<style:style style:name="boldcell" style:family="table-cell">
<style:text-properties fo:font-weight="bold"/>
</style:style>
</office:styles>
<office:automatic-styles/>
<office:master-styles/>
</office:document-styles>`;
const manifestXml = `<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest manifest:version="1.2" xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
<manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.spreadsheet" manifest:full-path="/"/>
<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="content.xml"/>
<manifest:file-entry manifest:media-type="text/xml" manifest:full-path="styles.xml"/>
</manifest:manifest>`;
/**
* Crée un fichier .ods à partir d'un Map de feuilles de calcul
* @param {Map<SheetName, SheetRawContent>} sheetsData
* @returns {Promise<ArrayBuffer>}
*/
2026-02-05 03:32:14 -07:00
export async function createOdsFile(sheetsData, currencyData = null) {
// Create a new zip writer
const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet'));
2026-02-05 01:10:10 -07:00
// The “mimetype” file shall be the first file of the zip file.
// It shall not be compressed, and it shall not use an 'extra field' in its header.
// https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752809_826425813
zipWriter.add(
"mimetype",
new TextReader("application/vnd.oasis.opendocument.spreadsheet"),
{
compressionMethod: 0,
level: 0,
dataDescriptor: false,
extendedTimestamp: false,
}
);
2026-02-05 03:32:14 -07:00
const contentXml = generateContentFileXMLString(sheetsData, currencyData);
zipWriter.add("content.xml", new TextReader(contentXml), {level: 9});
zipWriter.add("styles.xml", new TextReader(stylesXml));
zipWriter.add('META-INF/manifest.xml', new TextReader(manifestXml));
// Close the zip writer and get the ArrayBuffer
const zipFile = await zipWriter.close();
return zipFile.arrayBuffer();
}
/**
* Generate the content.xml file with spreadsheet data
2026-02-05 01:10:10 -07:00
* @param {Map<SheetName, SheetRawContent>} sheetsData
* @returns {string}
*/
2026-02-05 03:32:14 -07:00
function generateContentFileXMLString(sheetsData, currencyData) {
const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content');
const root = doc.documentElement;
// Set up namespaces
root.setAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
root.setAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
root.setAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
root.setAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
root.setAttribute('office:version', '1.2');
2026-02-05 01:54:56 -07:00
const styleNode = doc.createElement("office:automatic-styles");
2026-02-05 03:32:14 -07:00
var currencyStyleName = "currencyStyle";
if (currencyData != null) {
currencyStyleName = `currency${currencyData.currencyCode.toUpperCase()}`;
const numberStyle = doc.createElement("number:currency-style");
numberStyle.setAttribute("style:name", currencyStyleName);
const numberCurrencySymbolStyle = doc.createElement("number:currency-symbol");
numberCurrencySymbolStyle.setAttribute("number:language", "en");
numberCurrencySymbolStyle.setAttribute("number:country", currencyData.countryCode.toUpperCase());
numberCurrencySymbolStyle.textContent = currencyData.currencySymbol;
numberStyle.appendChild(numberCurrencySymbolStyle);
const numberCurrencyStyle = doc.createElement("number:number");
numberCurrencyStyle.setAttribute("number:min-integer-digits", "1");
numberCurrencyStyle.setAttribute("number:decimal-places", `${currencyData.decimalPlaces}`);
numberCurrencyStyle.setAttribute("number:min-decimal-places", `${currencyData.decimalPlaces}`);
numberCurrencyStyle.setAttribute("number:grouping", "true");
numberStyle.appendChild(numberCurrencyStyle);
styleNode.appendChild(numberStyle);
const currencyCellStyleNode = doc.createElement("style:style");
currencyCellStyleNode.setAttribute("style:name", "currencycell");
currencyCellStyleNode.setAttribute("style:family", "table-cell");
currencyCellStyleNode.setAttribute("style:data-style-name", currencyStyleName);
const currencyCellTableCellProperties = doc.createElement("style:table-cell-properties");
currencyCellStyleNode.appendChild(currencyCellTableCellProperties);
styleNode.appendChild(currencyCellStyleNode);
}
2026-02-05 01:54:56 -07:00
const boldCellStyleNode = doc.createElement("style:style");
boldCellStyleNode.setAttribute("style:name", "boldcell");
boldCellStyleNode.setAttribute("style:family", "table-cell");
const boldCellTextPropsNode = doc.createElement("style:text-properties");
boldCellTextPropsNode.setAttribute("fo:font-weight", "bold");
boldCellStyleNode.appendChild(boldCellTextPropsNode);
styleNode.appendChild(boldCellStyleNode);
2026-02-05 03:32:14 -07:00
2026-02-05 01:54:56 -07:00
root.appendChild(styleNode);
const bodyNode = doc.createElement('office:body');
root.appendChild(bodyNode);
const spreadsheetNode = doc.createElement('office:spreadsheet');
bodyNode.appendChild(spreadsheetNode);
// Iterate through sheets
sheetsData.forEach((sheetData, sheetName) => {
const tableNode = doc.createElement('table:table');
tableNode.setAttribute('table:name', sheetName);
spreadsheetNode.appendChild(tableNode);
2026-02-05 01:54:56 -07:00
var columnsWidthChars = {};
for (let r = 0; r < sheetData.length; r++) {
for (let c = 0; c < sheetData[r].length; c++) {
2026-02-05 03:32:14 -07:00
var len = ((sheetData[r][c].display ?? sheetData[r][c].value) + "").length;
2026-02-05 01:54:56 -07:00
if (typeof columnsWidthChars[c] == "undefined") {
2026-02-05 03:32:14 -07:00
columnsWidthChars[c] = len;
2026-02-05 01:54:56 -07:00
}
2026-02-05 03:32:14 -07:00
columnsWidthChars[c] = Math.max(columnsWidthChars[c], len);
2026-02-05 01:54:56 -07:00
}
}
for (var prop in columnsWidthChars) {
var columnNode = doc.createElement('table:table-column');
columnNode.setAttribute("table:style-name", "colwidth" + columnsWidthChars[prop]);
tableNode.appendChild(columnNode);
var columnWidthNode = doc.createElement("style:style");
columnWidthNode.setAttribute("style:name", "colwidth" + columnsWidthChars[prop]);
columnWidthNode.setAttribute("style:family", "table-column");
const columnWidthPropsNode = doc.createElement("style:table-column-properties");
columnWidthPropsNode.setAttribute("style:column-width", `${columnsWidthChars[prop] * 0.26}cm`);
columnWidthNode.appendChild(columnWidthPropsNode);
styleNode.appendChild(columnWidthNode);
}
// Iterate through rows
sheetData.forEach((row) => {
const rowNode = doc.createElement('table:table-row');
tableNode.appendChild(rowNode);
// Iterate through cells in row
row.forEach((cell) => {
const cellNode = doc.createElement('table:table-cell');
const cellType = convertCellType(cell.type);
cellNode.setAttribute('office:value-type', cellType);
2026-02-05 01:10:10 -07:00
if (cell.style && cell.style == "bold") {
cellNode.setAttribute('table:style-name', "boldcell");
}
// Add value attribute based on type
if (cell.value !== null && cell.value !== undefined) {
switch (cellType) {
case 'float':
cellNode.setAttribute('office:value', cell.value.toString());
break;
case 'percentage':
cellNode.setAttribute('office:value', cell.value.toString());
cellNode.setAttribute('office:value-type', 'percentage');
break;
2026-02-05 03:32:14 -07:00
case 'currency':
cellNode.setAttribute('office:value', cell.value.toString());
cellNode.setAttribute('office:value-type', 'currency');
if (currencyData != null) {
cellNode.setAttribute("table:style-name", "currencycell");
cellNode.setAttribute('office:currency', currencyData.currencyCode.toUpperCase());
}
break;
case 'date':
cellNode.setAttribute('office:date-value', cell.value.toString());
break;
case 'boolean':
cellNode.setAttribute('office:boolean-value', cell.value ? 'true' : 'false');
break;
default:
const textNode = doc.createElement('text:p');
textNode.textContent = cell.value.toString();
cellNode.appendChild(textNode);
break;
}
if (cellType !== 'string') {
const textNode = doc.createElement('text:p');
2026-02-05 03:32:14 -07:00
if (typeof cell.display != "undefined") {
textNode.textContent = cell.display.toString();
} else {
textNode.textContent = cell.value.toString();
}
cellNode.appendChild(textNode);
}
}
rowNode.appendChild(cellNode);
});
});
});
return serializeToString(doc);
}
/**
* Convert cell type to OpenDocument format type
2026-02-05 01:10:10 -07:00
* @param {SheetCellRawContent['type']} type
* @returns {SheetCellRawContent['type']}
*/
function convertCellType(type) {
const typeMap = {
'float': 'float',
'percentage': 'percentage',
'currency': 'currency',
'date': 'date',
'time': 'time',
'boolean': 'boolean',
'string': 'string',
'n': 'float',
's': 'string',
'd': 'date',
'b': 'boolean'
};
return typeMap[type] || 'string';
}