Harden image processing, improve image quality

This commit is contained in:
2026-04-20 18:10:48 +04:00
parent 4f55f26851
commit ec31d1a59b
7 changed files with 193 additions and 49 deletions

View File

@@ -1,63 +1,125 @@
import { encode } from 'blurhash';
self.onmessage = async (e) => {
const { id, file, maxDimension, quality, computeBlurhash } = e.data;
// Ignore internal browser/Vite/extension pings that don't match our exact job signature
if (e.data?.type !== 'PROCESS_IMAGE') return;
const { id, file, targetWidth, targetHeight, quality, computeBlurhash } =
e.data;
try {
// 1. Decode image off main thread
const bitmap = await createImageBitmap(file);
let finalCanvas;
let finalCtx;
let width = bitmap.width;
let height = bitmap.height;
// --- 1. Attempt Hardware Resizing (Happy Path) ---
try {
const resizedBitmap = await createImageBitmap(file, {
resizeWidth: targetWidth,
resizeHeight: targetHeight,
resizeQuality: 'high',
});
// 2. Calculate aspect-ratio preserving dimensions
if (width > height) {
if (width > maxDimension) {
height = Math.round(height * (maxDimension / width));
width = maxDimension;
finalCanvas = new OffscreenCanvas(targetWidth, targetHeight);
finalCtx = finalCanvas.getContext('2d');
if (!finalCtx) {
throw new Error('Failed to get 2d context from OffscreenCanvas');
}
} else {
if (height > maxDimension) {
width = Math.round(width * (maxDimension / height));
height = maxDimension;
finalCtx.drawImage(resizedBitmap, 0, 0, targetWidth, targetHeight);
resizedBitmap.close();
} catch (hwError) {
console.warn(
'Hardware resize failed, falling back to stepped software scaling:',
hwError
);
// --- 2. Fallback to Stepped Software Scaling (Robust Path) ---
// Bypass Android File descriptor bug by reading into memory
const buffer = await file.arrayBuffer();
const blob = new Blob([buffer], { type: file.type });
const source = await createImageBitmap(blob);
let srcWidth = source.width;
let srcHeight = source.height;
let currentCanvas = new OffscreenCanvas(srcWidth, srcHeight);
let ctx = currentCanvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(source, 0, 0);
// Step down by halves until near target
while (
currentCanvas.width * 0.5 > targetWidth &&
currentCanvas.height * 0.5 > targetHeight
) {
const nextCanvas = new OffscreenCanvas(
Math.floor(currentCanvas.width * 0.5),
Math.floor(currentCanvas.height * 0.5)
);
const nextCtx = nextCanvas.getContext('2d');
nextCtx.imageSmoothingEnabled = true;
nextCtx.imageSmoothingQuality = 'high';
nextCtx.drawImage(
currentCanvas,
0,
0,
nextCanvas.width,
nextCanvas.height
);
currentCanvas = nextCanvas;
}
// Final resize to exact target
finalCanvas = new OffscreenCanvas(targetWidth, targetHeight);
finalCtx = finalCanvas.getContext('2d');
finalCtx.imageSmoothingEnabled = true;
finalCtx.imageSmoothingQuality = 'high';
finalCtx.drawImage(currentCanvas, 0, 0, targetWidth, targetHeight);
source.close();
}
// 3. Create OffscreenCanvas and draw
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get 2d context from OffscreenCanvas');
}
ctx.drawImage(bitmap, 0, 0, width, height);
// 4. Generate Blurhash (if requested)
// --- 3. Generate Blurhash (if requested) ---
let blurhash = null;
if (computeBlurhash) {
const imageData = ctx.getImageData(0, 0, width, height);
blurhash = encode(imageData.data, width, height, 4, 3);
try {
const imageData = finalCtx.getImageData(
0,
0,
targetWidth,
targetHeight
);
blurhash = encode(imageData.data, targetWidth, targetHeight, 4, 3);
} catch (blurhashError) {
console.warn(
'Could not generate blurhash (possible canvas fingerprinting protection):',
blurhashError
);
}
}
// 5. Compress to JPEG Blob
const blob = await canvas.convertToBlob({
// --- 4. Compress to JPEG Blob ---
const finalBlob = await finalCanvas.convertToBlob({
type: 'image/jpeg',
quality: quality,
});
const dim = `${width}x${height}`;
const dim = `${targetWidth}x${targetHeight}`;
// 6. Send results back to main thread
// --- 5. Send results back to main thread ---
self.postMessage({
id,
success: true,
blob,
blob: finalBlob,
dim,
blurhash,
});
bitmap.close();
} catch (error) {
self.postMessage({
id,