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 { 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 { return new Promise((resolve) => canvas.toBlob(resolve, "image/webp", quality)); } /** * 按裁剪区域导出 WebP。 * 自动按 0.92 → 0.65 降质量,直到满足大小上限;仍超限则抛出错误。 */ async function exportWebp( canvas: HTMLCanvasElement, maxBytes: number ): Promise { 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 { 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 { 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 { 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); }