xlsx.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. "use strict";
  2. const fs = require('fs');
  3. const JSZip = require('jszip');
  4. const {
  5. PassThrough
  6. } = require('readable-stream');
  7. const ZipStream = require('../utils/zip-stream');
  8. const StreamBuf = require('../utils/stream-buf');
  9. const utils = require('../utils/utils');
  10. const XmlStream = require('../utils/xml-stream');
  11. const {
  12. bufferToString
  13. } = require('../utils/browser-buffer-decode');
  14. const StylesXform = require('./xform/style/styles-xform');
  15. const CoreXform = require('./xform/core/core-xform');
  16. const SharedStringsXform = require('./xform/strings/shared-strings-xform');
  17. const RelationshipsXform = require('./xform/core/relationships-xform');
  18. const ContentTypesXform = require('./xform/core/content-types-xform');
  19. const AppXform = require('./xform/core/app-xform');
  20. const WorkbookXform = require('./xform/book/workbook-xform');
  21. const WorksheetXform = require('./xform/sheet/worksheet-xform');
  22. const DrawingXform = require('./xform/drawing/drawing-xform');
  23. const TableXform = require('./xform/table/table-xform');
  24. const CommentsXform = require('./xform/comment/comments-xform');
  25. const VmlNotesXform = require('./xform/comment/vml-notes-xform');
  26. const theme1Xml = require('./xml/theme1');
  27. function fsReadFileAsync(filename, options) {
  28. return new Promise((resolve, reject) => {
  29. fs.readFile(filename, options, (error, data) => {
  30. if (error) {
  31. reject(error);
  32. } else {
  33. resolve(data);
  34. }
  35. });
  36. });
  37. }
  38. class XLSX {
  39. constructor(workbook) {
  40. this.workbook = workbook;
  41. }
  42. // ===============================================================================
  43. // Workbook
  44. // =========================================================================
  45. // Read
  46. async readFile(filename, options) {
  47. if (!(await utils.fs.exists(filename))) {
  48. throw new Error(`File not found: ${filename}`);
  49. }
  50. const stream = fs.createReadStream(filename);
  51. try {
  52. const workbook = await this.read(stream, options);
  53. stream.close();
  54. return workbook;
  55. } catch (error) {
  56. stream.close();
  57. throw error;
  58. }
  59. }
  60. parseRels(stream) {
  61. const xform = new RelationshipsXform();
  62. return xform.parseStream(stream);
  63. }
  64. parseWorkbook(stream) {
  65. const xform = new WorkbookXform();
  66. return xform.parseStream(stream);
  67. }
  68. parseSharedStrings(stream) {
  69. const xform = new SharedStringsXform();
  70. return xform.parseStream(stream);
  71. }
  72. reconcile(model, options) {
  73. const workbookXform = new WorkbookXform();
  74. const worksheetXform = new WorksheetXform(options);
  75. const drawingXform = new DrawingXform();
  76. const tableXform = new TableXform();
  77. workbookXform.reconcile(model);
  78. // reconcile drawings with their rels
  79. const drawingOptions = {
  80. media: model.media,
  81. mediaIndex: model.mediaIndex
  82. };
  83. Object.keys(model.drawings).forEach(name => {
  84. const drawing = model.drawings[name];
  85. const drawingRel = model.drawingRels[name];
  86. if (drawingRel) {
  87. drawingOptions.rels = drawingRel.reduce((o, rel) => {
  88. o[rel.Id] = rel;
  89. return o;
  90. }, {});
  91. (drawing.anchors || []).forEach(anchor => {
  92. const hyperlinks = anchor.picture && anchor.picture.hyperlinks;
  93. if (hyperlinks && drawingOptions.rels[hyperlinks.rId]) {
  94. hyperlinks.hyperlink = drawingOptions.rels[hyperlinks.rId].Target;
  95. delete hyperlinks.rId;
  96. }
  97. });
  98. drawingXform.reconcile(drawing, drawingOptions);
  99. }
  100. });
  101. // reconcile tables with the default styles
  102. const tableOptions = {
  103. styles: model.styles
  104. };
  105. Object.values(model.tables).forEach(table => {
  106. tableXform.reconcile(table, tableOptions);
  107. });
  108. const sheetOptions = {
  109. styles: model.styles,
  110. sharedStrings: model.sharedStrings,
  111. media: model.media,
  112. mediaIndex: model.mediaIndex,
  113. date1904: model.properties && model.properties.date1904,
  114. drawings: model.drawings,
  115. comments: model.comments,
  116. tables: model.tables,
  117. vmlDrawings: model.vmlDrawings
  118. };
  119. model.worksheets.forEach(worksheet => {
  120. worksheet.relationships = model.worksheetRels[worksheet.sheetNo];
  121. worksheetXform.reconcile(worksheet, sheetOptions);
  122. });
  123. // delete unnecessary parts
  124. delete model.worksheetHash;
  125. delete model.worksheetRels;
  126. delete model.globalRels;
  127. delete model.sharedStrings;
  128. delete model.workbookRels;
  129. delete model.sheetDefs;
  130. delete model.styles;
  131. delete model.mediaIndex;
  132. delete model.drawings;
  133. delete model.drawingRels;
  134. delete model.vmlDrawings;
  135. }
  136. async _processWorksheetEntry(stream, model, sheetNo, options, path) {
  137. const xform = new WorksheetXform(options);
  138. const worksheet = await xform.parseStream(stream);
  139. worksheet.sheetNo = sheetNo;
  140. model.worksheetHash[path] = worksheet;
  141. model.worksheets.push(worksheet);
  142. }
  143. async _processCommentEntry(stream, model, name) {
  144. const xform = new CommentsXform();
  145. const comments = await xform.parseStream(stream);
  146. model.comments[`../${name}.xml`] = comments;
  147. }
  148. async _processTableEntry(stream, model, name) {
  149. const xform = new TableXform();
  150. const table = await xform.parseStream(stream);
  151. model.tables[`../tables/${name}.xml`] = table;
  152. }
  153. async _processWorksheetRelsEntry(stream, model, sheetNo) {
  154. const xform = new RelationshipsXform();
  155. const relationships = await xform.parseStream(stream);
  156. model.worksheetRels[sheetNo] = relationships;
  157. }
  158. async _processMediaEntry(entry, model, filename) {
  159. const lastDot = filename.lastIndexOf('.');
  160. // if we can't determine extension, ignore it
  161. if (lastDot >= 1) {
  162. const extension = filename.substr(lastDot + 1);
  163. const name = filename.substr(0, lastDot);
  164. await new Promise((resolve, reject) => {
  165. const streamBuf = new StreamBuf();
  166. streamBuf.on('finish', () => {
  167. model.mediaIndex[filename] = model.media.length;
  168. model.mediaIndex[name] = model.media.length;
  169. const medium = {
  170. type: 'image',
  171. name,
  172. extension,
  173. buffer: streamBuf.toBuffer()
  174. };
  175. model.media.push(medium);
  176. resolve();
  177. });
  178. entry.on('error', error => {
  179. reject(error);
  180. });
  181. entry.pipe(streamBuf);
  182. });
  183. }
  184. }
  185. async _processDrawingEntry(entry, model, name) {
  186. const xform = new DrawingXform();
  187. const drawing = await xform.parseStream(entry);
  188. model.drawings[name] = drawing;
  189. }
  190. async _processDrawingRelsEntry(entry, model, name) {
  191. const xform = new RelationshipsXform();
  192. const relationships = await xform.parseStream(entry);
  193. model.drawingRels[name] = relationships;
  194. }
  195. async _processVmlDrawingEntry(entry, model, name) {
  196. const xform = new VmlNotesXform();
  197. const vmlDrawing = await xform.parseStream(entry);
  198. model.vmlDrawings[`../drawings/${name}.vml`] = vmlDrawing;
  199. }
  200. async _processThemeEntry(entry, model, name) {
  201. await new Promise((resolve, reject) => {
  202. // TODO: stream entry into buffer and store the xml in the model.themes[]
  203. const stream = new StreamBuf();
  204. entry.on('error', reject);
  205. stream.on('error', reject);
  206. stream.on('finish', () => {
  207. model.themes[name] = stream.read().toString();
  208. resolve();
  209. });
  210. entry.pipe(stream);
  211. });
  212. }
  213. /**
  214. * @deprecated since version 4.0. You should use `#read` instead. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md
  215. */
  216. createInputStream() {
  217. throw new Error('`XLSX#createInputStream` is deprecated. You should use `XLSX#read` instead. This method will be removed in version 5.0. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md');
  218. }
  219. async read(stream, options) {
  220. // TODO: Remove once node v8 is deprecated
  221. // Detect and upgrade old streams
  222. if (!stream[Symbol.asyncIterator] && stream.pipe) {
  223. stream = stream.pipe(new PassThrough());
  224. }
  225. const chunks = [];
  226. for await (const chunk of stream) {
  227. chunks.push(chunk);
  228. }
  229. return this.load(Buffer.concat(chunks), options);
  230. }
  231. async load(data, options) {
  232. let buffer;
  233. if (options && options.base64) {
  234. buffer = Buffer.from(data.toString(), 'base64');
  235. } else {
  236. buffer = data;
  237. }
  238. const model = {
  239. worksheets: [],
  240. worksheetHash: {},
  241. worksheetRels: [],
  242. themes: {},
  243. media: [],
  244. mediaIndex: {},
  245. drawings: {},
  246. drawingRels: {},
  247. comments: {},
  248. tables: {},
  249. vmlDrawings: {}
  250. };
  251. const zip = await JSZip.loadAsync(buffer);
  252. for (const entry of Object.values(zip.files)) {
  253. /* eslint-disable no-await-in-loop */
  254. if (!entry.dir) {
  255. let entryName = entry.name;
  256. if (entryName[0] === '/') {
  257. entryName = entryName.substr(1);
  258. }
  259. let stream;
  260. if (entryName.match(/xl\/media\//) ||
  261. // themes are not parsed as stream
  262. entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/)) {
  263. stream = new PassThrough();
  264. stream.write(await entry.async('nodebuffer'));
  265. } else {
  266. // use object mode to avoid buffer-string convention
  267. stream = new PassThrough({
  268. writableObjectMode: true,
  269. readableObjectMode: true
  270. });
  271. let content;
  272. // https://www.npmjs.com/package/process
  273. if (process.browser) {
  274. // running in browser, use TextDecoder if possible
  275. content = bufferToString(await entry.async('nodebuffer'));
  276. } else {
  277. // running in node.js
  278. content = await entry.async('string');
  279. }
  280. const chunkSize = 16 * 1024;
  281. for (let i = 0; i < content.length; i += chunkSize) {
  282. stream.write(content.substring(i, i + chunkSize));
  283. }
  284. }
  285. stream.end();
  286. switch (entryName) {
  287. case '_rels/.rels':
  288. model.globalRels = await this.parseRels(stream);
  289. break;
  290. case 'xl/workbook.xml':
  291. {
  292. const workbook = await this.parseWorkbook(stream);
  293. model.sheets = workbook.sheets;
  294. model.definedNames = workbook.definedNames;
  295. model.views = workbook.views;
  296. model.properties = workbook.properties;
  297. model.calcProperties = workbook.calcProperties;
  298. break;
  299. }
  300. case 'xl/_rels/workbook.xml.rels':
  301. model.workbookRels = await this.parseRels(stream);
  302. break;
  303. case 'xl/sharedStrings.xml':
  304. model.sharedStrings = new SharedStringsXform();
  305. await model.sharedStrings.parseStream(stream);
  306. break;
  307. case 'xl/styles.xml':
  308. model.styles = new StylesXform();
  309. await model.styles.parseStream(stream);
  310. break;
  311. case 'docProps/app.xml':
  312. {
  313. const appXform = new AppXform();
  314. const appProperties = await appXform.parseStream(stream);
  315. model.company = appProperties.company;
  316. model.manager = appProperties.manager;
  317. break;
  318. }
  319. case 'docProps/core.xml':
  320. {
  321. const coreXform = new CoreXform();
  322. const coreProperties = await coreXform.parseStream(stream);
  323. Object.assign(model, coreProperties);
  324. break;
  325. }
  326. default:
  327. {
  328. let match = entryName.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
  329. if (match) {
  330. await this._processWorksheetEntry(stream, model, match[1], options, entryName);
  331. break;
  332. }
  333. match = entryName.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
  334. if (match) {
  335. await this._processWorksheetRelsEntry(stream, model, match[1]);
  336. break;
  337. }
  338. match = entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/);
  339. if (match) {
  340. await this._processThemeEntry(stream, model, match[1]);
  341. break;
  342. }
  343. match = entryName.match(/xl\/media\/([a-zA-Z0-9]+[.][a-zA-Z0-9]{3,4})$/);
  344. if (match) {
  345. await this._processMediaEntry(stream, model, match[1]);
  346. break;
  347. }
  348. match = entryName.match(/xl\/drawings\/([a-zA-Z0-9]+)[.]xml/);
  349. if (match) {
  350. await this._processDrawingEntry(stream, model, match[1]);
  351. break;
  352. }
  353. match = entryName.match(/xl\/(comments\d+)[.]xml/);
  354. if (match) {
  355. await this._processCommentEntry(stream, model, match[1]);
  356. break;
  357. }
  358. match = entryName.match(/xl\/tables\/(table\d+)[.]xml/);
  359. if (match) {
  360. await this._processTableEntry(stream, model, match[1]);
  361. break;
  362. }
  363. match = entryName.match(/xl\/drawings\/_rels\/([a-zA-Z0-9]+)[.]xml[.]rels/);
  364. if (match) {
  365. await this._processDrawingRelsEntry(stream, model, match[1]);
  366. break;
  367. }
  368. match = entryName.match(/xl\/drawings\/(vmlDrawing\d+)[.]vml/);
  369. if (match) {
  370. await this._processVmlDrawingEntry(stream, model, match[1]);
  371. break;
  372. }
  373. }
  374. }
  375. }
  376. }
  377. this.reconcile(model, options);
  378. // apply model
  379. this.workbook.model = model;
  380. return this.workbook;
  381. }
  382. // =========================================================================
  383. // Write
  384. async addMedia(zip, model) {
  385. await Promise.all(model.media.map(async medium => {
  386. if (medium.type === 'image') {
  387. const filename = `xl/media/${medium.name}.${medium.extension}`;
  388. if (medium.filename) {
  389. const data = await fsReadFileAsync(medium.filename);
  390. return zip.append(data, {
  391. name: filename
  392. });
  393. }
  394. if (medium.buffer) {
  395. return zip.append(medium.buffer, {
  396. name: filename
  397. });
  398. }
  399. if (medium.base64) {
  400. const dataimg64 = medium.base64;
  401. const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
  402. return zip.append(content, {
  403. name: filename,
  404. base64: true
  405. });
  406. }
  407. }
  408. throw new Error('Unsupported media');
  409. }));
  410. }
  411. addDrawings(zip, model) {
  412. const drawingXform = new DrawingXform();
  413. const relsXform = new RelationshipsXform();
  414. model.worksheets.forEach(worksheet => {
  415. const {
  416. drawing
  417. } = worksheet;
  418. if (drawing) {
  419. drawingXform.prepare(drawing, {});
  420. let xml = drawingXform.toXml(drawing);
  421. zip.append(xml, {
  422. name: `xl/drawings/${drawing.name}.xml`
  423. });
  424. xml = relsXform.toXml(drawing.rels);
  425. zip.append(xml, {
  426. name: `xl/drawings/_rels/${drawing.name}.xml.rels`
  427. });
  428. }
  429. });
  430. }
  431. addTables(zip, model) {
  432. const tableXform = new TableXform();
  433. model.worksheets.forEach(worksheet => {
  434. const {
  435. tables
  436. } = worksheet;
  437. tables.forEach(table => {
  438. tableXform.prepare(table, {});
  439. const tableXml = tableXform.toXml(table);
  440. zip.append(tableXml, {
  441. name: `xl/tables/${table.target}`
  442. });
  443. });
  444. });
  445. }
  446. async addContentTypes(zip, model) {
  447. const xform = new ContentTypesXform();
  448. const xml = xform.toXml(model);
  449. zip.append(xml, {
  450. name: '[Content_Types].xml'
  451. });
  452. }
  453. async addApp(zip, model) {
  454. const xform = new AppXform();
  455. const xml = xform.toXml(model);
  456. zip.append(xml, {
  457. name: 'docProps/app.xml'
  458. });
  459. }
  460. async addCore(zip, model) {
  461. const coreXform = new CoreXform();
  462. zip.append(coreXform.toXml(model), {
  463. name: 'docProps/core.xml'
  464. });
  465. }
  466. async addThemes(zip, model) {
  467. const themes = model.themes || {
  468. theme1: theme1Xml
  469. };
  470. Object.keys(themes).forEach(name => {
  471. const xml = themes[name];
  472. const path = `xl/theme/${name}.xml`;
  473. zip.append(xml, {
  474. name: path
  475. });
  476. });
  477. }
  478. async addOfficeRels(zip) {
  479. const xform = new RelationshipsXform();
  480. const xml = xform.toXml([{
  481. Id: 'rId1',
  482. Type: XLSX.RelType.OfficeDocument,
  483. Target: 'xl/workbook.xml'
  484. }, {
  485. Id: 'rId2',
  486. Type: XLSX.RelType.CoreProperties,
  487. Target: 'docProps/core.xml'
  488. }, {
  489. Id: 'rId3',
  490. Type: XLSX.RelType.ExtenderProperties,
  491. Target: 'docProps/app.xml'
  492. }]);
  493. zip.append(xml, {
  494. name: '_rels/.rels'
  495. });
  496. }
  497. async addWorkbookRels(zip, model) {
  498. let count = 1;
  499. const relationships = [{
  500. Id: `rId${count++}`,
  501. Type: XLSX.RelType.Styles,
  502. Target: 'styles.xml'
  503. }, {
  504. Id: `rId${count++}`,
  505. Type: XLSX.RelType.Theme,
  506. Target: 'theme/theme1.xml'
  507. }];
  508. if (model.sharedStrings.count) {
  509. relationships.push({
  510. Id: `rId${count++}`,
  511. Type: XLSX.RelType.SharedStrings,
  512. Target: 'sharedStrings.xml'
  513. });
  514. }
  515. model.worksheets.forEach(worksheet => {
  516. worksheet.rId = `rId${count++}`;
  517. relationships.push({
  518. Id: worksheet.rId,
  519. Type: XLSX.RelType.Worksheet,
  520. Target: `worksheets/sheet${worksheet.id}.xml`
  521. });
  522. });
  523. const xform = new RelationshipsXform();
  524. const xml = xform.toXml(relationships);
  525. zip.append(xml, {
  526. name: 'xl/_rels/workbook.xml.rels'
  527. });
  528. }
  529. async addSharedStrings(zip, model) {
  530. if (model.sharedStrings && model.sharedStrings.count) {
  531. zip.append(model.sharedStrings.xml, {
  532. name: 'xl/sharedStrings.xml'
  533. });
  534. }
  535. }
  536. async addStyles(zip, model) {
  537. const {
  538. xml
  539. } = model.styles;
  540. if (xml) {
  541. zip.append(xml, {
  542. name: 'xl/styles.xml'
  543. });
  544. }
  545. }
  546. async addWorkbook(zip, model) {
  547. const xform = new WorkbookXform();
  548. zip.append(xform.toXml(model), {
  549. name: 'xl/workbook.xml'
  550. });
  551. }
  552. async addWorksheets(zip, model) {
  553. // preparation phase
  554. const worksheetXform = new WorksheetXform();
  555. const relationshipsXform = new RelationshipsXform();
  556. const commentsXform = new CommentsXform();
  557. const vmlNotesXform = new VmlNotesXform();
  558. // write sheets
  559. model.worksheets.forEach(worksheet => {
  560. let xmlStream = new XmlStream();
  561. worksheetXform.render(xmlStream, worksheet);
  562. zip.append(xmlStream.xml, {
  563. name: `xl/worksheets/sheet${worksheet.id}.xml`
  564. });
  565. if (worksheet.rels && worksheet.rels.length) {
  566. xmlStream = new XmlStream();
  567. relationshipsXform.render(xmlStream, worksheet.rels);
  568. zip.append(xmlStream.xml, {
  569. name: `xl/worksheets/_rels/sheet${worksheet.id}.xml.rels`
  570. });
  571. }
  572. if (worksheet.comments.length > 0) {
  573. xmlStream = new XmlStream();
  574. commentsXform.render(xmlStream, worksheet);
  575. zip.append(xmlStream.xml, {
  576. name: `xl/comments${worksheet.id}.xml`
  577. });
  578. xmlStream = new XmlStream();
  579. vmlNotesXform.render(xmlStream, worksheet);
  580. zip.append(xmlStream.xml, {
  581. name: `xl/drawings/vmlDrawing${worksheet.id}.vml`
  582. });
  583. }
  584. });
  585. }
  586. _finalize(zip) {
  587. return new Promise((resolve, reject) => {
  588. zip.on('finish', () => {
  589. resolve(this);
  590. });
  591. zip.on('error', reject);
  592. zip.finalize();
  593. });
  594. }
  595. prepareModel(model, options) {
  596. // ensure following properties have sane values
  597. model.creator = model.creator || 'ExcelJS';
  598. model.lastModifiedBy = model.lastModifiedBy || 'ExcelJS';
  599. model.created = model.created || new Date();
  600. model.modified = model.modified || new Date();
  601. model.useSharedStrings = options.useSharedStrings !== undefined ? options.useSharedStrings : true;
  602. model.useStyles = options.useStyles !== undefined ? options.useStyles : true;
  603. // Manage the shared strings
  604. model.sharedStrings = new SharedStringsXform();
  605. // add a style manager to handle cell formats, fonts, etc.
  606. model.styles = model.useStyles ? new StylesXform(true) : new StylesXform.Mock();
  607. // prepare all of the things before the render
  608. const workbookXform = new WorkbookXform();
  609. const worksheetXform = new WorksheetXform();
  610. workbookXform.prepare(model);
  611. const worksheetOptions = {
  612. sharedStrings: model.sharedStrings,
  613. styles: model.styles,
  614. date1904: model.properties.date1904,
  615. drawingsCount: 0,
  616. media: model.media
  617. };
  618. worksheetOptions.drawings = model.drawings = [];
  619. worksheetOptions.commentRefs = model.commentRefs = [];
  620. let tableCount = 0;
  621. model.tables = [];
  622. model.worksheets.forEach(worksheet => {
  623. // assign unique filenames to tables
  624. worksheet.tables.forEach(table => {
  625. tableCount++;
  626. table.target = `table${tableCount}.xml`;
  627. table.id = tableCount;
  628. model.tables.push(table);
  629. });
  630. worksheetXform.prepare(worksheet, worksheetOptions);
  631. });
  632. // TODO: workbook drawing list
  633. }
  634. async write(stream, options) {
  635. options = options || {};
  636. const {
  637. model
  638. } = this.workbook;
  639. const zip = new ZipStream.ZipWriter(options.zip);
  640. zip.pipe(stream);
  641. this.prepareModel(model, options);
  642. // render
  643. await this.addContentTypes(zip, model);
  644. await this.addOfficeRels(zip, model);
  645. await this.addWorkbookRels(zip, model);
  646. await this.addWorksheets(zip, model);
  647. await this.addSharedStrings(zip, model); // always after worksheets
  648. await this.addDrawings(zip, model);
  649. await this.addTables(zip, model);
  650. await Promise.all([this.addThemes(zip, model), this.addStyles(zip, model)]);
  651. await this.addMedia(zip, model);
  652. await Promise.all([this.addApp(zip, model), this.addCore(zip, model)]);
  653. await this.addWorkbook(zip, model);
  654. return this._finalize(zip);
  655. }
  656. writeFile(filename, options) {
  657. const stream = fs.createWriteStream(filename);
  658. return new Promise((resolve, reject) => {
  659. stream.on('finish', () => {
  660. resolve();
  661. });
  662. stream.on('error', error => {
  663. reject(error);
  664. });
  665. this.write(stream, options).then(() => {
  666. stream.end();
  667. }).catch(err => {
  668. reject(err);
  669. });
  670. });
  671. }
  672. async writeBuffer(options) {
  673. const stream = new StreamBuf();
  674. await this.write(stream, options);
  675. return stream.read();
  676. }
  677. }
  678. XLSX.RelType = require('./rel-type');
  679. module.exports = XLSX;
  680. //# sourceMappingURL=xlsx.js.map