虎扑图片解混淆助手

虎扑论坛图片解混淆工具,支持小番茄混淆算法,可对图片进行加密、解密、恢复原图和下载操作。

// ==UserScript==
// @name         虎扑图片解混淆助手
// @namespace    https://greasyfork.org/
// @version      1.0.3
// @description  虎扑论坛图片解混淆工具,支持小番茄混淆算法,可对图片进行加密、解密、恢复原图和下载操作。
// @license      MIT
// @author       Kira Diana
// @match        https://bbs.hupu.com/*
// @grant        none
// ==/UserScript==

/**
 * 此脚本的小番茄混淆代码来源于:https://xfqtphx.netlify.app/
 * 本人对代码进行了修改,以适配虎扑论坛。
 */

let pic_list = {};
let isOriginalPicPage = false;
let SIZE = 4294967296;
let first_config;
let defaultTomatoKey = 1;
let initialized = false;

function gilbert2d(width, height) {
    /**
     * Generalized Hilbert ('gilbert') space-filling curve for arbitrary-sized
     * 2D rectangular grids. Generates discrete 2D coordinates to fill a rectangle
     * of size (width x 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);
    const h = Math.abs(bx + by);

    const dax = Math.sign(ax), day = Math.sign(ay); // unit major direction
    const dbx = Math.sign(bx), dby = Math.sign(by); // unit orthogonal direction

    if (h === 1) {
        // trivial row fill
        for (let i = 0; i < w; i++) {
            coordinates.push([x, y]);
            x += dax;
            y += day;
        }
        return;
    }

    if (w === 1) {
        // trivial column fill
        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);
    const h2 = Math.abs(bx2 + by2);

    if (2 * w > 3 * h) {
        if ((w2 % 2) && (w > 2)) {
            // prefer even steps
            ax2 += dax;
            ay2 += day;
        }

        // long case: split in two parts only
        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)) {
            // prefer even steps
            bx2 += dbx;
            by2 += dby;
        }

        // standard case: one step up, one long horizontal, one step down
        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);
    }
}

// 添加获取原图URL的函数
function getOriginalImageUrl(url) {
    // 移除URL中的压缩参数
    if (url.includes('?x-oss-process=')) {
        return url.split('?x-oss-process=')[0];
    }
    return url;
}

// 更新encryptTomato函数
function encryptTomato(img, key){
    // 创建新的Image对象加载原图
    const originalImg = new Image();
    originalImg.crossOrigin = 'anonymous';

    // 获取图片ID
    const picId = img.getAttribute('pic_id');
    // 使用原图URL
    const originalUrl = pic_list[picId]?.originalUrl || getOriginalImageUrl(img.src);

    originalImg.onload = function() {
        const cvs = document.createElement("canvas");
        const width = cvs.width = originalImg.naturalWidth;
        const height = cvs.height = originalImg.naturalHeight;
        const ctx = cvs.getContext("2d");
        ctx.drawImage(originalImg, 0, 0);
        const imgdata = ctx.getImageData(0, 0, width, height);
        const imgdata2 = new ImageData(width, height);
        const curve = gilbert2d(width, height);

        // 计算偏移量
        const baseOffset = Math.round((Math.sqrt(5) - 1) / 2 * width * height);
        const offset = Math.round(baseOffset * parseFloat(key));

        for(let i = 0; i < width * height; i++){
            const old_pos = curve[i];
            const new_pos = curve[(i + offset) % (width * height)];
            const old_p = 4 * (old_pos[0] + old_pos[1] * width);
            const new_p = 4 * (new_pos[0] + new_pos[1] * width);
            imgdata2.data.set(imgdata.data.slice(old_p, old_p + 4), new_p);
        }

        ctx.putImageData(imgdata2, 0, 0);
        cvs.toBlob(b => {
            URL.revokeObjectURL(img.src);
            img.src = URL.createObjectURL(b);
            resizeImage(img);
        }, "image/jpeg", 0.95);
    };

    // 加载原图
    originalImg.src = originalUrl;
}

// 更新decryptTomato函数
function decryptTomato(img, key){
    // 创建新的Image对象加载原图
    const originalImg = new Image();
    originalImg.crossOrigin = 'anonymous';

    // 获取图片ID
    const picId = img.getAttribute('pic_id');
    // 使用原图URL
    const originalUrl = pic_list[picId]?.originalUrl || getOriginalImageUrl(img.src);

    originalImg.onload = function() {
        const cvs = document.createElement("canvas");
        const width = cvs.width = originalImg.naturalWidth;
        const height = cvs.height = originalImg.naturalHeight;
        const ctx = cvs.getContext("2d");
        ctx.drawImage(originalImg, 0, 0);
        const imgdata = ctx.getImageData(0, 0, width, height);
        const imgdata2 = new ImageData(width, height);
        const curve = gilbert2d(width, height);

        // 计算偏移量
        const baseOffset = Math.round((Math.sqrt(5) - 1) / 2 * width * height);
        const offset = Math.round(baseOffset * parseFloat(key));

        for(let i = 0; i < width * height; i++){
            const old_pos = curve[i];
            const new_pos = curve[(i + offset) % (width * height)];
            const old_p = 4 * (old_pos[0] + old_pos[1] * width);
            const new_p = 4 * (new_pos[0] + new_pos[1] * width);
            imgdata2.data.set(imgdata.data.slice(new_p, new_p + 4), old_p);
        }

        ctx.putImageData(imgdata2, 0, 0);
        cvs.toBlob(b => {
            URL.revokeObjectURL(img.src);
            img.src = URL.createObjectURL(b);
            resizeImage(img);
        }, "image/jpeg", 0.95);
    };

    // 加载原图
    originalImg.src = originalUrl;
}

// 添加setsrc辅助函数
function setsrc(img, src){
    URL.revokeObjectURL(img.src);
    img.src = src;
    img.style.display = "inline-block";
}

// 更新encrypt函数,使用requestAnimationFrame优化
function encrypt(event) {
    let container = event.target.parentNode;
    let img = container.nextSibling;
    if (!img) {
        return;
    }
    container.setAttribute('activated', 'true');
    let key = container.querySelector('.key-input').value;

    if (!checkKeyValidity('tomato', key)) {
        alert('小番茄算法仅支持大于0小于等于1.618的小数作为密钥');
        return;
    }

    if (!first_config) {
        first_config = { method: 'tomato', key: key };
        document.querySelectorAll('.method-select').forEach(e => {
            let container = e.parentNode;
            if (container.getAttribute('activated') == 'true') {
                return;
            }
            e.value = 'tomato';
            let keyInput = container.querySelector('.key-input');
            keyInput.value = key;
        });
    }
    if (!img.crossOrigin) {
        img.crossOrigin = 'anonymous';
    }
    img.style.display = 'none';
    let msg = container.querySelector('.msg');
    msg.style.display = '';
    // 使用requestAnimationFrame优化性能
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            console.time();
            encryptTomato(img, key);
            console.timeEnd();
            resizeImage(img);
            img.style.display = 'inline-block';
            msg.style.display = 'none';
        });
    });
}

// 更新decrypt函数,使用requestAnimationFrame优化
function decrypt(event) {
    let container = event.target.parentNode;
    let img = container.nextSibling;
    if (!img) {
        return;
    }
    container.setAttribute('activated', 'true');
    let key = container.querySelector('.key-input').value;

    if (!checkKeyValidity('tomato', key)) {
        alert('小番茄算法仅支持大于0小于等于1.618的小数作为密钥');
        return;
    }

    if (!first_config) {
        first_config = { method: 'tomato', key: key };
        document.querySelectorAll('.method-select').forEach(e => {
            let container = e.parentNode;
            if (container.getAttribute('activated') == 'true') {
                return;
            }
            e.value = 'tomato';
            let keyInput = container.querySelector('.key-input');
            keyInput.value = key;
        });
    }
    if (!img.crossOrigin) {
        img.crossOrigin = 'anonymous';
    }
    img.style.display = 'none';
    let msg = container.querySelector('.msg');
    msg.style.display = '';
    // 使用requestAnimationFrame优化性能
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            console.time();
            decryptTomato(img, key);
            console.timeEnd();
            resizeImage(img);
            img.style.display = 'inline-block';
            msg.style.display = 'none';
        });
    });
}

function resizeImage(img) {
    if (img.width < img.naturalWidth) {
        img.height = img.naturalHeight * img.width / img.naturalWidth;
    } else if (img.height < img.naturalHeight) {
        img.width = img.naturalWidth * img.height / img.naturalHeight;
    }
}

function pickImage(event) {
    if (event.target.files.length <= 0) {
        return;
    }
    let container = event.target.parentNode;
    let img = container.nextSibling;
    if (!img) {
        return;
    }
    let url = URL.createObjectURL(event.target.files[0]);
    img.src = url;
    event.target.value = '';

    img.onload = () => resizeImage(img);
}

function encrypt(event) {
    let container = event.target.parentNode;
    let img = container.nextSibling;
    if (!img) {
        return;
    }
    container.setAttribute('activated', 'true');
    let key = container.querySelector('.key-input').value;

    if (!checkKeyValidity('tomato', key)) {
        alert('小番茄算法仅支持大于0小于等于1.618的小数作为密钥');
        return;
    }

    if (!first_config) {
        first_config = { method: 'tomato', key: key };
        document.querySelectorAll('.method-select').forEach(e => {
            let container = e.parentNode;
            if (container.getAttribute('activated') == 'true') {
                return;
            }
            e.value = 'tomato';
            let keyInput = container.querySelector('.key-input');
            keyInput.value = key;
        });
    }
    let delay = 100;
    if (!img.crossOrigin) {
        img.crossOrigin = 'anonymous';
    }
    img.style.display = 'none';
    let msg = container.querySelector('.msg');
    msg.style.display = '';
    setTimeout(() => {
        console.time();
        encryptTomato(img, key);
        console.timeEnd();
        resizeImage(img);
        img.style.display = 'inline-block';
        msg.style.display = 'none';
    }, delay);
}

function decrypt(event) {
    let container = event.target.parentNode;
    let img = container.nextSibling;
    if (!img) {
        return;
    }
    container.setAttribute('activated', 'true');
    let key = container.querySelector('.key-input').value;

    if (!checkKeyValidity('tomato', key)) {
        alert('小番茄算法仅支持大于0小于等于1.618的小数作为密钥');
        return;
    }

    if (!first_config) {
        first_config = { method: 'tomato', key: key };
        document.querySelectorAll('.method-select').forEach(e => {
            let container = e.parentNode;
            if (container.getAttribute('activated') == 'true') {
                return;
            }
            e.value = 'tomato';
            let keyInput = container.querySelector('.key-input');
            keyInput.value = key;
        });
    }
    let delay = 100;
    if (!img.crossOrigin) {
        img.crossOrigin = 'anonymous';
    }
    img.style.display = 'none';
    let msg = container.querySelector('.msg');
    msg.style.display = '';
    setTimeout(() => {
        console.time();
        decryptTomato(img, key);
        console.timeEnd();
        resizeImage(img);
        img.style.display = 'inline-block';
        msg.style.display = 'none';
    }, delay);
}

function restore(event) {
    let container = event.target.parentNode;
    let img = container.nextSibling;
    if (!img) {
        return;
    }
    let id = img.getAttribute('pic_id');
    if (pic_list[id]) {
        img.src = pic_list[id].url;
        img.width = pic_list[id].width;
        img.height = pic_list[id].height;
    }
    resizeImage(img);
}

function download(event) {
    let container = event.target.parentNode;
    let img = container.nextSibling;
    if (!img) {
        return;
    }
    let image = new Image();
    image.src = img.src;
    image.setAttribute("crossOrigin", "anonymous");

    image.onload = function() {
        let a = document.createElement("a");
        a.download = Date.now() + ".png";
        if (img.src.startsWith('blob:') || img.src.startsWith('data:')) {
            a.href = image.src;
        } else {
            let canvas = document.createElement("canvas");
            canvas.width = image.naturalWidth;
            canvas.height = image.naturalHeight;
            canvas.getContext("2d").drawImage(image, 0, 0, image.width, image.height);
            a.href = canvas.toDataURL({ format: 'png', quality: 1 });
        }
        a.click();
    };
}

// 添加用户界面
function addButton(img) {
    // 检查是否已经添加过控件
    if (img.previousElementSibling && img.previousElementSibling.classList.contains('pic-decrypt-container')) {
        console.log('控件已存在,跳过:', img);
        return;
    }

    // 跳过base64图片
    if (img.src.indexOf('data:image') === 0) {
        console.log('跳过base64图片:', img);
        return;
    }

    // 跳过虎扑logo图片
    if (img.src.includes('channel/website/static/images/basketball-nba-logo.png')) {
        console.log('跳过虎扑logo图片:', img);
        return;
    }

    // 跳过头像图片 (根据class和src特征判断)
    if (img.classList.contains('avatar') || img.src.includes('user/')) {
        console.log('跳过头像图片:', img);
        return;
    }

    // 跳过表情包图片 (根据尺寸和class判断)
    // 假设表情包通常较小 (宽高小于200px) 且有特定class
    if ((img.naturalWidth < 500 && img.naturalHeight < 500) || img.classList.contains('表情相关的class')) {
        console.log('跳过表情包图片:', img);
        return;
    }

    let container = document.createElement('div');
    container.className = 'pic-decrypt-container';
    // 设置为水平紧凑布局
    container.style.cssText = 'margin: 3px 0; padding: 5px; background-color: rgba(255, 255, 255, 0.95); border-radius: 4px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); text-align: center; z-index: 100; clear: both; font-size: 12px; white-space: nowrap;';

    // 水平布局的按钮组
    container.innerHTML = `
        <select class="method-select" disabled style="display:none;">
            <option value="tomato">🍅小番茄</option>
        </select>
        <input class="key-input" type="text" placeholder="密钥" value="${defaultTomatoKey}" style="width: 50px; height: 20px; font-size: 12px; margin: 0 3px;">
        <input class="normal-btn decrypt" type="button" value="解混淆" style="background-color: #eb3678;color:#fff;">
        <input class="normal-btn restore" type="button" value="还原" style="background-color: #fb773c;color:#fff;">
        <input class="normal-btn download" type="button" value="保存" style="background-color: #3385ff;color:#fff;">
        <p class="msg" style="display: none; font-size: 14px; margin: 5px;">处理中...</p>
    `;

    // 内联样式,确保按钮水平排列
    const styleText = `
    .normal-btn, .method-select, .key-input {
        height: 22px;
        line-height: 22px;
        font-size: 12px;
        padding: 0 6px;
        margin: 2px 3px;
        border-radius: 3px;
        display: inline-block;
        position: relative;
        vertical-align: middle;
        text-align: center;
    }

    .normal-btn {
        border: 0;
        cursor: pointer;
        min-width: 50px;
    }

    .method-select, .key-input {
        border: 1px solid #888;
        color: #000;
    }

    .key-input:hover {
        cursor: text;
    }
    `;

    // 添加样式到容器
    const styleElement = document.createElement('style');
    styleElement.textContent = styleText;
    container.appendChild(styleElement);

    // 设置图片ID
    let pic_id = img.getAttribute('pic_id') || 'pic_' + Date.now();
    img.setAttribute('pic_id', pic_id);

    // 存储原图信息
    const originalUrl = getOriginalImageUrl(img.src);
    pic_list[pic_id] = {
        'url': img.src,
        'originalUrl': originalUrl,
        'width': img.width,
        'height': img.height
    };

    // 插入控件到图片前面
    try {
        img.parentNode.insertBefore(container, img);
        console.log('成功添加控件到图片:', img);
    } catch (e) {
        console.error('添加控件失败:', e, img);
        // 尝试直接添加到body(后备方案)
        document.body.appendChild(container);
        container.style.position = 'fixed';
        container.style.top = '10px';
        container.style.left = '10px';
        console.log('已添加控件到页面顶部');
    }

    // 设置控件事件
    if (first_config) {
        let keyInput = container.querySelector('.key-input');
        keyInput.value = first_config.key;
    }
    container.querySelector('.decrypt').onclick = decrypt;
    container.querySelector('.restore').onclick = restore;
    container.querySelector('.download').onclick = download;
}

// 修改虎扑图片获取逻辑,增强兼容性
function loadPicList() {
    console.log('开始查找图片...');

    // 尝试多种可能的图片选择器
    const selectors = [
        '.quote-content img',
        '.bbs-img',
        '.thread-content img',
        '.text img',
        '.post-content img',
        '.article-content img',
        '.topic-content img',
        'img[class*="bbs-img-"]', // 匹配包含bbs-img-的class
        'img[data-original]', // 匹配懒加载图片
        '#j_p_postlist img', // 帖子列表中的图片
        '.pics-wrap img' // 图片包裹容器中的图片
    ];

    let images = [];
    selectors.forEach(selector => {
        const found = document.querySelectorAll(selector);
        images = [...images, ...found];
        console.log(`选择器 '${selector}' 找到 ${found.length} 张图片`);
    });

    // 去重
    images = [...new Set(images)];
    console.log(`去重后找到 ${images.length} 张图片`);

    if (images.length === 0) {
        // 尝试直接获取所有图片
        images = document.querySelectorAll('img');
        console.log(`尝试直接获取所有图片,找到 ${images.length} 张`);
    }

    images.forEach(img => {
        addButton(img);
    });

    console.log('图片处理完成');
}

// 以下函数保持不变
function getElementTop(element) {
    var actualTop = element.offsetTop;
    var current = element.offsetParent;
    while (current !== null) {
        actualTop += current.offsetTop;
        current = current.offsetParent;
    }
    return actualTop;
}

// 获取距离浏览器可视区垂直中点最近的图片
function getCenterImg() {
    let minDelta = 100000000;
    let centerImg = null;
    let viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
    let centerScrollTop = document.documentElement.scrollTop + viewPortHeight / 2;
    let images = document.querySelectorAll('.quote-content img, .bbs-img, .thread-content img');
    for (let i = 0; i < images.length; ++i) {
        let img = images[i];
        let top = getElementTop(img);
        let bottom = top + img.height;
        let delta = Math.abs((top + bottom) / 2 - centerScrollTop);
        if (delta < minDelta) {
            centerImg = img;
            minDelta = delta;
        }
    }
    return centerImg;
}

function checkKeyValidity(method, key) {
    switch (method) {
        case 'tomato':
            try {
                return parseFloat(key) > 0 && parseFloat(key) <= 1.618;
            } catch (e) {
                return false;
            }
        default:
            return true;
    }
}

function scrollToNextImage(isReverse) {
    let images = document.querySelectorAll('.quote-content img, .bbs-img, .thread-content img');
    let centerImg = getCenterImg();
    if (!centerImg) return;

    for (let i = 0; i < images.length; ++i) {
        if (!images[i].isEqualNode(centerImg)) {
            continue;
        }
        let scroll;
        if (isReverse) {
            if (i > 0) {
                scroll = getElementTop(images[i - 1].previousElementSibling);
            } else {
                scroll = getElementTop(centerImg.previousElementSibling);
            }
        } else {
            let top = getElementTop(centerImg.previousElementSibling);
            if (Math.abs(top - 60 - document.documentElement.scrollTop) > 50) {
                scroll = top;
            } else if (i < images.length - 1) {
                scroll = getElementTop(images[i + 1].previousElementSibling);
            } else {
                scroll = getElementTop(centerImg.previousElementSibling);
            }
        }
        if (scroll > 60) {
            scroll -= 60;
        }
        document.documentElement.scrollTo({ top: scroll, behavior: 'smooth' });
        break;
    }
}

function main() {
    // 检查是否是虎扑帖子页面
    if (!location.href.match(/bbs\.hupu\.com\/\d+/g)) {
        return;
    }

    // 初始加载图片
    loadPicList();

    // 定时检查新图片
    setInterval(() => {
        loadPicList();
    }, 2000);
}

main();