aria-cropper
v0.2.21
Published
React image uploader & cropper with enforced output sizes.
Downloads
180
Maintainers
Readme
AriaCropper
AriaCropper — یک کامپوننت React/Next.js سبک برای آپلود، برش (Crop) و تبدیل فرمت تصاویر با امکان اجباریکردن ابعاد خروجی، قفل نسبت تصویر، تنظیم کیفیت، و تولید خروجی ساختاریافته برای ارسال به سرور.
امکانات کلیدی
- 🔒 ابعاد خروجی اجباری (مثلاً 1200×1200) با قفل نسبت تصویر
- 🎯 Crop دقیق با پیشنمایش زنده (Inline/Modal)
- 🧰 تبدیل فرمت (
webp/jpeg/png) + تنظیم کیفیت - 🧾 خروجی استاندارد به شکل
ImageChangeSetبرای استفاده مستقیم در فرمها - 🧪 اعتبارسنجیهای کاربردی: نوع فایل، حجم، حداقل ابعاد
- ♿️ توجه به Accessibility (کلیدها، لیبلها، متن جایگزین)
- ⚡️ الگوهای پیشنهادی برای Next.js (app dir) و API Route
این README طوری نوشته شده که بتوانید آن را مستقیماً جایگزین
README.mdبسته کنید.
نصب
# بسته اصلی (از npm)
npm install aria-cropper cropperjs
# یا اگر scoped دارید
npm install @your-scope/aria-cropper cropperjsنکته:
cropperjsیک peer dependency است. فراموش نکنید استایل آن را نیز ایمپورت کنید:
// in a client component or global CSS
import 'cropperjs/dist/cropper.css'راهاندازی سریع (Quick Start)
'use client'
import React, { useState } from 'react'
import AriaCropper, { type ImageChangeSet } from 'aria-cropper'
import 'cropperjs/dist/cropper.css'
export default function Demo() {
const [change, setChange] = useState<ImageChangeSet | null>(null)
return (
<div className="space-y-4">
<AriaCropper
value={null}
requiredWidth={1200}
requiredHeight={1200}
previewWidth={300}
outputFormat="webp"
quality={0.9}
onChange={setChange}
/>
<pre className="text-xs bg-gray-50 p-3 rounded border">
{JSON.stringify(change, null, 2)}
</pre>
</div>
)
}سناریوی پیشفرض پیشنهادی: خروجی 1200×1200 WebP با پیشنمایش 300px و اکشن
replace.
انواع داده (Types)
ImageInfo
export type ImageInfo = {
url: string // Blob URL یا URL سرور
width: number // عرض واقعی خروجی
height: number // ارتفاع واقعی خروجی
format: 'webp' | 'jpeg' | 'png'
size: number // بایت
fileName?: string // اختیاری: نام فایل خروجی پیشنهادی
meta?: Record<string, any>
}ImageChangeSet
export type ImageChangeSet =
| { action: 'keep' }
| { action: 'remove' }
| { action: 'replace', file: File, info: ImageInfo }
| { action: 'format_change', info: ImageInfo }keep: تصویر قبلی بدون تغییر نگه داشته میشود.remove: درخواست حذف تصویر موجود.replace: تصویر جدید برش داده شده (فایل + اطلاعات) آماده آپلود.format_change: فقط فرمت/کیفیت/پردازش تغییر کرده (بدون نیاز به فایل جدید؛ برای گردشهای کاری خاص).
معمولترین حالتی که به سرور ارسال میشود
replaceاست (شاملFile).
API کامپوننت (Props)
export type AriaCropperProps = {
/** مقدار اولیه (تصویر موجود یا null) */
value: ImageInfo | null
/** هندلر تغییرات – خروجی استاندارد */
onChange: (change: ImageChangeSet) => void
/**
* ابعاد خروجی اجباری. هر دو را بدهید تا نسبت قفل شود.
* اگر فقط یکی را بدهید، نسبت براساس تصویر/کِرُپر حفظ میشود.
*/
requiredWidth?: number
requiredHeight?: number
/** حداقل ابعاد ورودی قبل از Crop (اختیاری) */
minWidth?: number
minHeight?: number
/** محدودیت بزرگنمایی بالاتر از ابعاد ورودی */
allowUpscale?: boolean // پیشفرض: false
/** قفل نسبت تصویر (اگر required* داده شده باشد معمولاً true میشود) */
lockAspectRatio?: boolean // پیشفرض: true وقتی required* هر دو مقدار داشته باشند
/** اندازه پیشنمایش (پهنای باکس Preview) */
previewWidth?: number // پیشفرض: 200
/** انتخاب فرمت خروجی */
outputFormat?: 'webp' | 'jpeg' | 'png' // پیشفرض: 'webp'
/** کیفیت خروجی (۰..۱) */
quality?: number // پیشفرض: 0.9
/** رنگ پسزمینه برای فرمتهای بدون شفافیت (مثل JPEG) */
background?: 'transparent' | string // پیشفرض: 'transparent'
/** پذیرش نوع فایل ورودی */
accept?: string // پیشفرض: 'image/*'
/** سقف حجم فایل ورودی (MB) */
maxFileSizeMB?: number // پیشفرض: 10
/** غیرفعال کردن کنترلها */
disabled?: boolean
/** نمایش رابط برش به صورت درجا یا مودال */
uiMode?: 'inline' | 'modal' // پیشفرض: 'inline'
/** برچسبها و متنهای رابط کاربری (i18n) */
i18n?: Partial<typeof defaultI18n>
/** کالبکهای کمکی */
onOpen?: () => void
onClose?: () => void
onError?: (error: AriaCropperError) => void
/** ارسال مستقیم تنظیمات به cropperjs (اختیاری) */
cropperOptions?: Partial<Cropper.Options>
}خطاها (AriaCropperError)
export type AriaCropperError = {
code:
| 'INVALID_TYPE'
| 'FILE_TOO_LARGE'
| 'DIMENSION_TOO_SMALL'
| 'DECODE_FAILED'
| 'CROP_FAILED'
| 'UNSUPPORTED_FORMAT'
message: string
detail?: any
}الگوهای رایج استفاده
1) سناریوی پروفایل کاربر (Next.js – app dir)
'use client'
import { useState } from 'react'
import AriaCropper, { type ImageChangeSet } from 'aria-cropper'
import 'cropperjs/dist/cropper.css'
export default function ProfilePhotoField() {
const [photoChange, setPhotoChange] = useState<ImageChangeSet | null>(null)
const handleSubmit = async () => {
if (!photoChange) return
const fd = changeSetToFormData(photoChange, { field: 'photo' })
const res = await fetch('/api/profile/photo', { method: 'POST', body: fd })
// پاسخ را بررسی کنید…
}
return (
<div className="space-y-4">
<AriaCropper
value={null}
requiredWidth={1200}
requiredHeight={1200}
previewWidth={300}
outputFormat="webp"
onChange={setPhotoChange}
/>
<button onClick={handleSubmit} className="px-3 py-2 rounded bg-blue-600 text-white">
ذخیره
</button>
</div>
)
}2) فقط تبدیل فرمت بدون تعویض فایل (format_change)
<AriaCropper
value={{ url: existingUrl, width: 800, height: 800, format: 'jpeg', size: 120000 }}
outputFormat="webp"
onChange={(c) => {
if (c.action === 'format_change') {
// میتوانید فقط meta را به سرور بفرستید یا در کلاینت ذخیره کنید
}
}}
/>3) حذف تصویر
<AriaCropper
value={currentImage}
onChange={(c) => {
if (c.action === 'remove') {
// آلارم/تأیید و سپس ارسال به سرور
}
}}
/>کمکیها (Helpers)
changeSetToFormData
export function changeSetToFormData(
change: ImageChangeSet,
opts?: { field?: string; meta?: Record<string, any> }
) {
const fd = new FormData()
const field = opts?.field ?? 'file'
if (opts?.meta) fd.append('meta', JSON.stringify(opts.meta))
switch (change.action) {
case 'replace':
fd.append(field, change.file, change.info.fileName ?? inferFileName(change.info))
fd.append('info', JSON.stringify(change.info))
fd.append('action', 'replace')
break
case 'remove':
fd.append('action', 'remove')
break
case 'format_change':
fd.append('action', 'format_change')
fd.append('info', JSON.stringify(change.info))
break
case 'keep':
fd.append('action', 'keep')
break
}
return fd
}
function inferFileName(info: ImageInfo) {
const base = info.fileName?.replace(/\.[^.]+$/, '') || 'image'
const ext = info.format === 'jpeg' ? 'jpg' : info.format
return `${base}.${ext}`
}شِمای پاسخ سرور (پیشنهادی)
// Next.js Route Handler (app/api/profile/photo/route.ts)
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const form = await req.formData()
const action = form.get('action') as string
if (action === 'replace') {
const file = form.get('file') as File
// ✅ اینجا MIME/اندازه/ابعاد را دوباره سمت سرور اعتبارسنجی کنید
// سپس در S3/Filesystem ذخیره و URL نهایی را برگردانید
return NextResponse.json({ ok: true, url: '/uploads/...' })
}
if (action === 'remove') {
// حذف فایل قبلی و بروزرسانی DB
return NextResponse.json({ ok: true })
}
if (action === 'format_change') {
// معمولاً تغییر کلاینتی است؛ ولی میتوانید در سرور هم پردازش کنید
return NextResponse.json({ ok: true })
}
return NextResponse.json({ ok: true })
}نکات SSR و Next.js
- این کامپوننت Client Component است. در فایل استفادهشونده، حتماً
"use client"بالای فایل باشد. - اگر با خطای
window is not definedروبهرو شدید، از Dynamic Import بدون SSR استفاده کنید:
import dynamic from 'next/dynamic'
const AriaCropper = dynamic(() => import('aria-cropper'), { ssr: false })- اگر بسته را بهصورت local و TypeScript منتشر کردهاید و Next نتوانست آن را transpile کند، در
next.config.jsازtranspilePackagesکمک بگیرید:
// next.config.js
module.exports = {
transpilePackages: ['aria-cropper'],
}- برای استایل CropperJS،
import 'cropperjs/dist/cropper.css'را در یک Client Component یا درglobals.cssانجام دهید.
اعتبارسنجی و بهینهسازی کیفیت
- حداقل ابعاد: با
minWidth/minHeightورودیهایی که خیلی کوچکاند را رد کنید. - جلوگیری از Upscale: با
allowUpscale={false}از افت کیفیت جلوگیری کنید. - کیفیت تطبیقی: برای JPEG/WebP بین
0.75 .. 0.95را تست کنید؛ بالاتر معمولاً بهبود محسوسی ندارد. - پسزمینه: برای JPEG اگر تصویر شفاف است، با
backgroundیک رنگ پسزمینه تعریف کنید (مثلاً#fff).
دسترسپذیری (A11y)
- همه دکمهها باید label مناسب داشته باشند (i18n را تنظیم کنید).
- با کیبورد بتوان به دکمههای «انتخاب فایل»، «تأیید برش»، «حذف» و… دسترسی داشت.
- برای تصویر نهایی alt مناسب در مصرفکنندهی کامپوننت تنظیم کنید.
بینالمللیسازی (i18n)
export const defaultI18n = {
upload: 'انتخاب تصویر',
replace: 'تغییر تصویر',
remove: 'حذف',
cancel: 'انصراف',
confirm: 'تأیید',
cropTitle: 'برش تصویر',
errors: {
INVALID_TYPE: 'نوع فایل نامعتبر است.',
FILE_TOO_LARGE: 'حجم فایل بیش از حد مجاز است.',
DIMENSION_TOO_SMALL: 'ابعاد تصویر برای این برش کافی نیست.',
DECODE_FAILED: 'خواندن تصویر ناموفق بود.',
CROP_FAILED: 'برش تصویر ناموفق بود.',
UNSUPPORTED_FORMAT: 'فرمت پشتیبانی نمیشود.',
},
}سفارشیسازی UI و تم
- کلاسها را با Tailwind یا CSS سفارشی کنید.
- حالت نمایش Modal یا Inline را با
uiModeتغییر دهید. - از
cropperOptionsبرای تغییر رفتار CropperJS (مثلviewMode,dragMode,guides,zoomable، و …) استفاده کنید.
مثال:
<AriaCropper
requiredWidth={1600}
requiredHeight={900}
outputFormat="jpeg"
background="#fff"
cropperOptions={{
viewMode: 1,
dragMode: 'move',
autoCropArea: 1,
guides: true,
zoomOnWheel: true,
}}
/>سناریوهای ادغام پیشرفته
ادغام با فرمهای چندمرحلهای
- مقدار
ImageChangeSetرا در استیت فرم نگه دارید و در مرحلهی نهایی باFormDataارسال کنید. - اگر کاربر به مرحله قبل برگشت، مقدار
keep/removeرا همینجا اعمال کنید.
ادغام با ImageUploaderV3 (نمونه پیشنهادی)
<ImageUploaderV3
value={form.profilePhoto || null}
onChange={(change) => setPhotoChange(change)}
requiredWidth={1200}
requiredHeight={1200}
previewWidth={300}
outputFormat="webp"
/>ImageUploaderV3میتواند درون خود ازAriaCropper(inline یا modal) استفاده کند و خروجی را به شکلImageChangeSetبالا بدهد.
رفع اشکال (Troubleshooting)
«Module not found: Can't resolve 'aria-cropper'»
نصب را بررسی کنید:
npm i aria-cropper cropperjsاگر پکیج لوکال است،
package.jsonباید مسیرهای درست داشته باشد:"main": "dist/index.js""module": "dist/index.mjs""types": "dist/index.d.ts"
در Next.js ممکن است به
transpilePackagesنیاز باشد.
«window is not defined» در SSR
- از Dynamic Import با
ssr:falseاستفاده کنید (بخش SSR بالا).
تصویر خروجی تار است / وارونه شده
- Upscale را غیرفعال کنید یا کیفیت را افزایش دهید.
- برای Orientation، مرورگرهای مدرن EXIF را تا حدی لحاظ میکنند؛ اگر ورودی موبایل مشکل دارد، قبل از crop از
createImageBitmapیا کتابخانههای EXIF جهت تصحیح استفاده کنید.
WebP نمایش نمیشود
- مرورگرهای امروزی WebP را پشتیبانی میکنند؛ اگر مجبور به سازگاری هستید، یک fallback به JPEG در سمت سرور در نظر بگیرید.
امنیت و اعتبارسنجی سمت سرور
- MIME-Type را در سرور دوباره بررسی کنید (صرفاً به ورودی کلاینت اعتماد نکنید).
- حداکثر اندازه فایل، حداکثر ابعاد (مگاپیکسل) و پسوند را کنترل کنید.
- نام فایل را sanitize کنید؛ یا نامدهی را در سرور انجام دهید.
- اگر در فضای عمومی آپلود میکنید، URLهای امضا شده/موقتی یا مسیرهای امن را مد نظر داشته باشید.
Roadmap
- ناحیه برش دایرهای/بیضوی
- چندتصویری (گالری)
- پیشپردازش EXIF درونساخت
- پشتیبانی از Worker/OffscreenCanvas برای Decode سنگین
مشارکت (Contributing)
- PRها و Issueها پذیرفته میشوند.
- قبل از PR:
pnpm build/npm run buildو اجرای lint/test.
لایسنس
MIT © AriaCropper Authors
