1const fs = require('fs') 2const os = require('os'); 3const path = require('path') 4const url = require('url') 5const electron = require('electron') 6const {Menu: menu, shell} = require('electron') 7const ipcMain = electron.ipcMain 8const dialog = electron.dialog 9const app = electron.app 10const BrowserWindow = electron.BrowserWindow 11const crc = require('crc'); 12const zlib = require('zlib'); 13const log = require('electron-log') 14const program = require('commander') 15const {autoUpdater} = require("electron-updater") 16const PDFDocument = require('pdf-lib').PDFDocument; 17const Store = require('electron-store'); 18const store = new Store(); 19const ProgressBar = require('electron-progressbar'); 20const remoteMain = require("@electron/remote/main") 21remoteMain.initialize() 22const disableUpdate = require('./disableUpdate').disableUpdate() || 23 process.env.DRAWIO_DISABLE_UPDATE === 'true' || 24 fs.existsSync('/.flatpak-info'); //This file indicates running in flatpak sandbox 25autoUpdater.logger = log 26autoUpdater.logger.transports.file.level = 'info' 27autoUpdater.autoDownload = false 28 29const __DEV__ = process.env.DRAWIO_ENV === 'dev' 30 31let windowsRegistry = [] 32let cmdQPressed = false 33let firstWinLoaded = false 34let firstWinFilePath = null 35 36//Read config file 37var queryObj = { 38 'dev': __DEV__ ? 1 : 0, 39 'test': __DEV__ ? 1 : 0, 40 'gapi': 0, 41 'db': 0, 42 'od': 0, 43 'gh': 0, 44 'gl': 0, 45 'tr': 0, 46 'browser': 0, 47 'picker': 0, 48 'mode': 'device', 49 'export': 'https://convert.diagrams.net/node/export', 50 'disableUpdate': disableUpdate? 1 : 0 51}; 52 53try 54{ 55 if (fs.existsSync(process.cwd() + '/urlParams.json')) 56 { 57 let urlParams = JSON.parse(fs.readFileSync(process.cwd() + '/urlParams.json')); 58 59 for (var param in urlParams) 60 { 61 queryObj[param] = urlParams[param]; 62 } 63 } 64} 65catch(e) 66{ 67 console.log('Error in urlParams.json file: ' + e.message); 68} 69 70function createWindow (opt = {}) 71{ 72 let options = Object.assign( 73 { 74 frame: false, 75 backgroundColor: '#FFF', 76 width: 1600, 77 height: 1200, 78 webViewTag: false, 79 'web-security': true, 80 webPreferences: { 81 // preload: path.resolve('./preload.js'), 82 nodeIntegration: true, 83 nodeIntegrationInWorker: true, 84 spellcheck: (os.platform() == "darwin" ? true : false), 85 contextIsolation: false, 86 nativeWindowOpen: true 87 } 88 }, opt) 89 90 let mainWindow = new BrowserWindow(options) 91 remoteMain.enable(mainWindow.webContents) 92 windowsRegistry.push(mainWindow) 93 94 if (__DEV__) 95 { 96 console.log('createWindow', opt) 97 } 98 99 let ourl = url.format( 100 { 101 pathname: `${__dirname}/index.html`, 102 protocol: 'file:', 103 query: queryObj, 104 slashes: true 105 }) 106 107 mainWindow.loadURL(ourl) 108 109 // Open the DevTools. 110 if (__DEV__) 111 { 112 mainWindow.webContents.openDevTools() 113 } 114 115 mainWindow.on('close', (event) => 116 { 117 const win = event.sender 118 const index = windowsRegistry.indexOf(win) 119 120 if (__DEV__) 121 { 122 console.log('Window on close', index) 123 } 124 125 const contents = win.webContents 126 127 if (contents != null) 128 { 129 contents.executeJavaScript('if(typeof global.__emt_isModified === \'function\'){global.__emt_isModified()}', true) 130 .then((isModified) => 131 { 132 if (__DEV__) 133 { 134 console.log('__emt_isModified', isModified) 135 } 136 137 if (isModified) 138 { 139 var choice = dialog.showMessageBoxSync( 140 win, 141 { 142 type: 'question', 143 buttons: ['Cancel', 'Discard Changes'], 144 title: 'Confirm', 145 message: 'The document has unsaved changes. Do you really want to quit without saving?' //mxResources.get('allChangesLost') 146 }) 147 148 if (choice === 1) 149 { 150 win.destroy() 151 } 152 else 153 { 154 cmdQPressed = false 155 } 156 } 157 else 158 { 159 win.destroy() 160 } 161 }) 162 163 event.preventDefault() 164 } 165 }) 166 167 // Emitted when the window is closed. 168 mainWindow.on('closed', (event/*:WindowEvent*/) => 169 { 170 const index = windowsRegistry.indexOf(event.sender) 171 172 if (__DEV__) 173 { 174 console.log('Window closed idx:%d', index) 175 } 176 177 windowsRegistry.splice(index, 1) 178 }) 179 180 return mainWindow 181} 182 183// This method will be called when Electron has finished 184// initialization and is ready to create browser windows. 185// Some APIs can only be used after this event occurs. 186app.on('ready', e => 187{ 188 //asynchronous 189 ipcMain.on('asynchronous-message', (event, arg) => 190 { 191 console.log(arg) // prints "ping" 192 event.sender.send('asynchronous-reply', 'pong') 193 }) 194 //synchronous 195 ipcMain.on('winman', (event, arg) => 196 { 197 if (__DEV__) 198 { 199 console.log('ipcMain.on winman', arg) 200 } 201 202 if (arg.action === 'newfile') 203 { 204 event.returnValue = createWindow(arg.opt).id 205 206 return 207 } 208 209 event.returnValue = 'pong' 210 }) 211 212 let argv = process.argv 213 214 // https://github.com/electron/electron/issues/4690#issuecomment-217435222 215 if (process.defaultApp != true) 216 { 217 argv.unshift(null) 218 } 219 220 var validFormatRegExp = /^(pdf|svg|png|jpeg|jpg|vsdx|xml)$/; 221 222 function argsRange(val) 223 { 224 return val.split('..').map(Number); 225 } 226 227 try 228 { 229 program 230 .version(app.getVersion()) 231 .usage('[options] [input file/folder]') 232 .allowUnknownOption() //-h and --help are considered unknown!! 233 .option('-c, --create', 'creates a new empty file if no file is passed') 234 .option('-k, --check', 'does not overwrite existing files') 235 .option('-x, --export', 'export the input file/folder based on the given options') 236 .option('-r, --recursive', 'for a folder input, recursively convert all files in sub-folders also') 237 .option('-o, --output <output file/folder>', 'specify the output file/folder. If omitted, the input file name is used for output with the specified format as extension') 238 .option('-f, --format <format>', 239 'if output file name extension is specified, this option is ignored (file type is determined from output extension, possible export formats are pdf, png, jpg, svg, vsdx, and xml)', 240 validFormatRegExp, 'pdf') 241 .option('-q, --quality <quality>', 242 'output image quality for JPEG (default: 90)', parseInt) 243 .option('-t, --transparent', 244 'set transparent background for PNG') 245 .option('-e, --embed-diagram', 246 'includes a copy of the diagram (for PNG and PDF formats only)') 247 .option('-b, --border <border>', 248 'sets the border width around the diagram (default: 0)', parseInt) 249 .option('-s, --scale <scale>', 250 'scales the diagram size', parseFloat) 251 .option('--width <width>', 252 'fits the generated image/pdf into the specified width, preserves aspect ratio.', parseInt) 253 .option('--height <height>', 254 'fits the generated image/pdf into the specified height, preserves aspect ratio.', parseInt) 255 .option('--crop', 256 'crops PDF to diagram size') 257 .option('-a, --all-pages', 258 'export all pages (for PDF format only)') 259 .option('-p, --page-index <pageIndex>', 260 'selects a specific page, if not specified and the format is an image, the first page is selected', parseInt) 261 .option('-g, --page-range <from>..<to>', 262 'selects a page range (for PDF format only)', argsRange) 263 .option('-u, --uncompressed', 264 'Uncompressed XML output (for XML format only)') 265 .parse(argv) 266 } 267 catch(e) 268 { 269 //On parse error, return [exit and commander will show the error message] 270 return; 271 } 272 273 var options = program.opts(); 274 275 //Start export mode? 276 if (options.export) 277 { 278 var dummyWin = new BrowserWindow({ 279 show : false, 280 webPreferences: { 281 nodeIntegration: true, 282 contextIsolation: false, 283 nativeWindowOpen: true 284 } 285 }); 286 287 windowsRegistry.push(dummyWin); 288 289 try 290 { 291 //Prepare arguments and confirm it's valid 292 var format = null; 293 var outType = null; 294 295 //Format & Output 296 if (options.output) 297 { 298 try 299 { 300 var outStat = fs.statSync(options.output); 301 302 if (outStat.isDirectory()) 303 { 304 outType = {isDir: true}; 305 } 306 else //If we can get file stat, then it exists 307 { 308 throw 'Error: Output file already exists'; 309 } 310 } 311 catch(e) //on error, file doesn't exist and it is not a dir 312 { 313 outType = {isFile: true}; 314 315 format = path.extname(options.output).substr(1); 316 317 if (!validFormatRegExp.test(format)) 318 { 319 format = null; 320 } 321 } 322 } 323 324 if (format == null) 325 { 326 format = options.format; 327 } 328 329 var from = null, to = null; 330 331 if (options.pageIndex != null && options.pageIndex >= 0) 332 { 333 from = options.pageIndex; 334 } 335 else if (options.pageRange && options.pageRange.length == 2) 336 { 337 from = options.pageRange[0] >= 0 ? options.pageRange[0] : null; 338 to = options.pageRange[1] >= 0 ? options.pageRange[1] : null; 339 } 340 341 var expArgs = { 342 format: format, 343 w: options.width > 0 ? options.width : null, 344 h: options.height > 0 ? options.height : null, 345 border: options.border > 0 ? options.border : 0, 346 bg: options.transparent ? 'none' : '#ffffff', 347 from: from, 348 to: to, 349 allPages: format == 'pdf' && options.allPages, 350 scale: (options.crop && (options.scale == null || options.scale == 1)) ? 1.00001: (options.scale || 1), //any value other than 1 crops the pdf 351 embedXml: options.embedDiagram? '1' : '0', 352 jpegQuality: options.quality, 353 uncompressed: options.uncompressed 354 }; 355 356 var paths = program.args; 357 358 // If a file is passed 359 if (paths !== undefined && paths[0] != null) 360 { 361 var inStat = null; 362 363 try 364 { 365 inStat = fs.statSync(paths[0]); 366 } 367 catch(e) 368 { 369 throw 'Error: input file/directory not found'; 370 } 371 372 var files = []; 373 374 function addDirectoryFiles(dir, isRecursive) 375 { 376 fs.readdirSync(dir).forEach(function(file) 377 { 378 var filePath = path.join(dir, file); 379 stat = fs.statSync(filePath); 380 381 if (stat.isFile() && path.basename(filePath).charAt(0) != '.') 382 { 383 files.push(filePath); 384 } 385 if (stat.isDirectory() && isRecursive) 386 { 387 addDirectoryFiles(filePath, isRecursive) 388 } 389 }); 390 } 391 392 if (inStat.isFile()) 393 { 394 files.push(paths[0]); 395 } 396 else if (inStat.isDirectory()) 397 { 398 addDirectoryFiles(paths[0], options.recursive); 399 } 400 401 if (files.length > 0) 402 { 403 var fileIndex = 0; 404 405 function processOneFile() 406 { 407 var curFile = files[fileIndex]; 408 409 try 410 { 411 var ext = path.extname(curFile); 412 413 expArgs.xml = fs.readFileSync(curFile, ext === '.png' || ext === '.vsdx' ? null : 'utf-8'); 414 415 if (ext === '.png') 416 { 417 expArgs.xml = Buffer.from(expArgs.xml).toString('base64'); 418 startExport(); 419 } 420 else if (ext === '.vsdx') 421 { 422 dummyWin.loadURL(`file://${__dirname}/vsdxImporter.html`); 423 424 const contents = dummyWin.webContents; 425 426 contents.on('did-finish-load', function() 427 { 428 contents.send('import', expArgs.xml); 429 430 ipcMain.once('import-success', function(evt, xml) 431 { 432 expArgs.xml = xml; 433 startExport(); 434 }); 435 436 ipcMain.once('import-error', function() 437 { 438 console.error('Error: cannot import VSDX file: ' + curFile); 439 next(); 440 }); 441 }); 442 } 443 else 444 { 445 startExport(); 446 } 447 448 function next() 449 { 450 fileIndex++; 451 452 if (fileIndex < files.length) 453 { 454 processOneFile(); 455 } 456 else 457 { 458 cmdQPressed = true; 459 dummyWin.destroy(); 460 } 461 }; 462 463 function startExport() 464 { 465 var mockEvent = { 466 reply: function(msg, data) 467 { 468 try 469 { 470 if (data == null || data.length == 0) 471 { 472 console.error('Error: Export failed: ' + curFile); 473 } 474 else if (msg == 'export-success') 475 { 476 var outFileName = null; 477 478 if (outType != null) 479 { 480 if (outType.isDir) 481 { 482 outFileName = path.join(options.output, path.basename(curFile)) + '.' + format; 483 } 484 else 485 { 486 outFileName = options.output; 487 } 488 } 489 else if (inStat.isFile()) 490 { 491 outFileName = path.join(path.dirname(paths[0]), path.basename(paths[0], 492 path.extname(paths[0]))) + '.' + format; 493 494 } 495 else //dir 496 { 497 outFileName = path.join(path.dirname(curFile), path.basename(curFile, 498 path.extname(curFile))) + '.' + format; 499 } 500 501 try 502 { 503 var counter = 0; 504 var realFileName = outFileName; 505 506 if (program.rawArgs.indexOf('-k') > -1 || program.rawArgs.indexOf('--check') > -1) 507 { 508 while (fs.existsSync(realFileName)) 509 { 510 counter++; 511 realFileName = path.join(path.dirname(outFileName), path.basename(outFileName, 512 path.extname(outFileName))) + '-' + counter + path.extname(outFileName); 513 } 514 } 515 516 fs.writeFileSync(realFileName, data, format == 'vsdx'? 'base64' : null, { flag: 'wx' }); 517 console.log(curFile + ' -> ' + outFileName); 518 } 519 catch(e) 520 { 521 console.error('Error writing to file: ' + outFileName); 522 } 523 } 524 else 525 { 526 console.error('Error: ' + data + ': ' + curFile); 527 } 528 529 next(); 530 } 531 finally 532 { 533 mockEvent.finalize(); 534 } 535 } 536 }; 537 538 exportDiagram(mockEvent, expArgs, true); 539 }; 540 } 541 catch(e) 542 { 543 console.error('Error reading file: ' + curFile); 544 next(); 545 } 546 } 547 548 processOneFile(); 549 } 550 else 551 { 552 throw 'Error: input file/directory not found or directory is empty'; 553 } 554 } 555 else 556 { 557 throw 'Error: An input file must be specified'; 558 } 559 } 560 catch(e) 561 { 562 console.error(e); 563 564 cmdQPressed = true; 565 dummyWin.destroy(); 566 } 567 568 return; 569 } 570 else if (program.rawArgs.indexOf('-h') > -1 || program.rawArgs.indexOf('--help') > -1 || program.rawArgs.indexOf('-V') > -1 || program.rawArgs.indexOf('--version') > -1) //To prevent execution when help/version arg is used 571 { 572 return; 573 } 574 575 //Prevent multiple instances of the application (casuses issues with configuration) 576 const gotTheLock = app.requestSingleInstanceLock() 577 578 if (!gotTheLock) 579 { 580 app.quit() 581 } 582 else 583 { 584 app.on('second-instance', (event, commandLine, workingDirectory) => { 585 //Create another window 586 let win = createWindow() 587 588 let loadEvtCount = 0; 589 590 function loadFinished() 591 { 592 loadEvtCount++; 593 594 if (loadEvtCount == 2) 595 { 596 //Open the file if new app request is from opening a file 597 var potFile = commandLine.pop(); 598 599 if (fs.existsSync(potFile)) 600 { 601 win.webContents.send('args-obj', {args: [potFile]}); 602 } 603 } 604 } 605 606 //Order of these two events is not guaranteed, so wait for them async. 607 //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly 608 ipcMain.once('app-load-finished', loadFinished); 609 610 win.webContents.on('did-finish-load', function() 611 { 612 win.webContents.zoomFactor = 1; 613 win.webContents.setVisualZoomLevelLimits(1, 1); 614 loadFinished(); 615 }); 616 }) 617 } 618 619 let win = createWindow() 620 621 let loadEvtCount = 0; 622 623 function loadFinished() 624 { 625 loadEvtCount++; 626 627 if (loadEvtCount == 2) 628 { 629 //Sending entire program is not allowed in Electron 9 as it is not native JS object 630 win.webContents.send('args-obj', {args: program.args, create: options.create}); 631 } 632 } 633 634 //Order of these two events is not guaranteed, so wait for them async. 635 //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly 636 ipcMain.once('app-load-finished', loadFinished); 637 638 win.webContents.on('did-finish-load', function() 639 { 640 if (firstWinFilePath != null) 641 { 642 if (program.args != null) 643 { 644 program.args.push(firstWinFilePath); 645 } 646 else 647 { 648 program.args = [firstWinFilePath]; 649 } 650 } 651 652 firstWinLoaded = true; 653 654 win.webContents.zoomFactor = 1; 655 win.webContents.setVisualZoomLevelLimits(1, 1); 656 loadFinished(); 657 }); 658 659 let updateNoAvailAdded = false; 660 661 function checkForUpdatesFn() 662 { 663 autoUpdater.checkForUpdates(); 664 store.set('dontCheckUpdates', false); 665 666 if (!updateNoAvailAdded) 667 { 668 updateNoAvailAdded = true; 669 autoUpdater.on('update-not-available', (info) => { 670 dialog.showMessageBox( 671 { 672 type: 'info', 673 title: 'No updates found', 674 message: 'You application is up-to-date', 675 }) 676 }) 677 } 678 }; 679 680 let checkForUpdates = { 681 label: 'Check for updates', 682 click: checkForUpdatesFn 683 } 684 685 ipcMain.on('checkForUpdates', checkForUpdatesFn); 686 687 if (process.platform === 'darwin') 688 { 689 let template = [{ 690 label: app.name, 691 submenu: [ 692 { 693 label: 'About ' + app.name, 694 click() { shell.openExternal('https://www.diagrams.net'); } 695 }, 696 { 697 label: 'Support', 698 click() { shell.openExternal('https://github.com/jgraph/drawio-desktop/issues'); } 699 }, 700 checkForUpdates, 701 { type: 'separator' }, 702 { role: 'hide' }, 703 { role: 'hideothers' }, 704 { role: 'unhide' }, 705 { type: 'separator' }, 706 { role: 'quit' } 707 ] 708 }, { 709 label: 'Edit', 710 submenu: [ 711 { role: 'undo' }, 712 { role: 'redo' }, 713 { type: 'separator' }, 714 { role: 'cut' }, 715 { role: 'copy' }, 716 { role: 'paste' }, 717 { role: 'pasteAndMatchStyle' }, 718 { role: 'selectAll' } 719 ] 720 }] 721 722 if (disableUpdate) 723 { 724 template[0].submenu.splice(2, 1); 725 } 726 727 const menuBar = menu.buildFromTemplate(template) 728 menu.setApplicationMenu(menuBar) 729 } 730 else //hide menubar in win/linux 731 { 732 menu.setApplicationMenu(null) 733 } 734 735 autoUpdater.setFeedURL({ 736 provider: 'github', 737 repo: 'drawio-desktop', 738 owner: 'jgraph' 739 }) 740 741 if (!disableUpdate && !store.get('dontCheckUpdates')) 742 { 743 autoUpdater.checkForUpdates() 744 } 745}) 746 747//Quit from the dock context menu should quit the application directly 748if (process.platform === 'darwin') 749{ 750 app.on('before-quit', function() { 751 cmdQPressed = true; 752 }); 753} 754 755// Quit when all windows are closed. 756app.on('window-all-closed', function () 757{ 758 if (__DEV__) 759 { 760 console.log('window-all-closed', windowsRegistry.length) 761 } 762 763 // On OS X it is common for applications and their menu bar 764 // to stay active until the user quits explicitly with Cmd + Q 765 if (cmdQPressed || process.platform !== 'darwin') 766 { 767 app.quit() 768 } 769}) 770 771app.on('activate', function () 772{ 773 if (__DEV__) 774 { 775 console.log('app on activate', windowsRegistry.length) 776 } 777 778 // On OS X it's common to re-create a window in the app when the 779 // dock icon is clicked and there are no other windows open. 780 if (windowsRegistry.length === 0) 781 { 782 createWindow() 783 } 784}) 785 786app.on('will-finish-launching', function() 787{ 788 app.on("open-file", function(event, path) 789 { 790 event.preventDefault(); 791 792 if (firstWinLoaded) 793 { 794 let win = createWindow(); 795 796 let loadEvtCount = 0; 797 798 function loadFinished() 799 { 800 loadEvtCount++; 801 802 if (loadEvtCount == 2) 803 { 804 win.webContents.send('args-obj', {args: [path]}); 805 } 806 } 807 808 //Order of these two events is not guaranteed, so wait for them async. 809 //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly 810 ipcMain.once('app-load-finished', loadFinished); 811 812 win.webContents.on('did-finish-load', function() 813 { 814 win.webContents.zoomFactor = 1; 815 win.webContents.setVisualZoomLevelLimits(1, 1); 816 loadFinished(); 817 }); 818 } 819 else 820 { 821 firstWinFilePath = path 822 } 823 }); 824}); 825 826autoUpdater.on('error', e => log.error('@error@\n', e)) 827 828autoUpdater.on('update-available', (a, b) => 829{ 830 log.info('@update-available@\n', a, b) 831 832 dialog.showMessageBox( 833 { 834 type: 'question', 835 buttons: ['Ok', 'Cancel', 'Don\'t Ask Again'], 836 title: 'Confirm Update', 837 message: 'Update available.\n\nWould you like to download and install new version?', 838 detail: 'Application will automatically restart to apply update after download', 839 }).then( result => 840 { 841 if (result.response === 0) 842 { 843 autoUpdater.downloadUpdate() 844 845 var progressBar = new ProgressBar({ 846 title: 'draw.io Update', 847 text: 'Downloading draw.io update...' 848 }); 849 850 function reportUpdateError(e) 851 { 852 progressBar.detail = 'Error occurred while fetching updates. ' + (e && e.message? e.message : e) 853 progressBar._window.setClosable(true); 854 } 855 856 autoUpdater.on('error', e => { 857 if (progressBar._window != null) 858 { 859 reportUpdateError(e); 860 } 861 else 862 { 863 progressBar.on('ready', function() { 864 reportUpdateError(e); 865 }); 866 } 867 }) 868 869 var firstTimeProg = true; 870 871 autoUpdater.on('download-progress', (d) => { 872 //On mac, download-progress event is not called, so the indeterminate progress will continue until download is finished 873 log.info('@update-progress@\n', d); 874 875 var percent = d.percent; 876 877 if (percent) 878 { 879 percent = Math.round(percent * 100)/100; 880 } 881 882 if (firstTimeProg) 883 { 884 firstTimeProg = false; 885 progressBar.close(); 886 887 progressBar = new ProgressBar({ 888 indeterminate: false, 889 title: 'draw.io Update', 890 text: 'Downloading draw.io update...', 891 detail: `${percent}% ...`, 892 initialValue: percent 893 }); 894 895 progressBar 896 .on('completed', function() { 897 progressBar.detail = 'Download completed.'; 898 }) 899 .on('aborted', function(value) { 900 log.info(`progress aborted... ${value}`); 901 }) 902 .on('progress', function(value) { 903 progressBar.detail = `${value}% ...`; 904 }) 905 .on('ready', function() { 906 //InitialValue doesn't set the UI! so this is needed to render it correctly 907 progressBar.value = percent; 908 }); 909 } 910 else 911 { 912 progressBar.value = percent; 913 } 914 }); 915 916 autoUpdater.on('update-downloaded', (info) => { 917 if (!progressBar.isCompleted()) 918 { 919 progressBar.close() 920 } 921 922 log.info('@update-downloaded@\n', info) 923 // Ask user to update the app 924 dialog.showMessageBox( 925 { 926 type: 'question', 927 buttons: ['Install', 'Later'], 928 defaultId: 0, 929 message: 'A new version of ' + app.name + ' has been downloaded', 930 detail: 'It will be installed the next time you restart the application', 931 }).then(result => 932 { 933 if (result.response === 0) 934 { 935 setTimeout(() => autoUpdater.quitAndInstall(), 1) 936 } 937 }) 938 }); 939 } 940 else if (result.response === 2) 941 { 942 //save in settings don't check for updates 943 log.info('@dont check for updates!@') 944 store.set('dontCheckUpdates', true) 945 } 946 }) 947}) 948 949//Pdf export 950const MICRON_TO_PIXEL = 264.58 //264.58 micron = 1 pixel 951const PNG_CHUNK_IDAT = 1229209940; 952const LARGE_IMAGE_AREA = 30000000; 953 954//NOTE: Key length must not be longer than 79 bytes (not checked) 955function writePngWithText(origBuff, key, text, compressed, base64encoded) 956{ 957 var isDpi = key == 'dpi'; 958 var inOffset = 0; 959 var outOffset = 0; 960 var data = text; 961 var dataLen = isDpi? 9 : key.length + data.length + 1; //we add 1 zeros with non-compressed data, for pHYs it's 2 of 4-byte-int + 1 byte 962 963 //prepare compressed data to get its size 964 if (compressed) 965 { 966 data = zlib.deflateRawSync(encodeURIComponent(text)); 967 dataLen = key.length + data.length + 2; //we add 2 zeros with compressed data 968 } 969 970 var outBuff = Buffer.allocUnsafe(origBuff.length + dataLen + 4); //4 is the header size "zTXt", "tEXt" or "pHYs" 971 972 try 973 { 974 var magic1 = origBuff.readUInt32BE(inOffset); 975 inOffset += 4; 976 var magic2 = origBuff.readUInt32BE(inOffset); 977 inOffset += 4; 978 979 if (magic1 != 0x89504e47 && magic2 != 0x0d0a1a0a) 980 { 981 throw new Error("PNGImageDecoder0"); 982 } 983 984 outBuff.writeUInt32BE(magic1, outOffset); 985 outOffset += 4; 986 outBuff.writeUInt32BE(magic2, outOffset); 987 outOffset += 4; 988 } 989 catch (e) 990 { 991 log.error(e.message, {stack: e.stack}); 992 throw new Error("PNGImageDecoder1"); 993 } 994 995 try 996 { 997 while (inOffset < origBuff.length) 998 { 999 var length = origBuff.readInt32BE(inOffset); 1000 inOffset += 4; 1001 var type = origBuff.readInt32BE(inOffset) 1002 inOffset += 4; 1003 1004 if (type == PNG_CHUNK_IDAT) 1005 { 1006 // Insert zTXt chunk before IDAT chunk 1007 outBuff.writeInt32BE(dataLen, outOffset); 1008 outOffset += 4; 1009 1010 var typeSignature = isDpi? 'pHYs' : (compressed ? "zTXt" : "tEXt"); 1011 outBuff.write(typeSignature, outOffset); 1012 1013 outOffset += 4; 1014 1015 if (isDpi) 1016 { 1017 var dpm = Math.round(parseInt(text) / 0.0254) || 3937; //One inch is equal to exactly 0.0254 meters. 3937 is 100dpi 1018 1019 outBuff.writeInt32BE(dpm, outOffset); 1020 outBuff.writeInt32BE(dpm, outOffset + 4); 1021 outBuff.writeInt8(1, outOffset + 8); 1022 outOffset += 9; 1023 1024 data = Buffer.allocUnsafe(9); 1025 data.writeInt32BE(dpm, 0); 1026 data.writeInt32BE(dpm, 4); 1027 data.writeInt8(1, 8); 1028 } 1029 else 1030 { 1031 outBuff.write(key, outOffset); 1032 outOffset += key.length; 1033 outBuff.writeInt8(0, outOffset); 1034 outOffset ++; 1035 1036 if (compressed) 1037 { 1038 outBuff.writeInt8(0, outOffset); 1039 outOffset ++; 1040 data.copy(outBuff, outOffset); 1041 } 1042 else 1043 { 1044 outBuff.write(data, outOffset); 1045 } 1046 1047 outOffset += data.length; 1048 } 1049 1050 var crcVal = 0xffffffff; 1051 crcVal = crc.crcjam(typeSignature, crcVal); 1052 crcVal = crc.crcjam(data, crcVal); 1053 1054 // CRC 1055 outBuff.writeInt32BE(crcVal ^ 0xffffffff, outOffset); 1056 outOffset += 4; 1057 1058 // Writes the IDAT chunk after the zTXt 1059 outBuff.writeInt32BE(length, outOffset); 1060 outOffset += 4; 1061 outBuff.writeInt32BE(type, outOffset); 1062 outOffset += 4; 1063 1064 origBuff.copy(outBuff, outOffset, inOffset); 1065 1066 // Encodes the buffer using base64 if requested 1067 return base64encoded? outBuff.toString('base64') : outBuff; 1068 } 1069 1070 outBuff.writeInt32BE(length, outOffset); 1071 outOffset += 4; 1072 outBuff.writeInt32BE(type, outOffset); 1073 outOffset += 4; 1074 1075 origBuff.copy(outBuff, outOffset, inOffset, inOffset + length + 4);// +4 to move past the crc 1076 1077 inOffset += length + 4; 1078 outOffset += length + 4; 1079 } 1080 } 1081 catch (e) 1082 { 1083 log.error(e.message, {stack: e.stack}); 1084 throw e; 1085 } 1086} 1087 1088//TODO Create a lightweight html file similar to export3.html for exporting to vsdx 1089function exportVsdx(event, args, directFinalize) 1090{ 1091 let win = createWindow({ 1092 show : false 1093 }); 1094 1095 let loadEvtCount = 0; 1096 1097 function loadFinished() 1098 { 1099 loadEvtCount++; 1100 1101 if (loadEvtCount == 2) 1102 { 1103 win.webContents.send('export-vsdx', args); 1104 1105 ipcMain.once('export-vsdx-finished', (evt, data) => 1106 { 1107 var hasError = false; 1108 1109 if (data == null) 1110 { 1111 hasError = true; 1112 } 1113 1114 //Set finalize here since it is call in the reply below 1115 function finalize() 1116 { 1117 win.destroy(); 1118 }; 1119 1120 if (directFinalize === true) 1121 { 1122 event.finalize = finalize; 1123 } 1124 else 1125 { 1126 //Destroy the window after response being received by caller 1127 ipcMain.once('export-finalize', finalize); 1128 } 1129 1130 if (hasError) 1131 { 1132 event.reply('export-error'); 1133 } 1134 else 1135 { 1136 event.reply('export-success', data); 1137 } 1138 }); 1139 } 1140 } 1141 1142 //Order of these two events is not guaranteed, so wait for them async. 1143 //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly 1144 ipcMain.once('app-load-finished', loadFinished); 1145 win.webContents.on('did-finish-load', loadFinished); 1146}; 1147 1148async function mergePdfs(pdfFiles, xml) 1149{ 1150 //Pass throgh single files 1151 if (pdfFiles.length == 1 && xml == null) 1152 { 1153 return pdfFiles[0]; 1154 } 1155 1156 try 1157 { 1158 const pdfDoc = await PDFDocument.create(); 1159 pdfDoc.setCreator('diagrams.net'); 1160 1161 if (xml != null) 1162 { 1163 //Embed diagram XML as file attachment 1164 await pdfDoc.attach(Buffer.from(xml).toString('base64'), 'diagram.xml', { 1165 mimeType: 'application/vnd.jgraph.mxfile', 1166 description: 'Diagram Content' 1167 }); 1168 } 1169 1170 for (var i = 0; i < pdfFiles.length; i++) 1171 { 1172 const pdfFile = await PDFDocument.load(pdfFiles[i].buffer); 1173 const pages = await pdfDoc.copyPages(pdfFile, pdfFile.getPageIndices()); 1174 pages.forEach(p => pdfDoc.addPage(p)); 1175 } 1176 1177 const pdfBytes = await pdfDoc.save(); 1178 return Buffer.from(pdfBytes); 1179 } 1180 catch(e) 1181 { 1182 throw new Error('Error during PDF combination: ' + e.message); 1183 } 1184} 1185 1186//TODO Use canvas to export images if math is not used to speedup export (no capturePage). Requires change to export3.html also 1187function exportDiagram(event, args, directFinalize) 1188{ 1189 if (args.format == 'vsdx') 1190 { 1191 exportVsdx(event, args, directFinalize); 1192 return; 1193 } 1194 1195 var browser = null; 1196 1197 try 1198 { 1199 browser = new BrowserWindow({ 1200 webPreferences: { 1201 backgroundThrottling: false, 1202 nodeIntegration: true, 1203 contextIsolation: false, 1204 nativeWindowOpen: true 1205 }, 1206 show : false, 1207 frame: false, 1208 enableLargerThanScreen: true, 1209 transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'), 1210 parent: windowsRegistry[0] //set parent to first opened window. Not very accurate, but useful when all visible windows are closed 1211 }); 1212 1213 browser.loadURL(`file://${__dirname}/export3.html`); 1214 1215 const contents = browser.webContents; 1216 var pageByPage = (args.format == 'pdf' && !args.print), from, pdfs; 1217 1218 if (pageByPage) 1219 { 1220 from = args.allPages? 0 : parseInt(args.from || 0); 1221 to = args.allPages? 1000 : parseInt(args.to || 1000) + 1; //The 'to' will be corrected later 1222 pdfs = []; 1223 1224 args.from = from; 1225 args.to = from; 1226 args.allPages = false; 1227 } 1228 1229 contents.on('did-finish-load', function() 1230 { 1231 //Set finalize here since it is call in the reply below 1232 function finalize() 1233 { 1234 browser.destroy(); 1235 }; 1236 1237 if (directFinalize === true) 1238 { 1239 event.finalize = finalize; 1240 } 1241 else 1242 { 1243 //Destroy the window after response being received by caller 1244 ipcMain.once('export-finalize', finalize); 1245 } 1246 1247 function renderingFinishHandler(evt, renderInfo) 1248 { 1249 var pageCount = renderInfo.pageCount, bounds = null; 1250 //For some reason, Electron 9 doesn't send this object as is without stringifying. Usually when variable is external to function own scope 1251 try 1252 { 1253 bounds = JSON.parse(renderInfo.bounds); 1254 } 1255 catch(e) 1256 { 1257 bounds = null; 1258 } 1259 1260 var pdfOptions = {pageSize: 'A4'}; 1261 var hasError = false; 1262 1263 if (bounds == null || bounds.width < 5 || bounds.height < 5) //very small page size never return from printToPDF 1264 { 1265 //A workaround to detect errors in the input file or being empty file 1266 hasError = true; 1267 } 1268 else 1269 { 1270 //Chrome generates Pdf files larger than requested pixels size and requires scaling 1271 var fixingScale = 0.959; 1272 1273 var w = Math.ceil(bounds.width * fixingScale); 1274 1275 // +0.1 fixes cases where adding 1px below is not enough 1276 // Increase this if more cropped PDFs have extra empty pages 1277 var h = Math.ceil(bounds.height * fixingScale + 0.1); 1278 1279 pdfOptions = { 1280 printBackground: true, 1281 pageSize : { 1282 width: w * MICRON_TO_PIXEL, 1283 height: (h + 2) * MICRON_TO_PIXEL //the extra 2 pixels to prevent adding an extra empty page 1284 }, 1285 marginsType: 1 // no margin 1286 } 1287 } 1288 1289 var base64encoded = args.base64 == '1'; 1290 1291 if (hasError) 1292 { 1293 event.reply('export-error'); 1294 } 1295 else if (args.format == 'png' || args.format == 'jpg' || args.format == 'jpeg') 1296 { 1297 //Adds an extra pixel to prevent scrollbars from showing 1298 var newBounds = {width: Math.ceil(bounds.width + bounds.x) + 1, height: Math.ceil(bounds.height + bounds.y) + 1}; 1299 browser.setBounds(newBounds); 1300 1301 //TODO The browser takes sometime to show the graph (also after resize it takes some time to render) 1302 // 1 sec is most probably enough (for small images, 5 for large ones) BUT not a stable solution 1303 setTimeout(function() 1304 { 1305 browser.capturePage().then(function(img) 1306 { 1307 //Image is double the given bounds, so resize is needed! 1308 var tScale = 1; 1309 1310 //If user defined width and/or height, enforce it precisely here. Height override width 1311 if (args.h) 1312 { 1313 tScale = args.h / newBounds.height; 1314 } 1315 else if (args.w) 1316 { 1317 tScale = args.w / newBounds.width; 1318 } 1319 1320 newBounds.width *= tScale; 1321 newBounds.height *= tScale; 1322 img = img.resize(newBounds); 1323 1324 var data = args.format == 'png'? img.toPNG() : img.toJPEG(args.jpegQuality || 90); 1325 1326 if (args.dpi != null && args.format == 'png') 1327 { 1328 data = writePngWithText(data, 'dpi', args.dpi); 1329 } 1330 1331 if (args.embedXml == "1" && args.format == 'png') 1332 { 1333 data = writePngWithText(data, "mxGraphModel", args.xml, true, 1334 base64encoded); 1335 } 1336 else 1337 { 1338 if (base64encoded) 1339 { 1340 data = data.toString('base64'); 1341 } 1342 } 1343 1344 event.reply('export-success', data); 1345 }); 1346 }, bounds.width * bounds.height < LARGE_IMAGE_AREA? 1000 : 5000); 1347 } 1348 else if (args.format == 'pdf') 1349 { 1350 if (args.print) 1351 { 1352 pdfOptions = { 1353 scaleFactor: args.pageScale, 1354 printBackground: true, 1355 pageSize : { 1356 width: args.pageWidth * MICRON_TO_PIXEL, 1357 //This height adjustment fixes the output. TODO Test more cases 1358 height: (args.pageHeight * 1.025) * MICRON_TO_PIXEL 1359 }, 1360 marginsType: 1 // no margin 1361 }; 1362 1363 contents.print(pdfOptions, (success, errorType) => 1364 { 1365 //Consider all as success 1366 event.reply('export-success', {}); 1367 }); 1368 } 1369 else 1370 { 1371 contents.printToPDF(pdfOptions).then(async (data) => 1372 { 1373 pdfs.push(data); 1374 to = to > pageCount? pageCount : to; 1375 from++; 1376 1377 if (from < to) 1378 { 1379 args.from = from; 1380 args.to = from; 1381 ipcMain.once('render-finished', renderingFinishHandler); 1382 contents.send('render', args); 1383 } 1384 else 1385 { 1386 data = await mergePdfs(pdfs, args.embedXml == '1' ? args.xml : null); 1387 event.reply('export-success', data); 1388 } 1389 }) 1390 .catch((error) => 1391 { 1392 event.reply('export-error', error); 1393 }); 1394 } 1395 } 1396 else if (args.format == 'svg') 1397 { 1398 contents.send('get-svg-data'); 1399 1400 ipcMain.once('svg-data', (evt, data) => 1401 { 1402 event.reply('export-success', data); 1403 }); 1404 } 1405 else 1406 { 1407 event.reply('export-error', 'Error: Unsupported format'); 1408 } 1409 }; 1410 1411 ipcMain.once('render-finished', renderingFinishHandler); 1412 1413 if (args.format == 'xml') 1414 { 1415 ipcMain.once('xml-data', (evt, data) => 1416 { 1417 event.reply('export-success', data); 1418 }); 1419 1420 ipcMain.once('xml-data-error', () => 1421 { 1422 event.reply('export-error'); 1423 }); 1424 } 1425 1426 args.border = args.border || 0; 1427 args.scale = args.scale || 1; 1428 1429 contents.send('render', args); 1430 }); 1431 } 1432 catch (e) 1433 { 1434 if (browser != null) 1435 { 1436 browser.destroy(); 1437 } 1438 1439 event.reply('export-error', e); 1440 console.log('export-error', e); 1441 } 1442}; 1443 1444ipcMain.on('export', exportDiagram);