ข้ามไปยังเนื้อหาหลัก

การประมวลผลรูปภาพ

ประมวลผลรูปภาพฝั่งไคลเอนต์ก่อนอัปโหลดเพื่อลด bandwidth และค่าใช้จ่ายในการจัดเก็บ

การบีบอัดรูปภาพ

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

// รูปแบบ output
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, // ความสูงครอป
});

คอมโพเนนต์ Image Editor

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 (lossless)
const png = await convertImage(file, 'image/png');

รับ Metadata รูปภาพ

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

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

การประมวลผลแบบ Pipeline

เชื่อมโยงหลายการดำเนินการ:

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;
}

// ใช้ใน uploader
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