118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
const PUZZLE_REFERENCE_IMAGE_MAX_EDGE = 1536;
|
|
const PUZZLE_REFERENCE_IMAGE_COMPRESS_TRIGGER_BYTES = 1536 * 1024;
|
|
export const PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH = 10 * 1024 * 1024;
|
|
|
|
type PuzzleReferenceImageSize = {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
function readFileAsDataUrl(file: File) {
|
|
return new Promise<string>((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onerror = () => reject(new Error('参考图读取失败,请重试。'));
|
|
reader.onload = () => {
|
|
if (typeof reader.result !== 'string') {
|
|
reject(new Error('参考图读取失败,请重试。'));
|
|
return;
|
|
}
|
|
resolve(reader.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function ensureReferenceImageWithinLimit(dataUrl: string) {
|
|
if (dataUrl.length > PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH) {
|
|
throw new Error('参考图过大,请换一张尺寸更小的图片。');
|
|
}
|
|
return dataUrl;
|
|
}
|
|
|
|
function loadReferenceImage(dataUrl: string) {
|
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
const image = new Image();
|
|
image.onload = () => resolve(image);
|
|
image.onerror = () => reject(new Error('参考图读取失败,请重试。'));
|
|
image.src = dataUrl;
|
|
});
|
|
}
|
|
|
|
function resolveCompressedImageSize(
|
|
image: HTMLImageElement,
|
|
): PuzzleReferenceImageSize {
|
|
const sourceWidth = image.naturalWidth || image.width;
|
|
const sourceHeight = image.naturalHeight || image.height;
|
|
if (sourceWidth <= 0 || sourceHeight <= 0) {
|
|
throw new Error('参考图读取失败,请重试。');
|
|
}
|
|
|
|
const scale = Math.min(
|
|
1,
|
|
PUZZLE_REFERENCE_IMAGE_MAX_EDGE / Math.max(sourceWidth, sourceHeight),
|
|
);
|
|
return {
|
|
width: Math.max(1, Math.round(sourceWidth * scale)),
|
|
height: Math.max(1, Math.round(sourceHeight * scale)),
|
|
};
|
|
}
|
|
|
|
function shouldCompressReferenceImage(file: File, dataUrl: string) {
|
|
return (
|
|
file.size > PUZZLE_REFERENCE_IMAGE_COMPRESS_TRIGGER_BYTES ||
|
|
dataUrl.length > PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH
|
|
);
|
|
}
|
|
|
|
async function compressReferenceImageDataUrl(file: File, dataUrl: string) {
|
|
if (
|
|
typeof document === 'undefined' ||
|
|
typeof Image === 'undefined' ||
|
|
!shouldCompressReferenceImage(file, dataUrl)
|
|
) {
|
|
return dataUrl;
|
|
}
|
|
|
|
const image = await loadReferenceImage(dataUrl);
|
|
const size = resolveCompressedImageSize(image);
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = size.width;
|
|
canvas.height = size.height;
|
|
const context = canvas.getContext('2d');
|
|
if (!context) {
|
|
return dataUrl;
|
|
}
|
|
|
|
// 中文注释:参考图只作为生成提示,不需要保留手机原图体积;压到单边 1536 内给 JSON body 留余量。
|
|
context.imageSmoothingEnabled = true;
|
|
context.imageSmoothingQuality = 'high';
|
|
context.fillStyle = '#ffffff';
|
|
context.fillRect(0, 0, size.width, size.height);
|
|
context.drawImage(image, 0, 0, size.width, size.height);
|
|
|
|
const candidates = [0.84, 0.76, 0.68].map((quality) =>
|
|
canvas.toDataURL('image/jpeg', quality),
|
|
);
|
|
return candidates.reduce((best, current) =>
|
|
current.length < best.length ? current : best,
|
|
);
|
|
}
|
|
|
|
export async function readPuzzleReferenceImageAsDataUrl(file: File) {
|
|
const dataUrl = await readFileAsDataUrl(file);
|
|
try {
|
|
const compressedDataUrl = await compressReferenceImageDataUrl(
|
|
file,
|
|
dataUrl,
|
|
);
|
|
return ensureReferenceImageWithinLimit(
|
|
compressedDataUrl.length < dataUrl.length ? compressedDataUrl : dataUrl,
|
|
);
|
|
} catch (error) {
|
|
if (dataUrl.length <= PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH) {
|
|
return dataUrl;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|