diff --git a/electron/electron.js b/electron/electron.js
index abef2130c..ff8ace348 100644
--- a/electron/electron.js
+++ b/electron/electron.js
@@ -3,9 +3,6 @@ const fs = require('fs')
const os = require("os");
const path = require('path')
const spawn = require("child_process").spawn;
-const fsProm = require('fs/promises');
-const crc = require('crc');
-const zlib = require('zlib');
// Web 服务相关
const express = require('express')
@@ -18,7 +15,6 @@ const {
dialog,
clipboard,
nativeImage,
- shell,
globalShortcut,
nativeTheme,
Tray,
@@ -38,9 +34,6 @@ const loger = require("electron-log");
const electronConf = require('electron-config')
const Screenshots = require("electron-screenshots-tool").Screenshots;
-// PDF 处理
-const PDFDocument = require('pdf-lib').PDFDocument;
-
// 本地模块和配置
const utils = require('./lib/utils');
const navigation = require('./lib/navigation');
@@ -48,28 +41,20 @@ const config = require('./package.json');
const electronDown = require("./electron-down");
const electronMenu = require("./electron-menu");
const { startMCPServer, stopMCPServer } = require("./lib/mcp");
+const {onRenderer} = require("./lib/renderer");
+const {onExport} = require("./lib/pdf-export");
+const {allowedCalls, isWin} = require("./lib/other");
// 实例初始化
const userConf = new electronConf()
const store = new Store();
-// 平台检测常量
-const isMac = process.platform === 'darwin'
-const isWin = process.platform === 'win32'
-
-// URL 和调用验证正则
-const allowedUrls = /^(?:https?|mailto|tel|callto):/i;
-const allowedCalls = /^(?:mailto|tel|callto):/i;
-
// 路径和缓存配置
const cacheDir = path.join(os.tmpdir(), 'dootask-cache')
const updaterLockFile = path.join(cacheDir, '.dootask_updater.lock');
// 应用状态标志
-let enableStoreBkp = true,
- dialogOpen = false,
- enablePlugins = false,
- isReady = false,
+let isReady = false,
willQuitApp = false,
isDevelopMode = false;
@@ -971,7 +956,7 @@ function createWebTabWindow(args) {
event: 'stop-loading',
id: browserView.webContents.id,
}).then(_ => { })
-
+
// 加载完成暗黑模式下把窗口背景色改成白色,避免透明网站背景色穿透
if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#FFFFFF')
@@ -1487,17 +1472,17 @@ ipcMain.on('webTabGetNavigationState', (event) => {
if (!item) {
return
}
-
+
const canGoBack = item.view.webContents.canGoBack()
const canGoForward = item.view.webContents.canGoForward()
-
+
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack,
canGoForward
}).then(_ => { })
-
+
event.returnValue = "ok"
})
@@ -1924,1098 +1909,8 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
})
//================================================================
-// Pdf export
+//================================================================
//================================================================
-const MICRON_TO_PIXEL = 264.58 //264.58 micron = 1 pixel
-const PIXELS_PER_INCH = 100.117 // Usually it is 100 pixels per inch but this give better results
-const PNG_CHUNK_IDAT = 1229209940;
-const LARGE_IMAGE_AREA = 30000000;
-
-//NOTE: Key length must not be longer than 79 bytes (not checked)
-function writePngWithText(origBuff, key, text, compressed, base64encoded) {
- let isDpi = key == 'dpi';
- let inOffset = 0;
- let outOffset = 0;
- let data = text;
- let 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
-
- //prepare compressed data to get its size
- if (compressed) {
- data = zlib.deflateRawSync(encodeURIComponent(text));
- dataLen = key.length + data.length + 2; //we add 2 zeros with compressed data
- }
-
- let outBuff = Buffer.allocUnsafe(origBuff.length + dataLen + 4); //4 is the header size "zTXt", "tEXt" or "pHYs"
-
- try {
- let magic1 = origBuff.readUInt32BE(inOffset);
- inOffset += 4;
- let magic2 = origBuff.readUInt32BE(inOffset);
- inOffset += 4;
-
- if (magic1 != 0x89504e47 && magic2 != 0x0d0a1a0a) {
- throw new Error("PNGImageDecoder0");
- }
-
- outBuff.writeUInt32BE(magic1, outOffset);
- outOffset += 4;
- outBuff.writeUInt32BE(magic2, outOffset);
- outOffset += 4;
- } catch (e) {
- loger.error(e.message, {stack: e.stack});
- throw new Error("PNGImageDecoder1");
- }
-
- try {
- while (inOffset < origBuff.length) {
- let length = origBuff.readInt32BE(inOffset);
- inOffset += 4;
- let type = origBuff.readInt32BE(inOffset)
- inOffset += 4;
-
- if (type == PNG_CHUNK_IDAT) {
- // Insert zTXt chunk before IDAT chunk
- outBuff.writeInt32BE(dataLen, outOffset);
- outOffset += 4;
-
- let typeSignature = isDpi ? 'pHYs' : (compressed ? "zTXt" : "tEXt");
- outBuff.write(typeSignature, outOffset);
-
- outOffset += 4;
-
- if (isDpi) {
- let dpm = Math.round(parseInt(text) / 0.0254) || 3937; //One inch is equal to exactly 0.0254 meters. 3937 is 100dpi
-
- outBuff.writeInt32BE(dpm, outOffset);
- outBuff.writeInt32BE(dpm, outOffset + 4);
- outBuff.writeInt8(1, outOffset + 8);
- outOffset += 9;
-
- data = Buffer.allocUnsafe(9);
- data.writeInt32BE(dpm, 0);
- data.writeInt32BE(dpm, 4);
- data.writeInt8(1, 8);
- } else {
- outBuff.write(key, outOffset);
- outOffset += key.length;
- outBuff.writeInt8(0, outOffset);
- outOffset++;
-
- if (compressed) {
- outBuff.writeInt8(0, outOffset);
- outOffset++;
- data.copy(outBuff, outOffset);
- } else {
- outBuff.write(data, outOffset);
- }
-
- outOffset += data.length;
- }
-
- let crcVal = 0xffffffff;
- crcVal = crc.crcjam(typeSignature, crcVal);
- crcVal = crc.crcjam(data, crcVal);
-
- // CRC
- outBuff.writeInt32BE(crcVal ^ 0xffffffff, outOffset);
- outOffset += 4;
-
- // Writes the IDAT chunk after the zTXt
- outBuff.writeInt32BE(length, outOffset);
- outOffset += 4;
- outBuff.writeInt32BE(type, outOffset);
- outOffset += 4;
-
- origBuff.copy(outBuff, outOffset, inOffset);
-
- // Encodes the buffer using base64 if requested
- return base64encoded ? outBuff.toString('base64') : outBuff;
- }
-
- outBuff.writeInt32BE(length, outOffset);
- outOffset += 4;
- outBuff.writeInt32BE(type, outOffset);
- outOffset += 4;
-
- origBuff.copy(outBuff, outOffset, inOffset, inOffset + length + 4);// +4 to move past the crc
-
- inOffset += length + 4;
- outOffset += length + 4;
- }
- } catch (e) {
- loger.error(e.message, {stack: e.stack});
- throw e;
- }
-}
-
-//TODO Create a lightweight html file similar to export3.html for exporting to vsdx
-function exportVsdx(event, args, directFinalize) {
- let win = new BrowserWindow({
- width: 1280,
- height: 800,
- show: false,
- webPreferences: {
- preload: path.join(__dirname, 'electron-preload.js'),
- webSecurity: true,
- nodeIntegration: true,
- contextIsolation: true,
- },
- })
-
- let loadEvtCount = 0;
-
- function loadFinished() {
- loadEvtCount++;
-
- if (loadEvtCount == 2) {
- win.webContents.send('export-vsdx', args);
-
- ipcMain.once('export-vsdx-finished', (evt, data) => {
- let hasError = false;
-
- if (data == null) {
- hasError = true;
- }
-
- //Set finalize here since it is call in the reply below
- function finalize() {
- win.destroy();
- }
-
- if (directFinalize === true) {
- event.finalize = finalize;
- } else {
- //Destroy the window after response being received by caller
- ipcMain.once('export-finalize', finalize);
- }
-
- if (hasError) {
- event.reply('export-error');
- } else {
- event.reply('export-success', data);
- }
- });
- }
- }
-
- //Order of these two events is not guaranteed, so wait for them async.
- //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly
- ipcMain.once('app-load-finished', loadFinished);
- win.webContents.on('did-finish-load', loadFinished);
-}
-
-async function mergePdfs(pdfFiles, xml) {
- //Pass throgh single files
- if (pdfFiles.length == 1 && xml == null) {
- return pdfFiles[0];
- }
-
- try {
- const pdfDoc = await PDFDocument.create();
- pdfDoc.setCreator(config.name);
-
- if (xml != null) {
- //Embed diagram XML as file attachment
- await pdfDoc.attach(Buffer.from(xml).toString('base64'), config.name + '.xml', {
- mimeType: 'application/vnd.jgraph.mxfile',
- description: config.name + ' Content'
- });
- }
-
- for (let i = 0; i < pdfFiles.length; i++) {
- const pdfFile = await PDFDocument.load(pdfFiles[i].buffer);
- const pages = await pdfDoc.copyPages(pdfFile, pdfFile.getPageIndices());
- pages.forEach(p => pdfDoc.addPage(p));
- }
-
- const pdfBytes = await pdfDoc.save();
- return Buffer.from(pdfBytes);
- } catch (e) {
- throw new Error('Error during PDF combination: ' + e.message);
- }
-}
-
-//TODO Use canvas to export images if math is not used to speedup export (no capturePage). Requires change to export3.html also
-function exportDiagram(event, args, directFinalize) {
- if (args.format == 'vsdx') {
- exportVsdx(event, args, directFinalize);
- return;
- }
-
- let browser = null;
-
- try {
- browser = new BrowserWindow({
- webPreferences: {
- preload: path.join(__dirname, 'electron-preload.js'),
- backgroundThrottling: false,
- contextIsolation: true,
- disableBlinkFeatures: 'Auxclick' // Is this needed?
- },
- show: false,
- frame: false,
- enableLargerThanScreen: true,
- transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'),
- });
-
- if (serverUrl) {
- browser.loadURL(serverUrl + 'drawio/webapp/export3.html').then(_ => { }).catch(_ => { })
- } else {
- browser.loadFile('./public/drawio/webapp/export3.html').then(_ => { }).catch(_ => { })
- }
-
- const contents = browser.webContents;
- let pageByPage = (args.format == 'pdf' && !args.print), from, to, pdfs;
-
- if (pageByPage) {
- from = args.allPages ? 0 : parseInt(args.from || 0);
- to = args.allPages ? 1000 : parseInt(args.to || 1000) + 1; //The 'to' will be corrected later
- pdfs = [];
-
- args.from = from;
- args.to = from;
- args.allPages = false;
- }
-
- contents.on('did-finish-load', function () {
- //Set finalize here since it is call in the reply below
- function finalize() {
- browser.destroy();
- }
-
- if (directFinalize === true) {
- event.finalize = finalize;
- } else {
- //Destroy the window after response being received by caller
- ipcMain.once('export-finalize', finalize);
- }
-
- function renderingFinishHandler(evt, renderInfo) {
- if (renderInfo == null) {
- event.reply('export-error');
- return;
- }
-
- let pageCount = renderInfo.pageCount, bounds = null;
- //For some reason, Electron 9 doesn't send this object as is without stringifying. Usually when variable is external to function own scope
- try {
- bounds = JSON.parse(renderInfo.bounds);
- } catch (e) {
- bounds = null;
- }
-
- let pdfOptions = {pageSize: 'A4'};
- let hasError = false;
-
- if (bounds == null || bounds.width < 5 || bounds.height < 5) //very small page size never return from printToPDF
- {
- //A workaround to detect errors in the input file or being empty file
- hasError = true;
- } else {
- pdfOptions = {
- printBackground: true,
- pageSize: {
- width: bounds.width / PIXELS_PER_INCH,
- height: (bounds.height + 2) / PIXELS_PER_INCH //the extra 2 pixels to prevent adding an extra empty page
- },
- margins: {
- top: 0,
- bottom: 0,
- left: 0,
- right: 0
- } // no margin
- }
- }
-
- let base64encoded = args.base64 == '1';
-
- if (hasError) {
- event.reply('export-error');
- } else if (args.format == 'png' || args.format == 'jpg' || args.format == 'jpeg') {
- //Adds an extra pixel to prevent scrollbars from showing
- let newBounds = {
- width: Math.ceil(bounds.width + bounds.x) + 1,
- height: Math.ceil(bounds.height + bounds.y) + 1
- };
- browser.setBounds(newBounds);
-
- //TODO The browser takes sometime to show the graph (also after resize it takes some time to render)
- // 1 sec is most probably enough (for small images, 5 for large ones) BUT not a stable solution
- setTimeout(function () {
- browser.capturePage().then(function (img) {
- //Image is double the given bounds, so resize is needed!
- let tScale = 1;
-
- //If user defined width and/or height, enforce it precisely here. Height override width
- if (args.h) {
- tScale = args.h / newBounds.height;
- } else if (args.w) {
- tScale = args.w / newBounds.width;
- }
-
- newBounds.width *= tScale;
- newBounds.height *= tScale;
- img = img.resize(newBounds);
-
- let data = args.format == 'png' ? img.toPNG() : img.toJPEG(args.jpegQuality || 90);
-
- if (args.dpi != null && args.format == 'png') {
- data = writePngWithText(data, 'dpi', args.dpi);
- }
-
- if (args.embedXml == "1" && args.format == 'png') {
- data = writePngWithText(data, "mxGraphModel", args.xml, true,
- base64encoded);
- } else {
- if (base64encoded) {
- data = data.toString('base64');
- }
- }
-
- event.reply('export-success', data);
- });
- }, bounds.width * bounds.height < LARGE_IMAGE_AREA ? 1000 : 5000);
- } else if (args.format == 'pdf') {
- if (args.print) {
- pdfOptions = {
- scaleFactor: args.pageScale,
- printBackground: true,
- pageSize: {
- width: args.pageWidth * MICRON_TO_PIXEL,
- //This height adjustment fixes the output. TODO Test more cases
- height: (args.pageHeight * 1.025) * MICRON_TO_PIXEL
- },
- marginsType: 1 // no margin
- };
-
- contents.print(pdfOptions, (success, errorType) => {
- //Consider all as success
- event.reply('export-success', {});
- });
- } else {
- contents.printToPDF(pdfOptions).then(async (data) => {
- pdfs.push(data);
- to = to > pageCount ? pageCount : to;
- from++;
-
- if (from < to) {
- args.from = from;
- args.to = from;
- ipcMain.once('render-finished', renderingFinishHandler);
- contents.send('render', args);
- } else {
- data = await mergePdfs(pdfs, args.embedXml == '1' ? args.xml : null);
- event.reply('export-success', data);
- }
- })
- .catch((error) => {
- event.reply('export-error', error);
- });
- }
- } else if (args.format == 'svg') {
- contents.send('get-svg-data');
-
- ipcMain.once('svg-data', (evt, data) => {
- event.reply('export-success', data);
- });
- } else {
- event.reply('export-error', 'Error: Unsupported format');
- }
- }
-
- ipcMain.once('render-finished', renderingFinishHandler);
-
- if (args.format == 'xml') {
- ipcMain.once('xml-data', (evt, data) => {
- event.reply('export-success', data);
- });
-
- ipcMain.once('xml-data-error', () => {
- event.reply('export-error');
- });
- }
-
- args.border = args.border || 0;
- args.scale = args.scale || 1;
-
- contents.send('render', args);
- });
- } catch (e) {
- if (browser != null) {
- browser.destroy();
- }
-
- event.reply('export-error', e);
- console.log('export-error', e);
- }
-}
-
-ipcMain.on('export', exportDiagram);
-
-//================================================================
-// Renderer Helper functions
-//================================================================
-
-const {O_SYNC, O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY} = fs.constants;
-const DRAFT_PREFEX = '.$';
-const OLD_DRAFT_PREFEX = '~$';
-const DRAFT_EXT = '.dtmp';
-const BKP_PREFEX = '.$';
-const OLD_BKP_PREFEX = '~$';
-const BKP_EXT = '.bkp';
-
-/**
- * Checks the file content type
- * Confirm content is xml, pdf, png, jpg, svg, vsdx ...
- */
-function checkFileContent(body, enc) {
- if (body != null) {
- let head, headBinay;
-
- if (typeof body === 'string') {
- if (enc == 'base64') {
- headBinay = Buffer.from(body.substring(0, 22), 'base64');
- head = headBinay.toString();
- } else {
- head = body.substring(0, 16);
- headBinay = Buffer.from(head);
- }
- } else {
- head = new TextDecoder("utf-8").decode(body.subarray(0, 16));
- headBinay = body;
- }
-
- let c1 = head[0],
- c2 = head[1],
- c3 = head[2],
- c4 = head[3],
- c5 = head[4],
- c6 = head[5],
- c7 = head[6],
- c8 = head[7],
- c9 = head[8],
- c10 = head[9],
- c11 = head[10],
- c12 = head[11],
- c13 = head[12],
- c14 = head[13],
- c15 = head[14],
- c16 = head[15];
-
- let cc1 = headBinay[0],
- cc2 = headBinay[1],
- cc3 = headBinay[2],
- cc4 = headBinay[3],
- cc5 = headBinay[4],
- cc6 = headBinay[5],
- cc7 = headBinay[6],
- cc8 = headBinay[7],
- cc9 = headBinay[8],
- cc10 = headBinay[9],
- cc11 = headBinay[10],
- cc12 = headBinay[11],
- cc13 = headBinay[12],
- cc14 = headBinay[13],
- cc15 = headBinay[14],
- cc16 = headBinay[15];
-
- if (c1 == '<') {
- // text/html
- if (c2 == '!'
- || ((c2 == 'h'
- && (c3 == 't' && c4 == 'm' && c5 == 'l'
- || c3 == 'e' && c4 == 'a' && c5 == 'd')
- || (c2 == 'b' && c3 == 'o' && c4 == 'd'
- && c5 == 'y')))
- || ((c2 == 'H'
- && (c3 == 'T' && c4 == 'M' && c5 == 'L'
- || c3 == 'E' && c4 == 'A' && c5 == 'D')
- || (c2 == 'B' && c3 == 'O' && c4 == 'D'
- && c5 == 'Y')))) {
- return true;
- }
-
- // application/xml
- if (c2 == '?' && c3 == 'x' && c4 == 'm' && c5 == 'l'
- && c6 == ' ') {
- return true;
- }
-
- // application/svg+xml
- if (c2 == 's' && c3 == 'v' && c4 == 'g' && c5 == ' ') {
- return true;
- }
- }
-
- // big and little (identical) endian UTF-8 encodings, with BOM
- // application/xml
- if (cc1 == 0xef && cc2 == 0xbb && cc3 == 0xbf) {
- if (c4 == '<' && c5 == '?' && c6 == 'x') {
- return true;
- }
- }
-
- // big and little endian UTF-16 encodings, with byte order mark
- // application/xml
- if (cc1 == 0xfe && cc2 == 0xff) {
- if (cc3 == 0 && c4 == '<' && cc5 == 0 && c6 == '?' && cc7 == 0
- && c8 == 'x') {
- return true;
- }
- }
-
- // application/xml
- if (cc1 == 0xff && cc2 == 0xfe) {
- if (c3 == '<' && cc4 == 0 && c5 == '?' && cc6 == 0 && c7 == 'x'
- && cc8 == 0) {
- return true;
- }
- }
-
- // big and little endian UTF-32 encodings, with BOM
- // application/xml
- if (cc1 == 0x00 && cc2 == 0x00 && cc3 == 0xfe && cc4 == 0xff) {
- if (cc5 == 0 && cc6 == 0 && cc7 == 0 && c8 == '<' && cc9 == 0
- && cc10 == 0 && cc11 == 0 && c12 == '?' && cc13 == 0
- && cc14 == 0 && cc15 == 0 && c16 == 'x') {
- return true;
- }
- }
-
- // application/xml
- if (cc1 == 0xff && cc2 == 0xfe && cc3 == 0x00 && cc4 == 0x00) {
- if (c5 == '<' && cc6 == 0 && cc7 == 0 && cc8 == 0 && c9 == '?'
- && cc10 == 0 && cc11 == 0 && cc12 == 0 && c13 == 'x'
- && cc14 == 0 && cc15 == 0 && cc16 == 0) {
- return true;
- }
- }
-
- // application/pdf (%PDF-)
- if (cc1 == 37 && cc2 == 80 && cc3 == 68 && cc4 == 70 && cc5 == 45) {
- return true;
- }
-
- // image/png
- if ((cc1 == 137 && cc2 == 80 && cc3 == 78 && cc4 == 71 && cc5 == 13
- && cc6 == 10 && cc7 == 26 && cc8 == 10) ||
- (cc1 == 194 && cc2 == 137 && cc3 == 80 && cc4 == 78 && cc5 == 71 && cc6 == 13 //Our embedded PNG+XML
- && cc7 == 10 && cc8 == 26 && cc9 == 10)) {
- return true;
- }
-
- // image/jpeg
- if (cc1 == 0xFF && cc2 == 0xD8 && cc3 == 0xFF) {
- if (cc4 == 0xE0 || cc4 == 0xEE) {
- return true;
- }
-
- /**
- * File format used by digital cameras to store images.
- * Exif Format can be read by any application supporting
- * JPEG. Exif Spec can be found at:
- * http://www.pima.net/standards/it10/PIMA15740/Exif_2-1.PDF
- */
- if ((cc4 == 0xE1) && (c7 == 'E' && c8 == 'x' && c9 == 'i'
- && c10 == 'f' && cc11 == 0)) {
- return true;
- }
- }
-
- // vsdx, vssx (also zip, jar, odt, ods, odp, docx, xlsx, pptx, apk, aar)
- if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x04) {
- return true;
- } else if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x06) {
- return true;
- }
-
- // mxfile, mxlibrary, mxGraphModel
- if (c1 == '<' && c2 == 'm' && c3 == 'x') {
- return true;
- }
- }
-
- return false;
-}
-
-function isConflict(origStat, stat) {
- return stat != null && origStat != null && stat.mtimeMs != origStat.mtimeMs;
-}
-
-function getDraftFileName(fileObject) {
- let filePath = fileObject.path;
- let draftFileName = '', counter = 1, uniquePart = '';
-
- do {
- draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
- uniquePart = '_' + counter++;
- } while (fs.existsSync(draftFileName));
-
- return draftFileName;
-}
-
-async function getFileDrafts(fileObject) {
- let filePath = fileObject.path;
- let draftsPaths = [], drafts = [], draftFileName, counter = 1, uniquePart = '';
-
- do {
- draftsPaths.push(draftFileName);
- draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
- uniquePart = '_' + counter++;
- } while (fs.existsSync(draftFileName)); //TODO this assume continuous drafts names
-
- //Port old draft files to new prefex
- counter = 1;
- uniquePart = '';
- let draftExists = false;
-
- do {
- draftFileName = path.join(path.dirname(filePath), OLD_DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
- draftExists = fs.existsSync(draftFileName);
-
- if (draftExists) {
- const newDraftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
- await fsProm.rename(draftFileName, newDraftFileName);
- draftsPaths.push(newDraftFileName);
- }
-
- uniquePart = '_' + counter++;
- } while (draftExists); //TODO this assume continuous drafts names
-
- //Skip the first null element
- for (let i = 1; i < draftsPaths.length; i++) {
- try {
- let stat = await fsProm.lstat(draftsPaths[i]);
- drafts.push({
- data: await fsProm.readFile(draftsPaths[i], 'utf8'),
- created: stat.ctimeMs,
- modified: stat.mtimeMs,
- path: draftsPaths[i]
- });
- } catch (e) {
- } // Ignore
- }
-
- return drafts;
-}
-
-async function saveDraft(fileObject, data) {
- if (!checkFileContent(data)) {
- throw new Error('Invalid file data');
- } else {
- let draftFileName = fileObject.draftFileName || getDraftFileName(fileObject);
- await fsProm.writeFile(draftFileName, data, 'utf8');
-
- if (isWin) {
- try {
- // Add Hidden attribute:
- spawn('attrib', ['+h', draftFileName], {shell: true});
- } catch (e) {
- }
- }
-
- return draftFileName;
- }
-}
-
-async function saveFile(fileObject, data, origStat, overwrite, defEnc) {
- if (!checkFileContent(data)) {
- throw new Error('Invalid file data');
- }
-
- let retryCount = 0;
- let backupCreated = false;
- let bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
- const oldBkpPath = path.join(path.dirname(fileObject.path), OLD_BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
- let writeEnc = defEnc || fileObject.encoding;
-
- let writeFile = async function () {
- let fh;
-
- try {
- // O_SYNC is for sync I/O and reduce risk of file corruption
- fh = await fsProm.open(fileObject.path, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
- await fsProm.writeFile(fh, data, writeEnc);
- } finally {
- await fh?.close();
- }
-
- let stat2 = await fsProm.stat(fileObject.path);
- // Workaround for possible writing errors is to check the written
- // contents of the file and retry 3 times before showing an error
- let writtenData = await fsProm.readFile(fileObject.path, writeEnc);
-
- if (data != writtenData) {
- retryCount++;
-
- if (retryCount < 3) {
- return await writeFile();
- } else {
- throw new Error('all saving trials failed');
- }
- } else {
- //We'll keep the backup file in case the original file is corrupted. TODO When should we delete the backup file?
- if (backupCreated) {
- //fs.unlink(bkpPath, (err) => {}); //Ignore errors!
-
- //Delete old backup file with old prefix
- if (fs.existsSync(oldBkpPath)) {
- fs.unlink(oldBkpPath, (err) => {
- }); //Ignore errors
- }
- }
-
- return stat2;
- }
- };
-
- async function doSaveFile(isNew) {
- if (enableStoreBkp && !isNew) {
- //Copy file to back up file (after conflict and stat is checked)
- let bkpFh;
-
- try {
- //Use file read then write to open the backup file direct sync write to reduce the chance of file corruption
- let fileContent = await fsProm.readFile(fileObject.path, writeEnc);
- bkpFh = await fsProm.open(bkpPath, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
- await fsProm.writeFile(bkpFh, fileContent, writeEnc);
- backupCreated = true;
- } catch (e) {
- if (__DEV__) {
- console.log('Backup file writing failed', e); //Ignore
- }
- } finally {
- await bkpFh?.close();
-
- if (isWin) {
- try {
- // Add Hidden attribute:
- spawn('attrib', ['+h', bkpPath], {shell: true});
- } catch (e) {
- }
- }
- }
- }
-
- return await writeFile();
- }
-
- if (overwrite) {
- return await doSaveFile(true);
- } else {
- let stat = fs.existsSync(fileObject.path) ?
- await fsProm.stat(fileObject.path) : null;
-
- if (stat && isConflict(origStat, stat)) {
- throw new Error('conflict');
- } else {
- return await doSaveFile(stat == null);
- }
- }
-}
-
-async function writeFile(path, data, enc) {
- if (!checkFileContent(data, enc)) {
- throw new Error('Invalid file data');
- } else {
- return await fsProm.writeFile(path, data, enc);
- }
-}
-
-function getAppDataFolder() {
- try {
- let appDataDir = app.getPath('appData');
- let drawioDir = appDataDir + '/' + config.name;
-
- if (!fs.existsSync(drawioDir)) //Usually this dir already exists
- {
- fs.mkdirSync(drawioDir);
- }
-
- return drawioDir;
- } catch (e) {
- }
-
- return '.';
-}
-
-function getDocumentsFolder() {
- //On windows, misconfigured Documents folder cause an exception
- try {
- return app.getPath('documents');
- } catch (e) {
- }
-
- return '.';
-}
-
-function checkFileExists(pathParts) {
- let filePath = path.join(...pathParts);
- return {exists: fs.existsSync(filePath), path: filePath};
-}
-
-async function showOpenDialog(defaultPath, filters, properties) {
- let win = BrowserWindow.getFocusedWindow();
-
- return dialog.showOpenDialog(win, {
- defaultPath: defaultPath,
- filters: filters,
- properties: properties
- });
-}
-
-async function showSaveDialog(defaultPath, filters) {
- let win = BrowserWindow.getFocusedWindow();
-
- return dialog.showSaveDialog(win, {
- defaultPath: defaultPath,
- filters: filters
- });
-}
-
-async function installPlugin(filePath) {
- if (!enablePlugins) return {};
-
- let pluginsDir = path.join(getAppDataFolder(), '/plugins');
-
- if (!fs.existsSync(pluginsDir)) {
- fs.mkdirSync(pluginsDir);
- }
-
- let pluginName = path.basename(filePath);
- let dstFile = path.join(pluginsDir, pluginName);
-
- if (fs.existsSync(dstFile)) {
- throw new Error('fileExists');
- } else {
- await fsProm.copyFile(filePath, dstFile);
- }
-
- return {pluginName: pluginName, selDir: path.dirname(filePath)};
-}
-
-function getPluginFile(plugin) {
- if (!enablePlugins) return null;
-
- const prefix = path.join(getAppDataFolder(), '/plugins/');
- const pluginFile = path.join(prefix, plugin);
-
- if (pluginFile.startsWith(prefix) && fs.existsSync(pluginFile)) {
- return pluginFile;
- }
-
- return null;
-}
-
-async function uninstallPlugin(plugin) {
- const pluginFile = getPluginFile(plugin);
-
- if (pluginFile != null) {
- fs.unlinkSync(pluginFile);
- }
-}
-
-function dirname(path_p) {
- return path.dirname(path_p);
-}
-
-async function readFile(filename, encoding) {
- let data = await fsProm.readFile(filename, encoding);
-
- if (checkFileContent(data, encoding)) {
- return data;
- }
-
- throw new Error('Invalid file data');
-}
-
-async function fileStat(file) {
- return await fsProm.stat(file);
-}
-
-async function isFileWritable(file) {
- try {
- await fsProm.access(file, fs.constants.W_OK);
- return true;
- } catch (e) {
- return false;
- }
-}
-
-function clipboardAction(method, data) {
- if (method == 'writeText') {
- clipboard.writeText(data);
- } else if (method == 'readText') {
- return clipboard.readText();
- } else if (method == 'writeImage') {
- clipboard.write({
- image:
- nativeImage.createFromDataURL(data.dataUrl), html: '
'
- });
- }
-}
-
-async function deleteFile(file) {
- // Reading the header of the file to confirm it is a file we can delete
- let fh = await fsProm.open(file, O_RDONLY);
- let buffer = Buffer.allocUnsafe(16);
- await fh.read(buffer, 0, 16);
- await fh.close();
-
- if (checkFileContent(buffer)) {
- await fsProm.unlink(file);
- }
-}
-
-async function windowAction(method) {
- let win = BrowserWindow.getFocusedWindow();
-
- if (win) {
- if (method == 'minimize') {
- win.minimize();
- } else if (method == 'maximize') {
- win.maximize();
- } else if (method == 'unmaximize') {
- win.unmaximize();
- } else if (method == 'close') {
- win.close();
- } else if (method == 'isMaximized') {
- return win.isMaximized();
- } else if (method == 'removeAllListeners') {
- win.removeAllListeners();
- }
- }
-}
-
-async function openExternal(url) {
- //Only open http(s), mailto, tel, and callto links
- if (allowedUrls.test(url)) {
- await shell.openExternal(url)
- }
-}
-
-async function watchFile(path) {
- let win = BrowserWindow.getFocusedWindow();
-
- if (win) {
- fs.watchFile(path, (curr, prev) => {
- try {
- win.webContents.send('fileChanged', {
- path: path,
- curr: curr,
- prev: prev
- });
- } catch (e) {
- // Ignore
- }
- });
- }
-}
-
-async function unwatchFile(path) {
- fs.unwatchFile(path);
-}
-
-function getCurDir() {
- return __dirname;
-}
-
-ipcMain.on("rendererReq", async (event, args) => {
- try {
- let ret = null;
-
- switch (args.action) {
- case 'saveFile':
- ret = await saveFile(args.fileObject, args.data, args.origStat, args.overwrite, args.defEnc);
- break;
- case 'writeFile':
- ret = await writeFile(args.path, args.data, args.enc);
- break;
- case 'saveDraft':
- ret = await saveDraft(args.fileObject, args.data);
- break;
- case 'getFileDrafts':
- ret = await getFileDrafts(args.fileObject);
- break;
- case 'getDocumentsFolder':
- ret = await getDocumentsFolder();
- break;
- case 'checkFileExists':
- ret = checkFileExists(args.pathParts);
- break;
- case 'showOpenDialog':
- dialogOpen = true;
- ret = await showOpenDialog(args.defaultPath, args.filters, args.properties);
- ret = ret.filePaths;
- dialogOpen = false;
- break;
- case 'showSaveDialog':
- dialogOpen = true;
- ret = await showSaveDialog(args.defaultPath, args.filters);
- ret = ret.canceled ? null : ret.filePath;
- dialogOpen = false;
- break;
- case 'installPlugin':
- ret = await installPlugin(args.filePath);
- break;
- case 'uninstallPlugin':
- ret = await uninstallPlugin(args.plugin);
- break;
- case 'getPluginFile':
- ret = getPluginFile(args.plugin);
- break;
- case 'isPluginsEnabled':
- ret = enablePlugins;
- break;
- case 'dirname':
- ret = await dirname(args.path);
- break;
- case 'readFile':
- ret = await readFile(args.filename, args.encoding);
- break;
- case 'clipboardAction':
- ret = clipboardAction(args.method, args.data);
- break;
- case 'deleteFile':
- ret = await deleteFile(args.file);
- break;
- case 'fileStat':
- ret = await fileStat(args.file);
- break;
- case 'isFileWritable':
- ret = await isFileWritable(args.file);
- break;
- case 'windowAction':
- ret = await windowAction(args.method);
- break;
- case 'openExternal':
- ret = await openExternal(args.url);
- break;
- case 'openDownloadWindow':
- ret = await electronDown.open(args.language || 'zh', args.theme || 'light');
- break;
- case 'updateDownloadWindow':
- ret = await electronDown.updateWindow(args.language, args.theme);
- break;
- case 'createDownload':
- ret = await electronDown.createDownload(mainWindow, args.url, args.options || {});
- break;
- case 'watchFile':
- ret = await watchFile(args.path);
- break;
- case 'unwatchFile':
- ret = await unwatchFile(args.path);
- break;
- case 'getCurDir':
- ret = getCurDir();
- break;
- }
-
- event.reply('mainResp', {success: true, data: ret, reqId: args.reqId});
- } catch (e) {
- event.reply('mainResp', {error: true, msg: e.message, e: e, reqId: args.reqId});
- loger.error('Renderer request error', e.message, e.stack);
- }
-});
+onExport()
+onRenderer(mainWindow)
diff --git a/electron/lib/other.js b/electron/lib/other.js
new file mode 100644
index 000000000..55e8403bb
--- /dev/null
+++ b/electron/lib/other.js
@@ -0,0 +1,15 @@
+// 平台检测常量
+const isMac = process.platform === 'darwin'
+const isWin = process.platform === 'win32'
+
+// URL 和调用验证正则
+const allowedUrls = /^(?:https?|mailto|tel|callto):/i;
+const allowedCalls = /^(?:mailto|tel|callto):/i;
+
+module.exports = {
+ isMac,
+ isWin,
+
+ allowedUrls,
+ allowedCalls
+}
diff --git a/electron/lib/pdf-export.js b/electron/lib/pdf-export.js
new file mode 100644
index 000000000..e773d81a2
--- /dev/null
+++ b/electron/lib/pdf-export.js
@@ -0,0 +1,439 @@
+const path = require('path')
+const {BrowserWindow, ipcMain} = require('electron')
+const loger = require("electron-log");
+const crc = require("crc");
+const zlib = require('zlib');
+
+const PDFDocument = require('pdf-lib').PDFDocument;
+const config = require('../package.json');
+
+const MICRON_TO_PIXEL = 264.58 //264.58 micron = 1 pixel
+const PIXELS_PER_INCH = 100.117 // Usually it is 100 pixels per inch but this give better results
+const PNG_CHUNK_IDAT = 1229209940;
+const LARGE_IMAGE_AREA = 30000000;
+
+const pdfExport = {
+ //NOTE: Key length must not be longer than 79 bytes (not checked)
+ writePngWithText(origBuff, key, text, compressed, base64encoded) {
+ let isDpi = key == 'dpi';
+ let inOffset = 0;
+ let outOffset = 0;
+ let data = text;
+ let 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
+
+ //prepare compressed data to get its size
+ if (compressed) {
+ data = zlib.deflateRawSync(encodeURIComponent(text));
+ dataLen = key.length + data.length + 2; //we add 2 zeros with compressed data
+ }
+
+ let outBuff = Buffer.allocUnsafe(origBuff.length + dataLen + 4); //4 is the header size "zTXt", "tEXt" or "pHYs"
+
+ try {
+ let magic1 = origBuff.readUInt32BE(inOffset);
+ inOffset += 4;
+ let magic2 = origBuff.readUInt32BE(inOffset);
+ inOffset += 4;
+
+ if (magic1 != 0x89504e47 && magic2 != 0x0d0a1a0a) {
+ throw new Error("PNGImageDecoder0");
+ }
+
+ outBuff.writeUInt32BE(magic1, outOffset);
+ outOffset += 4;
+ outBuff.writeUInt32BE(magic2, outOffset);
+ outOffset += 4;
+ } catch (e) {
+ loger.error(e.message, {stack: e.stack});
+ throw new Error("PNGImageDecoder1");
+ }
+
+ try {
+ while (inOffset < origBuff.length) {
+ let length = origBuff.readInt32BE(inOffset);
+ inOffset += 4;
+ let type = origBuff.readInt32BE(inOffset)
+ inOffset += 4;
+
+ if (type == PNG_CHUNK_IDAT) {
+ // Insert zTXt chunk before IDAT chunk
+ outBuff.writeInt32BE(dataLen, outOffset);
+ outOffset += 4;
+
+ let typeSignature = isDpi ? 'pHYs' : (compressed ? "zTXt" : "tEXt");
+ outBuff.write(typeSignature, outOffset);
+
+ outOffset += 4;
+
+ if (isDpi) {
+ let dpm = Math.round(parseInt(text) / 0.0254) || 3937; //One inch is equal to exactly 0.0254 meters. 3937 is 100dpi
+
+ outBuff.writeInt32BE(dpm, outOffset);
+ outBuff.writeInt32BE(dpm, outOffset + 4);
+ outBuff.writeInt8(1, outOffset + 8);
+ outOffset += 9;
+
+ data = Buffer.allocUnsafe(9);
+ data.writeInt32BE(dpm, 0);
+ data.writeInt32BE(dpm, 4);
+ data.writeInt8(1, 8);
+ } else {
+ outBuff.write(key, outOffset);
+ outOffset += key.length;
+ outBuff.writeInt8(0, outOffset);
+ outOffset++;
+
+ if (compressed) {
+ outBuff.writeInt8(0, outOffset);
+ outOffset++;
+ data.copy(outBuff, outOffset);
+ } else {
+ outBuff.write(data, outOffset);
+ }
+
+ outOffset += data.length;
+ }
+
+ let crcVal = 0xffffffff;
+ crcVal = crc.crcjam(typeSignature, crcVal);
+ crcVal = crc.crcjam(data, crcVal);
+
+ // CRC
+ outBuff.writeInt32BE(crcVal ^ 0xffffffff, outOffset);
+ outOffset += 4;
+
+ // Writes the IDAT chunk after the zTXt
+ outBuff.writeInt32BE(length, outOffset);
+ outOffset += 4;
+ outBuff.writeInt32BE(type, outOffset);
+ outOffset += 4;
+
+ origBuff.copy(outBuff, outOffset, inOffset);
+
+ // Encodes the buffer using base64 if requested
+ return base64encoded ? outBuff.toString('base64') : outBuff;
+ }
+
+ outBuff.writeInt32BE(length, outOffset);
+ outOffset += 4;
+ outBuff.writeInt32BE(type, outOffset);
+ outOffset += 4;
+
+ origBuff.copy(outBuff, outOffset, inOffset, inOffset + length + 4);// +4 to move past the crc
+
+ inOffset += length + 4;
+ outOffset += length + 4;
+ }
+ } catch (e) {
+ loger.error(e.message, {stack: e.stack});
+ throw e;
+ }
+ },
+
+ //TODO Create a lightweight html file similar to export3.html for exporting to vsdx
+ exportVsdx(event, args, directFinalize) {
+ let win = new BrowserWindow({
+ width: 1280,
+ height: 800,
+ show: false,
+ webPreferences: {
+ preload: path.join(__dirname, 'electron-preload.js'),
+ webSecurity: true,
+ nodeIntegration: true,
+ contextIsolation: true,
+ },
+ })
+
+ let loadEvtCount = 0;
+
+ function loadFinished() {
+ loadEvtCount++;
+
+ if (loadEvtCount == 2) {
+ win.webContents.send('export-vsdx', args);
+
+ ipcMain.once('export-vsdx-finished', (evt, data) => {
+ let hasError = false;
+
+ if (data == null) {
+ hasError = true;
+ }
+
+ //Set finalize here since it is call in the reply below
+ function finalize() {
+ win.destroy();
+ }
+
+ if (directFinalize === true) {
+ event.finalize = finalize;
+ } else {
+ //Destroy the window after response being received by caller
+ ipcMain.once('export-finalize', finalize);
+ }
+
+ if (hasError) {
+ event.reply('export-error');
+ } else {
+ event.reply('export-success', data);
+ }
+ });
+ }
+ }
+
+ //Order of these two events is not guaranteed, so wait for them async.
+ //TOOD There is still a chance we catch another window 'app-load-finished' if user created multiple windows quickly
+ ipcMain.once('app-load-finished', loadFinished);
+ win.webContents.on('did-finish-load', loadFinished);
+ },
+
+ async mergePdfs(pdfFiles, xml) {
+ //Pass throgh single files
+ if (pdfFiles.length == 1 && xml == null) {
+ return pdfFiles[0];
+ }
+
+ try {
+ const pdfDoc = await PDFDocument.create();
+ pdfDoc.setCreator(config.name);
+
+ if (xml != null) {
+ //Embed diagram XML as file attachment
+ await pdfDoc.attach(Buffer.from(xml).toString('base64'), config.name + '.xml', {
+ mimeType: 'application/vnd.jgraph.mxfile',
+ description: config.name + ' Content'
+ });
+ }
+
+ for (let i = 0; i < pdfFiles.length; i++) {
+ const pdfFile = await PDFDocument.load(pdfFiles[i].buffer);
+ const pages = await pdfDoc.copyPages(pdfFile, pdfFile.getPageIndices());
+ pages.forEach(p => pdfDoc.addPage(p));
+ }
+
+ const pdfBytes = await pdfDoc.save();
+ return Buffer.from(pdfBytes);
+ } catch (e) {
+ throw new Error('Error during PDF combination: ' + e.message);
+ }
+ },
+
+ exportDiagram(event, args, directFinalize) {
+ if (args.format == 'vsdx') {
+ pdfExport.exportVsdx(event, args, directFinalize);
+ return;
+ }
+
+ let browser = null;
+
+ try {
+ browser = new BrowserWindow({
+ webPreferences: {
+ preload: path.join(__dirname, 'electron-preload.js'),
+ backgroundThrottling: false,
+ contextIsolation: true,
+ disableBlinkFeatures: 'Auxclick' // Is this needed?
+ },
+ show: false,
+ frame: false,
+ enableLargerThanScreen: true,
+ transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'),
+ });
+
+ if (serverUrl) {
+ browser.loadURL(serverUrl + 'drawio/webapp/export3.html').then(_ => { }).catch(_ => { })
+ } else {
+ browser.loadFile('./public/drawio/webapp/export3.html').then(_ => { }).catch(_ => { })
+ }
+
+ const contents = browser.webContents;
+ let pageByPage = (args.format == 'pdf' && !args.print), from, to, pdfs;
+
+ if (pageByPage) {
+ from = args.allPages ? 0 : parseInt(args.from || 0);
+ to = args.allPages ? 1000 : parseInt(args.to || 1000) + 1; //The 'to' will be corrected later
+ pdfs = [];
+
+ args.from = from;
+ args.to = from;
+ args.allPages = false;
+ }
+
+ contents.on('did-finish-load', function () {
+ //Set finalize here since it is call in the reply below
+ function finalize() {
+ browser.destroy();
+ }
+
+ if (directFinalize === true) {
+ event.finalize = finalize;
+ } else {
+ //Destroy the window after response being received by caller
+ ipcMain.once('export-finalize', finalize);
+ }
+
+ function renderingFinishHandler(evt, renderInfo) {
+ if (renderInfo == null) {
+ event.reply('export-error');
+ return;
+ }
+
+ let pageCount = renderInfo.pageCount, bounds = null;
+ //For some reason, Electron 9 doesn't send this object as is without stringifying. Usually when variable is external to function own scope
+ try {
+ bounds = JSON.parse(renderInfo.bounds);
+ } catch (e) {
+ bounds = null;
+ }
+
+ let pdfOptions = {pageSize: 'A4'};
+ let hasError = false;
+
+ if (bounds == null || bounds.width < 5 || bounds.height < 5) //very small page size never return from printToPDF
+ {
+ //A workaround to detect errors in the input file or being empty file
+ hasError = true;
+ } else {
+ pdfOptions = {
+ printBackground: true,
+ pageSize: {
+ width: bounds.width / PIXELS_PER_INCH,
+ height: (bounds.height + 2) / PIXELS_PER_INCH //the extra 2 pixels to prevent adding an extra empty page
+ },
+ margins: {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0
+ } // no margin
+ }
+ }
+
+ let base64encoded = args.base64 == '1';
+
+ if (hasError) {
+ event.reply('export-error');
+ } else if (args.format == 'png' || args.format == 'jpg' || args.format == 'jpeg') {
+ //Adds an extra pixel to prevent scrollbars from showing
+ let newBounds = {
+ width: Math.ceil(bounds.width + bounds.x) + 1,
+ height: Math.ceil(bounds.height + bounds.y) + 1
+ };
+ browser.setBounds(newBounds);
+
+ //TODO The browser takes sometime to show the graph (also after resize it takes some time to render)
+ // 1 sec is most probably enough (for small images, 5 for large ones) BUT not a stable solution
+ setTimeout(function () {
+ browser.capturePage().then(function (img) {
+ //Image is double the given bounds, so resize is needed!
+ let tScale = 1;
+
+ //If user defined width and/or height, enforce it precisely here. Height override width
+ if (args.h) {
+ tScale = args.h / newBounds.height;
+ } else if (args.w) {
+ tScale = args.w / newBounds.width;
+ }
+
+ newBounds.width *= tScale;
+ newBounds.height *= tScale;
+ img = img.resize(newBounds);
+
+ let data = args.format == 'png' ? img.toPNG() : img.toJPEG(args.jpegQuality || 90);
+
+ if (args.dpi != null && args.format == 'png') {
+ data = pdfExport.writePngWithText(data, 'dpi', args.dpi);
+ }
+
+ if (args.embedXml == "1" && args.format == 'png') {
+ data = pdfExport.writePngWithText(data, "mxGraphModel", args.xml, true,
+ base64encoded);
+ } else {
+ if (base64encoded) {
+ data = data.toString('base64');
+ }
+ }
+
+ event.reply('export-success', data);
+ });
+ }, bounds.width * bounds.height < LARGE_IMAGE_AREA ? 1000 : 5000);
+ } else if (args.format == 'pdf') {
+ if (args.print) {
+ pdfOptions = {
+ scaleFactor: args.pageScale,
+ printBackground: true,
+ pageSize: {
+ width: args.pageWidth * MICRON_TO_PIXEL,
+ //This height adjustment fixes the output. TODO Test more cases
+ height: (args.pageHeight * 1.025) * MICRON_TO_PIXEL
+ },
+ marginsType: 1 // no margin
+ };
+
+ contents.print(pdfOptions, (success, errorType) => {
+ //Consider all as success
+ event.reply('export-success', {});
+ });
+ } else {
+ contents.printToPDF(pdfOptions).then(async (data) => {
+ pdfs.push(data);
+ to = to > pageCount ? pageCount : to;
+ from++;
+
+ if (from < to) {
+ args.from = from;
+ args.to = from;
+ ipcMain.once('render-finished', renderingFinishHandler);
+ contents.send('render', args);
+ } else {
+ data = await pdfExport.mergePdfs(pdfs, args.embedXml == '1' ? args.xml : null);
+ event.reply('export-success', data);
+ }
+ })
+ .catch((error) => {
+ event.reply('export-error', error);
+ });
+ }
+ } else if (args.format == 'svg') {
+ contents.send('get-svg-data');
+
+ ipcMain.once('svg-data', (evt, data) => {
+ event.reply('export-success', data);
+ });
+ } else {
+ event.reply('export-error', 'Error: Unsupported format');
+ }
+ }
+
+ ipcMain.once('render-finished', renderingFinishHandler);
+
+ if (args.format == 'xml') {
+ ipcMain.once('xml-data', (evt, data) => {
+ event.reply('export-success', data);
+ });
+
+ ipcMain.once('xml-data-error', () => {
+ event.reply('export-error');
+ });
+ }
+
+ args.border = args.border || 0;
+ args.scale = args.scale || 1;
+
+ contents.send('render', args);
+ });
+ } catch (e) {
+ if (browser != null) {
+ browser.destroy();
+ }
+
+ event.reply('export-error', e);
+ console.log('export-error', e);
+ }
+ },
+}
+
+const onExport = () => {
+ ipcMain.on('export', pdfExport.exportDiagram);
+}
+
+module.exports = {pdfExport, onExport};
diff --git a/electron/lib/renderer.js b/electron/lib/renderer.js
new file mode 100644
index 000000000..5e62e1286
--- /dev/null
+++ b/electron/lib/renderer.js
@@ -0,0 +1,675 @@
+const fs = require("fs");
+const {BrowserWindow, shell, app, dialog, clipboard, nativeImage, ipcMain} = require("electron");
+const fsProm = require("fs/promises");
+const path = require("path");
+const {spawn} = require("child_process");
+const config = require("../package.json");
+const electronDown = require("../electron-down");
+const loger = require("electron-log");
+const {allowedUrls, isWin} = require("./other");
+
+const {O_SYNC, O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY} = fs.constants;
+const DRAFT_PREFEX = '.$';
+const OLD_DRAFT_PREFEX = '~$';
+const DRAFT_EXT = '.dtmp';
+const BKP_PREFEX = '.$';
+const OLD_BKP_PREFEX = '~$';
+const BKP_EXT = '.bkp';
+
+let enableStoreBkp = true,
+ dialogOpen = false,
+ enablePlugins = false;
+
+const renderer = {
+ checkFileContent(body, enc) {
+ if (body != null) {
+ let head, headBinay;
+
+ if (typeof body === 'string') {
+ if (enc == 'base64') {
+ headBinay = Buffer.from(body.substring(0, 22), 'base64');
+ head = headBinay.toString();
+ } else {
+ head = body.substring(0, 16);
+ headBinay = Buffer.from(head);
+ }
+ } else {
+ head = new TextDecoder("utf-8").decode(body.subarray(0, 16));
+ headBinay = body;
+ }
+
+ let c1 = head[0],
+ c2 = head[1],
+ c3 = head[2],
+ c4 = head[3],
+ c5 = head[4],
+ c6 = head[5],
+ c7 = head[6],
+ c8 = head[7],
+ c9 = head[8],
+ c10 = head[9],
+ c11 = head[10],
+ c12 = head[11],
+ c13 = head[12],
+ c14 = head[13],
+ c15 = head[14],
+ c16 = head[15];
+
+ let cc1 = headBinay[0],
+ cc2 = headBinay[1],
+ cc3 = headBinay[2],
+ cc4 = headBinay[3],
+ cc5 = headBinay[4],
+ cc6 = headBinay[5],
+ cc7 = headBinay[6],
+ cc8 = headBinay[7],
+ cc9 = headBinay[8],
+ cc10 = headBinay[9],
+ cc11 = headBinay[10],
+ cc12 = headBinay[11],
+ cc13 = headBinay[12],
+ cc14 = headBinay[13],
+ cc15 = headBinay[14],
+ cc16 = headBinay[15];
+
+ if (c1 == '<') {
+ // text/html
+ if (c2 == '!'
+ || ((c2 == 'h'
+ && (c3 == 't' && c4 == 'm' && c5 == 'l'
+ || c3 == 'e' && c4 == 'a' && c5 == 'd')
+ || (c2 == 'b' && c3 == 'o' && c4 == 'd'
+ && c5 == 'y')))
+ || ((c2 == 'H'
+ && (c3 == 'T' && c4 == 'M' && c5 == 'L'
+ || c3 == 'E' && c4 == 'A' && c5 == 'D')
+ || (c2 == 'B' && c3 == 'O' && c4 == 'D'
+ && c5 == 'Y')))) {
+ return true;
+ }
+
+ // application/xml
+ if (c2 == '?' && c3 == 'x' && c4 == 'm' && c5 == 'l'
+ && c6 == ' ') {
+ return true;
+ }
+
+ // application/svg+xml
+ if (c2 == 's' && c3 == 'v' && c4 == 'g' && c5 == ' ') {
+ return true;
+ }
+ }
+
+ // big and little (identical) endian UTF-8 encodings, with BOM
+ // application/xml
+ if (cc1 == 0xef && cc2 == 0xbb && cc3 == 0xbf) {
+ if (c4 == '<' && c5 == '?' && c6 == 'x') {
+ return true;
+ }
+ }
+
+ // big and little endian UTF-16 encodings, with byte order mark
+ // application/xml
+ if (cc1 == 0xfe && cc2 == 0xff) {
+ if (cc3 == 0 && c4 == '<' && cc5 == 0 && c6 == '?' && cc7 == 0
+ && c8 == 'x') {
+ return true;
+ }
+ }
+
+ // application/xml
+ if (cc1 == 0xff && cc2 == 0xfe) {
+ if (c3 == '<' && cc4 == 0 && c5 == '?' && cc6 == 0 && c7 == 'x'
+ && cc8 == 0) {
+ return true;
+ }
+ }
+
+ // big and little endian UTF-32 encodings, with BOM
+ // application/xml
+ if (cc1 == 0x00 && cc2 == 0x00 && cc3 == 0xfe && cc4 == 0xff) {
+ if (cc5 == 0 && cc6 == 0 && cc7 == 0 && c8 == '<' && cc9 == 0
+ && cc10 == 0 && cc11 == 0 && c12 == '?' && cc13 == 0
+ && cc14 == 0 && cc15 == 0 && c16 == 'x') {
+ return true;
+ }
+ }
+
+ // application/xml
+ if (cc1 == 0xff && cc2 == 0xfe && cc3 == 0x00 && cc4 == 0x00) {
+ if (c5 == '<' && cc6 == 0 && cc7 == 0 && cc8 == 0 && c9 == '?'
+ && cc10 == 0 && cc11 == 0 && cc12 == 0 && c13 == 'x'
+ && cc14 == 0 && cc15 == 0 && cc16 == 0) {
+ return true;
+ }
+ }
+
+ // application/pdf (%PDF-)
+ if (cc1 == 37 && cc2 == 80 && cc3 == 68 && cc4 == 70 && cc5 == 45) {
+ return true;
+ }
+
+ // image/png
+ if ((cc1 == 137 && cc2 == 80 && cc3 == 78 && cc4 == 71 && cc5 == 13
+ && cc6 == 10 && cc7 == 26 && cc8 == 10) ||
+ (cc1 == 194 && cc2 == 137 && cc3 == 80 && cc4 == 78 && cc5 == 71 && cc6 == 13 //Our embedded PNG+XML
+ && cc7 == 10 && cc8 == 26 && cc9 == 10)) {
+ return true;
+ }
+
+ // image/jpeg
+ if (cc1 == 0xFF && cc2 == 0xD8 && cc3 == 0xFF) {
+ if (cc4 == 0xE0 || cc4 == 0xEE) {
+ return true;
+ }
+
+ /**
+ * File format used by digital cameras to store images.
+ * Exif Format can be read by any application supporting
+ * JPEG. Exif Spec can be found at:
+ * http://www.pima.net/standards/it10/PIMA15740/Exif_2-1.PDF
+ */
+ if ((cc4 == 0xE1) && (c7 == 'E' && c8 == 'x' && c9 == 'i'
+ && c10 == 'f' && cc11 == 0)) {
+ return true;
+ }
+ }
+
+ // vsdx, vssx (also zip, jar, odt, ods, odp, docx, xlsx, pptx, apk, aar)
+ if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x04) {
+ return true;
+ } else if (cc1 == 0x50 && cc2 == 0x4B && cc3 == 0x03 && cc4 == 0x06) {
+ return true;
+ }
+
+ // mxfile, mxlibrary, mxGraphModel
+ if (c1 == '<' && c2 == 'm' && c3 == 'x') {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ isConflict(origStat, stat) {
+ return stat != null && origStat != null && stat.mtimeMs != origStat.mtimeMs;
+ },
+
+ getDraftFileName(fileObject) {
+ let filePath = fileObject.path;
+ let draftFileName = '', counter = 1, uniquePart = '';
+
+ do {
+ draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
+ uniquePart = '_' + counter++;
+ } while (fs.existsSync(draftFileName));
+
+ return draftFileName;
+ },
+
+ async getFileDrafts(fileObject) {
+ let filePath = fileObject.path;
+ let draftsPaths = [], drafts = [], draftFileName, counter = 1, uniquePart = '';
+
+ do {
+ draftsPaths.push(draftFileName);
+ draftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
+ uniquePart = '_' + counter++;
+ } while (fs.existsSync(draftFileName)); //TODO this assume continuous drafts names
+
+ //Port old draft files to new prefex
+ counter = 1;
+ uniquePart = '';
+ let draftExists = false;
+
+ do {
+ draftFileName = path.join(path.dirname(filePath), OLD_DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
+ draftExists = fs.existsSync(draftFileName);
+
+ if (draftExists) {
+ const newDraftFileName = path.join(path.dirname(filePath), DRAFT_PREFEX + path.basename(filePath) + uniquePart + DRAFT_EXT);
+ await fsProm.rename(draftFileName, newDraftFileName);
+ draftsPaths.push(newDraftFileName);
+ }
+
+ uniquePart = '_' + counter++;
+ } while (draftExists); //TODO this assume continuous drafts names
+
+ //Skip the first null element
+ for (let i = 1; i < draftsPaths.length; i++) {
+ try {
+ let stat = await fsProm.lstat(draftsPaths[i]);
+ drafts.push({
+ data: await fsProm.readFile(draftsPaths[i], 'utf8'),
+ created: stat.ctimeMs,
+ modified: stat.mtimeMs,
+ path: draftsPaths[i]
+ });
+ } catch (e) {
+ } // Ignore
+ }
+
+ return drafts;
+ },
+
+ async saveDraft(fileObject, data) {
+ if (!renderer.checkFileContent(data)) {
+ throw new Error('Invalid file data');
+ } else {
+ let draftFileName = fileObject.draftFileName || renderer.getDraftFileName(fileObject);
+ await fsProm.writeFile(draftFileName, data, 'utf8');
+
+ if (isWin) {
+ try {
+ // Add Hidden attribute:
+ spawn('attrib', ['+h', draftFileName], {shell: true});
+ } catch (e) {
+ }
+ }
+
+ return draftFileName;
+ }
+ },
+
+ async saveFile(fileObject, data, origStat, overwrite, defEnc) {
+ if (!renderer.checkFileContent(data)) {
+ throw new Error('Invalid file data');
+ }
+
+ let retryCount = 0;
+ let backupCreated = false;
+ let bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
+ const oldBkpPath = path.join(path.dirname(fileObject.path), OLD_BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
+ let writeEnc = defEnc || fileObject.encoding;
+
+ let writeFile = async function () {
+ let fh;
+
+ try {
+ // O_SYNC is for sync I/O and reduce risk of file corruption
+ fh = await fsProm.open(fileObject.path, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
+ await fsProm.writeFile(fh, data, writeEnc);
+ } finally {
+ await fh?.close();
+ }
+
+ let stat2 = await fsProm.stat(fileObject.path);
+ // Workaround for possible writing errors is to check the written
+ // contents of the file and retry 3 times before showing an error
+ let writtenData = await fsProm.readFile(fileObject.path, writeEnc);
+
+ if (data != writtenData) {
+ retryCount++;
+
+ if (retryCount < 3) {
+ return await writeFile();
+ } else {
+ throw new Error('all saving trials failed');
+ }
+ } else {
+ //We'll keep the backup file in case the original file is corrupted. TODO When should we delete the backup file?
+ if (backupCreated) {
+ //fs.unlink(bkpPath, (err) => {}); //Ignore errors!
+
+ //Delete old backup file with old prefix
+ if (fs.existsSync(oldBkpPath)) {
+ fs.unlink(oldBkpPath, () => {
+ //Ignore errors
+ });
+ }
+ }
+
+ return stat2;
+ }
+ };
+
+ async function doSaveFile(isNew) {
+ if (enableStoreBkp && !isNew) {
+ //Copy file to back up file (after conflict and stat is checked)
+ let bkpFh;
+
+ try {
+ //Use file read then write to open the backup file direct sync write to reduce the chance of file corruption
+ let fileContent = await fsProm.readFile(fileObject.path, writeEnc);
+ bkpFh = await fsProm.open(bkpPath, O_SYNC | O_CREAT | O_WRONLY | O_TRUNC);
+ await fsProm.writeFile(bkpFh, fileContent, writeEnc);
+ backupCreated = true;
+ } catch (e) {
+ if (__DEV__) {
+ console.log('Backup file writing failed', e); //Ignore
+ }
+ } finally {
+ await bkpFh?.close();
+
+ if (isWin) {
+ try {
+ // Add Hidden attribute:
+ spawn('attrib', ['+h', bkpPath], {shell: true});
+ } catch (e) {
+ }
+ }
+ }
+ }
+
+ return await writeFile();
+ }
+
+ if (overwrite) {
+ return await doSaveFile(true);
+ } else {
+ let stat = fs.existsSync(fileObject.path) ?
+ await fsProm.stat(fileObject.path) : null;
+
+ if (stat && renderer.isConflict(origStat, stat)) {
+ throw new Error('conflict');
+ } else {
+ return await doSaveFile(stat == null);
+ }
+ }
+ },
+
+ async writeFile(path, data, enc) {
+ if (!renderer.checkFileContent(data, enc)) {
+ throw new Error('Invalid file data');
+ } else {
+ return await fsProm.writeFile(path, data, enc);
+ }
+ },
+
+ getAppDataFolder() {
+ try {
+ let appDataDir = app.getPath('appData');
+ let drawioDir = appDataDir + '/' + config.name;
+
+ if (!fs.existsSync(drawioDir)) //Usually this dir already exists
+ {
+ fs.mkdirSync(drawioDir);
+ }
+
+ return drawioDir;
+ } catch (e) {
+ }
+
+ return '.';
+ },
+
+ getDocumentsFolder() {
+ //On windows, misconfigured Documents folder cause an exception
+ try {
+ return app.getPath('documents');
+ } catch (e) {
+ }
+
+ return '.';
+ },
+
+ checkFileExists(pathParts) {
+ let filePath = path.join(...pathParts);
+ return {exists: fs.existsSync(filePath), path: filePath};
+ },
+
+ async showOpenDialog(defaultPath, filters, properties) {
+ let win = BrowserWindow.getFocusedWindow();
+
+ return dialog.showOpenDialog(win, {
+ defaultPath: defaultPath,
+ filters: filters,
+ properties: properties
+ });
+ },
+
+ async showSaveDialog(defaultPath, filters) {
+ let win = BrowserWindow.getFocusedWindow();
+
+ return dialog.showSaveDialog(win, {
+ defaultPath: defaultPath,
+ filters: filters
+ });
+ },
+
+ async installPlugin(filePath) {
+ if (!enablePlugins) return {};
+
+ let pluginsDir = path.join(renderer.getAppDataFolder(), '/plugins');
+
+ if (!fs.existsSync(pluginsDir)) {
+ fs.mkdirSync(pluginsDir);
+ }
+
+ let pluginName = path.basename(filePath);
+ let dstFile = path.join(pluginsDir, pluginName);
+
+ if (fs.existsSync(dstFile)) {
+ throw new Error('fileExists');
+ } else {
+ await fsProm.copyFile(filePath, dstFile);
+ }
+
+ return {pluginName: pluginName, selDir: path.dirname(filePath)};
+ },
+
+ getPluginFile(plugin) {
+ if (!enablePlugins) return null;
+
+ const prefix = path.join(renderer.getAppDataFolder(), '/plugins/');
+ const pluginFile = path.join(prefix, plugin);
+
+ if (pluginFile.startsWith(prefix) && fs.existsSync(pluginFile)) {
+ return pluginFile;
+ }
+
+ return null;
+ },
+
+ async uninstallPlugin(plugin) {
+ const pluginFile = renderer.getPluginFile(plugin);
+
+ if (pluginFile != null) {
+ fs.unlinkSync(pluginFile);
+ }
+ },
+
+ dirname(path_p) {
+ return path.dirname(path_p);
+ },
+
+ async readFile(filename, encoding) {
+ let data = await fsProm.readFile(filename, encoding);
+
+ if (renderer.checkFileContent(data, encoding)) {
+ return data;
+ }
+
+ throw new Error('Invalid file data');
+ },
+
+ async fileStat(file) {
+ return await fsProm.stat(file);
+ },
+
+ async isFileWritable(file) {
+ try {
+ await fsProm.access(file, fs.constants.W_OK);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ clipboardAction(method, data) {
+ if (method == 'writeText') {
+ clipboard.writeText(data);
+ } else if (method == 'readText') {
+ return clipboard.readText();
+ } else if (method == 'writeImage') {
+ clipboard.write({
+ image:
+ nativeImage.createFromDataURL(data.dataUrl), html: '
'
+ });
+ }
+ },
+
+ async deleteFile(file) {
+ // Reading the header of the file to confirm it is a file we can delete
+ let fh = await fsProm.open(file, O_RDONLY);
+ let buffer = Buffer.allocUnsafe(16);
+ await fh.read(buffer, 0, 16);
+ await fh.close();
+
+ if (renderer.checkFileContent(buffer)) {
+ await fsProm.unlink(file);
+ }
+ },
+
+ async windowAction(method) {
+ let win = BrowserWindow.getFocusedWindow();
+
+ if (win) {
+ if (method == 'minimize') {
+ win.minimize();
+ } else if (method == 'maximize') {
+ win.maximize();
+ } else if (method == 'unmaximize') {
+ win.unmaximize();
+ } else if (method == 'close') {
+ win.close();
+ } else if (method == 'isMaximized') {
+ return win.isMaximized();
+ } else if (method == 'removeAllListeners') {
+ win.removeAllListeners();
+ }
+ }
+ },
+
+ async openExternal(url) {
+ //Only open http(s), mailto, tel, and callto links
+ if (allowedUrls.test(url)) {
+ await shell.openExternal(url)
+ }
+ },
+
+ async watchFile(path) {
+ let win = BrowserWindow.getFocusedWindow();
+
+ if (win) {
+ fs.watchFile(path, (curr, prev) => {
+ try {
+ win.webContents.send('fileChanged', {
+ path: path,
+ curr: curr,
+ prev: prev
+ });
+ } catch (e) {
+ // Ignore
+ }
+ });
+ }
+ },
+
+ async unwatchFile(path) {
+ fs.unwatchFile(path);
+ },
+}
+
+const onRenderer = (mainWindow) => {
+ ipcMain.on("rendererReq", async (event, args) => {
+ try {
+ let ret = null;
+
+ switch (args.action) {
+ case 'saveFile':
+ ret = await renderer.saveFile(args.fileObject, args.data, args.origStat, args.overwrite, args.defEnc);
+ break;
+ case 'writeFile':
+ ret = await renderer.writeFile(args.path, args.data, args.enc);
+ break;
+ case 'saveDraft':
+ ret = await renderer.saveDraft(args.fileObject, args.data);
+ break;
+ case 'getFileDrafts':
+ ret = await renderer.getFileDrafts(args.fileObject);
+ break;
+ case 'getDocumentsFolder':
+ ret = await renderer.getDocumentsFolder();
+ break;
+ case 'checkFileExists':
+ ret = renderer.checkFileExists(args.pathParts);
+ break;
+ case 'showOpenDialog':
+ dialogOpen = true;
+ ret = await renderer.showOpenDialog(args.defaultPath, args.filters, args.properties);
+ ret = ret.filePaths;
+ dialogOpen = false;
+ break;
+ case 'showSaveDialog':
+ dialogOpen = true;
+ ret = await renderer.showSaveDialog(args.defaultPath, args.filters);
+ ret = ret.canceled ? null : ret.filePath;
+ dialogOpen = false;
+ break;
+ case 'installPlugin':
+ ret = await renderer.installPlugin(args.filePath);
+ break;
+ case 'uninstallPlugin':
+ ret = await renderer.uninstallPlugin(args.plugin);
+ break;
+ case 'getPluginFile':
+ ret = renderer.getPluginFile(args.plugin);
+ break;
+ case 'isPluginsEnabled':
+ ret = enablePlugins;
+ break;
+ case 'dirname':
+ ret = await renderer.dirname(args.path);
+ break;
+ case 'readFile':
+ ret = await renderer.readFile(args.filename, args.encoding);
+ break;
+ case 'clipboardAction':
+ ret = renderer.clipboardAction(args.method, args.data);
+ break;
+ case 'deleteFile':
+ ret = await renderer.deleteFile(args.file);
+ break;
+ case 'fileStat':
+ ret = await renderer.fileStat(args.file);
+ break;
+ case 'isFileWritable':
+ ret = await renderer.isFileWritable(args.file);
+ break;
+ case 'windowAction':
+ ret = await renderer.windowAction(args.method);
+ break;
+ case 'openExternal':
+ ret = await renderer.openExternal(args.url);
+ break;
+ case 'openDownloadWindow':
+ ret = await electronDown.open(args.language || 'zh', args.theme || 'light');
+ break;
+ case 'updateDownloadWindow':
+ ret = await electronDown.updateWindow(args.language, args.theme);
+ break;
+ case 'createDownload':
+ ret = await electronDown.createDownload(mainWindow, args.url, args.options || {});
+ break;
+ case 'watchFile':
+ ret = await renderer.watchFile(args.path);
+ break;
+ case 'unwatchFile':
+ ret = await renderer.unwatchFile(args.path);
+ break;
+ case 'getCurDir':
+ ret = __dirname;
+ break;
+ }
+
+ event.reply('mainResp', {success: true, data: ret, reqId: args.reqId});
+ } catch (e) {
+ event.reply('mainResp', {error: true, msg: e.message, e: e, reqId: args.reqId});
+ loger.error('Renderer request error', e.message, e.stack);
+ }
+ });
+}
+
+module.exports = { renderer, onRenderer };