feat: add pipeline API and download utilities

- processImageWithPipeline for full pipeline processing
- ZIP archive creation with JSZip
- File download helpers
- Output filename generation

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-13 18:11:09 -04:00
parent ee71b91868
commit 0d6ec68294

View File

@@ -0,0 +1,102 @@
// High-level pipeline API for image processing
import type { Device, ImageEntry, PipelineConfig } from '$lib/types';
import { getExtension } from '$lib/types';
import JSZip from 'jszip';
import { initPhoton } from './photon';
import { workerPool } from './workerPool';
let photonReady = false;
export async function ensurePhotonReady(): Promise<void> {
if (photonReady) return;
await initPhoton();
photonReady = true;
}
export async function processImage(file: File, device: Device): Promise<Blob> {
const { blob } = await processImageWithPipeline(file, device, {
crop: { enabled: true, mode: 'center' },
resize: { enabled: true, mode: 'cover' },
brightness: { enabled: false, value: 0 },
contrast: { enabled: false, value: 0 },
gamma: { enabled: false, value: 1.0 },
autoLevels: { enabled: false, clipPercent: 0.5 },
greyscale: { enabled: true, method: 'luminosity' },
sharpen: { enabled: false, amount: 0 },
quantize: { enabled: true, levels: device.greyLevels },
dither: { enabled: false, algorithm: 'floyd-steinberg' }
});
return blob;
}
export async function processImageWithPreview(
file: File,
device: Device
): Promise<{ blob: Blob; dataUrl: string }> {
return processImageWithPipeline(file, device, {
crop: { enabled: true, mode: 'center' },
resize: { enabled: true, mode: 'cover' },
brightness: { enabled: false, value: 0 },
contrast: { enabled: false, value: 0 },
gamma: { enabled: false, value: 1.0 },
autoLevels: { enabled: false, clipPercent: 0.5 },
greyscale: { enabled: true, method: 'luminosity' },
sharpen: { enabled: false, amount: 0 },
quantize: { enabled: true, levels: device.greyLevels },
dither: { enabled: false, algorithm: 'floyd-steinberg' }
});
}
export async function processImageWithPipeline(
file: File,
device: Device,
config: PipelineConfig
): Promise<{ blob: Blob; dataUrl: string }> {
return workerPool.process(file, device, config);
}
export function getOutputFilename(originalFilename: string, device: Device): string {
const lastDot = originalFilename.lastIndexOf('.');
const stem = lastDot > 0 ? originalFilename.substring(0, lastDot) : originalFilename;
const ext = getExtension(device.outputFormat);
return `${stem}_${device.id}.${ext}`;
}
export async function createZipArchive(images: ImageEntry[], device: Device): Promise<Blob> {
const zip = new JSZip();
for (const image of images) {
if (image.processedBlob) {
const filename = getOutputFilename(image.filename, device);
zip.file(filename, image.processedBlob);
}
}
return zip.generateAsync({ type: 'blob' });
}
export function downloadFile(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export function downloadImage(image: ImageEntry, device: Device): void {
if (!image.processedBlob) return;
const filename = getOutputFilename(image.filename, device);
downloadFile(image.processedBlob, filename);
}
export async function downloadAllAsZip(images: ImageEntry[], device: Device): Promise<void> {
const completedImages = images.filter((img) => img.status === 'complete' && img.processedBlob);
if (completedImages.length === 0) return;
const zipBlob = await createZipArchive(completedImages, device);
downloadFile(zipBlob, '2eink_covers.zip');
}