mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-21 08:28:12 +00:00
将 electron.js 中的 PDF 导出、渲染器辅助函数和工具函数拆分为独立模块: - electron/lib/pdf-export.js: PDF 导出相关功能 - electron/lib/renderer.js: 渲染器辅助函数 - electron/lib/other.js: 平台检测和 URL 验证常量 此重构提高了代码可维护性,减少了主文件的复杂度。
440 lines
18 KiB
JavaScript
Vendored
440 lines
18 KiB
JavaScript
Vendored
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};
|