// ==UserScript== // @name 图片裁剪压缩工具 // @namespace local.image.crop.compress // @version 1.0.0 // @description 处理本地图片:可查看当前宽高/文件大小,按指定像素裁剪或缩放(等比/居中裁剪/拉伸),按质量或目标文件大小压缩。支持 PNG/JPEG/JPG/ICO/GIF/WebP/BMP/AVIF,可拖入或粘贴截图。 // @author faymi // @license MIT // @match *://*/* // @match file:///* // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; // ---------------- 基础工具 ---------------- const PREFIX = 'qic'; const sleep = ms => new Promise(r => setTimeout(r, ms)); function fmtSize(bytes) { if (!bytes || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0, v = Number(bytes); while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } return `${v >= 100 ? Math.round(v) : v.toFixed(1)} ${units[i]}`; } function extOf(name) { const m = /\.([a-zA-Z0-9]+)$/.exec(name || ''); return m ? m[1].toLowerCase() : ''; } function stemOf(name) { const i = (name || '').lastIndexOf('.'); return i > 0 ? name.slice(0, i) : (name || 'file'); } function safeName(name) { return String(name).replace(/[\\/:*?"<>|\x00-\x1f]/g, '_').trim() || 'image'; } function fileLooksImage(file) { if (file && file.type && file.type.startsWith('image/')) return true; const exts = ['png', 'jpg', 'jpeg', 'ico', 'gif', 'webp', 'bmp', 'avif', 'svg']; return exts.includes(extOf(file && file.name)); } function toMime(fmt) { return { png: 'image/png', jpeg: 'image/jpeg', webp: 'image/webp' }[fmt] || null; } function mimeToExt(mime) { if (mime === 'image/jpeg') return 'jpg'; if (mime === 'image/webp') return 'webp'; return 'png'; } function canvasToBlob(canvas, type, quality) { return new Promise((resolve, reject) => { canvas.toBlob(b => (b ? resolve(b) : reject(new Error('浏览器编码失败,可能不支持该输出格式'))), type, quality); }); } function supportsCanvasType(type) { try { const c = document.createElement('canvas'); return c.toDataURL(type).indexOf(`data:${type}`) === 0; } catch (_) { return false; } } // ---------------- 状态 ---------------- const state = { items: [], // { fid, file, name, size, w, h, imgUrl, img, out, rowEls } seq: 0, panelOpen: false, }; let els = {}; let qualityTouched = false; // 用户是否主动调整过质量(默认 85 视为未设置) // ---------------- 图片解码 ---------------- async function ensureImage(item) { if (item.img && item.img.complete && item.img.naturalWidth) return item.img; if (!item.imgUrl) item.imgUrl = URL.createObjectURL(item.file); const img = new Image(); img.decoding = 'async'; await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = () => reject(new Error('无法解码该图片(格式可能不受浏览器支持)')); img.src = item.imgUrl; }); item.img = img; return img; } async function measure(item) { const img = await ensureImage(item); item.w = img.naturalWidth; item.h = img.naturalHeight; return img; } // ---------------- 几何计算 ---------------- // mode: 'none' | 'fit' | 'cover' | 'stretch' // 返回 { ow, oh, sx, sy, sw, sh, changed },changed=false 表示无需改变像素尺寸 function computeGeom(mode, pos, sw, sh, rawW, rawH) { const tw = Math.max(0, parseInt(rawW, 10) || 0); const th = Math.max(0, parseInt(rawH, 10) || 0); if (!tw && !th) return { ow: sw, oh: sh, sx: 0, sy: 0, sw: sw, sh: sh, changed: false }; let w = tw, h = th; if (w && !h) h = Math.max(1, Math.round(sh * w / sw)); if (h && !w) w = Math.max(1, Math.round(sw * h / sh)); if (mode === 'none') return { ow: sw, oh: sh, sx: 0, sy: 0, sw: sw, sh: sh, changed: false }; if (mode === 'stretch') { return { ow: w, oh: h, sx: 0, sy: 0, sw: sw, sh: sh, changed: true }; } if (mode === 'fit') { const s = Math.min(w / sw, h / sh); const ow = Math.max(1, Math.round(sw * s)); const oh = Math.max(1, Math.round(sh * s)); const changed = ow !== sw || oh !== sh; return { ow, oh, sx: 0, sy: 0, sw, sh, changed }; } // cover:等比放大/缩小后居中(或按位置)裁剪,输出恰好 w×h let srw = sw, srh = sh; const targetRatio = w / h; const srcRatio = sw / sh; if (srcRatio > targetRatio) { srh = sh; srw = Math.round(sh * targetRatio); } else { srw = sw; srh = Math.round(sw / targetRatio); } const marginX = sw - srw; const marginY = sh - srh; let sx, sy; if (pos === 'left') { sx = 0; } else if (pos === 'right') { sx = marginX; } else if (pos === 'top') { sx = marginX / 2; } else if (pos === 'bottom') { sx = marginX / 2; } else { sx = marginX / 2; } if (pos === 'top') { sy = 0; } else if (pos === 'bottom') { sy = marginY; } else if (pos === 'left' || pos === 'right') { sy = marginY / 2; } else { sy = marginY / 2; } sx = Math.round(sx); sy = Math.round(sy); const changed = w !== sw || h !== sh; return { ow: w, oh: h, sx, sy, sw: srw, sh: srh, changed }; } // ---------------- 单张处理 ---------------- async function processItem(item, opts) { const img = await ensureImage(item); const sw = img.naturalWidth, sh = img.naturalHeight; const geom = computeGeom(opts.mode, opts.pos, sw, sh, opts.width, opts.height); const srcMime = item.file.type || ''; const srcExt = extOf(item.file.name); const fmt = opts.format; // 'source'|'png'|'jpeg'|'webp' const wantTranscode = fmt !== 'source'; const isGif = srcExt === 'gif' || srcMime === 'image/gif'; const isIco = srcExt === 'ico' || /icon/i.test(srcMime); const isBmp = srcExt === 'bmp' || srcMime === 'image/bmp'; const isLossySource = srcMime === 'image/jpeg' || srcMime === 'image/webp'; const passthruRaw = isGif || isIco || isBmp || srcMime === 'image/avif'; // 目标输出格式(浏览器 canvas 只能编码 png/jpeg/webp) let type; if (wantTranscode) { type = toMime(fmt); } else if (isLossySource) { type = srcMime; // JPEG/WebP:可按质量重编码 } else if (passthruRaw) { type = 'image/png'; // 需要变换时 GIF/ICO/BMP/AVIF 转静态 PNG } else { type = srcMime || 'image/png'; } if (!['image/png', 'image/jpeg', 'image/webp'].includes(type)) type = 'image/png'; if (type === 'image/webp' && !supportsCanvasType('image/webp')) type = 'image/png'; // 未做任何改动时:GIF 动图/ICO/BMP/AVIF/PNG 原样保留; // JPEG/WebP 仅在用户主动调低质量时重编码(即为“压缩”),否则也原样保留。 const untouched = !geom.changed && !opts.maxBytes && fmt === 'source'; if (untouched) { const canPassthru = passthruRaw || srcMime === 'image/png' || (isLossySource && (!opts.qualityTouched || opts.quality >= 100)); if (canPassthru) { let warn = ''; if (isGif && opts.qualityTouched && opts.quality < 100) { warn = 'GIF 动图已原样保留,无法按质量压缩;如需压缩请输出 JPEG/WebP(将转为静态图)'; } else if (srcMime === 'image/png' && opts.qualityTouched && opts.quality < 100) { warn = 'PNG 为无损格式,质量参数不生效;如需压缩请改尺寸或输出 JPEG/WebP'; } return { blob: item.file, w: sw, h: sh, type: srcMime || 'application/octet-stream', changed: false, warn, name: item.name, }; } } let canvasType = type; if (opts.maxBytes > 0 && !['image/jpeg', 'image/webp'].includes(canvasType)) { canvasType = supportsCanvasType('image/webp') ? 'image/webp' : 'image/jpeg'; } if (geom.ow * geom.oh > 100000000) { throw new Error(`输出尺寸过大(${geom.ow}×${geom.oh}),请减小目标宽高`); } const canvas = document.createElement('canvas'); canvas.width = geom.ow; canvas.height = geom.oh; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('无法创建画布'); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; if (canvasType === 'image/jpeg') { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); } if (geom.changed) { ctx.drawImage(img, geom.sx, geom.sy, geom.sw, geom.sh, 0, 0, geom.ow, geom.oh); } else { ctx.drawImage(img, 0, 0); } const quality = (opts.quality != null ? opts.quality : 90) / 100; let blob; let warn = ''; if (opts.maxBytes > 0 && ['image/jpeg', 'image/webp'].includes(canvasType)) { let lo = 5, hi = 96, best = null; for (let i = 0; i < 9; i++) { const q = Math.max(5, Math.min(95, Math.round((lo + hi) / 2))); const b = await canvasToBlob(canvas, canvasType, q / 100); if (b.size <= opts.maxBytes) { best = b; lo = q + 1; } else hi = q - 1; } blob = best || await canvasToBlob(canvas, canvasType, 0.05); if (blob.size > opts.maxBytes) warn = `已尽力压缩,仍为 ${fmtSize(blob.size)}(>${Math.round(opts.maxBytes / 1024)}KB)`; if (canvasType !== type) warn = (warn ? warn + ';' : '') + `已自动转 ${mimeToExt(canvasType).toUpperCase()} 以满足大小`; } else { blob = await canvasToBlob(canvas, canvasType, quality); } const changed = geom.changed || wantTranscode || opts.maxBytes > 0 || (isLossySource && fmt === 'source' && opts.qualityTouched && opts.quality < 100); const outName = `${stemOf(item.file.name)}_${canvas.width}x${canvas.height}.${mimeToExt(canvasType)}`; return { blob, w: canvas.width, h: canvas.height, type: canvasType, changed, warn, name: outName }; } // ---------------- UI ---------------- function css() { const s = document.createElement('style'); s.id = `${PREFIX}-style`; s.textContent = ` #${PREFIX}-toggle{position:fixed;right:20px;bottom:24px;z-index:2147483647;width:46px;height:46px;border-radius:50%;background:#1677ff;color:#fff;font-size:22px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.35);user-select:none;transition:transform .15s;font-family:Segoe UI,Microsoft YaHei,sans-serif;} #${PREFIX}-toggle:hover{transform:scale(1.08);} #${PREFIX}-panel{position:fixed;right:20px;bottom:82px;z-index:2147483646;width:470px;max-width:calc(100vw - 36px);max-height:82vh;background:#202124;border:1px solid #3c4043;border-radius:12px;color:#e8eaed;font-family:Segoe UI,Microsoft YaHei,sans-serif;font-size:13px;display:none;flex-direction:column;box-shadow:0 8px 28px rgba(0,0,0,.6);overflow:hidden;} .${PREFIX}-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#292b2f;border-bottom:1px solid #3c4043;} .${PREFIX}-close{cursor:pointer;color:#9aa0a6;font-size:16px;padding:0 6px;} .${PREFIX}-close:hover{color:#fff;} .${PREFIX}-toolbar{display:flex;gap:6px;padding:8px 10px;border-bottom:1px solid #3c4043;flex-wrap:wrap;align-items:center;} .${PREFIX}-btn{padding:4px 10px;border:1px solid #5f6368;border-radius:6px;background:#292b2f;color:#e8eaed;font-size:12px;cursor:pointer;white-space:nowrap;} .${PREFIX}-btn:hover{background:#3c4043;} .${PREFIX}-btn:disabled{opacity:.5;cursor:not-allowed;} .${PREFIX}-btn.primary{background:#1677ff;border-color:#1677ff;} .${PREFIX}-btn.primary:hover{background:#3c8cff;} .${PREFIX}-list{flex:1;overflow-y:auto;min-height:120px;max-height:36vh;padding:4px 6px;} .${PREFIX}-empty{color:#9aa0a6;text-align:center;padding:18px 10px;font-size:12px;line-height:1.8;border:1px dashed #3c4043;border-radius:8px;margin:8px 6px;} .${PREFIX}-row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-bottom:1px solid #33353a;border-radius:6px;} .${PREFIX}-row:hover{background:#2b2d31;} .${PREFIX}-row.selected{background:#1e2a3a;} .${PREFIX}-thumb{width:44px;height:44px;object-fit:contain;background:#17181a;border-radius:6px;flex:none;border:1px solid #3c4043;} .${PREFIX}-info{flex:1;min-width:0;} .${PREFIX}-name{font-size:12px;color:#e8eaed;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .${PREFIX}-meta{font-size:11px;color:#9aa0a6;word-break:break-all;line-height:1.5;} .${PREFIX}-meta .ok{color:#7bd88f;} .${PREFIX}-meta .err{color:#f28b82;} .${PREFIX}-meta .warn{color:#f5c26b;} .${PREFIX}-ops{display:flex;flex-direction:column;gap:4px;flex:none;} .${PREFIX}-mini{font-size:11px;padding:1px 6px;} .${PREFIX}-settings{padding:10px 12px;border-top:1px solid #3c4043;background:#1c1d1f;overflow-y:auto;max-height:34vh;} .${PREFIX}-field{display:flex;align-items:center;gap:6px;margin:5px 0;flex-wrap:wrap;} .${PREFIX}-field label{color:#c8ccd1;font-size:12px;min-width:72px;flex:none;} .${PREFIX}-input, .${PREFIX}-select{background:#292b2f;border:1px solid #5f6368;color:#e8eaed;border-radius:6px;padding:4px 7px;font-size:12px;outline:none;} .${PREFIX}-input:focus,.${PREFIX}-select:focus{border-color:#1677ff;} .${PREFIX}-hint{font-size:11px;color:#9aa0a6;margin:3px 0;} .${PREFIX}-current{color:#8ab4f8;font-size:12px;} .${PREFIX}-log{max-height:90px;overflow-y:auto;padding:6px 12px;border-top:1px solid #3c4043;background:#1b1c1e;font-size:11px;color:#9aa0a6;} .${PREFIX}-range{width:130px;} `; document.head.appendChild(s); } function buildUI() { css(); const toggle = document.createElement('div'); toggle.id = `${PREFIX}-toggle`; toggle.textContent = '🖼'; toggle.title = '图片裁剪压缩工具'; const panel = document.createElement('div'); panel.id = `${PREFIX}-panel`; panel.innerHTML = `
🖼 图片裁剪压缩工具 ✕
支持 PNG/JPEG/JPG/ICO/GIF/WebP 等,可拖入或粘贴
当前:未选择图片
×
只填一项时另一项按原图宽高比自动计算;两项留空则不改变尺寸。
(仅在“裁剪到目标尺寸”时生效)
%(JPEG/WebP 有效,PNG 无损不受影响)
KB(自动调质量,超出时自动转 WebP/JPEG)
`; document.body.appendChild(toggle); document.body.appendChild(panel); els = { toggle, panel, close: panel.querySelector(`.${PREFIX}-close`), fileInput: panel.querySelector('input[type=file]'), list: panel.querySelector(`#${PREFIX}-list`), summary: panel.querySelector(`#${PREFIX}-summary`), mode: panel.querySelector(`#${PREFIX}-mode`), w: panel.querySelector(`#${PREFIX}-w`), h: panel.querySelector(`#${PREFIX}-h`), pos: panel.querySelector(`#${PREFIX}-pos`), fmt: panel.querySelector(`#${PREFIX}-fmt`), qrange: panel.querySelector(`#${PREFIX}-qrange`), qnum: panel.querySelector(`#${PREFIX}-qnum`), maxchk: panel.querySelector(`#${PREFIX}-maxchk`), maxkb: panel.querySelector(`#${PREFIX}-maxkb`), processBtn: panel.querySelector(`#${PREFIX}-process`), downloadBtn: panel.querySelector(`#${PREFIX}-download`), logEl: panel.querySelector(`#${PREFIX}-log`), }; // 事件 toggle.addEventListener('click', togglePanel); els.close.addEventListener('click', () => setPanel(false)); panel.querySelector('[data-act=pick]').addEventListener('click', () => els.fileInput.click()); panel.querySelector('[data-act=all]').addEventListener('click', () => { state.items.forEach(it => { it.checked = true; refreshRowCheck(it); }); updateSummary(); log(`已全选 ${state.items.length} 张`); }); panel.querySelector('[data-act=clear]').addEventListener('click', clearAll); els.fileInput.addEventListener('change', () => { addFiles(els.fileInput.files); els.fileInput.value = ''; }); els.list.addEventListener('click', onListClick); els.qrange.addEventListener('input', () => { qualityTouched = true; els.qnum.value = els.qrange.value; }); els.qnum.addEventListener('input', () => { qualityTouched = true; els.qrange.value = els.qnum.value; }); els.maxchk.addEventListener('change', () => { els.maxkb.disabled = !els.maxchk.checked; }); els.processBtn.addEventListener('click', () => processSelected(false)); els.downloadBtn.addEventListener('click', () => processSelected(true)); // 拖拽 ['dragenter', 'dragover'].forEach(ev => document.addEventListener(ev, e => { if (state.panelOpen) { e.preventDefault(); e.stopPropagation(); } }, true)); document.addEventListener('drop', e => { if (!state.panelOpen) return; e.preventDefault(); e.stopPropagation(); if (e.dataTransfer && e.dataTransfer.files.length) addFiles(e.dataTransfer.files); }, true); // 粘贴截图 document.addEventListener('paste', e => { if (!state.panelOpen) return; const files = e.clipboardData && e.clipboardData.files; if (files && files.length) { const imgs = [...files].filter(fileLooksImage); if (imgs.length) { e.preventDefault(); addFiles(imgs); } } }); } function togglePanel() { setPanel(!state.panelOpen); } function setPanel(open) { state.panelOpen = open; els.panel.style.display = open ? 'flex' : 'none'; els.toggle.style.display = open ? 'none' : 'flex'; if (open) updateSummary(); } function log(msg, cls) { const line = document.createElement('div'); line.style.cssText = 'margin:2px 0;line-height:1.5;word-break:break-all;'; if (cls) line.classList.add(cls); line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; els.logEl.appendChild(line); while (els.logEl.children.length > 150) els.logEl.removeChild(els.logEl.firstChild); els.logEl.scrollTop = els.logEl.scrollHeight; } // ---------------- 列表 ---------------- function addFiles(fileList) { const files = [...fileList].filter(fileLooksImage); if (!files.length) { log('没有可识别的图片文件', 'err'); return; } files.forEach(file => { const item = { fid: ++state.seq, file, name: file.name || `图片${state.seq}.png`, size: file.size, w: 0, h: 0, imgUrl: null, img: null, out: null, checked: true, loading: true, rowEls: null, }; state.items.push(item); appendRow(item); item.loading = false; measure(item) .then(() => { refreshRowMeta(item); updateSummary(); }) .catch(err => { item.loadError = err.message; refreshRowMeta(item); }); }); setPanel(true); log(`已添加 ${files.length} 张图片,共 ${state.items.length} 张`); updateSummary(); } function rowTemplate() { const row = document.createElement('div'); row.className = `${PREFIX}-row`; row.innerHTML = `
`; return row; } function appendRow(item) { const row = rowTemplate(); row.dataset.fid = item.fid; if (!item.imgUrl) item.imgUrl = URL.createObjectURL(item.file); const thumb = row.querySelector(`.${PREFIX}-thumb`); thumb.src = item.imgUrl; row.querySelector(`.${PREFIX}-name`).textContent = item.name; row.querySelector('input').checked = item.checked; item.rowEls = { row, thumb, name: row.querySelector(`.${PREFIX}-name`), meta: row.querySelector(`.${PREFIX}-meta`) }; els.list.appendChild(row); refreshRowMeta(item); } function originalMeta(item) { if (item.loadError) return `加载失败:${escapeHtml(item.loadError)}`; if (!item.w) return '读取尺寸中…'; const type = (item.file.type || 'image').replace('image/', '').toUpperCase(); return `${item.w}×${item.h}px · ${fmtSize(item.size)} · ${type}`; } function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function refreshRowMeta(item) { if (!item.rowEls) return; let html = originalMeta(item); if (item.out) { const okCls = item.out.warn ? 'warn' : 'ok'; const outExt = (extOf(item.out.name) || mimeToExt(item.out.type)).toUpperCase(); html += `
→ ${item.out.w}×${item.out.h}px · ${fmtSize(item.out.blob.size)} · ${outExt}${item.out.warn ? ' · ' + escapeHtml(item.out.warn) : ''}`; } item.rowEls.meta.innerHTML = html; item.rowEls.row.classList.toggle('selected', item.checked); } function refreshRowCheck(item) { if (item.rowEls) { item.rowEls.row.querySelector('input').checked = item.checked; item.rowEls.row.classList.toggle('selected', item.checked); } } function onListClick(e) { const row = e.target.closest(`.${PREFIX}-row`); if (!row) return; const item = state.items.find(i => i.fid === Number(row.dataset.fid)); if (!item) return; const btn = e.target.closest('button'); if (btn && btn.dataset.act === 'del') { removeItem(item); return; } if (btn && btn.dataset.act === 'dl') { processOneAndDownload(item); return; } const cb = e.target.closest('input[type=checkbox]'); if (cb) { item.checked = cb.checked; refreshRowCheck(item); updateSummary(); return; } if (e.target.closest(`.${PREFIX}-thumb`) || e.target.closest(`.${PREFIX}-info`)) { item.checked = !item.checked; refreshRowCheck(item); updateSummary(); } } function removeItem(item) { const idx = state.items.indexOf(item); if (idx >= 0) state.items.splice(idx, 1); if (item.rowEls) item.rowEls.row.remove(); if (item.imgUrl) { URL.revokeObjectURL(item.imgUrl); item.imgUrl = null; } if (!state.items.length) { els.list.innerHTML = `
暂无图片
点击“📁 选择图片”,或把图片文件拖到这里 / 直接 Ctrl+V 粘贴截图
`; } updateSummary(); } function clearAll() { state.items.forEach(item => { if (item.imgUrl) URL.revokeObjectURL(item.imgUrl); }); state.items = []; els.list.innerHTML = `
暂无图片
点击“📁 选择图片”,或把图片文件拖到这里 / 直接 Ctrl+V 粘贴截图
`; updateSummary(); log('已清空'); } function selectedItems() { return state.items.filter(i => i.checked && !i.loadError); } function updateSummary() { const sel = selectedItems(); const imgNum = state.items.length; if (!imgNum) { els.summary.textContent = '当前:未选择图片'; els.processBtn.disabled = true; els.downloadBtn.disabled = true; return; } if (sel.length === 1) { const it = sel[0]; const dim = it.w ? `${it.w}×${it.h}px` : '读取中…'; els.summary.textContent = `当前:${dim} · ${fmtSize(it.size)} · ${it.name}`; } else { els.summary.textContent = `当前:共 ${imgNum} 张,选中 ${sel.length} 张(参数将应用到所有选中图片)`; } const hasSel = sel.length > 0; els.processBtn.disabled = !hasSel; els.downloadBtn.disabled = !hasSel; } // ---------------- 处理 ---------------- function readOpts() { const maxKB = els.maxchk.checked ? (parseFloat(els.maxkb.value) || 0) : 0; return { mode: els.mode.value, width: els.w.value.trim(), height: els.h.value.trim(), pos: els.pos.value, format: els.fmt.value, quality: Math.max(5, Math.min(100, parseInt(els.qnum.value, 10) || 85)), qualityTouched, maxBytes: maxKB > 0 ? Math.round(maxKB * 1024) : 0, }; } async function runProcessItem(it, opts) { const res = await processItem(it, opts); if (!it.out || it.out.blob !== res.blob) { if (it.out && it.out.url) URL.revokeObjectURL(it.out.url); res.url = URL.createObjectURL(res.blob); } it.out = res; refreshRowMeta(it); log(`完成:${res.name || it.name} → ${fmtSize(res.blob.size)}${res.warn ? '(' + res.warn + ')' : ''}`); return res; } async function processSelected(andDownload) { const items = selectedItems(); if (!items.length) { log('请先勾选要处理的图片', 'err'); return; } const opts = readOpts(); els.processBtn.disabled = true; els.downloadBtn.disabled = true; let ok = 0; try { for (let i = 0; i < items.length; i++) { const it = items[i]; log(`[${i + 1}/${items.length}] 处理中:${it.name}`); try { await runProcessItem(it, opts); ok++; } catch (e) { log(`失败:${it.name} —— ${e.message}`, 'err'); } await sleep(60); } log(`处理完成:成功 ${ok}/${items.length}`); if (andDownload) { const withOut = items.filter(i => i.out); if (withOut.length) { log(`开始下载 ${withOut.length} 个文件…`); await downloadItems(withOut); } } } finally { updateSummary(); } } async function processOneAndDownload(item) { if (item.loadError) { log(`无法处理:${item.name}(${item.loadError})`, 'err'); return; } const opts = readOpts(); els.processBtn.disabled = true; els.downloadBtn.disabled = true; try { await runProcessItem(item, opts); await downloadItems([item]); } catch (e) { log(`失败:${item.name} —— ${e.message}`, 'err'); } finally { updateSummary(); } } async function downloadItems(items) { for (let i = 0; i < items.length; i++) { const it = items[i]; if (!it.out) { log(`跳过(无结果):${it.name}`, 'err'); continue; } const name = safeName(it.out.name || it.name); const url = it.out.url || URL.createObjectURL(it.out.blob); if (!it.out.url) it.out.url = url; const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); log(`[${i + 1}/${items.length}] 已开始下载:${name}`); await sleep(500); } } // ---------------- 初始化 ---------------- function init() { if (document.getElementById(`${PREFIX}-toggle`)) return; if (!document.body) { document.addEventListener('DOMContentLoaded', init); return; } buildUI(); els.list.innerHTML = `
暂无图片
点击“📁 选择图片”,或把图片文件拖到这里 / 直接 Ctrl+V 粘贴截图
`; updateSummary(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();