feat: 本地图片文件压缩

This commit is contained in:
streakingman 2023-05-04 23:52:21 +08:00
parent e553e5360f
commit ffbb38495f
2 changed files with 142 additions and 31 deletions

View File

@ -15,6 +15,8 @@ import {
randomString, randomString,
wrapThemeDefaultSounds, wrapThemeDefaultSounds,
LAST_UPLOAD_TIME_STORAGE_KEY, LAST_UPLOAD_TIME_STORAGE_KEY,
canvasToFile,
createCanvasByImgSrc,
} from '../utils'; } from '../utils';
import { copy } from 'clipboard'; import { copy } from 'clipboard';
import { CloseIcon } from './CloseIcon'; import { CloseIcon } from './CloseIcon';
@ -163,7 +165,7 @@ const ConfigDialog: FC<{
type: 'bgm' | 'background' | 'sound' | 'icon'; type: 'bgm' | 'background' | 'sound' | 'icon';
file?: File; file?: File;
idx?: number; idx?: number;
}) => void = ({ type, file, idx }) => { }) => void = async ({ type, file, idx }) => {
if (!file) return; if (!file) return;
switch (type) { switch (type) {
case 'bgm': case 'bgm':
@ -181,16 +183,20 @@ const ConfigDialog: FC<{
break; break;
case 'background': case 'background':
setBackgroundError(''); setBackgroundError('');
if (enableFileSizeValidate && file.size > 80 * 1024) { try {
return setBackgroundError('请选择80k以内全损画质的图片'); const compressFile = await canvasToFile({
} canvas: await createCanvasByImgSrc({
getFileBase64String(file) imgSrc: await getFileBase64String(file),
.then((res) => { }),
updateCustomTheme('background', res); maxFileSize: 20 * 1024,
})
.catch((e) => {
setBackgroundError(e);
}); });
const compressFileBase64 = await getFileBase64String(
compressFile
);
updateCustomTheme('background', compressFileBase64);
} catch (e: any) {
setBackgroundError(e);
}
break; break;
case 'sound': case 'sound':
setSoundError(''); setSoundError('');
@ -208,23 +214,27 @@ const ConfigDialog: FC<{
case 'icon': case 'icon':
if (idx == null) return; if (idx == null) return;
setIconErrors(makeIconErrors(idx, '')); setIconErrors(makeIconErrors(idx, ''));
if (enableFileSizeValidate && file.size > 5 * 1024) { try {
return setIconErrors( const compressFile = await canvasToFile({
makeIconErrors(idx, '请选择5k以内的图片文件') canvas: await createCanvasByImgSrc({
imgSrc: await getFileBase64String(file),
}),
maxFileSize: 4 * 1024,
});
const compressFileBase64 = await getFileBase64String(
compressFile
); );
}
getFileBase64String(file)
.then((res) => {
updateCustomTheme( updateCustomTheme(
'icons', 'icons',
customTheme.icons.map((icon, _idx) => customTheme.icons.map((icon, _idx) =>
_idx === idx ? { ...icon, content: res } : icon _idx === idx
? { ...icon, content: compressFileBase64 }
: icon
) )
); );
}) } catch (e: any) {
.catch((e) => {
setIconErrors(makeIconErrors(idx, e)); setIconErrors(makeIconErrors(idx, e));
}); }
break; break;
} }
}; };
@ -410,7 +420,7 @@ const ConfigDialog: FC<{
</InputContainer> </InputContainer>
<InputContainer label={'BGM'}> <InputContainer label={'BGM'}>
<div className={style.tip}> <div className={style.tip}>
80k以下 80k以下使
</div> </div>
<input <input
type={'file'} type={'file'}
@ -432,7 +442,7 @@ const ConfigDialog: FC<{
</InputContainer> </InputContainer>
<InputContainer label={'背景图'}> <InputContainer label={'背景图'}>
<div className={style.tip}> <div className={style.tip}>
80k以下 使
</div> </div>
<input <input
type={'file'} type={'file'}
@ -538,7 +548,7 @@ const ConfigDialog: FC<{
onChange={(e) => onNewSoundChange('name', e.target.value)} onChange={(e) => onNewSoundChange('name', e.target.value)}
/> />
<div className={style.tip}> <div className={style.tip}>
10k以下 10k以下使
</div> </div>
<input <input
type={'file'} type={'file'}
@ -562,7 +572,7 @@ const ConfigDialog: FC<{
</InputContainer> </InputContainer>
<InputContainer label={'图标素材'} required> <InputContainer label={'图标素材'} required>
<div className={style.tip}> <div className={style.tip}>
5k以下56*56 使
</div> </div>
</InputContainer> </InputContainer>
{customTheme.icons.map((icon, idx) => ( {customTheme.icons.map((icon, idx) => (
@ -687,7 +697,7 @@ const ConfigDialog: FC<{
</div> </div>
)} )}
<div className={style.tip}> <div className={style.tip}>
使mp3外链 使mp3外链
👉 👉
<input <input
type={'checkbox'} type={'checkbox'}

View File

@ -171,3 +171,104 @@ export const timestampToUsedTimeString: (time: number) => string = (time) => {
return '时间转换出错'; 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<void> = 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<HTMLCanvasElement> = 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<File> = 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;
}
};