mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-05-24 06:46:08 +08:00
feat: 本地图片文件压缩
This commit is contained in:
parent
e553e5360f
commit
ffbb38495f
|
@ -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'}
|
||||||
|
|
101
src/utils.ts
101
src/utils.ts
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user