Skip to content

Commit

Permalink
perf: ⚡ 使用 OffscreenCanvas 重构优化
Browse files Browse the repository at this point in the history
  • Loading branch information
hymbz committed Jul 28, 2024
1 parent 85659ce commit b01629a
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 79 deletions.
7 changes: 7 additions & 0 deletions .xo-config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = {
// 使用 process
"n/prefer-global/process": ["error", "always"],


//
// 项目特有的规则
//
Expand Down Expand Up @@ -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",
Expand Down
42 changes: 8 additions & 34 deletions src/components/Manga/actions/translation/cotrans.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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));
};
Expand All @@ -96,23 +81,12 @@ const mergeImage = async (rawImage: Blob, maskUri: string) => {
const resize = async (blob: Blob, w: number, h: number): Promise<Blob> => {
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);
Expand Down
40 changes: 14 additions & 26 deletions src/helper/detectAd.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -84,15 +79,8 @@ const imgToCanvas = async (img: HTMLImageElement | string) => {
const url = typeof img === 'string' ? img : img.src;
const res = await request<Blob>(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);

Expand All @@ -114,15 +102,15 @@ const qrCodeWhiteList = [

/** 判断是否含有二维码 */
const hasQrCode = async (
imgCanvas: HTMLCanvasElement,
imgCanvas: HTMLCanvasElement | OffscreenCanvas,
scanRegion?: QrScanner.ScanRegion,
qrEngine?: AsyncReturnType<typeof QrScanner.createQrEngine>,
canvas?: HTMLCanvasElement,
canvas?: HTMLCanvasElement | OffscreenCanvas,
) => {
try {
const { data } = await QrScanner.scanImage(imgCanvas, {
qrEngine,
canvas,
canvas: canvas as HTMLCanvasElement,
scanRegion,
alsoTryWithoutScanRegion: true,
});
Expand All @@ -135,9 +123,9 @@ const hasQrCode = async (
};

const isAdImg = async (
imgCanvas: HTMLCanvasElement,
imgCanvas: HTMLCanvasElement | OffscreenCanvas,
qrEngine?: AsyncReturnType<typeof QrScanner.createQrEngine>,
canvas?: HTMLCanvasElement,
canvas?: HTMLCanvasElement | OffscreenCanvas,
) => {
// 黑白图肯定不是广告
if (!isColorImg(imgCanvas)) return false;
Expand Down Expand Up @@ -167,7 +155,7 @@ const isAdImg = async (
const byContent =
(
qrEngine?: AsyncReturnType<typeof QrScanner.createQrEngine>,
canvas?: HTMLCanvasElement,
canvas?: HTMLCanvasElement | OffscreenCanvas,
) =>
async (img: HTMLImageElement | string) =>
isAdImg(await imgToCanvas(img), qrEngine, canvas);
Expand All @@ -178,7 +166,7 @@ export const getAdPageByContent = async (
adList = new Set<number>(),
) => {
const qrEngine = await QrScanner.createQrEngine();
const canvas = document.createElement('canvas');
const canvas = new OffscreenCanvas(1, 1);
return getAdPage(imgList, byContent(qrEngine, canvas), adList);
};

Expand Down
53 changes: 36 additions & 17 deletions src/helper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,20 +269,35 @@ export const waitDom = (selector: string) =>
wait(() => querySelector(selector));

/** 等待指定的图片元素加载完成 */
export const waitImgLoad = (img: HTMLImageElement, timeout = 1000 * 10) =>
new Promise<ErrorEvent | null>((resolve) => {
const id = window.setTimeout(
() => resolve(new ErrorEvent('timeout')),
timeout,
export const waitImgLoad = (
target: HTMLImageElement | string,
timeout?: number,
) =>
new Promise<HTMLImageElement>((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;
});

/** 将指定的布尔值转换为字符串或未定义 */
Expand Down Expand Up @@ -349,19 +364,23 @@ 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<Blob>((resolve, reject) => {
) => {
if (canvas instanceof OffscreenCanvas)
return canvas.convertToBlob({ type, quality });

return new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(blob) =>
blob ? resolve(blob) : reject(new Error('Canvas toBlob failed')),
type,
quality,
);
});
};

/**
* 求 a 和 b 的差集,相当于从 a 中删去和 b 相同的属性
Expand Down
5 changes: 3 additions & 2 deletions src/site/jm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down

0 comments on commit b01629a

Please sign in to comment.