NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Rongguang Image Decoder + Local Encryptor
// @name:zh-CN 荣光解码助手 + 本地加密
// @namespace https://yourname.example.com/
// @version 1.0
// @description Decode images when title contains "荣光", plus floating button to encrypt local images
// @match *://shuiyuan.sjtu.edu.cn/t/topic/*
// @author Your Name
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
// === Check if the special keyword "荣光" exists in the topic title ===
const specialElem = document.querySelector("#ember3 > div.drop-down-mode.d-header-wrap > header > div > div > div.two-rows.extra-info-wrapper > div > div > h1 > a > span");
if (!specialElem || !specialElem.textContent.includes("荣光")) {
console.log("荣光 keyword not found, Rongguang Image Decoder Helper is disabled.");
return;
}
const processed = new WeakSet();
const cacheMap = new WeakMap();
/** === Worker script === **/
const workerScript = `
self.onmessage = async function(e) {
const { buffer, width, height, mode } = e.data;
const blob = new Blob([buffer]);
const imgBitmap = await createImageBitmap(blob);
const canvas = new OffscreenCanvas(width || imgBitmap.width, height || imgBitmap.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(imgBitmap, 0, 0);
const imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height);
const imgdata2 = new ImageData(canvas.width, canvas.height);
function gilbert2d(width, height) {
const coordinates = [];
if (width >= height) generate2d(0, 0, width, 0, 0, height, coordinates);
else generate2d(0, 0, 0, height, width, 0, coordinates);
return coordinates;
}
function generate2d(x, y, ax, ay, bx, by, coordinates) {
const w = Math.abs(ax + ay), h = Math.abs(bx + by);
const dax = Math.sign(ax), day = Math.sign(ay);
const dbx = Math.sign(bx), dby = Math.sign(by);
if (h === 1) { for (let i = 0; i < w; i++) { coordinates.push([x, y]); x += dax; y += day; } return; }
if (w === 1) { for (let i = 0; i < h; i++) { coordinates.push([x, y]); x += dbx; y += dby; } return; }
let ax2 = Math.floor(ax / 2), ay2 = Math.floor(ay / 2);
let bx2 = Math.floor(bx / 2), by2 = Math.floor(by / 2);
const w2 = Math.abs(ax2 + ay2), h2 = Math.abs(bx2 + by2);
if (2 * w > 3 * h) {
if ((w2 % 2) && (w > 2)) { ax2 += dax; ay2 += day; }
generate2d(x, y, ax2, ay2, bx, by, coordinates);
generate2d(x + ax2, y + ay2, ax - ax2, ay - ay2, bx, by, coordinates);
} else {
if ((h2 % 2) && (h > 2)) { bx2 += dbx; by2 += dby; }
generate2d(x, y, bx2, by2, ax2, ay2, coordinates);
generate2d(x + bx2, y + by2, ax, ay, bx - bx2, by - by2, coordinates);
generate2d(x + (ax - dax) + (bx2 - dbx), y + (ay - day) + (by2 - dby),
-bx2, -by2, -(ax - ax2), -(ay - ay2), coordinates);
}
}
const curve = gilbert2d(canvas.width, canvas.height);
const offset = Math.round((Math.sqrt(5) - 1) / 2 * canvas.width * canvas.height);
let i = 0, total = canvas.width * canvas.height;
const CHUNK_SIZE = 5000;
function processChunk() {
const end = Math.min(i + CHUNK_SIZE, total);
for (; i < end; i++) {
let srcIndex, dstIndex;
if (mode === "decode") {
const old_pos = curve[i];
const new_pos = curve[(i + offset) % total];
srcIndex = 4 * (new_pos[0] + new_pos[1] * canvas.width);
dstIndex = 4 * (old_pos[0] + old_pos[1] * canvas.width);
} else {
const new_pos = curve[i];
const old_pos = curve[(i + offset) % total];
srcIndex = 4 * (new_pos[0] + new_pos[1] * canvas.width);
dstIndex = 4 * (old_pos[0] + old_pos[1] * canvas.width);
}
imgdata2.data.set(imgdata.data.slice(srcIndex, srcIndex + 4), dstIndex);
}
self.postMessage({ progress: Math.floor((i / total) * 100) });
if (i < total) setTimeout(processChunk, 0);
else {
ctx.putImageData(imgdata2, 0, 0);
canvas.convertToBlob({ type: "image/jpeg", quality: 0.95 })
.then(blob => {
const reader = new FileReader();
reader.onloadend = () => {
self.postMessage({ done: true, dataUrl: reader.result });
};
reader.readAsDataURL(blob);
});
}
}
processChunk();
};
`;
function createWorker() {
const blob = new Blob([workerScript], { type: "application/javascript" });
return new Worker(URL.createObjectURL(blob));
}
function updateButtonProgress(btn, percent, text) {
btn.textContent = text;
btn.style.background = `linear-gradient(to right, rgba(40, 167, 69, 0.6) ${percent}%, rgba(255, 255, 255, 0.15) ${percent}%)`;
}
function downloadImageWithProgress(url, btn) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percent = Math.floor((event.loaded / event.total) * 50);
updateButtonProgress(btn, percent, `Downloading… ${percent}%`);
}
};
xhr.onload = () => {
if (xhr.status === 200) {
updateButtonProgress(btn, 50, `Download complete, decoding…`);
resolve(xhr.response);
} else reject(new Error(`Download failed`));
};
xhr.onerror = () => reject(new Error("Network error"));
xhr.send();
});
}
async function toggleDecryption(originalUrl, img, btn) {
const cache = cacheMap.get(img);
if (!cache) {
btn.disabled = true;
try {
const arrayBuffer = await downloadImageWithProgress(originalUrl, btn);
const testBlob = new Blob([arrayBuffer]);
const testImg = new Image();
testImg.src = URL.createObjectURL(testBlob);
await new Promise(r => testImg.onload = r);
const worker = createWorker();
worker.postMessage({ buffer: arrayBuffer, width: testImg.width, height: testImg.height, mode: "decode" }, [arrayBuffer]);
worker.onmessage = (e) => {
if (e.data.progress !== undefined) {
updateButtonProgress(btn, e.data.progress, `Decoding… ${e.data.progress}%`);
}
if (e.data.done) {
img.removeAttribute("srcset");
img.removeAttribute("loading");
img.src = e.data.dataUrl;
cacheMap.set(img, { decodedUrl: e.data.dataUrl, originalUrl, state: "decoded" });
btn.disabled = false;
btn.textContent = "Show Original";
btn.style.background = "rgba(255, 255, 255, 0.15)";
}
};
} catch (err) {
btn.textContent = "Load Failed";
btn.style.background = "rgba(255,0,0,0.5)";
}
} else {
if (cache.state === "decoded") {
img.src = cache.originalUrl;
cache.state = "original";
btn.textContent = "Show Decoded";
btn.style.background = "rgba(40, 167, 69, 0.4)";
} else {
img.src = cache.decodedUrl;
cache.state = "decoded";
btn.textContent = "Show Original";
btn.style.background = "rgba(255, 255, 255, 0.15)";
}
}
}
function attachButton(img) {
const aTag = img.closest('a.lightbox');
if (!aTag || !aTag.dataset.downloadHref || processed.has(img)) return;
processed.add(img);
const originalUrl = new URL(aTag.dataset.downloadHref, window.location.origin).href;
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
wrapper.style.display = "inline-block";
aTag.parentNode.insertBefore(wrapper, aTag);
wrapper.appendChild(aTag);
const btn = document.createElement("button");
btn.innerText = "Decode";
btn.style.position = "absolute";
btn.style.left = "0px";
btn.style.top = "0px";
btn.style.zIndex = "1000";
btn.style.minWidth = "140px";
btn.style.height = "36px";
btn.style.padding = "6px 16px";
btn.style.borderRadius = "12px";
btn.style.background = "rgba(255, 255, 255, 0.15)";
btn.style.color = "#fff";
btn.style.border = "1px solid rgba(255,255,255,0.3)";
btn.style.backdropFilter = "blur(8px)";
btn.style.webkitBackdropFilter = "blur(8px)";
btn.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
btn.style.fontWeight = "500";
btn.style.fontSize = "14px";
btn.style.cursor = "pointer";
btn.style.whiteSpace = "nowrap";
btn.onclick = () => toggleDecryption(originalUrl, img, btn);
wrapper.appendChild(btn);
}
function processAllImages() {
document.querySelectorAll('.cooked a.lightbox > img').forEach(attachButton);
}
window.addEventListener('load', processAllImages);
const observer = new MutationObserver(m => {
m.forEach(mu => mu.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.matches?.('.cooked a.lightbox > img')) attachButton(node);
else node.querySelectorAll?.('.cooked a.lightbox > img').forEach(attachButton);
}
}));
});
observer.observe(document.body, { childList: true, subtree: true });
/** === 新增:右上角圆形浮动按钮用于本地加密 === **/
const floatBtn = document.createElement("button");
floatBtn.textContent = "🔒";
Object.assign(floatBtn.style, {
position: "fixed",
top: "20px",
right: "20px",
width: "50px",
height: "50px",
borderRadius: "50%",
background: "rgba(255,255,255,0.15)",
color: "#fff",
border: "1px solid rgba(255,255,255,0.3)",
backdropFilter: "blur(8px)",
webkitBackdropFilter: "blur(8px)",
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
fontSize: "20px",
cursor: "pointer",
zIndex: "9999"
});
document.body.appendChild(floatBtn);
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "image/*";
fileInput.style.display = "none";
document.body.appendChild(fileInput);
floatBtn.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", () => {
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(evt) {
const arrayBuffer = evt.target.result;
const img = new Image();
img.src = URL.createObjectURL(new Blob([arrayBuffer]));
img.onload = () => {
const worker = createWorker();
worker.onmessage = (e) => {
if (e.data.done) {
const a = document.createElement("a");
a.href = e.data.dataUrl;
a.download = "encrypted.jpg";
a.click();
}
};
worker.postMessage({ buffer: arrayBuffer, width: img.width, height: img.height, mode: "encode" }, [arrayBuffer]);
};
};
reader.readAsArrayBuffer(file);
});
})();