135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
import { Upload } from 'ant-design-vue'
|
|||
|
|
|
||
|
|
/** حداکثر حجم مجاز فایل ورودی و خروجی آپلود (۱ مگابایت) */
|
||
|
|
export const MAX_UPLOAD_IMAGE_BYTES = 1024 * 1024
|
||
|
|
|
||
|
|
const MAX_DIMENSION = 1920
|
||
|
|
const QUALITY_STEPS = [0.86, 0.78, 0.7, 0.62, 0.54, 0.46]
|
||
|
|
|
||
|
|
function stripExtension(name: string): string {
|
||
|
|
return name.replace(/\.[^.]+$/u, '') || 'image'
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadImageElement(file: File): Promise<HTMLImageElement> {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const url = URL.createObjectURL(file)
|
||
|
|
const img = new Image()
|
||
|
|
img.onload = () => {
|
||
|
|
URL.revokeObjectURL(url)
|
||
|
|
resolve(img)
|
||
|
|
}
|
||
|
|
img.onerror = () => {
|
||
|
|
URL.revokeObjectURL(url)
|
||
|
|
reject(new Error('خواندن تصویر ناموفق بود'))
|
||
|
|
}
|
||
|
|
img.src = url
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function canvasToBlob(
|
||
|
|
canvas: HTMLCanvasElement,
|
||
|
|
type: string,
|
||
|
|
quality: number
|
||
|
|
): Promise<Blob | null> {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
canvas.toBlob((blob) => resolve(blob), type, quality)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function drawScaled(
|
||
|
|
img: HTMLImageElement,
|
||
|
|
maxDimension: number
|
||
|
|
): HTMLCanvasElement {
|
||
|
|
const scale = Math.min(1, maxDimension / Math.max(img.naturalWidth, img.naturalHeight))
|
||
|
|
const width = Math.max(1, Math.round(img.naturalWidth * scale))
|
||
|
|
const height = Math.max(1, Math.round(img.naturalHeight * scale))
|
||
|
|
const canvas = document.createElement('canvas')
|
||
|
|
canvas.width = width
|
||
|
|
canvas.height = height
|
||
|
|
const ctx = canvas.getContext('2d')
|
||
|
|
if (!ctx) {
|
||
|
|
throw new Error('Canvas در دسترس نیست')
|
||
|
|
}
|
||
|
|
ctx.drawImage(img, 0, 0, width, height)
|
||
|
|
return canvas
|
||
|
|
}
|
||
|
|
|
||
|
|
async function encodeWebpUnderLimit(
|
||
|
|
img: HTMLImageElement,
|
||
|
|
maxBytes: number
|
||
|
|
): Promise<Blob> {
|
||
|
|
let maxDimension = MAX_DIMENSION
|
||
|
|
|
||
|
|
for (let resizePass = 0; resizePass < 4; resizePass += 1) {
|
||
|
|
const canvas = drawScaled(img, maxDimension)
|
||
|
|
|
||
|
|
for (const quality of QUALITY_STEPS) {
|
||
|
|
const blob = await canvasToBlob(canvas, 'image/webp', quality)
|
||
|
|
if (blob && blob.size > 0 && blob.size <= maxBytes) {
|
||
|
|
return blob
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
maxDimension = Math.round(maxDimension * 0.75)
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error('پس از تبدیل به WebP حجم فایل بیش از حد مجاز ماند')
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* تصویر را به WebP تبدیل میکند (با کاهش کیفیت/ابعاد در صورت نیاز)
|
||
|
|
* و برای آپلود آماده میکند. اگر ورودی از قبل WebP و زیر سقف باشد، همان را برمیگرداند.
|
||
|
|
*/
|
||
|
|
export async function prepareImageForUpload(
|
||
|
|
file: File,
|
||
|
|
maxBytes: number = MAX_UPLOAD_IMAGE_BYTES
|
||
|
|
): Promise<File> {
|
||
|
|
if (!file || file.size < 1) {
|
||
|
|
throw new Error('فایل خالی است')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!file.type.startsWith('image/')) {
|
||
|
|
throw new Error('فقط فایل تصویری مجاز است')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (file.size > maxBytes) {
|
||
|
|
throw new Error('حداکثر حجم بارگذاری ۱ مگابایت میباشد')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (file.type === 'image/webp' && file.size <= maxBytes) {
|
||
|
|
return file
|
||
|
|
}
|
||
|
|
|
||
|
|
const img = await loadImageElement(file)
|
||
|
|
const blob = await encodeWebpUnderLimit(img, maxBytes)
|
||
|
|
const baseName = stripExtension(file.name)
|
||
|
|
|
||
|
|
return new File([blob], `${baseName}.webp`, {
|
||
|
|
type: 'image/webp',
|
||
|
|
lastModified: Date.now()
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/** برای beforeUpload آنتدیزاین: تبدیل به WebP یا رد آپلود */
|
||
|
|
export async function beforeUploadConvertToWebp(file: File) {
|
||
|
|
try {
|
||
|
|
return await prepareImageForUpload(file)
|
||
|
|
} catch (error) {
|
||
|
|
const msg = error instanceof Error ? error.message : 'خطا در آمادهسازی تصویر'
|
||
|
|
return Promise.reject(msg)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** نسخهٔ مناسب ant-design Upload با پیام خطا و LIST_IGNORE */
|
||
|
|
export function createWebpBeforeUpload(messageError: (msg: string) => void) {
|
||
|
|
return async (file: File) => {
|
||
|
|
try {
|
||
|
|
return await prepareImageForUpload(file)
|
||
|
|
} catch (error) {
|
||
|
|
const msg = error instanceof Error ? error.message : 'خطا در آمادهسازی تصویر'
|
||
|
|
messageError(msg)
|
||
|
|
return Upload.LIST_IGNORE
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|