您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Resize any image client-side (CORS-safe). Alt+Right-Click or hover chip to open. Blob preview + anchor download. Remembers last settings. Timestamped filenames with tokens.
// ==UserScript== // @name Universal Image Resizer & Downloader (by Eliminater74) // @namespace https://greasyfork.org/en/users/123456-eliminater74 // @version 1.5 // @description Resize any image client-side (CORS-safe). Alt+Right-Click or hover chip to open. Blob preview + anchor download. Remembers last settings. Timestamped filenames with tokens. // @author Eliminater74 // @license MIT // @match *://*/* // @icon https://www.tiktok.com/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM_download // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @connect * // @run-at document-end // ==/UserScript== (function () { 'use strict'; // ========================= // Defaults (with timestamp in filename by default) // ========================= const DEFAULTS = { width: 3000, height: 3000, mode: 'contain', // 'contain' | 'cover' | 'stretch' bgColor: '#000000', // used for letterbox in 'contain' format: 'image/jpeg', // 'image/jpeg' | 'image/png' | 'image/webp' quality: 0.95, // for JPEG/WEBP filenameTpl: '{name}_{w}x{h}_{YYYY}-{MM}-{DD}_{hh}{mm}{ss}', // tokens below minImgEdge: 160, // only show chip if image larger than this showHoverChip: true, chipCorner: 'top-left', // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' useAltRightClick: true // true = Alt+RightClick; false = plain RightClick (replaces native menu on images) }; const STORE_KEY = 'uird:lastOptions'; const saved = safeGet(STORE_KEY); let state = { selectedImg: null, lastHoverImg: null, options: { ...DEFAULTS, ...(saved || {}) } }; // ========================= // Tiny DOM helpers // ========================= function h(tag, attrs = {}, children = []) { const el = document.createElement(tag); Object.entries(attrs).forEach(([k, v]) => { if (k === 'style' && typeof v === 'object') Object.assign(el.style, v); else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v); else el.setAttribute(k, v); }); (Array.isArray(children) ? children : [children]).forEach(c => { if (c == null) return; el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); }); return el; } function injectCSS(css) { document.head.appendChild(h('style', { type: 'text/css' }, css)); } injectCSS(` .uird-chip{position:absolute;z-index:2147483647;background:rgba(0,0,0,.85);color:#fff;font:12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;padding:4px 8px;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;user-select:none;box-shadow:0 2px 8px rgba(0,0,0,.4);pointer-events:auto} .uird-panel{position:fixed;right:20px;bottom:20px;width:340px;background:#111;color:#eee;border:1px solid #333;border-radius:10px;z-index:2147483647;box-shadow:0 8px 24px rgba(0,0,0,.6);font:13px/1.3 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif} .uird-panel .hdr{padding:8px 12px;background:#1a1a1a;border-bottom:1px solid #333;border-radius:10px 10px 0 0;display:flex;align-items:center;justify-content:space-between;cursor:move} .uird-panel .body{padding:10px 12px 12px;display:grid;grid-template-columns:1fr 1fr;gap:8px} .uird-panel label{opacity:.9;font-size:12px} .uird-panel input[type="number"],.uird-panel input[type="text"],.uird-panel select{width:100%;padding:6px 8px;background:#0f0f0f;color:#eee;border:1px solid #333;border-radius:6px;outline:none} .uird-panel input[type="color"]{width:100%;height:32px;padding:0;border:1px solid #333;border-radius:6px;background:#0f0f0f} .uird-panel .row-2{grid-column:span 2} .uird-panel .btns{grid-column:span 2;display:flex;gap:8px} .uird-panel button{flex:1;padding:8px 10px;background:#212121;color:#fff;border:1px solid #3a3a3a;border-radius:8px;cursor:pointer} .uird-panel button:hover{background:#2a2a2a} .uird-tag{display:inline-block;padding:4px 8px;border:1px solid #444;border-radius:6px;background:#181818;cursor:pointer;margin-right:6px;margin-bottom:6px} .uird-subtle{opacity:.65;font-size:11px} `); // ========================= // Hover chip (fixed selection) // ========================= let chip, chipHover = false, clearSelTimer; function ensureChip() { if (chip) return chip; chip = h('div', { class: 'uird-chip', style: { display: 'none' } }, 'Resize'); chip.addEventListener('mouseenter', () => { chipHover = true; }); chip.addEventListener('mouseleave', () => { chipHover = false; scheduleClearSelection(); }); chip.addEventListener('click', () => { if (!state.selectedImg && state.lastHoverImg) state.selectedImg = state.lastHoverImg; if (state.selectedImg) openPanel(); }); document.body.appendChild(chip); return chip; } function positionChipFor(img) { const rect = img.getBoundingClientRect(); const ch = ensureChip(); const pad = 8; let x, y; switch (state.options.chipCorner) { case 'top-right': x = scrollX + rect.left + rect.width - 70; y = scrollY + rect.top + pad; break; case 'bottom-left': x = scrollX + rect.left + pad; y = scrollY + rect.top + rect.height - 30; break; case 'bottom-right': x = scrollX + rect.left + rect.width - 70; y = scrollY + rect.top + rect.height - 30; break; default: x = scrollX + rect.left + pad; y = scrollY + rect.top + pad; } ch.style.left = `${x}px`; ch.style.top = `${y}px`; ch.style.display = 'block'; } function scheduleClearSelection() { clearTimeout(clearSelTimer); clearSelTimer = setTimeout(() => { if (!chipHover) { state.selectedImg = null; ensureChip().style.display = 'none'; } }, 250); } // ========================= // Draggable panel // ========================= function makeDraggable(panel, handle) { let sx, sy, sl, st, dragging = false; handle.addEventListener('mousedown', (e) => { dragging = true; sx = e.clientX; sy = e.clientY; const r = panel.getBoundingClientRect(); sl = r.left; st = r.top; e.preventDefault(); }); addEventListener('mousemove', (e) => { if (!dragging) return; panel.style.left = (sl + (e.clientX - sx)) + 'px'; panel.style.top = (st + (e.clientY - sy)) + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); addEventListener('mouseup', () => dragging = false); } // ========================= // Persist helpers // ========================= function safeGet(key) { try { return GM_getValue(key); } catch { try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; } } } function safeSet(key, value) { try { GM_setValue(key, value); } catch { try { localStorage.setItem(key, JSON.stringify(value)); } catch {} } } function saveOptions() { safeSet(STORE_KEY, state.options); } // ========================= // Panel UI // ========================= let panel; function openPanel() { if (panel) { panel.style.display = 'block'; return; } panel = h('div', { class: 'uird-panel' }, [ h('div', { class: 'hdr' }, [ h('div', {}, 'Image Resizer'), h('div', {}, [ h('button', { style: { background: '#2a2a2a', marginRight: '6px', padding: '4px 8px', borderRadius: '6px', border: '1px solid #555' }, onclick: () => { state.options = { ...DEFAULTS }; rerenderInputs(); saveOptions(); } }, 'Reset'), h('button', { style: { background: '#3a3a3a', padding: '4px 8px', borderRadius: '6px', border: '1px solid #555' }, onclick: () => panel.style.display = 'none' }, '✕') ]) ]), h('div', { class: 'body' }, [ // Presets h('div', { class: 'row-2' }, [ h('span', { class: 'uird-tag', onclick: () => setWH(3000, 3000) }, '3000×3000'), h('span', { class: 'uird-tag', onclick: () => setWH(5120, 2880) }, '5120×2880'), h('span', { class: 'uird-tag', onclick: () => setWH(2048, 1152) }, '2048×1152'), h('span', { class: 'uird-tag', onclick: () => setWH(1280, 720) }, '1280×720'), ]), // Size numField('Width', 'width'), numField('Height', 'height'), // Mode selectField('Mode', 'mode', [ ['contain', 'contain (letterbox)'], ['cover', 'cover (crop)'], ['stretch', 'stretch (no aspect)'] ]), // BG color colorField('BG Color (contain)', 'bgColor'), // Format & quality selectField('Format', 'format', [ ['image/jpeg', 'JPEG'], ['image/png', 'PNG'], ['image/webp', 'WEBP'] ]), numField('Quality (JPEG/WEBP)', 'quality', { step: '0.01', min: '0', max: '1', isFloat: true }), // Filename template + help h('div', { class: 'row-2' }, [ h('label', {}, 'Filename Template'), h('input', { type: 'text', value: state.options.filenameTpl, oninput: e => { state.options.filenameTpl = e.target.value; saveOptions(); } }), h('div', { class: 'uird-subtle row-2' }, 'Tokens: {name} {w} {h} {ext} {ts} {YYYY} {MM} {DD} {hh} {mm} {ss} {site} {title}') ]), // Behavior toggles selectField('Chip Corner', 'chipCorner', [ ['top-left', 'top-left'], ['top-right', 'top-right'], ['bottom-left', 'bottom-left'], ['bottom-right', 'bottom-right'] ]), selectField('Right-Click trigger', 'useAltRightClick', [ ['true', 'Alt + Right-Click'], ['false', 'Right-Click (no menu)'] ], v => v === 'true'), // Buttons h('div', { class: 'btns' }, [ h('button', { onclick: () => processCurrentImage(false) }, 'Preview (new tab)'), h('button', { onclick: () => processCurrentImage(true) }, 'Download') ]), ]) ]); document.body.appendChild(panel); makeDraggable(panel, panel.querySelector('.hdr')); function setWH(w, h) { state.options.width = w; state.options.height = h; saveOptions(); rerenderInputs(); } function numField(label, key, cfg = {}) { const inp = h('input', { type: 'number', value: state.options[key], step: cfg.step || (cfg.isFloat ? '0.01' : '1'), min: cfg.min || '0', max: cfg.max || '20000', oninput: e => { state.options[key] = cfg.isFloat ? clampFloat(e.target.value, 0, 100, state.options[key]) : clampInt(e.target.value, 1, 20000); saveOptions(); } }); return h('div', {}, [ h('label', {}, label), inp ]); } function selectField(label, key, opts, map = v => v) { const sel = h('select', { onchange: e => { state.options[key] = map(e.target.value); saveOptions(); } }, opts.map(([v, t]) => h('option', { value: String(v), selected: String(state.options[key]) === String(v) }, t))); return h('div', {}, [ h('label', {}, label), sel ]); } function colorField(label, key) { const inp = h('input', { type: 'color', value: state.options[key], oninput: e => { state.options[key] = e.target.value; saveOptions(); } }); return h('div', {}, [ h('label', {}, label), inp ]); } function rerenderInputs() { // crude but effective: rebuild the body section (keeps code short) panel.remove(); panel = null; openPanel(); } } function clampInt(v, min, max) { v = parseInt(v || 0, 10); if (isNaN(v)) v = min; return Math.max(min, Math.min(max, v)); } function clampFloat(v, min, max, fallback) { v = parseFloat(v); if (isNaN(v)) return fallback; return Math.max(min, Math.min(max, v)); } // ========================= // CORS-safe image loading // ========================= function fetchImageArrayBuffer(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', onload: (res) => { if (res.status >= 200 && res.status < 300 && res.response) { const ct = res.responseHeaders?.match(/content-type:\s*([^\n\r;]+)/i)?.[1]?.trim() || ''; resolve({ buffer: res.response, contentType: ct }); } else reject(new Error('Failed to fetch image: ' + res.status)); }, onerror: err => reject(err) }); }); } async function loadBitmapFromURL(src) { if (/^(data|blob):/i.test(src)) { const resp = await fetch(src); const blob = await resp.blob(); const bmp = (self.createImageBitmap) ? await createImageBitmap(blob) : await blobToCanvas(URL.createObjectURL(blob)); return { bmp, blob, name: guessNameFromURL('image'), type: blob.type || 'image/png' }; } const { buffer, contentType } = await fetchImageArrayBuffer(src); const type = contentType || guessMimeFromURL(src) || 'image/png'; const blob = new Blob([buffer], { type }); const bmp = (self.createImageBitmap) ? await createImageBitmap(blob) : await blobToCanvas(URL.createObjectURL(blob)); return { bmp, blob, name: guessNameFromURL(src), type }; } function blobToCanvas(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const c = document.createElement('canvas'); c.width = img.naturalWidth; c.height = img.naturalHeight; c.getContext('2d').drawImage(img, 0, 0); URL.revokeObjectURL(url); resolve(c); }; img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); }; img.src = url; }); } function guessNameFromURL(url) { try { const u = new URL(url, location.href); const base = (u.pathname.split('/').pop() || 'image').split('?')[0].split('#')[0]; const noExt = base.replace(/\.[a-z0-9]+$/i, ''); return noExt || 'image'; } catch { return 'image'; } } function guessMimeFromURL(url) { const ext = (url.split('.').pop() || '').toLowerCase().split('?')[0]; if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'; if (ext === 'png') return 'image/png'; if (ext === 'webp') return 'image/webp'; return ''; } // ========================= // Resize math // ========================= function computeDrawParams(sw, sh, dw, dh, mode) { if (mode === 'stretch') return { sx: 0, sy: 0, sw, sh, dx: 0, dy: 0, dw, dh }; const sr = sw / sh, dr = dw / dh; if (mode === 'contain') { let w, h; if (sr > dr) { w = dw; h = Math.round(dw / sr); } else { h = dh; w = Math.round(dh * sr); } const dx = Math.round((dw - w) / 2), dy = Math.round((dh - h) / 2); return { sx: 0, sy: 0, sw, sh, dx, dy, dw: w, dh: h }; } // cover let cw, ch, cx, cy; if (sr > dr) { ch = sh; cw = Math.round(ch * dr); cx = Math.round((sw - cw) / 2); cy = 0; } else { cw = sw; ch = Math.round(cw / dr); cx = 0; cy = Math.round((sh - ch) / 2); } return { sx: cx, sy: cy, sw: cw, sh: ch, dx: 0, dy: 0, dw, dh }; } // ========================= // Filename templating // ========================= function formatFilename(tpl, baseName, w, h, ext) { const now = new Date(); const pad = n => String(n).padStart(2, '0'); const tokens = { '{name}': sanitize(baseName || 'image'), '{w}': String(w), '{h}': String(h), '{ext}': ext, '{ts}': String(Math.floor(now.getTime() / 1000)), '{YYYY}': String(now.getFullYear()), '{MM}': pad(now.getMonth() + 1), '{DD}': pad(now.getDate()), '{hh}': pad(now.getHours()), '{mm}': pad(now.getMinutes()), '{ss}': pad(now.getSeconds()), '{site}': sanitize(location.host || 'site'), '{title}': sanitize(document.title || 'untitled') }; let out = tpl; for (const [k, v] of Object.entries(tokens)) out = out.split(k).join(v); return out; } function sanitize(s) { return (s || '').replace(/[\\/:*?"<>|]+/g, '_').replace(/\s+/g, ' ').trim(); } // ========================= // Core: process + preview/download (blob-based) // ========================= async function processCurrentImage(doDownload) { const img = state.selectedImg || state.lastHoverImg; if (!img) return; const o = state.options; try { const src = img.currentSrc || img.src || img.dataset?.src || img.getAttribute('src'); const { bmp, name } = await loadBitmapFromURL(src); const canvas = document.createElement('canvas'); canvas.width = o.width; canvas.height = o.height; const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; if (o.mode === 'contain') { ctx.fillStyle = o.bgColor || '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); } const p = computeDrawParams( bmp.width || bmp.canvas?.width || canvas.width, bmp.height || bmp.canvas?.height || canvas.height, canvas.width, canvas.height, o.mode ); ctx.drawImage(bmp, p.sx, p.sy, p.sw, p.sh, p.dx, p.dy, p.dw, p.dh); const ext = o.format === 'image/png' ? 'png' : (o.format === 'image/webp' ? 'webp' : 'jpg'); const outBase = formatFilename(o.filenameTpl || DEFAULTS.filenameTpl, name, o.width, o.height, ext); const fileName = `${outBase}.${ext}`; const blob = await new Promise(res => canvas.toBlob(res, o.format, o.format === 'image/png' ? undefined : o.quality)); if (!blob) throw new Error('Failed to generate output blob'); const blobURL = URL.createObjectURL(blob); if (doDownload) { const a = document.createElement('a'); a.href = blobURL; a.download = fileName; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(blobURL), 5000); } else { const w = window.open('about:blank', '_blank', 'noopener'); if (w && w.document) { w.document.title = fileName; w.document.body.style.margin = '0'; const imgEl = w.document.createElement('img'); imgEl.src = blobURL; imgEl.style.display = 'block'; imgEl.style.maxWidth = '100%'; imgEl.style.maxHeight = '100vh'; w.document.body.appendChild(imgEl); w.addEventListener('unload', () => URL.revokeObjectURL(blobURL)); } else { GM_openInTab(blobURL, { active: true, insert: true }); } } } catch (err) { console.error('[Universal Image Resizer] Error:', err); alert('Image resize failed:\n' + (err?.message || err)); } } // ========================= // Hover + Right-Click bindings // ========================= function bindHoverForImages(root = document) { root.querySelectorAll('img').forEach(img => { if (img.__uirdBound) return; img.__uirdBound = true; img.addEventListener('mouseenter', () => { if (!state.options.showHoverChip) return; const r = img.getBoundingClientRect(); if (r.width < state.options.minImgEdge && r.height < state.options.minImgEdge) return; state.selectedImg = img; state.lastHoverImg = img; positionChipFor(img); }); img.addEventListener('mouseleave', () => { scheduleClearSelection(); }); // R to open panel img.addEventListener('keydown', (e) => { if (e.key === 'r' || e.key === 'R') { state.selectedImg = img; state.lastHoverImg = img; openPanel(); } }); img.tabIndex = img.tabIndex || 0; // Alt+RightClick (default) or plain RightClick img.addEventListener('contextmenu', (e) => { const wantAlt = state.options.useAltRightClick; const trigger = wantAlt ? e.altKey : true; if (trigger) { e.preventDefault(); state.selectedImg = img; state.lastHoverImg = img; openPanel(); } }); const recalc = () => state.selectedImg === img && positionChipFor(img); addEventListener('scroll', recalc, { passive: true }); addEventListener('resize', recalc); }); } const mo = new MutationObserver(muts => { for (const m of muts) { m.addedNodes && m.addedNodes.forEach(n => { if (n.nodeType === 1) { if (n.tagName === 'IMG') bindHoverForImages(n.parentNode || document); else bindHoverForImages(n); } }); } }); function init() { bindHoverForImages(document); mo.observe(document.documentElement, { childList: true, subtree: true }); } if (document.readyState === 'complete' || document.readyState === 'interactive') init(); else addEventListener('DOMContentLoaded', init); })();