1204 lines
38 KiB
JavaScript
1204 lines
38 KiB
JavaScript
const fs = require('fs')
|
||
const fse = require('fs-extra')
|
||
const os = require("os");
|
||
const path = require('path')
|
||
const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell} = require('electron')
|
||
const log = require("electron-log");
|
||
const fsProm = require('fs/promises');
|
||
const PDFDocument = require('pdf-lib').PDFDocument;
|
||
const crc = require('crc');
|
||
const zlib = require('zlib');
|
||
const utils = require('./utils');
|
||
const config = require('./package.json');
|
||
|
||
let mainWindow = null,
|
||
subWindow = [],
|
||
willQuitApp = false,
|
||
devloadUrl = "",
|
||
devloadCachePath = path.resolve(__dirname, ".devload"),
|
||
downloadList = [],
|
||
downloadCacheFile = path.join(app.getPath('cache'), config.name, '.downloadCache');
|
||
|
||
if (fs.existsSync(devloadCachePath)) {
|
||
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
|
||
}
|
||
|
||
if (fs.existsSync(downloadCacheFile)) {
|
||
downloadList = utils.jsonParse(fs.readFileSync(downloadCacheFile, 'utf8'), [])
|
||
} else {
|
||
fse.ensureDirSync(path.join(app.getPath('cache'), config.name))
|
||
}
|
||
|
||
function downloadUpdate(item) {
|
||
const chain = item.getURLChain()
|
||
if (chain.length == 0) {
|
||
return
|
||
}
|
||
let currentState = item.getState()
|
||
if (currentState == "progressing" && item.isPaused()) {
|
||
currentState = "paused"
|
||
}
|
||
//
|
||
const downloadItem = downloadList.find(item => item.url == chain[0])
|
||
if (downloadItem && downloadItem.state != currentState) {
|
||
downloadItem.state = currentState;
|
||
downloadItem.result = {
|
||
url: item.getURL(),
|
||
name: item.getFilename(),
|
||
savePath: item.getSavePath(),
|
||
mimeType: item.getMimeType(),
|
||
totalBytes: item.getTotalBytes(),
|
||
chain,
|
||
};
|
||
fs.writeFileSync(downloadCacheFile, utils.jsonStringify(downloadList), 'utf8');
|
||
//
|
||
if (currentState == 'completed') {
|
||
mainWindow.webContents.send("downloadDone", downloadItem)
|
||
log.info("下载完成", downloadItem)
|
||
} else {
|
||
mainWindow.webContents.send("downloadUpdate", downloadItem)
|
||
log.info("下载更新", downloadItem)
|
||
}
|
||
}
|
||
}
|
||
|
||
function createMainWindow() {
|
||
mainWindow = new BrowserWindow({
|
||
width: 1280,
|
||
height: 800,
|
||
center: true,
|
||
autoHideMenuBar: true,
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'electron-preload.js'),
|
||
webSecurity: true,
|
||
nodeIntegration: true,
|
||
nodeIntegrationInSubFrames: true,
|
||
contextIsolation: true,
|
||
nativeWindowOpen: true
|
||
}
|
||
})
|
||
mainWindow.webContents.setUserAgent(mainWindow.webContents.getUserAgent() + " MainTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
|
||
|
||
if (devloadUrl) {
|
||
mainWindow.loadURL(devloadUrl).then(r => {
|
||
|
||
})
|
||
} else {
|
||
mainWindow.loadFile('./public/index.html').then(r => {
|
||
|
||
})
|
||
}
|
||
|
||
mainWindow.on('page-title-updated', (event, title) => {
|
||
if (title == "index.html") {
|
||
event.preventDefault()
|
||
}
|
||
})
|
||
|
||
mainWindow.on('close', event => {
|
||
if (!willQuitApp) {
|
||
utils.onBeforeUnload(event, app)
|
||
}
|
||
})
|
||
|
||
mainWindow.webContents.session.on('will-download', (event, item) => {
|
||
item.setSavePath(path.join(app.getPath('cache'), config.name, item.getFilename()));
|
||
item.on('updated', () => {
|
||
downloadUpdate(item)
|
||
})
|
||
item.on('done', () => {
|
||
downloadUpdate(item)
|
||
})
|
||
})
|
||
}
|
||
|
||
function createSubWindow(args) {
|
||
if (!args) {
|
||
return;
|
||
}
|
||
|
||
if (typeof args !== "object") {
|
||
args = {
|
||
path: args,
|
||
config: {},
|
||
}
|
||
}
|
||
|
||
let name = args.name || "auto_" + utils.randomString(6);
|
||
let item = subWindow.find(item => item.name == name);
|
||
let browser = item ? item.browser : null;
|
||
if (browser) {
|
||
browser.focus();
|
||
if (args.force === false) {
|
||
return;
|
||
}
|
||
} else {
|
||
let config = args.config || {};
|
||
if (typeof args.title !== "undefined") {
|
||
config.title = args.title;
|
||
}
|
||
browser = new BrowserWindow(Object.assign({
|
||
width: 1280,
|
||
height: 800,
|
||
center: true,
|
||
parent: mainWindow,
|
||
autoHideMenuBar: true,
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'electron-preload.js'),
|
||
devTools: args.devTools !== false,
|
||
webSecurity: true,
|
||
nodeIntegration: true,
|
||
nodeIntegrationInSubFrames: true,
|
||
contextIsolation: true,
|
||
nativeWindowOpen: true
|
||
},
|
||
}, config))
|
||
browser.on('page-title-updated', (event, title) => {
|
||
if (title == "index.html" || args.titleFixed === true) {
|
||
event.preventDefault()
|
||
}
|
||
})
|
||
|
||
browser.on('close', event => {
|
||
utils.onBeforeUnload(event)
|
||
})
|
||
|
||
browser.on('closed', () => {
|
||
let index = subWindow.findIndex(item => item.name == name);
|
||
if (index > -1) {
|
||
subWindow.splice(index, 1)
|
||
}
|
||
})
|
||
|
||
subWindow.push({ name, browser })
|
||
}
|
||
browser.webContents.setUserAgent(browser.webContents.getUserAgent() + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0" + (args.userAgent ? (" " + args.userAgent) : ""));
|
||
|
||
if (devloadUrl) {
|
||
browser.loadURL(devloadUrl + '#' + (args.hash || args.path)).then(r => {
|
||
|
||
})
|
||
} else {
|
||
browser.loadFile('./public/index.html', {
|
||
hash: args.hash || args.path
|
||
}).then(r => {
|
||
|
||
})
|
||
}
|
||
}
|
||
|
||
app.whenReady().then(() => {
|
||
createMainWindow()
|
||
|
||
app.on('activate', () => {
|
||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||
})
|
||
})
|
||
|
||
app.on('window-all-closed', () => {
|
||
if (process.platform !== 'darwin') {
|
||
app.quit()
|
||
}
|
||
})
|
||
|
||
app.on('before-quit', () => {
|
||
willQuitApp = true
|
||
})
|
||
|
||
/**
|
||
* 下载文件
|
||
* @param args {url}
|
||
*/
|
||
ipcMain.on('downloadFile', (event, args) => {
|
||
event.returnValue = "ok"
|
||
//
|
||
let appendJson = {state: "progressing", startTime: utils.Time()}
|
||
let downloadItem = downloadList.find(({url}) => url == args.url)
|
||
if (downloadItem) {
|
||
switch (downloadItem.state) {
|
||
case "completed":
|
||
if (fs.existsSync(downloadItem.result.savePath)) { // 下载完成,文件存在
|
||
log.info("下载已完成", downloadItem)
|
||
mainWindow.webContents.send("downloadDone", downloadItem)
|
||
return
|
||
}
|
||
break;
|
||
case "progressing":
|
||
if (downloadItem.startTime + 480 > utils.Time()) { // 下载中,未超时(超时时间8分钟)
|
||
log.info("下载已存在", downloadItem)
|
||
return;
|
||
}
|
||
break;
|
||
}
|
||
downloadItem = Object.assign(downloadItem, appendJson)
|
||
} else {
|
||
downloadList.push(downloadItem = Object.assign(args, appendJson))
|
||
}
|
||
fs.writeFileSync(downloadCacheFile, utils.jsonStringify(downloadList), 'utf8');
|
||
mainWindow.webContents.downloadURL(downloadItem.url);
|
||
log.info("下载开始", downloadItem)
|
||
})
|
||
|
||
/**
|
||
* 打开文件
|
||
* @param args {path}
|
||
*/
|
||
ipcMain.on('openFile', (event, args) => {
|
||
utils.openFile(args.path)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 退出客户端
|
||
*/
|
||
ipcMain.on('windowQuit', (event) => {
|
||
event.returnValue = "ok"
|
||
app.quit();
|
||
})
|
||
|
||
/**
|
||
* 创建路由窗口
|
||
* @param args {path, ?}
|
||
*/
|
||
ipcMain.on('windowRouter', (event, args) => {
|
||
createSubWindow(args)
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 隐藏窗口(mac隐藏,其他关闭)
|
||
*/
|
||
ipcMain.on('windowHidden', (event) => {
|
||
if (process.platform === 'darwin') {
|
||
app.hide();
|
||
} else {
|
||
app.quit();
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 关闭窗口
|
||
*/
|
||
ipcMain.on('windowClose', (event) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
win.close()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 关闭窗口(强制)
|
||
*/
|
||
ipcMain.on('windowDestroy', (event) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
win.destroy()
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 设置窗口尺寸
|
||
* @param args {width, height, autoZoom, minWidth, minHeight, maxWidth, maxHeight}
|
||
*/
|
||
ipcMain.on('windowSize', (event, args) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
if (args.width || args.height) {
|
||
let [w, h] = win.getSize()
|
||
const width = args.width || w
|
||
const height = args.height || h
|
||
win.setSize(width, height, args.animate === true)
|
||
//
|
||
if (args.autoZoom === true) {
|
||
let move = false
|
||
let [x, y] = win.getPosition()
|
||
if (Math.abs(width - w) > 10) {
|
||
move = true
|
||
x -= (width - w) / 2
|
||
}
|
||
if (Math.abs(height - h) > 10) {
|
||
move = true
|
||
y -= (height - h) / 2
|
||
}
|
||
if (move) {
|
||
win.setPosition(Math.max(0, Math.floor(x)), Math.max(0, Math.floor(y)))
|
||
}
|
||
}
|
||
}
|
||
if (args.minWidth || args.minHeight) {
|
||
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
|
||
}
|
||
if (args.maxWidth || args.maxHeight) {
|
||
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
|
||
}
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 设置窗口最小尺寸
|
||
* @param args {minWidth, minHeight}
|
||
*/
|
||
ipcMain.on('windowMinSize', (event, args) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 设置窗口最大尺寸
|
||
* @param args {maxWidth, maxHeight}
|
||
*/
|
||
ipcMain.on('windowMaxSize', (event, args) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 窗口居中
|
||
*/
|
||
ipcMain.on('windowCenter', (event) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) {
|
||
win.center();
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 窗口最大化或恢复
|
||
*/
|
||
ipcMain.on('windowMax', (event) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win.isMaximized()) {
|
||
win.restore();
|
||
} else {
|
||
win.maximize();
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 给主窗口发送信息
|
||
* @param args {channel, data}
|
||
*/
|
||
ipcMain.on('sendForwardMain', (event, args) => {
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send(args.channel, args.data)
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
/**
|
||
* 设置Dock标记
|
||
* @param args
|
||
*/
|
||
ipcMain.on('setDockBadge', (event, args) => {
|
||
if(process.platform !== 'darwin'){
|
||
// Mac only
|
||
return;
|
||
}
|
||
if (utils.runNum(args) > 0) {
|
||
app.dock.setBadge(String(args))
|
||
} else {
|
||
app.dock.setBadge("")
|
||
}
|
||
event.returnValue = "ok"
|
||
})
|
||
|
||
//================================================================
|
||
// Pdf export
|
||
//================================================================
|
||
|
||
const MICRON_TO_PIXEL = 264.58 //264.58 micron = 1 pixel
|
||
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) {
|
||
log.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) {
|
||
log.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,
|
||
nativeWindowOpen: 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,
|
||
nativeWindowOpen: true
|
||
},
|
||
show: false,
|
||
frame: false,
|
||
enableLargerThanScreen: true,
|
||
transparent: args.format == 'png' && (args.bg == null || args.bg == 'none'),
|
||
});
|
||
|
||
browser.loadURL(`file://${__dirname}/export3.html`);
|
||
|
||
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 {
|
||
//Chrome generates Pdf files larger than requested pixels size and requires scaling
|
||
let fixingScale = 0.959;
|
||
|
||
let w = Math.ceil(bounds.width * fixingScale);
|
||
|
||
// +0.1 fixes cases where adding 1px below is not enough
|
||
// Increase this if more cropped PDFs have extra empty pages
|
||
let h = Math.ceil(bounds.height * fixingScale + 0.1);
|
||
|
||
pdfOptions = {
|
||
printBackground: true,
|
||
pageSize: {
|
||
width: w * MICRON_TO_PIXEL,
|
||
height: (h + 2) * MICRON_TO_PIXEL //the extra 2 pixels to prevent adding an extra empty page
|
||
},
|
||
marginsType: 1 // 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 {COPYFILE_EXCL} = fs.constants;
|
||
const DRAFT_PREFEX = '~$';
|
||
const DRAFT_EXT = '.dtmp';
|
||
const BKP_PREFEX = '~$';
|
||
const BKP_EXT = '.bkp';
|
||
|
||
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
|
||
|
||
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 (data == null || data.length == 0) {
|
||
throw new Error('empty data');
|
||
} else {
|
||
let draftFileName = fileObject.draftFileName || getDraftFileName(fileObject);
|
||
await fsProm.writeFile(draftFileName, data, 'utf8');
|
||
return draftFileName;
|
||
}
|
||
}
|
||
|
||
async function saveFile(fileObject, data, origStat, overwrite, defEnc) {
|
||
let retryCount = 0;
|
||
let backupCreated = false;
|
||
let bkpPath = path.join(path.dirname(fileObject.path), BKP_PREFEX + path.basename(fileObject.path) + BKP_EXT);
|
||
|
||
let writeFile = async function () {
|
||
if (data == null || data.length == 0) {
|
||
throw new Error('empty data');
|
||
} else {
|
||
let writeEnc = defEnc || fileObject.encoding;
|
||
|
||
await fsProm.writeFile(fileObject.path, data, writeEnc);
|
||
let stat2 = await fsProm.stat(fileObject.path);
|
||
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 {
|
||
if (backupCreated) {
|
||
fs.unlink(bkpPath, (err) => {
|
||
}); //Ignore errors!
|
||
}
|
||
|
||
return stat2;
|
||
}
|
||
}
|
||
};
|
||
|
||
async function doSaveFile() {
|
||
try {
|
||
await fsProm.copyFile(fileObject.path, bkpPath, COPYFILE_EXCL);
|
||
backupCreated = true;
|
||
} catch (e) {
|
||
} //Ignore
|
||
|
||
return await writeFile();
|
||
}
|
||
|
||
if (overwrite) {
|
||
return await doSaveFile();
|
||
} else {
|
||
let stat = fs.existsSync(fileObject.path) ?
|
||
await fsProm.stat(fileObject.path) : null;
|
||
|
||
if (stat && isConflict(origStat, stat)) {
|
||
new Error('conflict');
|
||
} else {
|
||
return await doSaveFile();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function writeFile(path, data, enc) {
|
||
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() {
|
||
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) {
|
||
return dialog.showOpenDialogSync({
|
||
defaultPath: defaultPath,
|
||
filters: filters,
|
||
properties: properties
|
||
});
|
||
}
|
||
|
||
async function showSaveDialog(defaultPath, filters) {
|
||
return dialog.showSaveDialogSync({
|
||
defaultPath: defaultPath,
|
||
filters: filters
|
||
});
|
||
}
|
||
|
||
async function installPlugin(filePath) {
|
||
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 uninstallPlugin(plugin) {
|
||
let pluginsFile = path.join(getAppDataFolder(), '/plugins', plugin);
|
||
|
||
if (fs.existsSync(pluginsFile)) {
|
||
fs.unlinkSync(pluginsFile);
|
||
}
|
||
return null
|
||
}
|
||
|
||
function dirname(path_p) {
|
||
return path.dirname(path_p);
|
||
}
|
||
|
||
async function readFile(filename, encoding) {
|
||
return await fsProm.readFile(filename, encoding);
|
||
}
|
||
|
||
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: '<img src="' +
|
||
data.dataUrl + '" width="' + data.w + '" height="' + data.h + '">'
|
||
});
|
||
}
|
||
}
|
||
|
||
async function deleteFile(file) {
|
||
await fsProm.unlink(file);
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
function openExternal(url) {
|
||
shell.openExternal(url).then(() => {}).catch(() => {});
|
||
return null
|
||
}
|
||
|
||
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
|
||
});
|
||
}
|
||
return null
|
||
}
|
||
|
||
function unwatchFile(path) {
|
||
fs.unwatchFile(path);
|
||
return null
|
||
}
|
||
|
||
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 'getAppDataFolder':
|
||
ret = getAppDataFolder();
|
||
break;
|
||
case 'getDocumentsFolder':
|
||
ret = await getDocumentsFolder();
|
||
break;
|
||
case 'checkFileExists':
|
||
ret = checkFileExists(args.pathParts);
|
||
break;
|
||
case 'showOpenDialog':
|
||
ret = await showOpenDialog(args.defaultPath, args.filters, args.properties);
|
||
break;
|
||
case 'showSaveDialog':
|
||
ret = await showSaveDialog(args.defaultPath, args.filters);
|
||
break;
|
||
case 'installPlugin':
|
||
ret = await installPlugin(args.filePath);
|
||
break;
|
||
case 'uninstallPlugin':
|
||
ret = await uninstallPlugin(args.plugin);
|
||
break;
|
||
case 'dirname':
|
||
ret = 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 = windowAction(args.method);
|
||
break;
|
||
case 'openExternal':
|
||
ret = await openExternal(args.url);
|
||
break;
|
||
case 'watchFile':
|
||
ret = await watchFile(args.path);
|
||
break;
|
||
case 'unwatchFile':
|
||
ret = await unwatchFile(args.path);
|
||
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});
|
||
}
|
||
});
|