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);