workbook-writer.js 9.8 KB

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