تخطي إلى المحتوى الرئيسي

معالجة الصور

معالجة الصور على جانب العميل قبل الرفع لتقليل استخدام النطاق الترددي وتكاليف التخزين.

ضغط الصور

import { useDropup } from '@samithahansaka/dropup';
import { compressImage } from '@samithahansaka/dropup/image';

function CompressedUploader() {
const { files, actions, getDropProps, getInputProps } = useDropup({
accept: 'image/*',
upload: { url: '/api/upload' },

// معالجة الملفات قبل الرفع
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
// ضغط الصورة
const compressed = await compressImage(file.file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8,
});

// استبدال بالنسخة المضغوطة
actions.updateFileMeta(file.id, {
originalSize: file.size,
compressedFile: compressed,
});
}
},
});

return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>سيتم ضغط الصور قبل الرفع</p>
</div>
);
}

خيارات الضغط

interface CompressOptions {
// الأبعاد القصوى (يحافظ على نسبة العرض إلى الارتفاع)
maxWidth?: number; // الافتراضي: 1920
maxHeight?: number; // الافتراضي: 1080

// الجودة (0-1)
quality?: number; // الافتراضي: 0.8

// صيغة الإخراج
type?: 'image/jpeg' | 'image/png' | 'image/webp'; // الافتراضي: الصيغة الأصلية
}

// أمثلة
await compressImage(file, { quality: 0.6 }); // جودة أقل
await compressImage(file, { maxWidth: 800, maxHeight: 600 }); // حجم أصغر
await compressImage(file, { type: 'image/webp' }); // تحويل إلى WebP

المعاينة مع الضغط

import { useDropup } from '@samithahansaka/dropup';
import { compressImage } from '@samithahansaka/dropup/image';
import { useState } from 'react';

function PreviewWithCompression() {
const [compressionStats, setCompressionStats] = useState<Map<string, {
original: number;
compressed: number;
}>>(new Map());

const { files, actions, getDropProps, getInputProps } = useDropup({
accept: 'image/*',

onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
const compressed = await compressImage(file.file, {
maxWidth: 1200,
quality: 0.75,
});

setCompressionStats(prev => new Map(prev).set(file.id, {
original: file.size,
compressed: compressed.size,
}));

// تخزين الملف المضغوط للرفع
actions.updateFileMeta(file.id, { compressedFile: compressed });
}
},
});

const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
};

return (
<div>
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>أفلت الصور للضغط</p>
</div>

<div style={styles.grid}>
{files.map(file => {
const stats = compressionStats.get(file.id);
const savings = stats
? ((1 - stats.compressed / stats.original) * 100).toFixed(0)
: 0;

return (
<div key={file.id} style={styles.card}>
{file.preview && (
<img src={file.preview} alt="" style={styles.preview} />
)}
<p>{file.name}</p>
{stats && (
<p style={styles.stats}>
{formatSize(stats.original)}{formatSize(stats.compressed)}
<span style={styles.savings}> (-{savings}%)</span>
</p>
)}
</div>
);
})}
</div>
</div>
);
}

const styles = {
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 40,
textAlign: 'center' as const,
marginBottom: 20,
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: 16,
},
card: {
border: '1px solid #eee',
borderRadius: 8,
padding: 12,
},
preview: {
width: '100%',
height: 150,
objectFit: 'cover' as const,
borderRadius: 4,
},
stats: {
fontSize: 12,
color: '#666',
},
savings: {
color: '#4caf50',
fontWeight: 'bold',
},
};

تغيير حجم الصورة

import { resizeImage } from '@samithahansaka/dropup/image';

// تغيير الحجم إلى أبعاد محددة
const resized = await resizeImage(file, {
width: 300,
height: 300,
mode: 'cover', // 'cover' | 'contain' | 'fill'
});

// إنشاء صورة مصغرة
const thumbnail = await resizeImage(file, {
width: 150,
height: 150,
mode: 'cover',
});

أوضاع تغيير الحجم

الوضعالوصف
coverيملأ المنطقة بالكامل، قد يقص
containيتناسب داخل المنطقة، قد يكون هناك مساحة فارغة
fillيمتد للملء (قد يشوه)

إصلاح اتجاه EXIF

بعض الكاميرات تحفظ الصور مع بيانات تدوير EXIF. أصلح الاتجاه قبل العرض:

import { fixOrientation } from '@samithahansaka/dropup/image';

const corrected = await fixOrientation(file);

قص الصورة

import { cropImage } from '@samithahansaka/dropup/image';

const cropped = await cropImage(file, {
x: 100, // بداية X
y: 50, // بداية Y
width: 400, // عرض القص
height: 400, // ارتفاع القص
});

مكون محرر الصور

import { useDropup } from '@samithahansaka/dropup';
import { compressImage, cropImage } from '@samithahansaka/dropup/image';
import { useState, useRef } from 'react';

function ImageEditor() {
const [selectedFile, setSelectedFile] = useState<DropupFile | null>(null);
const [cropArea, setCropArea] = useState({ x: 0, y: 0, width: 200, height: 200 });

const { files, actions, getDropProps, getInputProps } = useDropup({
accept: 'image/*',
maxFiles: 1,
multiple: false,
});

const handleCrop = async () => {
if (!selectedFile) return;

const cropped = await cropImage(selectedFile.file, cropArea);

// استبدال الأصلي بالنسخة المقصوصة
actions.updateFileMeta(selectedFile.id, {
processedFile: cropped,
});
};

const handleCompress = async () => {
if (!selectedFile) return;

const compressed = await compressImage(selectedFile.file, {
quality: 0.7,
});

actions.updateFileMeta(selectedFile.id, {
processedFile: compressed,
});
};

return (
<div style={styles.container}>
{files.length === 0 ? (
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>أفلت صورة للتحرير</p>
</div>
) : (
<div style={styles.editor}>
<div style={styles.preview}>
<img
src={files[0].preview}
alt=""
style={styles.image}
onClick={() => setSelectedFile(files[0])}
/>
</div>

<div style={styles.tools}>
<h4>الأدوات</h4>
<button onClick={handleCrop}>قص</button>
<button onClick={handleCompress}>ضغط</button>
<button onClick={() => actions.reset()}>إزالة</button>
</div>
</div>
)}
</div>
);
}

const styles = {
container: {
maxWidth: 600,
margin: '0 auto',
},
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 60,
textAlign: 'center' as const,
},
editor: {
display: 'flex',
gap: 20,
},
preview: {
flex: 1,
},
image: {
maxWidth: '100%',
borderRadius: 8,
},
tools: {
width: 150,
display: 'flex',
flexDirection: 'column' as const,
gap: 8,
},
};

تحويل صيغة الصورة

import { convertImage } from '@samithahansaka/dropup/image';

// تحويل إلى WebP لحجم ملف أصغر
const webp = await convertImage(file, 'image/webp');

// تحويل إلى JPEG
const jpeg = await convertImage(file, 'image/jpeg', { quality: 0.9 });

// تحويل إلى PNG (بدون فقدان)
const png = await convertImage(file, 'image/png');

الحصول على بيانات وصفية للصورة

import { getImageMetadata } from '@samithahansaka/dropup/image';

const metadata = await getImageMetadata(file);
console.log(metadata);
// {
// width: 1920,
// height: 1080,
// aspectRatio: 1.78,
// orientation: 1, // اتجاه EXIF
// hasAlpha: false,
// format: 'image/jpeg',
// }

المعالجة المتسلسلة

سلسل عمليات متعددة:

import {
compressImage,
fixOrientation,
resizeImage,
} from '@samithahansaka/dropup/image';

async function processImage(file: File): Promise<File> {
let processed = file;

// الخطوة 1: إصلاح الاتجاه
processed = await fixOrientation(processed);

// الخطوة 2: تغيير الحجم إذا كان كبيراً جداً
const metadata = await getImageMetadata(processed);
if (metadata.width > 2000 || metadata.height > 2000) {
processed = await resizeImage(processed, {
maxWidth: 2000,
maxHeight: 2000,
});
}

// الخطوة 3: الضغط
processed = await compressImage(processed, {
quality: 0.8,
type: 'image/webp',
});

return processed;
}

// الاستخدام في الرافع
const { files } = useDropup({
accept: 'image/*',
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
const processed = await processImage(file.file);
actions.updateFileMeta(file.id, { processedFile: processed });
}
},
});

دعم المتصفحات

معالجة الصور تستخدم Canvas API ومدعومة في جميع المتصفحات الحديثة:

الميزةChromeFirefoxSafariEdge
تغيير الحجم/القص
JPEG/PNG
WebP14+
إصلاح EXIF