Bangumi wiki 图片上传增强

支持直接粘贴,自动转换图片格式,自动压缩,裁切和马赛克,预览

// ==UserScript==
// @name         Bangumi wiki 图片上传增强
// @namespace    https://bgm.tv/group/topic/431819
// @version      1.4.3
// @description  支持直接粘贴,自动转换图片格式,自动压缩,裁切和马赛克,预览
// @author       You
// @match        https://bangumi.tv/character/*/upload_photo
// @match        https://bangumi.tv/character/*/upload_img
// @match        https://bangumi.tv/person/*/upload_photo
// @match        https://bangumi.tv/person/*/upload_img
// @match        https://bangumi.tv/subject/*/upload_img
// @match        https://bangumi.tv/*/new
// @match        https://bgm.tv/character/*/upload_photo
// @match        https://bgm.tv/character/*/upload_img
// @match        https://bgm.tv/person/*/upload_photo
// @match        https://bgm.tv/person/*/upload_img
// @match        https://bgm.tv/subject/*/upload_img
// @match        https://bgm.tv/*/new
// @match        https://chii.in/character/*/upload_photo
// @match        https://chii.in/character/*/upload_img
// @match        https://chii.in/person/*/upload_photo
// @match        https://chii.in/person/*/upload_img
// @match        https://chii.in/subject/*/upload_img
// @match        https://chii.in/*/new
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/cropper.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/image-compressor.min.js
// @resource     cropperCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/cropper.min.css
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

// #region 样式与DOM元素初始化
    // 注入必要的CSS样式
    const css = `
        .bgm-preview-container {
            display: flex;
            gap: 15px;
            margin-bottom: 10px;
            flex-wrap: wrap;
            align-items: flex-start;
            display: none;
        }

        .bgm-original-preview {
            max-width: 100%;
            position: relative;
            min-height: 100px;
        }

        .bgm-square-preview {
            width: 100px;
            height: 100px;
            overflow: hidden;
            border: 1px solid #ddd;
            position: relative;
        }

        .bgm-preview-image {
            max-width: 100%;
            max-height: 300px;
            border: 1px solid #ddd;
        }

        .bgm-square-image {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
            object-position: top center;
        }

        .bgm-square-image.cropper-mode {
            transform-origin: left top;
        }

        .bgm-preview-text {
            margin-top: 5px;
            font-size: 0.8em;
        }

        .bgm-controls {
            margin-top: 10px;
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }

        .bgm-clipboard-control {
            margin-bottom: 10px;
        }

        .bgm-mosaic-controls {
            margin-top: 10px;
            display: none;
        }

        .bgm-hint {
            font-size: 0.9em;
            margin-block: 5px;
        }

        .cropper-modal {
            opacity: 0 !important;
        }
        .cropper-container {
            border: 1px solid #ddd;
        }

        .slider-control {
            margin: 10px 0;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .slider-control label {
            font-size: 0.9em;
        }

        .slider-control input {
            flex-grow: 1;
            max-width: 200px;
        }

        .brush-size-info {
            font-size: 0.8em;
            margin-top: 5px;
        }

        .mosaic-canvas-container {
            display: inline-block;
            border: 1px solid #ddd;
        }

        /* 粘贴辅助区域 - 完全隐藏 */
        #pasteHelper {
            position: fixed;
            top: -100px;
            left: -100px;
            width: 1px;
            height: 1px;
            opacity: 0;
            outline: none;
            border: none;
            z-index: -1;
        }
    `;

    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // 添加Cropper.js CSS
    const cropperCSS = GM_getResourceText('cropperCSS');
    const cropperStyle = document.createElement('style');
    cropperStyle.textContent = cropperCSS;
    document.head.appendChild(cropperStyle);

    // 创建隐藏的粘贴辅助区域(用于兼容模式)
    const pasteHelper = document.createElement('textarea');
    pasteHelper.id = 'pasteHelper';
    pasteHelper.setAttribute('aria-hidden', 'true');
    document.body.appendChild(pasteHelper);

    // 直接获取文件输入框
    const fileInput = document.querySelector('input[type="file"][name="picfile"]');
    if (!fileInput) return;

    const isUploadPhotoPage = window.location.pathname.includes('/upload_photo');
    const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB

    // 创建UI容器
    const container = document.createElement('div');
    container.className = 'bgm-uploader-container';

    // 剪贴板按钮容器
    const clipboardControl = document.createElement('div');
    clipboardControl.className = 'bgm-clipboard-control';

    // 单一剪贴板按钮
    const getClipboardBtn = document.createElement('button');
    getClipboardBtn.id = 'getClipboardBtn';
    getClipboardBtn.textContent = '获取剪贴板内容';
    clipboardControl.appendChild(getClipboardBtn);

    // 预览区域
    const previewWrapper = document.createElement('div');
    previewWrapper.className = 'bgm-preview-container';

    const originalPreview = document.createElement('div');
    originalPreview.className = 'bgm-original-preview';

    const previewImage = document.createElement('img');
    previewImage.className = 'bgm-preview-image';

    const previewText = document.createElement('div');
    previewText.className = 'bgm-preview-text';

    let squarePreview = null;
    let squarePreviewImage = null;

    if (isUploadPhotoPage) {
        squarePreview = document.createElement('div');
        squarePreview.className = 'bgm-square-preview';
        squarePreview.style.display = 'none';

        squarePreviewImage = document.createElement('img');
        squarePreviewImage.className = 'bgm-square-image';

        squarePreview.appendChild(squarePreviewImage);
    }

    // 其他控制按钮
    const controls = document.createElement('div');
    controls.className = 'bgm-controls';

    const cropBtn = document.createElement('button');
    cropBtn.textContent = '裁切图片';
    cropBtn.style.display = 'none';

    const cropConfirmBtn = document.createElement('button');
    cropConfirmBtn.textContent = '确认裁切';
    cropConfirmBtn.style.display = 'none';

    const cropCancelBtn = document.createElement('button');
    cropCancelBtn.textContent = '取消裁切';
    cropCancelBtn.style.display = 'none';

    const mosaicBtn = document.createElement('button');
    mosaicBtn.textContent = '添加马赛克';
    mosaicBtn.style.display = 'none';

    const mosaicConfirmBtn = document.createElement('button');
    mosaicConfirmBtn.textContent = '完成马赛克';
    mosaicConfirmBtn.style.display = 'none';

    const mosaicCancelBtn = document.createElement('button');
    mosaicCancelBtn.textContent = '取消马赛克';
    mosaicCancelBtn.style.display = 'none';

    // 马赛克大小控制
    const mosaicControls = document.createElement('div');
    mosaicControls.className = 'bgm-mosaic-controls';

    const blockSizeControl = document.createElement('div');
    blockSizeControl.className = 'slider-control';

    const blockSizeLabel = document.createElement('label');
    blockSizeLabel.htmlFor = 'mosaicBlockSize';
    blockSizeLabel.textContent = '马赛克比例:';

    const blockSizeSlider = document.createElement('input');
    blockSizeSlider.type = 'range';
    blockSizeSlider.id = 'mosaicBlockSize';
    blockSizeSlider.min = '1';
    blockSizeSlider.max = '10';
    blockSizeSlider.value = '3';

    const blockSizeValue = document.createElement('span');
    blockSizeValue.id = 'blockSizeValue';
    blockSizeValue.textContent = '3%';

    const brushSizeInfo = document.createElement('div');
    brushSizeInfo.className = 'brush-size-info';
    brushSizeInfo.textContent = '实际大小: 计算中...';

    blockSizeControl.append(blockSizeLabel, blockSizeSlider, blockSizeValue);
    mosaicControls.appendChild(blockSizeControl);
    mosaicControls.appendChild(brushSizeInfo);

    // 组装UI
    originalPreview.appendChild(previewImage);
    previewWrapper.append(originalPreview);

    if (isUploadPhotoPage && squarePreview) {
        previewWrapper.appendChild(squarePreview);
    }

    controls.append(cropBtn, mosaicBtn, cropConfirmBtn, cropCancelBtn, mosaicConfirmBtn, mosaicCancelBtn);
    container.append(clipboardControl, previewWrapper, previewText, controls, mosaicControls);
    fileInput.parentNode.insertBefore(container, fileInput.nextSibling);
// #endregion

// #region 变量定义
    const allowedImageTypes = {
        'image/jpeg': 'jpg',
        'image/png': 'png',
        'image/gif': 'gif'
    };
    const allowedExtensions = Object.values(allowedImageTypes);
    let currentFile = null;
    let cropper = null;
    let isCropping = false;
    let isMosaicing = false;
    let mosaicSizePercent = 3;
    let originalCanvas = null;
    let tempCanvas = null;
    let canvasContainer = null;
    let ctx = null;
    let isDrawing = false;
    const MAX_CANVAS_WIDTH = 700;
    let canvasScale = 1.0;
// #endregion

// #region 工具函数
    // 显示/隐藏编辑按钮
    function showEditButtons() {
        cropBtn.style.display = 'inline-block';
        mosaicBtn.style.display = 'inline-block';
    }

    function hideEditButtons() {
        cropBtn.style.display = 'none';
        mosaicBtn.style.display = 'none';
    }

    // 转换图片为JPG格式
    function convertToJpg(file) {
        return new Promise((resolve, reject) => {
            if (allowedImageTypes[file.type]) {
                resolve(file);
                return;
            }

            previewText.textContent = `正在转换格式...`;
            previewWrapper.style.display = 'flex';

            const reader = new FileReader();

            reader.onload = function (e) {
                const img = new Image();
                img.crossOrigin = 'anonymous';

                img.onload = function () {
                    try {
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');

                        // 设置Canvas尺寸与图片一致
                        canvas.width = img.naturalWidth || img.width;
                        canvas.height = img.naturalHeight || img.height;

                        // 白色背景
                        ctx.fillStyle = '#ffffff';
                        ctx.fillRect(0, 0, canvas.width, canvas.height);

                        // 绘制图片,保持原始尺寸
                        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

                        canvas.toBlob(blob => {
                            if (!blob) {
                                throw new Error('格式转换失败');
                            }

                            const baseName = file.name.replace(/\.[^/.]+$/, "");
                            const newFileName = `${baseName}.jpg`;
                            const newFile = new File([blob], newFileName, { type: 'image/jpeg' });

                            resolve(newFile);
                        }, 'image/jpeg', 1);
                    } catch (error) {
                        reject(new Error(`转换失败: ${error.message}`));
                    }
                };

                img.onerror = function (e) {
                    console.error('图片加载错误:', e);
                    reject(new Error('无法加载图片'));
                };

                // 修复SVG的MIME类型
                let src = e.target.result;
                if (file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg')) {
                    src = src.replace('data:image/svg', 'data:image/svg+xml');
                }

                img.src = src;
            };

            reader.onerror = function () {
                reject(new Error('读取文件失败'));
            };

            reader.readAsDataURL(file);
        });
    }

    // 压缩图片至2MB以内
    function compressImage(file) {
        return new Promise((resolve, reject) => {
            if (file.size <= MAX_FILE_SIZE) {
                resolve(file);
                return;
            }

            previewText.textContent = `正在压缩图片...`;

            new ImageCompressor(file, {
                quality: 0.8,
                maxWidth: 2000,
                maxHeight: 2000,
                success(result) {
                    if (result.size > MAX_FILE_SIZE) {
                        new ImageCompressor(result, {
                            quality: 0.5,
                            maxWidth: 1600,
                            maxHeight: 1600,
                            success(secondResult) {
                                if (secondResult.size > MAX_FILE_SIZE) {
                                    new ImageCompressor(secondResult, {
                                        quality: 0.3,
                                        maxWidth: 1200,
                                        maxHeight: 1200,
                                        success(finalResult) {
                                            const compressedFile = new File(
                                                [finalResult],
                                                file.name,
                                                { type: file.type }
                                            );
                                            resolve(compressedFile);
                                        },
                                        error() {
                                            reject(new Error(`压缩失败`));
                                        }
                                    });
                                } else {
                                    const compressedFile = new File(
                                        [secondResult],
                                        file.name,
                                        { type: file.type }
                                    );
                                    resolve(compressedFile);
                                }
                            },
                            error() {
                                reject(new Error(`压缩失败`));
                            }
                        });
                    } else {
                        const compressedFile = new File(
                            [result],
                            file.name,
                            { type: file.type }
                        );
                        resolve(compressedFile);
                    }
                },
                error() {
                    reject(new Error(`压缩失败`));
                }
            });
        });
    }

    // 计算实际马赛克大小
    function calculateMosaicSize() {
        if (!originalCanvas) return 1;

        const baseSize = Math.min(originalCanvas.width, originalCanvas.height);
        return Math.max(2, Math.round(baseSize * mosaicSizePercent / 100));
    }

    // 更新马赛克大小显示
    function updateMosaicSizeDisplay() {
        if (isMosaicing && originalCanvas) {
            const pixelSize = calculateMosaicSize();
            brushSizeInfo.textContent = `实际大小: ${pixelSize}px`;
        }
    }

    // 检查是否是图片URL
    function isImageUrl(text) {
        try {
            if (text.startsWith('//')) text = `https:${text}`;
            const url = new URL(text);
            const ext = url.pathname.split('.').pop().toLowerCase();
            return allowedExtensions.includes(ext) ||
                   ['bmp', 'webp', 'tiff', 'svg', 'jpeg'].includes(ext);
        } catch {
            return false;
        }
    }

    // 从URL获取图片文件
    function fetchImageFromUrl(url) {
        previewText.textContent = '正在下载图片...';
        previewWrapper.style.display = 'flex';
        if (squarePreview) squarePreview.style.display = 'none';

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                overrideMimeType: 'image/*',
                onload: function (response) {
                    const blob = response.response;
                    let type = 'image/jpeg';

                    const extension = url.split('.').pop().toLowerCase();
                    if (extension === 'png') type = 'image/png';
                    else if (extension === 'gif') type = 'image/gif';
                    else if (extension === 'webp') type = 'image/webp';

                    const contentType = response.responseHeaders.match(/Content-Type:\s*(image\/\w+)/i);
                    if (contentType && contentType[1]) {
                        type = contentType[1];
                    }

                    const fileName = url.split('/').pop();
                    const correctedBlob = new Blob([blob], { type });
                    const file = new File([correctedBlob], fileName, { type });

                    convertToJpg(file)
                        .then(convertedFile => compressImage(convertedFile))
                        .then(compressedFile => resolve(compressedFile))
                        .catch(e => {
                            console.error(e);
                            reject(new Error('下载后处理失败'));
                        });
                },
                onerror: function () {
                    reject(new Error('图片下载失败'));
                }
            });
        });
    }

    // 更新正方形预览
    function updateSquarePreview() {
        if (!squarePreviewImage) return;

        if (isCropping && cropper) {
            const canvas = cropper.getCroppedCanvas();
            if (!canvas) return;

            const scale = 100 / canvas.width;
            squarePreviewImage.src = canvas.toDataURL();
            squarePreviewImage.style.width = `${canvas.width * scale}px`;
            squarePreviewImage.style.height = `${canvas.height * scale}px`;
            squarePreviewImage.style.left = '50%';
            squarePreviewImage.style.transform = 'translateX(-50%)';
            squarePreviewImage.classList.add('cropper-mode');
        } else {
            squarePreviewImage.src = previewImage.src;
            squarePreviewImage.style.width = '100%';
            squarePreviewImage.style.height = '100%';
            squarePreviewImage.style.left = '0';
            squarePreviewImage.style.transform = 'none';
            squarePreviewImage.classList.remove('cropper-mode');
        }
    }
// #endregion

// #region 裁切功能
    function initCropper() {
        if (cropper) {
            cropper.destroy();
        }

        isCropping = true;
        cropper = new Cropper(previewImage, {
            viewMode: 1,
            autoCropArea: 1,
            responsive: true,
            movable: true,
            rotatable: true,
            scalable: true,
            zoomable: true,
            background: false,
            ready: updateSquarePreview,
            cropend: updateSquarePreview
        });

        updateSquarePreview();
    }

    function startCropping(e) {
        e.preventDefault();
        if (!currentFile || isMosaicing) return;

        initCropper();
        cropBtn.style.display = 'none';
        mosaicBtn.style.display = 'none';
        cropConfirmBtn.style.display = 'inline-block';
        cropCancelBtn.style.display = 'inline-block';
        previewText.textContent = '调整裁切区域,点击"确认裁切"';
    }

    function confirmCrop(e) {
        e.preventDefault();
        if (!cropper || !isCropping) return;

        cropper.getCroppedCanvas().toBlob(blob => {
            const fileName = currentFile.name.replace(/\.[^/.]+$/, '') + '_cropped.png';
            const croppedFile = new File([blob], fileName, { type: 'image/png' });

            compressImage(croppedFile).then(compressedFile => {
                currentFile = compressedFile;

                const dataTransfer = new DataTransfer();
                dataTransfer.items.add(currentFile);
                fileInput.files = dataTransfer.files;

                updatePreview(currentFile);
                endCropping();
            }).catch(() => {
                previewText.textContent = `裁切后压缩失败`;
            });
        }, 'image/png');
    }

    function cancelCrop(e) {
        e.preventDefault();
        if (!isCropping) return;
        updatePreview(currentFile);
        endCropping();
    }

    function endCropping() {
        isCropping = false;
        if (cropper) {
            cropper.destroy();
            cropper = null;
        }
        showEditButtons();
        cropConfirmBtn.style.display = 'none';
        cropCancelBtn.style.display = 'none';
        updateSquarePreview();
    }
// #endregion

// #region 马赛克功能
    function initMosaic() {
        if (canvasContainer) {
            canvasContainer.remove();
        }

        canvasContainer = document.createElement('div');
        canvasContainer.className = 'mosaic-canvas-container';

        originalCanvas = document.createElement('canvas');
        const oCtx = originalCanvas.getContext('2d');

        tempCanvas = document.createElement('canvas');
        ctx = tempCanvas.getContext('2d');

        originalCanvas.width = previewImage.naturalWidth;
        originalCanvas.height = previewImage.naturalHeight;
        tempCanvas.width = previewImage.naturalWidth;
        tempCanvas.height = previewImage.naturalHeight;

        oCtx.drawImage(previewImage, 0, 0);
        ctx.drawImage(previewImage, 0, 0);

        canvasScale = 1.0;
        if (originalCanvas.width > MAX_CANVAS_WIDTH) {
            canvasScale = MAX_CANVAS_WIDTH / originalCanvas.width;
        }

        const scaledWidth = originalCanvas.width * canvasScale;
        const scaledHeight = originalCanvas.height * canvasScale;
        canvasContainer.style.width = `${scaledWidth}px`;
        canvasContainer.style.height = `${scaledHeight}px`;

        tempCanvas.style.width = `${scaledWidth}px`;
        tempCanvas.style.height = `${scaledHeight}px`;
        tempCanvas.style.display = 'block';

        canvasContainer.appendChild(tempCanvas);

        const parent = previewImage.parentNode;
        parent.replaceChild(canvasContainer, previewImage);

        bindTempCanvasEvents();

        updateSquarePreview();
        updateMosaicSizeDisplay();
    }

    function bindTempCanvasEvents() {
        if (!tempCanvas) return;

        tempCanvas.addEventListener('mousedown', function(e) {
            if (!isMosaicing) return;

            if (e.button === 0) {
                isDrawing = true;
                drawMosaic(e);
            }
        });

        tempCanvas.addEventListener('contextmenu', function(e) {
            if (isMosaicing) {
                e.preventDefault();
            }
        });
    }

    function startMosaicing(e) {
        e.preventDefault();
        if (!currentFile || isCropping) return;

        isMosaicing = true;
        initMosaic();

        cropBtn.style.display = 'none';
        mosaicBtn.style.display = 'none';
        mosaicConfirmBtn.style.display = 'inline-block';
        mosaicCancelBtn.style.display = 'inline-block';
        mosaicControls.style.display = 'block';
        previewText.textContent = '按住鼠标左键添加马赛克';
    }

    function drawMosaic(e) {
        if (!isDrawing || !ctx || !originalCanvas || !tempCanvas || e.button !== 0) return;

        const rect = tempCanvas.getBoundingClientRect();
        const x = Math.floor((e.clientX - rect.left) / canvasScale);
        const y = Math.floor((e.clientY - rect.top) / canvasScale);

        if (x < 0 || x >= originalCanvas.width || y < 0 || y >= originalCanvas.height) {
            return;
        }

        const blockSize = calculateMosaicSize();

        const imageData = ctx.getImageData(
            Math.max(0, x - blockSize/2),
            Math.max(0, y - blockSize/2),
            blockSize,
            blockSize
        );
        const data = imageData.data;

        let r = 0, g = 0, b = 0;
        let count = 0;

        for (let i = 0; i < data.length; i += 4) {
            r += data[i];
            g += data[i + 1];
            b += data[i + 2];
            count++;
        }

        r = Math.floor(r / count);
        g = Math.floor(g / count);
        b = Math.floor(b / count);

        ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
        ctx.fillRect(
            Math.max(0, x - blockSize/2),
            Math.max(0, y - blockSize/2),
            blockSize,
            blockSize
        );

        updateSquarePreview();
    }

    function confirmMosaic(e) {
        e.preventDefault();
        if (!isMosaicing || !tempCanvas || !canvasContainer) return;

        tempCanvas.toBlob(blob => {
            const fileName = currentFile.name.replace(/\.[^/.]+$/, '') + '_mosaic.jpg';
            const mosaicFile = new File([blob], fileName, { type: 'image/jpeg' });

            compressImage(mosaicFile).then(compressedFile => {
                currentFile = compressedFile;

                const dataTransfer = new DataTransfer();
                dataTransfer.items.add(currentFile);
                fileInput.files = dataTransfer.files;

                const parent = canvasContainer.parentNode;
                previewImage.src = tempCanvas.toDataURL();
                previewImage.style.maxWidth = '100%';
                previewImage.style.maxHeight = '300px';
                parent.replaceChild(previewImage, canvasContainer);

                originalCanvas = null;
                tempCanvas = null;
                canvasContainer = null;
                ctx = null;

                endMosaicing();
                updatePreview(currentFile);
            }).catch(() => {
                previewText.textContent = `马赛克处理失败`;
            });
        }, 'image/jpeg', 0.95);
    }

    function cancelMosaic(e) {
        e.preventDefault();
        if (!isMosaicing || !tempCanvas || !canvasContainer) return;

        const parent = canvasContainer.parentNode;
        previewImage.style.maxWidth = '100%';
        previewImage.style.maxHeight = '300px';
        parent.replaceChild(previewImage, canvasContainer);

        originalCanvas = null;
        tempCanvas = null;
        canvasContainer = null;
        ctx = null;

        endMosaicing();
        updatePreview(currentFile);
    }

    function endMosaicing() {
        isMosaicing = false;
        isDrawing = false;
        showEditButtons();
        mosaicConfirmBtn.style.display = 'none';
        mosaicCancelBtn.style.display = 'none';
        mosaicControls.style.display = 'none';
    }
// #endregion

// #region 预览更新
    async function updatePreview(file) {
        try {
            const convertedFile = await convertToJpg(file);
            const compressedFile = await compressImage(convertedFile);

            if (!compressedFile) {
                previewWrapper.style.display = 'none';
                controls.style.display = 'none';
                mosaicControls.style.display = 'none';
                hideEditButtons();
                if (squarePreview) squarePreview.style.display = 'none';
                return false;
            }

            const reader = new FileReader();
            reader.onload = function (e) {
                previewImage.src = e.target.result;
                let extraInfo = '';
                if (file.name !== convertedFile.name) {
                    extraInfo += ` (已转换格式)`;
                }
                if (convertedFile.size > compressedFile.size) {
                    const originalSizeMB = (convertedFile.size / (1024 * 1024)).toFixed(2);
                    const compressedSizeMB = (compressedFile.size / (1024 * 1024)).toFixed(2);
                    extraInfo += ` (已压缩: ${originalSizeMB}MB → ${compressedSizeMB}MB)`;
                }

                previewText.textContent = `${compressedFile.name} (${Math.round(compressedFile.size / 1024)}KB)${extraInfo}`;
                previewWrapper.style.display = 'flex';
                controls.style.display = 'flex';

                showEditButtons();

                if (isUploadPhotoPage && squarePreview) {
                    squarePreview.style.display = 'block';
                    updateSquarePreview();
                }

                endCropping();
                endMosaicing();
            };
            reader.readAsDataURL(compressedFile);
            currentFile = compressedFile;
            return true;
        } catch (error) {
            previewText.textContent = `处理失败: ${error.message}`;
            previewWrapper.style.display = 'flex';
            controls.style.display = 'flex';
            mosaicControls.style.display = 'none';
            hideEditButtons();
            if (squarePreview) squarePreview.style.display = 'none';
            return false;
        }
    }
// #endregion

// #region 剪贴板处理
    async function processClipboardData(clipboardData) {
        try {
            if (clipboardData.items) {
                for (let i = 0; i < clipboardData.items.length; i++) {
                    const item = clipboardData.items[i];
                    if (item.type.indexOf('image') !== -1) {
                        const blob = item.getAsFile();
                        if (blob) {
                            await updatePreview(blob);
                            const dataTransfer = new DataTransfer();
                            dataTransfer.items.add(currentFile || blob);
                            fileInput.files = dataTransfer.files;
                            return true;
                        }
                    }
                }
            }

            let text;
            if (typeof clipboardData.text === 'function') {
                text = await clipboardData.text();
            } else if (clipboardData.getData) {
                text = clipboardData.getData('text/plain');
            }

            if (text) {
                const trimmedText = text.trim();
                if (isImageUrl(trimmedText)) {
                    try {
                        const imageFile = await fetchImageFromUrl(trimmedText);
                        if (imageFile) {
                            await updatePreview(imageFile);
                            const dataTransfer = new DataTransfer();
                            dataTransfer.items.add(currentFile || imageFile);
                            fileInput.files = dataTransfer.files;
                            return true;
                        }
                    } catch (error) {
                        previewText.textContent = error.message;
                        previewWrapper.style.display = 'flex';
                        if (squarePreview) squarePreview.style.display = 'none';
                    }
                } else {
                    previewText.textContent = '未发现图片内容';
                    previewWrapper.style.display = 'flex';
                }
            }
        } catch {
            previewText.textContent = '处理内容失败';
            previewWrapper.style.display = 'flex';
        }
        return false;
    }

    async function handleClipboard(e) {
        e.preventDefault();
        if (navigator.clipboard && navigator.clipboard.read) {
            try {
                previewText.textContent = '正在获取剪贴板...';
                previewWrapper.style.display = 'flex';

                // 尝试读取图片
                const clipboardItems = await navigator.clipboard.read();
                for (const item of clipboardItems) {
                    for (const type of item.types) {
                        if (type.startsWith('image/')) {
                            const blob = await item.getType(type);
                            if (blob) {
                                const fileName = `clipboard-${Date.now()}.${type.split('/')[1] || 'jpg'}`;
                                const file = new File([blob], fileName, { type: blob.type });
                                await updatePreview(file);
                                const dataTransfer = new DataTransfer();
                                dataTransfer.items.add(currentFile || file);
                                fileInput.files = dataTransfer.files;
                                return;
                            }
                        }
                    }
                }

                // 尝试读取文本
                const text = await navigator.clipboard.readText();
                if (text) {
                    const trimmedText = text.trim();
                    if (isImageUrl(trimmedText)) {
                        const imageFile = await fetchImageFromUrl(trimmedText);
                        if (imageFile) {
                            await updatePreview(imageFile);
                            const dataTransfer = new DataTransfer();
                            dataTransfer.items.add(currentFile || imageFile);
                            fileInput.files = dataTransfer.files;
                            return;
                        }
                    } else {
                        previewText.textContent = '未发现图片内容';
                    }
                } else {
                    previewText.textContent = '剪贴板为空';
                }
            } catch {
                startCompatibilityMode();
            }
        } else {
            startCompatibilityMode();
        }
    }

    function startCompatibilityMode() {
        pasteHelper.value = '';
        pasteHelper.focus();

        previewText.textContent = '请在此粘贴内容';
        previewWrapper.style.display = 'flex';

        const handlePaste = async function(e) {
            e.stopPropagation();
            e.preventDefault();

            await processClipboardData(e.clipboardData);

            pasteHelper.blur();
            document.removeEventListener('paste', handlePaste);
            document.removeEventListener('click', handleClickOutside);
        };

        // 点击外部取消
        const handleClickOutside = function() {
            pasteHelper.blur();
            document.removeEventListener('paste', handlePaste);
            document.removeEventListener('click', handleClickOutside);
            if (previewText.textContent === '请在此粘贴内容') {
                previewText.textContent = '操作已取消';
            }
        };

        document.addEventListener('paste', handlePaste);
        setTimeout(() => {
            document.addEventListener('click', handleClickOutside);
        }, 100);
    }
// #endregion

// #region 事件监听
    // 裁切相关事件
    cropBtn.addEventListener('click', startCropping);
    cropConfirmBtn.addEventListener('click', confirmCrop);
    cropCancelBtn.addEventListener('click', cancelCrop);

    // 马赛克相关事件
    mosaicBtn.addEventListener('click', startMosaicing);
    mosaicConfirmBtn.addEventListener('click', confirmMosaic);
    mosaicCancelBtn.addEventListener('click', cancelMosaic);

    // 剪贴板按钮事件
    getClipboardBtn.addEventListener('click', handleClipboard);

    // 马赛克比例调整事件
    blockSizeSlider.addEventListener('input', function() {
        mosaicSizePercent = parseInt(this.value);
        blockSizeValue.textContent = `${mosaicSizePercent}%`;
        updateMosaicSizeDisplay();
    });

    // 鼠标绘制事件
    document.addEventListener('mousemove', function(e) {
        if (isMosaicing && isDrawing && tempCanvas && e.target === tempCanvas) {
            drawMosaic(e);
        }
    });

    document.addEventListener('mouseup', function() {
        isDrawing = false;
    });

    document.addEventListener('mouseleave', function() {
        isDrawing = false;
    });

    // 文件输入事件
    fileInput.addEventListener('change', async function () {
        if (this.files.length > 0) {
            await updatePreview(this.files[0]);
        } else {
            hideEditButtons();
        }
    });
    if (fileInput.files.length > 0) {
        updatePreview(fileInput.files[0]);
    }

    // 全局粘贴事件
    document.addEventListener('paste', async function (e) {
        const activeElement = document.activeElement;
        if (!activeElement ||
            !(activeElement.tagName === 'INPUT' ||
             activeElement.tagName === 'TEXTAREA' ||
             activeElement.isContentEditable)) {

            const handled = await processClipboardData(e.clipboardData);
            if (handled) {
                e.preventDefault();
            }
        }
    });

    // 添加提示信息
    const hint = document.createElement('div');
    hint.className = 'bgm-hint';
    hint.textContent = '提示:点击按钮获取剪贴板内容,或直接按 Ctrl+V 粘贴图片';
    fileInput.parentNode.insertBefore(hint, fileInput.nextSibling);
// #endregion
})();