From b01629a5e6d524af8627a61929571ca094f92525 Mon Sep 17 00:00:00 2001 From: hymbz Date: Sun, 28 Jul 2024 14:03:54 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20:zap:=20=E4=BD=BF=E7=94=A8=20OffscreenC?= =?UTF-8?q?anvas=20=E9=87=8D=E6=9E=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .xo-config.cjs | 7 +++ .../Manga/actions/translation/cotrans.ts | 42 +++------------ src/helper/detectAd.ts | 40 +++++--------- src/helper/index.ts | 53 +++++++++++++------ src/site/jm.tsx | 5 +- 5 files changed, 68 insertions(+), 79 deletions(-) diff --git a/.xo-config.cjs b/.xo-config.cjs index ccbcf091..fc9cc641 100644 --- a/.xo-config.cjs +++ b/.xo-config.cjs @@ -70,6 +70,7 @@ module.exports = { // 使用 process "n/prefer-global/process": ["error", "always"], + // // 项目特有的规则 // @@ -122,6 +123,12 @@ module.exports = { "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", + // 允许 return await + "@typescript-eslint/return-await": [ + "off", + "error-handling-correctness-only", + ], + // 在判断类型时允许使用 || "@typescript-eslint/prefer-nullish-coalescing": [ "error", diff --git a/src/components/Manga/actions/translation/cotrans.ts b/src/components/Manga/actions/translation/cotrans.ts index e4332414..a72b4a42 100644 --- a/src/components/Manga/actions/translation/cotrans.ts +++ b/src/components/Manga/actions/translation/cotrans.ts @@ -1,6 +1,6 @@ import { t } from 'helper/i18n'; import { log } from 'helper/logger'; -import { canvasToBlob } from 'helper'; +import { canvasToBlob, waitImgLoad } from 'helper'; import { store } from '../../store'; @@ -63,31 +63,16 @@ const waitTranslation = (id: string, i: number) => { /** 将翻译后的内容覆盖到原图上 */ const mergeImage = async (rawImage: Blob, maskUri: string) => { - const canvas = document.createElement('canvas'); + const img = await waitImgLoad(URL.createObjectURL(rawImage)); + const canvas = new OffscreenCanvas(img.width, img.height); const canvasCtx = canvas.getContext('2d')!; - - const img = new Image(); - img.src = URL.createObjectURL(rawImage); - await new Promise((resolve, reject) => { - img.onload = () => { - canvas.width = img.width; - canvas.height = img.height; - canvasCtx.drawImage(img, 0, 0); - resolve(null); - }; - - img.onerror = reject; - }); + canvasCtx.drawImage(img, 0, 0); const img2 = new Image(); img2.src = maskUri; img2.crossOrigin = 'anonymous'; - await new Promise((resolve) => { - img2.onload = () => { - canvasCtx.drawImage(img2, 0, 0); - resolve(null); - }; - }); + await waitImgLoad(img2); + canvasCtx.drawImage(img2, 0, 0); return URL.createObjectURL(await canvasToBlob(canvas)); }; @@ -96,23 +81,12 @@ const mergeImage = async (rawImage: Blob, maskUri: string) => { const resize = async (blob: Blob, w: number, h: number): Promise => { if (w <= 4096 && h <= 4096) return blob; - const img = new Image(); - img.src = URL.createObjectURL(blob); - await new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; - }); - - if (w <= 4096 && h <= 4096) return blob; - const scale = Math.min(4096 / w, 4096 / h); const width = Math.floor(w * scale); const height = Math.floor(h * scale); - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - + const img = await waitImgLoad(URL.createObjectURL(blob)); + const canvas = new OffscreenCanvas(width, height); const ctx = canvas.getContext('2d')!; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(img, 0, 0, width, height); diff --git a/src/helper/detectAd.ts b/src/helper/detectAd.ts index e7c808ca..f2d545a5 100644 --- a/src/helper/detectAd.ts +++ b/src/helper/detectAd.ts @@ -1,4 +1,4 @@ -import { log, request, wait } from 'main'; +import { log, request, wait, waitImgLoad } from 'main'; import QrScanner from 'qr-scanner'; import type { AsyncReturnType } from 'type-fest'; @@ -46,12 +46,10 @@ const isGrayscalePixel = (r: number, g: number, b: number) => r === g && r === b; /** 判断一张图是否是彩图 */ -const isColorImg = (imgCanvas: HTMLCanvasElement) => { - const canvas = document.createElement('canvas'); +const isColorImg = (imgCanvas: HTMLCanvasElement | OffscreenCanvas) => { // 缩小尺寸放弃细节,避免被黑白图上的小段彩色文字干扰 - canvas.width = 3; - canvas.height = 3; - const ctx = canvas.getContext('2d')!; + const canvas = new OffscreenCanvas(3, 3); + const ctx = canvas.getContext('2d', { alpha: false })!; ctx.drawImage(imgCanvas, 0, 0, canvas.width, canvas.height); const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -70,10 +68,7 @@ const imgToCanvas = async (img: HTMLImageElement | string) => { await wait(() => img.naturalHeight && img.naturalWidth, 1000 * 10); try { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - + const canvas = new OffscreenCanvas(img.width, img.height); const ctx = canvas.getContext('2d')!; ctx.drawImage(img, 0, 0); // 没被 CORS 污染就直接使用这个 canvas @@ -84,15 +79,8 @@ const imgToCanvas = async (img: HTMLImageElement | string) => { const url = typeof img === 'string' ? img : img.src; const res = await request(url, { responseType: 'blob' }); - const image = new Image(); - await new Promise((resolve, reject) => { - image.onload = resolve; - image.onerror = reject; - image.src = URL.createObjectURL(res.response); - }); - const canvas = document.createElement('canvas'); - canvas.width = image.width; - canvas.height = image.height; + const image = await waitImgLoad(URL.createObjectURL(res.response)); + const canvas = new OffscreenCanvas(image.width, image.height); const ctx = canvas.getContext('2d')!; ctx.drawImage(image, 0, 0); @@ -114,15 +102,15 @@ const qrCodeWhiteList = [ /** 判断是否含有二维码 */ const hasQrCode = async ( - imgCanvas: HTMLCanvasElement, + imgCanvas: HTMLCanvasElement | OffscreenCanvas, scanRegion?: QrScanner.ScanRegion, qrEngine?: AsyncReturnType, - canvas?: HTMLCanvasElement, + canvas?: HTMLCanvasElement | OffscreenCanvas, ) => { try { const { data } = await QrScanner.scanImage(imgCanvas, { qrEngine, - canvas, + canvas: canvas as HTMLCanvasElement, scanRegion, alsoTryWithoutScanRegion: true, }); @@ -135,9 +123,9 @@ const hasQrCode = async ( }; const isAdImg = async ( - imgCanvas: HTMLCanvasElement, + imgCanvas: HTMLCanvasElement | OffscreenCanvas, qrEngine?: AsyncReturnType, - canvas?: HTMLCanvasElement, + canvas?: HTMLCanvasElement | OffscreenCanvas, ) => { // 黑白图肯定不是广告 if (!isColorImg(imgCanvas)) return false; @@ -167,7 +155,7 @@ const isAdImg = async ( const byContent = ( qrEngine?: AsyncReturnType, - canvas?: HTMLCanvasElement, + canvas?: HTMLCanvasElement | OffscreenCanvas, ) => async (img: HTMLImageElement | string) => isAdImg(await imgToCanvas(img), qrEngine, canvas); @@ -178,7 +166,7 @@ export const getAdPageByContent = async ( adList = new Set(), ) => { const qrEngine = await QrScanner.createQrEngine(); - const canvas = document.createElement('canvas'); + const canvas = new OffscreenCanvas(1, 1); return getAdPage(imgList, byContent(qrEngine, canvas), adList); }; diff --git a/src/helper/index.ts b/src/helper/index.ts index d9e880ed..a5a14449 100644 --- a/src/helper/index.ts +++ b/src/helper/index.ts @@ -269,20 +269,35 @@ export const waitDom = (selector: string) => wait(() => querySelector(selector)); /** 等待指定的图片元素加载完成 */ -export const waitImgLoad = (img: HTMLImageElement, timeout = 1000 * 10) => - new Promise((resolve) => { - const id = window.setTimeout( - () => resolve(new ErrorEvent('timeout')), - timeout, +export const waitImgLoad = ( + target: HTMLImageElement | string, + timeout?: number, +) => + new Promise((resolve, reject) => { + const img = typeof target === 'string' ? new Image() : target; + + const id = timeout + ? window.setTimeout(() => reject(new Error('timeout')), timeout) + : undefined; + + img.addEventListener( + 'load', + () => { + window.clearTimeout(id); + resolve(img); + }, + { once: true }, ); - img.addEventListener('load', () => { - resolve(null); - window.clearTimeout(id); - }); - img.addEventListener('error', (e) => { - resolve(e); - window.clearTimeout(id); - }); + img.addEventListener( + 'error', + (e) => { + window.clearTimeout(id); + reject(new Error(e.message)); + }, + { once: true }, + ); + + if (typeof target === 'string') img.src = target; }); /** 将指定的布尔值转换为字符串或未定义 */ @@ -349,12 +364,15 @@ export const testImgUrl = (url: string) => img.src = url; }); -export const canvasToBlob = ( - canvas: HTMLCanvasElement, +export const canvasToBlob = async ( + canvas: HTMLCanvasElement | OffscreenCanvas, type?: string, quality = 1, -) => - new Promise((resolve, reject) => { +) => { + if (canvas instanceof OffscreenCanvas) + return canvas.convertToBlob({ type, quality }); + + return new Promise((resolve, reject) => { canvas.toBlob( (blob) => blob ? resolve(blob) : reject(new Error('Canvas toBlob failed')), @@ -362,6 +380,7 @@ export const canvasToBlob = ( quality, ); }); +}; /** * 求 a 和 b 的差集,相当于从 a 中删去和 b 相同的属性 diff --git a/src/site/jm.tsx b/src/site/jm.tsx index 71f8eb15..84f72a6e 100644 --- a/src/site/jm.tsx +++ b/src/site/jm.tsx @@ -90,8 +90,9 @@ import { } imgEle.src = URL.createObjectURL(res.response); - const err = await waitImgLoad(imgEle); - if (err) { + try { + await waitImgLoad(imgEle, 1000 * 10); + } catch { URL.revokeObjectURL(imgEle.src); imgEle.src = originalUrl; toast.warn(`加载原图时出错: ${imgEle.dataset.page}`);