mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-05-23 22:28:14 +08:00
feat: 本地图片文件压缩
This commit is contained in:
parent
e553e5360f
commit
ffbb38495f
|
@ -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以内的图片文件')
|
||||
try {
|
||||
const compressFile = await canvasToFile({
|
||||
canvas: await createCanvasByImgSrc({
|
||||
imgSrc: await getFileBase64String(file),
|
||||
}),
|
||||
maxFileSize: 4 * 1024,
|
||||
});
|
||||
const compressFileBase64 = await getFileBase64String(
|
||||
compressFile
|
||||
);
|
||||
}
|
||||
getFileBase64String(file)
|
||||
.then((res) => {
|
||||
updateCustomTheme(
|
||||
'icons',
|
||||
customTheme.icons.map((icon, _idx) =>
|
||||
_idx === idx ? { ...icon, content: res } : icon
|
||||
_idx === idx
|
||||
? { ...icon, content: compressFileBase64 }
|
||||
: icon
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
} catch (e: any) {
|
||||
setIconErrors(makeIconErrors(idx, e));
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -410,7 +420,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'BGM'}>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -432,7 +442,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'背景图'}>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到80k以下
|
||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -538,7 +548,7 @@ const ConfigDialog: FC<{
|
|||
onChange={(e) => onNewSoundChange('name', e.target.value)}
|
||||
/>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到10k以下
|
||||
接口上传体积有限制,上传文件请全力压缩到10k以下,推荐使用外链
|
||||
</div>
|
||||
<input
|
||||
type={'file'}
|
||||
|
@ -562,7 +572,7 @@ const ConfigDialog: FC<{
|
|||
</InputContainer>
|
||||
<InputContainer label={'图标素材'} required>
|
||||
<div className={style.tip}>
|
||||
接口上传体积有限制,上传文件请全力压缩到5k以下,推荐尺寸56*56
|
||||
接口上传体积有限制,上传的图片将会被严重压缩,推荐使用外链
|
||||
</div>
|
||||
</InputContainer>
|
||||
{customTheme.icons.map((icon, idx) => (
|
||||
|
@ -687,7 +697,7 @@ const ConfigDialog: FC<{
|
|||
</div>
|
||||
)}
|
||||
<div className={style.tip}>
|
||||
接口上传内容总体积有限制,上传文件失败请进一步压缩文件,或者使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
||||
接口上传内容总体积有限制,上传文件失败请尝试进一步压缩文件,推荐使用外链(自行搜索【免费图床】【免费mp3外链】【对象存储服务】等关键词)。
|
||||
本地整活,勾选右侧关闭文件大小校验👉
|
||||
<input
|
||||
type={'checkbox'}
|
||||
|
|
101
src/utils.ts
101
src/utils.ts
|
@ -171,3 +171,104 @@ export const timestampToUsedTimeString: (time: number) => 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<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;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user