| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- const fs = require('fs');
- const Archiver = require('archiver');
- const StreamBuf = require('../../utils/stream-buf');
- const RelType = require('../../xlsx/rel-type');
- const StylesXform = require('../../xlsx/xform/style/styles-xform');
- const SharedStrings = require('../../utils/shared-strings');
- const DefinedNames = require('../../doc/defined-names');
- const CoreXform = require('../../xlsx/xform/core/core-xform');
- const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
- const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
- const AppXform = require('../../xlsx/xform/core/app-xform');
- const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
- const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
- const WorksheetWriter = require('./worksheet-writer');
- const theme1Xml = require('../../xlsx/xml/theme1.js');
- class WorkbookWriter {
- constructor(options) {
- options = options || {};
- this.created = options.created || new Date();
- this.modified = options.modified || this.created;
- this.creator = options.creator || 'ExcelJS';
- this.lastModifiedBy = options.lastModifiedBy || 'ExcelJS';
- this.lastPrinted = options.lastPrinted;
- // using shared strings creates a smaller xlsx file but may use more memory
- this.useSharedStrings = options.useSharedStrings || false;
- this.sharedStrings = new SharedStrings();
- // style manager
- this.styles = options.useStyles ? new StylesXform(true) : new StylesXform.Mock(true);
- // defined names
- this._definedNames = new DefinedNames();
- this._worksheets = [];
- this.views = [];
- this.zipOptions = options.zip;
- this.media = [];
- this.commentRefs = [];
- this.zip = Archiver('zip', this.zipOptions);
- if (options.stream) {
- this.stream = options.stream;
- } else if (options.filename) {
- this.stream = fs.createWriteStream(options.filename);
- } else {
- this.stream = new StreamBuf();
- }
- this.zip.pipe(this.stream);
- // these bits can be added right now
- this.promise = Promise.all([this.addThemes(), this.addOfficeRels()]);
- }
- get definedNames() {
- return this._definedNames;
- }
- _openStream(path) {
- const stream = new StreamBuf({bufSize: 65536, batch: true});
- this.zip.append(stream, {name: path});
- stream.on('finish', () => {
- stream.emit('zipped');
- });
- return stream;
- }
- _commitWorksheets() {
- const commitWorksheet = function(worksheet) {
- if (!worksheet.committed) {
- return new Promise(resolve => {
- worksheet.stream.on('zipped', () => {
- resolve();
- });
- worksheet.commit();
- });
- }
- return Promise.resolve();
- };
- // if there are any uncommitted worksheets, commit them now and wait
- const promises = this._worksheets.map(commitWorksheet);
- if (promises.length) {
- return Promise.all(promises);
- }
- return Promise.resolve();
- }
- async commit() {
- // commit all worksheets, then add suplimentary files
- await this.promise;
- await this.addMedia();
- await this._commitWorksheets();
- await Promise.all([
- this.addContentTypes(),
- this.addApp(),
- this.addCore(),
- this.addSharedStrings(),
- this.addStyles(),
- this.addWorkbookRels(),
- ]);
- await this.addWorkbook();
- return this._finalize();
- }
- get nextId() {
- // find the next unique spot to add worksheet
- let i;
- for (i = 1; i < this._worksheets.length; i++) {
- if (!this._worksheets[i]) {
- return i;
- }
- }
- return this._worksheets.length || 1;
- }
- addImage(image) {
- const id = this.media.length;
- const medium = Object.assign({}, image, {type: 'image', name: `image${id}.${image.extension}`});
- this.media.push(medium);
- return id;
- }
- getImage(id) {
- return this.media[id];
- }
- addWorksheet(name, options) {
- // it's possible to add a worksheet with different than default
- // shared string handling
- // in fact, it's even possible to switch it mid-sheet
- options = options || {};
- const useSharedStrings =
- options.useSharedStrings !== undefined ? options.useSharedStrings : this.useSharedStrings;
- if (options.tabColor) {
- // eslint-disable-next-line no-console
- console.trace('tabColor option has moved to { properties: tabColor: {...} }');
- options.properties = Object.assign(
- {
- tabColor: options.tabColor,
- },
- options.properties
- );
- }
- const id = this.nextId;
- name = name || `sheet${id}`;
- const worksheet = new WorksheetWriter({
- id,
- name,
- workbook: this,
- useSharedStrings,
- properties: options.properties,
- state: options.state,
- pageSetup: options.pageSetup,
- views: options.views,
- autoFilter: options.autoFilter,
- headerFooter: options.headerFooter,
- });
- this._worksheets[id] = worksheet;
- return worksheet;
- }
- getWorksheet(id) {
- if (id === undefined) {
- return this._worksheets.find(() => true);
- }
- if (typeof id === 'number') {
- return this._worksheets[id];
- }
- if (typeof id === 'string') {
- return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
- }
- return undefined;
- }
- addStyles() {
- return new Promise(resolve => {
- this.zip.append(this.styles.xml, {name: 'xl/styles.xml'});
- resolve();
- });
- }
- addThemes() {
- return new Promise(resolve => {
- this.zip.append(theme1Xml, {name: 'xl/theme/theme1.xml'});
- resolve();
- });
- }
- addOfficeRels() {
- return new Promise(resolve => {
- const xform = new RelationshipsXform();
- const xml = xform.toXml([
- {Id: 'rId1', Type: RelType.OfficeDocument, Target: 'xl/workbook.xml'},
- {Id: 'rId2', Type: RelType.CoreProperties, Target: 'docProps/core.xml'},
- {Id: 'rId3', Type: RelType.ExtenderProperties, Target: 'docProps/app.xml'},
- ]);
- this.zip.append(xml, {name: '/_rels/.rels'});
- resolve();
- });
- }
- addContentTypes() {
- return new Promise(resolve => {
- const model = {
- worksheets: this._worksheets.filter(Boolean),
- sharedStrings: this.sharedStrings,
- commentRefs: this.commentRefs,
- media: this.media,
- };
- const xform = new ContentTypesXform();
- const xml = xform.toXml(model);
- this.zip.append(xml, {name: '[Content_Types].xml'});
- resolve();
- });
- }
- addMedia() {
- return Promise.all(
- this.media.map(medium => {
- if (medium.type === 'image') {
- const filename = `xl/media/${medium.name}`;
- if (medium.filename) {
- return this.zip.file(medium.filename, {name: filename});
- }
- if (medium.buffer) {
- return this.zip.append(medium.buffer, {name: filename});
- }
- if (medium.base64) {
- const dataimg64 = medium.base64;
- const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
- return this.zip.append(content, {name: filename, base64: true});
- }
- }
- throw new Error('Unsupported media');
- })
- );
- }
- addApp() {
- return new Promise(resolve => {
- const model = {
- worksheets: this._worksheets.filter(Boolean),
- };
- const xform = new AppXform();
- const xml = xform.toXml(model);
- this.zip.append(xml, {name: 'docProps/app.xml'});
- resolve();
- });
- }
- addCore() {
- return new Promise(resolve => {
- const coreXform = new CoreXform();
- const xml = coreXform.toXml(this);
- this.zip.append(xml, {name: 'docProps/core.xml'});
- resolve();
- });
- }
- addSharedStrings() {
- if (this.sharedStrings.count) {
- return new Promise(resolve => {
- const sharedStringsXform = new SharedStringsXform();
- const xml = sharedStringsXform.toXml(this.sharedStrings);
- this.zip.append(xml, {name: '/xl/sharedStrings.xml'});
- resolve();
- });
- }
- return Promise.resolve();
- }
- addWorkbookRels() {
- let count = 1;
- const relationships = [
- {Id: `rId${count++}`, Type: RelType.Styles, Target: 'styles.xml'},
- {Id: `rId${count++}`, Type: RelType.Theme, Target: 'theme/theme1.xml'},
- ];
- if (this.sharedStrings.count) {
- relationships.push({
- Id: `rId${count++}`,
- Type: RelType.SharedStrings,
- Target: 'sharedStrings.xml',
- });
- }
- this._worksheets.forEach(worksheet => {
- if (worksheet) {
- worksheet.rId = `rId${count++}`;
- relationships.push({
- Id: worksheet.rId,
- Type: RelType.Worksheet,
- Target: `worksheets/sheet${worksheet.id}.xml`,
- });
- }
- });
- return new Promise(resolve => {
- const xform = new RelationshipsXform();
- const xml = xform.toXml(relationships);
- this.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
- resolve();
- });
- }
- addWorkbook() {
- const {zip} = this;
- const model = {
- worksheets: this._worksheets.filter(Boolean),
- definedNames: this._definedNames.model,
- views: this.views,
- properties: {},
- calcProperties: {},
- };
- return new Promise(resolve => {
- const xform = new WorkbookXform();
- xform.prepare(model);
- zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
- resolve();
- });
- }
- _finalize() {
- return new Promise((resolve, reject) => {
- this.stream.on('error', reject);
- this.stream.on('finish', () => {
- resolve(this);
- });
- this.zip.on('error', reject);
- this.zip.finalize();
- });
- }
- }
- module.exports = WorkbookWriter;
|