workbook-writer.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. const fs = require('fs');
  2. const Archiver = require('archiver');
  3. const StreamBuf = require('../../utils/stream-buf');
  4. const RelType = require('../../xlsx/rel-type');
  5. const StylesXform = require('../../xlsx/xform/style/styles-xform');
  6. const SharedStrings = require('../../utils/shared-strings');
  7. const DefinedNames = require('../../doc/defined-names');
  8. const CoreXform = require('../../xlsx/xform/core/core-xform');
  9. const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
  10. const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
  11. const AppXform = require('../../xlsx/xform/core/app-xform');
  12. const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
  13. const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
  14. const WorksheetWriter = require('./worksheet-writer');
  15. const theme1Xml = require('../../xlsx/xml/theme1.js');
  16. class WorkbookWriter {
  17. constructor(options) {
  18. options = options || {};
  19. this.created = options.created || new Date();
  20. this.modified = options.modified || this.created;
  21. this.creator = options.creator || 'ExcelJS';
  22. this.lastModifiedBy = options.lastModifiedBy || 'ExcelJS';
  23. this.lastPrinted = options.lastPrinted;
  24. // using shared strings creates a smaller xlsx file but may use more memory
  25. this.useSharedStrings = options.useSharedStrings || false;
  26. this.sharedStrings = new SharedStrings();
  27. // style manager
  28. this.styles = options.useStyles ? new StylesXform(true) : new StylesXform.Mock(true);
  29. // defined names
  30. this._definedNames = new DefinedNames();
  31. this._worksheets = [];
  32. this.views = [];
  33. this.zipOptions = options.zip;
  34. this.media = [];
  35. this.commentRefs = [];
  36. this.zip = Archiver('zip', this.zipOptions);
  37. if (options.stream) {
  38. this.stream = options.stream;
  39. } else if (options.filename) {
  40. this.stream = fs.createWriteStream(options.filename);
  41. } else {
  42. this.stream = new StreamBuf();
  43. }
  44. this.zip.pipe(this.stream);
  45. // these bits can be added right now
  46. this.promise = Promise.all([this.addThemes(), this.addOfficeRels()]);
  47. }
  48. get definedNames() {
  49. return this._definedNames;
  50. }
  51. _openStream(path) {
  52. const stream = new StreamBuf({bufSize: 65536, batch: true});
  53. this.zip.append(stream, {name: path});
  54. stream.on('finish', () => {
  55. stream.emit('zipped');
  56. });
  57. return stream;
  58. }
  59. _commitWorksheets() {
  60. const commitWorksheet = function(worksheet) {
  61. if (!worksheet.committed) {
  62. return new Promise(resolve => {
  63. worksheet.stream.on('zipped', () => {
  64. resolve();
  65. });
  66. worksheet.commit();
  67. });
  68. }
  69. return Promise.resolve();
  70. };
  71. // if there are any uncommitted worksheets, commit them now and wait
  72. const promises = this._worksheets.map(commitWorksheet);
  73. if (promises.length) {
  74. return Promise.all(promises);
  75. }
  76. return Promise.resolve();
  77. }
  78. async commit() {
  79. // commit all worksheets, then add suplimentary files
  80. await this.promise;
  81. await this.addMedia();
  82. await this._commitWorksheets();
  83. await Promise.all([
  84. this.addContentTypes(),
  85. this.addApp(),
  86. this.addCore(),
  87. this.addSharedStrings(),
  88. this.addStyles(),
  89. this.addWorkbookRels(),
  90. ]);
  91. await this.addWorkbook();
  92. return this._finalize();
  93. }
  94. get nextId() {
  95. // find the next unique spot to add worksheet
  96. let i;
  97. for (i = 1; i < this._worksheets.length; i++) {
  98. if (!this._worksheets[i]) {
  99. return i;
  100. }
  101. }
  102. return this._worksheets.length || 1;
  103. }
  104. addImage(image) {
  105. const id = this.media.length;
  106. const medium = Object.assign({}, image, {type: 'image', name: `image${id}.${image.extension}`});
  107. this.media.push(medium);
  108. return id;
  109. }
  110. getImage(id) {
  111. return this.media[id];
  112. }
  113. addWorksheet(name, options) {
  114. // it's possible to add a worksheet with different than default
  115. // shared string handling
  116. // in fact, it's even possible to switch it mid-sheet
  117. options = options || {};
  118. const useSharedStrings =
  119. options.useSharedStrings !== undefined ? options.useSharedStrings : this.useSharedStrings;
  120. if (options.tabColor) {
  121. // eslint-disable-next-line no-console
  122. console.trace('tabColor option has moved to { properties: tabColor: {...} }');
  123. options.properties = Object.assign(
  124. {
  125. tabColor: options.tabColor,
  126. },
  127. options.properties
  128. );
  129. }
  130. const id = this.nextId;
  131. name = name || `sheet${id}`;
  132. const worksheet = new WorksheetWriter({
  133. id,
  134. name,
  135. workbook: this,
  136. useSharedStrings,
  137. properties: options.properties,
  138. state: options.state,
  139. pageSetup: options.pageSetup,
  140. views: options.views,
  141. autoFilter: options.autoFilter,
  142. headerFooter: options.headerFooter,
  143. });
  144. this._worksheets[id] = worksheet;
  145. return worksheet;
  146. }
  147. getWorksheet(id) {
  148. if (id === undefined) {
  149. return this._worksheets.find(() => true);
  150. }
  151. if (typeof id === 'number') {
  152. return this._worksheets[id];
  153. }
  154. if (typeof id === 'string') {
  155. return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
  156. }
  157. return undefined;
  158. }
  159. addStyles() {
  160. return new Promise(resolve => {
  161. this.zip.append(this.styles.xml, {name: 'xl/styles.xml'});
  162. resolve();
  163. });
  164. }
  165. addThemes() {
  166. return new Promise(resolve => {
  167. this.zip.append(theme1Xml, {name: 'xl/theme/theme1.xml'});
  168. resolve();
  169. });
  170. }
  171. addOfficeRels() {
  172. return new Promise(resolve => {
  173. const xform = new RelationshipsXform();
  174. const xml = xform.toXml([
  175. {Id: 'rId1', Type: RelType.OfficeDocument, Target: 'xl/workbook.xml'},
  176. {Id: 'rId2', Type: RelType.CoreProperties, Target: 'docProps/core.xml'},
  177. {Id: 'rId3', Type: RelType.ExtenderProperties, Target: 'docProps/app.xml'},
  178. ]);
  179. this.zip.append(xml, {name: '/_rels/.rels'});
  180. resolve();
  181. });
  182. }
  183. addContentTypes() {
  184. return new Promise(resolve => {
  185. const model = {
  186. worksheets: this._worksheets.filter(Boolean),
  187. sharedStrings: this.sharedStrings,
  188. commentRefs: this.commentRefs,
  189. media: this.media,
  190. };
  191. const xform = new ContentTypesXform();
  192. const xml = xform.toXml(model);
  193. this.zip.append(xml, {name: '[Content_Types].xml'});
  194. resolve();
  195. });
  196. }
  197. addMedia() {
  198. return Promise.all(
  199. this.media.map(medium => {
  200. if (medium.type === 'image') {
  201. const filename = `xl/media/${medium.name}`;
  202. if (medium.filename) {
  203. return this.zip.file(medium.filename, {name: filename});
  204. }
  205. if (medium.buffer) {
  206. return this.zip.append(medium.buffer, {name: filename});
  207. }
  208. if (medium.base64) {
  209. const dataimg64 = medium.base64;
  210. const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
  211. return this.zip.append(content, {name: filename, base64: true});
  212. }
  213. }
  214. throw new Error('Unsupported media');
  215. })
  216. );
  217. }
  218. addApp() {
  219. return new Promise(resolve => {
  220. const model = {
  221. worksheets: this._worksheets.filter(Boolean),
  222. };
  223. const xform = new AppXform();
  224. const xml = xform.toXml(model);
  225. this.zip.append(xml, {name: 'docProps/app.xml'});
  226. resolve();
  227. });
  228. }
  229. addCore() {
  230. return new Promise(resolve => {
  231. const coreXform = new CoreXform();
  232. const xml = coreXform.toXml(this);
  233. this.zip.append(xml, {name: 'docProps/core.xml'});
  234. resolve();
  235. });
  236. }
  237. addSharedStrings() {
  238. if (this.sharedStrings.count) {
  239. return new Promise(resolve => {
  240. const sharedStringsXform = new SharedStringsXform();
  241. const xml = sharedStringsXform.toXml(this.sharedStrings);
  242. this.zip.append(xml, {name: '/xl/sharedStrings.xml'});
  243. resolve();
  244. });
  245. }
  246. return Promise.resolve();
  247. }
  248. addWorkbookRels() {
  249. let count = 1;
  250. const relationships = [
  251. {Id: `rId${count++}`, Type: RelType.Styles, Target: 'styles.xml'},
  252. {Id: `rId${count++}`, Type: RelType.Theme, Target: 'theme/theme1.xml'},
  253. ];
  254. if (this.sharedStrings.count) {
  255. relationships.push({
  256. Id: `rId${count++}`,
  257. Type: RelType.SharedStrings,
  258. Target: 'sharedStrings.xml',
  259. });
  260. }
  261. this._worksheets.forEach(worksheet => {
  262. if (worksheet) {
  263. worksheet.rId = `rId${count++}`;
  264. relationships.push({
  265. Id: worksheet.rId,
  266. Type: RelType.Worksheet,
  267. Target: `worksheets/sheet${worksheet.id}.xml`,
  268. });
  269. }
  270. });
  271. return new Promise(resolve => {
  272. const xform = new RelationshipsXform();
  273. const xml = xform.toXml(relationships);
  274. this.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
  275. resolve();
  276. });
  277. }
  278. addWorkbook() {
  279. const {zip} = this;
  280. const model = {
  281. worksheets: this._worksheets.filter(Boolean),
  282. definedNames: this._definedNames.model,
  283. views: this.views,
  284. properties: {},
  285. calcProperties: {},
  286. };
  287. return new Promise(resolve => {
  288. const xform = new WorkbookXform();
  289. xform.prepare(model);
  290. zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
  291. resolve();
  292. });
  293. }
  294. _finalize() {
  295. return new Promise((resolve, reject) => {
  296. this.stream.on('error', reject);
  297. this.stream.on('finish', () => {
  298. resolve(this);
  299. });
  300. this.zip.on('error', reject);
  301. this.zip.finalize();
  302. });
  303. }
  304. }
  305. module.exports = WorkbookWriter;