Files
jiang13-bbs/frontend/lib/cropImage.ts
freefire 46e0cdc0b3 feat: 媒体库 blob 去重存储与品牌/注册等前端调整
新增:
- blob_store 二进制去重存储 + blob_dedup 测试

其它:
- 后端 settings/auth/media_library/upload/operations 服务与 handler 调整
- 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
2026-10-03 02:38:11 +08:00

146 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Area } from "react-easy-crop";
// 头像裁剪输出尺寸(正方形)。头像最大展示 88px,320px 足够覆盖高分屏
export const AVATAR_OUTPUT_SIZE = 320;
// 裁剪后 WebP 文件大小上限(与后端 service.AvatarMaxBytes 保持一致)
export const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
// 品牌 Logo 输出高度上限(与后端 brandMaxDim 对齐)
export const BRAND_MAX_DIM = 2048;
// 裁剪后品牌图大小上限(与后端 BrandLogoMaxBytes 对齐)
export const BRAND_MAX_BYTES = 2 * 1024 * 1024;
function loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", () => reject(new Error("图片加载失败,请换一张试试")));
img.src = src;
});
}
function canvasToWebp(canvas: HTMLCanvasElement, quality: number): Promise<Blob | null> {
return new Promise((resolve) => canvas.toBlob(resolve, "image/webp", quality));
}
/**
* 按裁剪区域导出 WebP。
* 自动按 0.92 → 0.65 降质量,直到满足大小上限;仍超限则抛出错误。
*/
async function exportWebp(
canvas: HTMLCanvasElement,
maxBytes: number
): Promise<Blob> {
let last: Blob | null = null;
for (const quality of [0.92, 0.85, 0.75, 0.65]) {
const blob = await canvasToWebp(canvas, quality);
if (!blob) throw new Error("当前浏览器不支持 WebP 导出,请使用最新版 Chrome/Edge/Safari");
last = blob;
if (blob.size <= maxBytes) return blob;
}
if (last && last.size <= maxBytes) return last;
throw new Error("裁剪后的图片仍超过大小限制,请缩小图片后再试");
}
/**
* 按裁剪区域导出正方形 WebP。
* 自动按 0.92 → 0.65 降质量,直到满足大小上限;仍超限则抛出错误。
*/
export async function getCroppedAvatarWebp(
imageSrc: string,
pixelCrop: Area
): Promise<Blob> {
const image = await loadImage(imageSrc);
const canvas = document.createElement("canvas");
canvas.width = AVATAR_OUTPUT_SIZE;
canvas.height = AVATAR_OUTPUT_SIZE;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("当前浏览器不支持图片裁剪");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
AVATAR_OUTPUT_SIZE,
AVATAR_OUTPUT_SIZE
);
return exportWebp(canvas, AVATAR_MAX_BYTES);
}
/**
* 按裁剪区域导出任意比例 WebP(品牌 Logo 用)。
* 输出高度限制在 BRAND_MAX_DIM 以内,宽度按比例自适应。
*/
export async function getCroppedBrandWebp(
imageSrc: string,
pixelCrop: Area,
aspect: number
): Promise<Blob> {
const image = await loadImage(imageSrc);
const canvas = document.createElement("canvas");
// 以高度为基准,宽度按纵横比计算;若超宽则按宽度反推高度
let outH = pixelCrop.height;
let outW = Math.round(outH * aspect);
if (outW > BRAND_MAX_DIM) {
outW = BRAND_MAX_DIM;
outH = Math.round(outW / aspect);
}
if (outH > BRAND_MAX_DIM) {
outH = BRAND_MAX_DIM;
outW = Math.round(outH * aspect);
}
canvas.width = outW;
canvas.height = outH;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("当前浏览器不支持图片裁剪");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
outW,
outH
);
return exportWebp(canvas, BRAND_MAX_BYTES);
}
/**
* 整图直传(「原图自适应」用):不做任何裁剪,仅按比例缩放到 BRAND_MAX_DIM 内并导出 WebP。
* 页眉图框高度固定、宽度按图片原始比例自适应。
*/
export async function getFullBrandWebp(imageSrc: string): Promise<Blob> {
const image = await loadImage(imageSrc);
const canvas = document.createElement("canvas");
const scale = Math.min(1, BRAND_MAX_DIM / Math.max(image.naturalWidth, image.naturalHeight));
const outW = Math.max(1, Math.round(image.naturalWidth * scale));
const outH = Math.max(1, Math.round(image.naturalHeight * scale));
canvas.width = outW;
canvas.height = outH;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("当前浏览器不支持图片处理");
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(image, 0, 0, outW, outH);
return exportWebp(canvas, BRAND_MAX_BYTES);
}