JavaScript
const MAX_LONG_EDGE = 1600;
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');
const infoEl = document.getElementById('info');
const resizedInp = document.getElementById('resizedFile');
const metaW = document.getElementById('metaW');
const metaH = document.getElementById('metaH');
const metaOrig = document.getElementById('metaOrig');
let currentObjectURL = null;
/** ① Worker本体を「普通の関数」として定義(外部スコープに依存しないことが大事) */
function workerMain() {
self.onmessage = async (e) => {
const { file, maxEdge } = e.data;
const outType = (file.type && file.type.includes('png')) ? 'image/png' : 'image/jpeg';
const quality = (outType === 'image/jpeg') ? 0.9 : 1.0;
try {
if (typeof OffscreenCanvas !== 'undefined' && 'createImageBitmap' in self) {
const bitmap = await createImageBitmap(file);
const width = bitmap.width;
const height = bitmap.height;
const longEdge = Math.max(width, height);
const scale = longEdge > maxEdge ? (maxEdge / longEdge) : 1;
const outW = Math.round(width * scale);
const outH = Math.round(height * scale);
const canvas = new OffscreenCanvas(outW, outH);
const ctx = canvas.getContext('2d', { alpha: true });
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(bitmap, 0, 0, outW, outH);
bitmap.close?.();
const blob = await canvas.convertToBlob({ type: outType, quality });
self.postMessage({ ok: true, blob, outW, outH, outType });
return;
}
// フォールバック指示(メインスレッドで実行してもらう)
infoEl.textContent = 'メインスレッドで処理中...';
self.postMessage({ ok: false, needFallback: true, reason: 'No OffscreenCanvas or createImageBitmap' });
} catch (err) {
self.postMessage({ ok: false, error: err?.message || String(err) });
}
};
}
/** ② 関数を toString() して Blob URL から Worker を生成 */
function createWorkerFromFunction(fn) {
// 直に IIFE してワーカー側で関数を起動
const src = `(${fn.toString()})();`;
const blob = new Blob([src], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
// 後で掃除できるようURLを持たせておく(任意)
worker.__url = url;
return worker;
}
const worker = createWorkerFromFunction(workerMain);
window.addEventListener('unload', () => {
try { worker.terminate(); } catch {}
if (worker.__url) URL.revokeObjectURL(worker.__url);
});
/** 古環境向けフォールバック(メインスレッドでリサイズ) */
async function resizeOnMainThread(file) {
const img = await new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const el = new Image();
el.onload = () => { URL.revokeObjectURL(url); resolve(el); };
el.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
el.src = url;
});
const width = img.naturalWidth || img.width;
const height = img.naturalHeight || img.height;
const longEdge = Math.max(width, height);
const scale = longEdge > MAX_LONG_EDGE ? (MAX_LONG_EDGE / longEdge) : 1;
const outW = Math.round(width * scale);
const outH = Math.round(height * scale);
const canvas = document.createElement('canvas');
canvas.width = outW; canvas.height = outH;
const ctx = canvas.getContext('2d', { alpha: true });
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, outW, outH);
const outType = (file.type && file.type.includes('png')) ? 'image/png' : 'image/jpeg';
const quality = (outType === 'image/jpeg') ? 0.9 : 1.0;
const blob = await new Promise(res => canvas.toBlob(res, outType, quality));
if (!blob) throw new Error('toBlob failed');
return { blob, outW, outH, outType };
}
fileInput.addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (!file) return;
infoEl.textContent = '処理中...';
try {
const result = await resizeInWorkerOrFallback(file);
await updatePreviewAndForm(file, result);
} catch (err) {
console.error(err);
infoEl.textContent = '処理に失敗しました。別の画像でお試しください。';
} finally {
e.target.value = ''; // 同じファイルでも再選択可
}
});
function resizeInWorkerOrFallback(file) {
return new Promise((resolve, reject) => {
const onMsg = (ev) => {
worker.removeEventListener('message', onMsg);
const d = ev.data;
if (d.ok) {
resolve({ blob: d.blob, outW: d.outW, outH: d.outH, outType: d.outType });
} else if (d.needFallback) {
resizeOnMainThread(file).then(resolve, reject);
} else {
reject(new Error(d.error || 'worker failed'));
}
};
worker.addEventListener('message', onMsg);
worker.postMessage({ file, maxEdge: MAX_LONG_EDGE });
});
}
async function updatePreviewAndForm(originalFile, { blob, outW, outH, outType }) {
// プレビュー
if (currentObjectURL) URL.revokeObjectURL(currentObjectURL);
currentObjectURL = URL.createObjectURL(blob);
preview.src = currentObjectURL;
// 情報
const kb = (blob.size / 1024).toFixed(1);
infoEl.textContent = `出力: ${outW}×${outH}, 形式: ${outType}, 約 ${kb} KB`;
// フォームへセット(DataTransferで file input に入れる)
const ext = (outType === 'image/png') ? 'png' : 'jpg';
const base = (originalFile.name || 'image').replace(/\.[^.]+$/, '');
const filename = `${base}_resized_${outW}x${outH}.${ext}`;
const resizedFile = new File([blob], filename, { type: outType });
let attached = false;
if (window.DataTransfer) {
try {
const dt = new DataTransfer();
dt.items.add(resizedFile);
resizedInp.files = dt.files;
attached = resizedInp.files && resizedInp.files.length > 0;
} catch {
console.error('ERROR :: Failed to attach resized file to input');
}
}
metaW.value = String(outW);
metaH.value = String(outH);
metaOrig.value = originalFile.name || '';
}