From ffbb38495f2c8c124f6039496f613e6c416d9db4 Mon Sep 17 00:00:00 2001 From: streakingman Date: Thu, 4 May 2023 23:52:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E5=9C=B0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8E=8B=E7=BC=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ConfigDialog.tsx | 72 +++++++++++++---------- src/utils.ts | 101 ++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 31 deletions(-) diff --git a/src/components/ConfigDialog.tsx b/src/components/ConfigDialog.tsx index 2960b15..cff44b3 100644 --- a/src/components/ConfigDialog.tsx +++ b/src/components/ConfigDialog.tsx @@ -15,6 +15,8 @@ import { randomString, wrapThemeDefaultSounds, LAST_UPLOAD_TIME_STORAGE_KEY, + canvasToFile, + createCanvasByImgSrc, } from '../utils'; import { copy } from 'clipboard'; import { CloseIcon } from './CloseIcon'; @@ -163,7 +165,7 @@ const ConfigDialog: FC<{ type: 'bgm' | 'background' | 'sound' | 'icon'; file?: File; idx?: number; - }) => void = ({ type, file, idx }) => { + }) => void = async ({ type, file, idx }) => { if (!file) return; switch (type) { case 'bgm': @@ -181,16 +183,20 @@ const ConfigDialog: FC<{ break; case 'background': setBackgroundError(''); - if (enableFileSizeValidate && file.size > 80 * 1024) { - return setBackgroundError('请选择80k以内全损画质的图片'); - } - getFileBase64String(file) - .then((res) => { - updateCustomTheme('background', res); - }) - .catch((e) => { - setBackgroundError(e); + try { + const compressFile = await canvasToFile({ + canvas: await createCanvasByImgSrc({ + imgSrc: await getFileBase64String(file), + }), + maxFileSize: 20 * 1024, }); + const compressFileBase64 = await getFileBase64String( + compressFile + ); + updateCustomTheme('background', compressFileBase64); + } catch (e: any) { + setBackgroundError(e); + } break; case 'sound': setSoundError(''); @@ -208,23 +214,27 @@ const ConfigDialog: FC<{ case 'icon': if (idx == null) return; setIconErrors(makeIconErrors(idx, '')); - if (enableFileSizeValidate && file.size > 5 * 1024) { - return setIconErrors( - makeIconErrors(idx, '请选择5k以内的图片文件') - ); - } - getFileBase64String(file) - .then((res) => { - updateCustomTheme( - 'icons', - customTheme.icons.map((icon, _idx) => - _idx === idx ? { ...icon, content: res } : icon - ) - ); - }) - .catch((e) => { - setIconErrors(makeIconErrors(idx, e)); + try { + const compressFile = await canvasToFile({ + canvas: await createCanvasByImgSrc({ + imgSrc: await getFileBase64String(file), + }), + maxFileSize: 4 * 1024, }); + const compressFileBase64 = await getFileBase64String( + compressFile + ); + updateCustomTheme( + 'icons', + customTheme.icons.map((icon, _idx) => + _idx === idx + ? { ...icon, content: compressFileBase64 } + : icon + ) + ); + } catch (e: any) { + setIconErrors(makeIconErrors(idx, e)); + } break; } }; @@ -410,7 +420,7 @@ const ConfigDialog: FC<{
- 接口上传体积有限制,上传文件请全力压缩到80k以下 + 接口上传体积有限制,上传文件请全力压缩到80k以下,推荐使用外链
- 接口上传体积有限制,上传文件请全力压缩到80k以下 + 接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
onNewSoundChange('name', e.target.value)} />
- 接口上传体积有限制,上传文件请全力压缩到10k以下 + 接口上传体积有限制,上传文件请全力压缩到10k以下,推荐使用外链
- 接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56 + 接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
{customTheme.icons.map((icon, idx) => ( @@ -687,7 +697,7 @@ const ConfigDialog: FC<{ )}
- 接口上传内容总体积有限制,上传文件失败请进一步压缩文件,或者使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。 + 接口上传内容总体积有限制,上传文件失败请尝试进一步压缩文件,推荐使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。 本地整活,勾选右侧关闭文件大小校验👉 string = (time) => { return '时间转换出错'; } }; + +export const dataURLToFile: (dataURL: string, filename?: string) => File = ( + // #endregion dataURLToFile + dataURL, + filename +) => { + const isDataURL = dataURL.startsWith('data:'); + if (!isDataURL) throw new Error('Data URL 错误'); + const _fileName = filename || new Date().getTime().toString(); + const mimeType = dataURL.match(/^data:([^;]+);/)?.[1] || ''; + // base64转二进制 + const binaryString = atob(dataURL.split(',')[1]); + let binaryStringLength = binaryString.length; + const bytes = new Uint8Array(binaryStringLength); + while (binaryStringLength--) { + bytes[binaryStringLength] = binaryString.charCodeAt(binaryStringLength); + } + return new File([bytes], _fileName, { type: mimeType }); +}; + +interface drawImgSrcInCanvasParams { + imgSrc: string; + canvas: HTMLCanvasElement; + scale?: number; +} +export const drawImgSrcInCanvas: ( + params: drawImgSrcInCanvasParams +) => Promise = async ({ imgSrc, canvas, scale = 1 }) => { + if (scale < 0) throw new Error('scale不能小于0'); + + const ctx = canvas.getContext('2d'); + if (!ctx || !(ctx instanceof CanvasRenderingContext2D)) { + throw new Error('Failed to get 2D context'); + } + const img = document.createElement('img'); + img.setAttribute('src', imgSrc); + + return new Promise((resolve, reject) => { + img.onload = () => { + const width = img.width * scale; + const height = img.height * scale; + canvas.width = width; + canvas.height = height; + ctx.drawImage(img, 0, 0, width, height); + resolve(); + }; + img.onerror = () => { + reject(new Error('图片加载失败')); + }; + }); +}; + +interface createCanvasByImgSrcParams { + imgSrc: HTMLImageElement['src']; + scale?: number; +} +export const createCanvasByImgSrc: ( + params: createCanvasByImgSrcParams +) => Promise = async ({ imgSrc, scale = 1 }) => { + if (scale < 0) throw new Error('scale不能小于0'); + const canvas = document.createElement('canvas'); + await drawImgSrcInCanvas({ + imgSrc, + canvas, + scale, + }); + return canvas; +}; + +interface CanvasToFileParams { + canvas: HTMLCanvasElement; + fileName?: string; + maxFileSize?: number; +} +export const canvasToFile: ( + params: CanvasToFileParams +) => Promise = async ({ canvas, fileName, maxFileSize }) => { + // #endregion canvasToFile + const MIME_TYPE = 'image/png'; + const dataURL = canvas.toDataURL(MIME_TYPE); + const _fileName = fileName || new Date().getTime().toString(); + const genFile = dataURLToFile(dataURL, _fileName); + // 判断是否需要压缩 + if (maxFileSize && genFile.size > maxFileSize) { + let scale = Math.sqrt(maxFileSize / genFile.size); + if (scale > 0.9) scale = 0.9; + // TODO 暂时通过canvas绘制缩放图像进行递归压缩,后续考虑其他方式 + // TODO 不断生成canvas, 调研内存是否会泄漏 + const _canvas = await createCanvasByImgSrc({ + imgSrc: dataURL, + scale, + }); + return canvasToFile({ + canvas: _canvas, + fileName: _fileName, + maxFileSize, + }); + } else { + return genFile; + } +};