api.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import gm from 'gm';
  2. import axios from 'axios';
  3. import sizeOf from 'image-size';
  4. import path from 'node:path';
  5. import fs from 'node:fs';
  6. import PDFDocument from 'pdfkit';
  7. import log from 'electron-log/renderer';
  8. import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas';
  9. import {
  10. getImagicPath,
  11. getTempPath,
  12. makeDirSync,
  13. getGmFontPath,
  14. getConfigData,
  15. } from './utils';
  16. import {
  17. DrawTrackItem,
  18. DrawTrackCircleOption,
  19. DrawTrackLineOption,
  20. DrawTrackTextOption,
  21. } from './types';
  22. GlobalFonts.registerFromPath(getGmFontPath(), 'song');
  23. // macos install gm imagemagick https://github.com/aheckmann/gm/blob/master/README.md
  24. const gmInst =
  25. process.platform === 'win32'
  26. ? gm.subClass({
  27. imageMagick: true,
  28. appPath: getImagicPath(),
  29. })
  30. : gm.subClass({ imageMagick: '7+' });
  31. function cropImage(imgPath: string): Promise<string> {
  32. return new Promise((resolve, reject) => {
  33. const outpath = path.join(getTempPath(), '001.png');
  34. gmInst(imgPath)
  35. .crop(500, 200, 0, 0)
  36. .write(outpath, (err) => {
  37. if (!err) {
  38. return resolve(outpath);
  39. }
  40. return reject(err);
  41. });
  42. });
  43. }
  44. function writeData(content: string) {
  45. const outpath = path.join(getTempPath(), 'task.txt');
  46. fs.writeFileSync(outpath, content, { flag: 'a' });
  47. }
  48. async function drawCanvasTrack(
  49. imgPath: string,
  50. drawTrackList: DrawTrackItem[],
  51. outpath: string
  52. ) {
  53. makeDirSync(path.dirname(outpath));
  54. const imgBuf = await fs.promises.readFile(imgPath);
  55. const curImage = await loadImage(imgBuf).catch((e) => {
  56. console.dir(e);
  57. });
  58. if (!curImage) return;
  59. const canvas = createCanvas(curImage.width, curImage.height);
  60. const ctx = canvas.getContext('2d');
  61. const defaultColor = '#f53f3f';
  62. const defaultFontSize = 22;
  63. ctx.drawImage(curImage, 0, 0, curImage.width, curImage.height);
  64. drawTrackList.forEach((track) => {
  65. // text
  66. if (track.type === 'text') {
  67. const { x, y, text, color, fontSize } =
  68. track.option as DrawTrackTextOption;
  69. const fsize = fontSize || defaultFontSize;
  70. const textColor = color || defaultColor;
  71. const ny = y + fsize;
  72. ctx.font = `bold ${fsize}px song`;
  73. ctx.fillStyle = textColor;
  74. ctx.fillText(text, x, ny);
  75. return;
  76. }
  77. // circle
  78. if (track.type === 'circle') {
  79. const { x0, y0, x1, y1 } = track.option as DrawTrackCircleOption;
  80. const radiusX = (x1 - x0) / 2;
  81. const radiusY = (y1 - y0) / 2;
  82. const centerX = x0 + radiusX;
  83. const centerY = y0 + radiusY;
  84. ctx.beginPath();
  85. ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);
  86. ctx.strokeStyle = defaultColor;
  87. ctx.lineWidth = 2;
  88. ctx.stroke();
  89. return;
  90. }
  91. // line
  92. if (track.type === 'line') {
  93. const { x0, y0, x1, y1 } = track.option as DrawTrackLineOption;
  94. ctx.beginPath();
  95. ctx.moveTo(x0, y0);
  96. ctx.lineTo(x1, y1);
  97. ctx.strokeStyle = defaultColor;
  98. ctx.lineWidth = 2;
  99. ctx.stroke();
  100. }
  101. });
  102. const imgBuffer = await canvas.encode('jpeg');
  103. await fs.promises.writeFile(outpath, imgBuffer);
  104. }
  105. function drawGmTrack(
  106. imgPath: string,
  107. drawTrackList: DrawTrackItem[],
  108. outpath: string
  109. ): Promise<string> {
  110. return new Promise((resolve, reject) => {
  111. const bname = path.basename(outpath);
  112. log.info(`开始绘制:${bname}`);
  113. const gmObj = gmInst(imgPath);
  114. makeDirSync(path.dirname(outpath));
  115. const defaultColor = '#f53f3f';
  116. const defaultFontSize = 22;
  117. gmObj.font(getGmFontPath());
  118. drawTrackList.forEach((track) => {
  119. // text
  120. if (track.type === 'text') {
  121. const { x, y, text, color, fontSize } =
  122. track.option as DrawTrackTextOption;
  123. const fsize = fontSize || defaultFontSize;
  124. const textColor = color || defaultColor;
  125. const ny = y + fsize;
  126. gmObj
  127. .stroke(textColor, 1)
  128. .fill(textColor)
  129. .fontSize(fsize)
  130. .drawText(x, ny, text);
  131. return;
  132. }
  133. // circle
  134. if (track.type === 'circle') {
  135. const { x0, y0, x1, y1 } = track.option as DrawTrackCircleOption;
  136. const rx = (x1 - x0) / 2;
  137. const ry = (y1 - y0) / 2;
  138. const cx = x0 + rx;
  139. const cy = y0 + ry;
  140. gmObj
  141. .stroke(defaultColor, 2)
  142. .fill('none')
  143. .drawEllipse(cx, cy, rx, ry, 0, 360);
  144. return;
  145. }
  146. // line
  147. if (track.type === 'line') {
  148. const { x0, y0, x1, y1 } = track.option as DrawTrackLineOption;
  149. gmObj.stroke(defaultColor, 2).fill('none').drawLine(x0, y0, x1, y1);
  150. }
  151. });
  152. gmObj.write(outpath, (err) => {
  153. log.info(`绘制完成:${bname}`);
  154. if (!err) {
  155. return resolve(outpath);
  156. }
  157. return reject(err);
  158. });
  159. });
  160. }
  161. interface TaskItem {
  162. url: string;
  163. outpath: string;
  164. drawTrackList: DrawTrackItem[];
  165. }
  166. async function drawTracks(taskList: TaskItem[]) {
  167. const downloads = taskList.map((item) =>
  168. drawCanvasTrack(item.url, item.drawTrackList, item.outpath)
  169. );
  170. await Promise.all(downloads);
  171. }
  172. async function downloadFile(url: string, outputPath: string): Promise<string> {
  173. return new Promise((resolve, reject) => {
  174. axios({
  175. url,
  176. method: 'GET',
  177. responseType: 'arraybuffer',
  178. })
  179. .then((response) => {
  180. fs.writeFileSync(outputPath, Buffer.from(response.data, 'binary'));
  181. resolve(outputPath);
  182. })
  183. .catch(() => {
  184. reject();
  185. });
  186. });
  187. }
  188. async function downloadImage(url: string, outputPath: string) {
  189. makeDirSync(path.dirname(outputPath));
  190. await downloadFile(url, outputPath);
  191. const size = sizeOf(outputPath);
  192. return {
  193. url: outputPath,
  194. width: size.width || 100,
  195. height: size.height || 100,
  196. };
  197. }
  198. function joinPath(paths: string[]) {
  199. return path.join(...paths);
  200. }
  201. function clearFilesSync(files: string[]) {
  202. files.forEach((item) => {
  203. if (fs.existsSync(item)) fs.unlinkSync(item);
  204. });
  205. }
  206. interface ImageItem {
  207. url: string;
  208. width: number;
  209. height: number;
  210. }
  211. async function imagesToPdf(
  212. images: ImageItem[],
  213. outpath: string
  214. ): Promise<string> {
  215. return new Promise((resolve, reject) => {
  216. const doc = new PDFDocument({ autoFirstPage: false });
  217. makeDirSync(path.dirname(outpath));
  218. const steam = fs.createWriteStream(outpath);
  219. doc.pipe(steam);
  220. images.forEach((image) => {
  221. const { url, width, height } = image;
  222. doc.addPage({ size: [width, height] });
  223. doc.image(url, 0, 0, { width, height });
  224. });
  225. doc.end();
  226. steam.on('finish', () => {
  227. resolve(outpath);
  228. });
  229. steam.on('error', (err) => {
  230. reject(err);
  231. });
  232. });
  233. }
  234. function logger(content: string, type?: 'info' | 'error') {
  235. if (type === 'error') {
  236. log.error(content);
  237. } else {
  238. log.info(content);
  239. }
  240. }
  241. const commonApi = {
  242. cropImage,
  243. drawGmTrack,
  244. drawCanvasTrack,
  245. drawTracks,
  246. joinPath,
  247. downloadImage,
  248. imagesToPdf,
  249. logger,
  250. getConfigData,
  251. writeData,
  252. clearFilesSync,
  253. };
  254. export type CommonApi = typeof commonApi;
  255. export default commonApi;