// ==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);
})();