import gm from 'gm'; import axios from 'axios'; import sizeOf from 'image-size'; import path from 'node:path'; import fs from 'node:fs'; import PDFDocument from 'pdfkit'; import log from 'electron-log/renderer'; import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas'; import { getImagicPath, getTempPath, makeDirSync, getGmFontPath, getConfigData, } from './utils'; import { DrawTrackItem, DrawTrackCircleOption, DrawTrackLineOption, DrawTrackTextOption, } from './types'; GlobalFonts.registerFromPath(getGmFontPath(), 'song'); // macos install gm imagemagick https://github.com/aheckmann/gm/blob/master/README.md const gmInst = process.platform === 'win32' ? gm.subClass({ imageMagick: true, appPath: getImagicPath(), }) : gm.subClass({ imageMagick: '7+' }); function cropImage(imgPath: string): Promise { return new Promise((resolve, reject) => { const outpath = path.join(getTempPath(), '001.png'); gmInst(imgPath) .crop(500, 200, 0, 0) .write(outpath, (err) => { if (!err) { return resolve(outpath); } return reject(err); }); }); } function writeData(content: string) { const outpath = path.join(getTempPath(), 'task.txt'); fs.writeFileSync(outpath, content, { flag: 'a' }); } async function drawCanvasTrack( imgPath: string, drawTrackList: DrawTrackItem[], outpath: string ) { makeDirSync(path.dirname(outpath)); const imgBuf = await fs.promises.readFile(imgPath); const curImage = await loadImage(imgBuf).catch((e) => { console.dir(e); }); if (!curImage) return; const canvas = createCanvas(curImage.width, curImage.height); const ctx = canvas.getContext('2d'); const defaultColor = '#f53f3f'; const defaultFontSize = 22; ctx.drawImage(curImage, 0, 0, curImage.width, curImage.height); drawTrackList.forEach((track) => { // text if (track.type === 'text') { const { x, y, text, color, fontSize } = track.option as DrawTrackTextOption; const fsize = fontSize || defaultFontSize; const textColor = color || defaultColor; const ny = y + fsize; ctx.font = `bold ${fsize}px song`; ctx.fillStyle = textColor; ctx.fillText(text, x, ny); return; } // circle if (track.type === 'circle') { const { x0, y0, x1, y1 } = track.option as DrawTrackCircleOption; const radiusX = (x1 - x0) / 2; const radiusY = (y1 - y0) / 2; const centerX = x0 + radiusX; const centerY = y0 + radiusY; ctx.beginPath(); ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); ctx.strokeStyle = defaultColor; ctx.lineWidth = 2; ctx.stroke(); return; } // line if (track.type === 'line') { const { x0, y0, x1, y1 } = track.option as DrawTrackLineOption; ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.strokeStyle = defaultColor; ctx.lineWidth = 2; ctx.stroke(); } }); const imgBuffer = await canvas.encode('jpeg'); await fs.promises.writeFile(outpath, imgBuffer); } function drawGmTrack( imgPath: string, drawTrackList: DrawTrackItem[], outpath: string ): Promise { return new Promise((resolve, reject) => { const bname = path.basename(outpath); log.info(`开始绘制:${bname}`); const gmObj = gmInst(imgPath); makeDirSync(path.dirname(outpath)); const defaultColor = '#f53f3f'; const defaultFontSize = 22; gmObj.font(getGmFontPath()); drawTrackList.forEach((track) => { // text if (track.type === 'text') { const { x, y, text, color, fontSize } = track.option as DrawTrackTextOption; const fsize = fontSize || defaultFontSize; const textColor = color || defaultColor; const ny = y + fsize; gmObj .stroke(textColor, 1) .fill(textColor) .fontSize(fsize) .drawText(x, ny, text); return; } // circle if (track.type === 'circle') { const { x0, y0, x1, y1 } = track.option as DrawTrackCircleOption; const rx = (x1 - x0) / 2; const ry = (y1 - y0) / 2; const cx = x0 + rx; const cy = y0 + ry; gmObj .stroke(defaultColor, 2) .fill('none') .drawEllipse(cx, cy, rx, ry, 0, 360); return; } // line if (track.type === 'line') { const { x0, y0, x1, y1 } = track.option as DrawTrackLineOption; gmObj.stroke(defaultColor, 2).fill('none').drawLine(x0, y0, x1, y1); } }); gmObj.write(outpath, (err) => { log.info(`绘制完成:${bname}`); if (!err) { return resolve(outpath); } return reject(err); }); }); } interface TaskItem { url: string; outpath: string; drawTrackList: DrawTrackItem[]; } async function drawTracks(taskList: TaskItem[]) { const downloads = taskList.map((item) => drawCanvasTrack(item.url, item.drawTrackList, item.outpath) ); await Promise.all(downloads); } async function downloadFile(url: string, outputPath: string): Promise { return new Promise((resolve, reject) => { axios({ url, method: 'GET', responseType: 'arraybuffer', }) .then((response) => { fs.writeFileSync(outputPath, Buffer.from(response.data, 'binary')); resolve(outputPath); }) .catch(() => { reject(); }); }); } async function downloadImage(url: string, outputPath: string) { makeDirSync(path.dirname(outputPath)); await downloadFile(url, outputPath); const size = sizeOf(outputPath); return { url: outputPath, width: size.width || 100, height: size.height || 100, }; } function joinPath(paths: string[]) { return path.join(...paths); } function clearFilesSync(files: string[]) { files.forEach((item) => { if (fs.existsSync(item)) fs.unlinkSync(item); }); } interface ImageItem { url: string; width: number; height: number; } async function imagesToPdf( images: ImageItem[], outpath: string ): Promise { return new Promise((resolve, reject) => { const doc = new PDFDocument({ autoFirstPage: false }); makeDirSync(path.dirname(outpath)); const steam = fs.createWriteStream(outpath); doc.pipe(steam); images.forEach((image) => { const { url, width, height } = image; doc.addPage({ size: [width, height] }); doc.image(url, 0, 0, { width, height }); }); doc.end(); steam.on('finish', () => { resolve(outpath); }); steam.on('error', (err) => { reject(err); }); }); } function logger(content: string, type?: 'info' | 'error') { if (type === 'error') { log.error(content); } else { log.info(content); } } const commonApi = { cropImage, drawGmTrack, drawCanvasTrack, drawTracks, joinPath, downloadImage, imagesToPdf, logger, getConfigData, writeData, clearFilesSync, }; export type CommonApi = typeof commonApi; export default commonApi;