Greasy Fork

一键文本转图片

按下快捷键将选中文字转为图片并复制到剪贴板,

目前为 2024-11-25 提交的版本。查看 最新版本

// ==UserScript==
// @name         一键文本转图片
// @namespace    mailto:[email protected]
// @version      2.0
// @description  按下快捷键将选中文字转为图片并复制到剪贴板,
// @author       Cycle Bai
// @license      LGPL
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // 默认配置
    const DEFAULT_CONFIG = {
        fontSize: 16,
        fontFamily: 'Arial, "Microsoft YaHei", sans-serif',
        padding: 20,
        maxWidth: 800,
        lineHeight: 1.5,
        backgroundColor: '#ffffff',
        textColor: '#333333',
        borderRadius: 8,
        shadowColor: 'rgba(0, 0, 0, 0.1)',
        shortcutKey: 'i',
        shortcutModifier: 'alt'
    };

    // 获取配置
    let config = {
        ...DEFAULT_CONFIG,
        ...GM_getValue('textToImageConfig', {})
    };

    // 添加设置面板样式
    GM_addStyle(`
        .t2i-settings-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.5);
            z-index: 9999;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .t2i-settings-panel {
            background: white;
            padding: 20px;
            border-radius: 8px;
            width: 400px;
            max-height: 80vh;
            overflow-y: auto;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }

        .t2i-settings-panel h2 {
            margin: 0 0 20px 0;
            font-size: 1.5em;
        }

        .t2i-settings-group {
            margin-bottom: 15px;
        }

        .t2i-settings-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }

        .t2i-settings-group input,
        .t2i-settings-group select {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin-bottom: 10px;
        }

        .t2i-settings-buttons {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            margin-top: 20px;
        }

        .t2i-settings-buttons button {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
        }

        .t2i-save-btn {
            background: #4caf50;
            color: white;
        }

        .t2i-cancel-btn {
            background: #f44336;
            color: white;
        }

        .t2i-reset-btn {
            background: #ff9800;
            color: white;
        }
    `);

    // 创建设置面板
    function createSettingsPanel() {
        const overlay = document.createElement('div');
        overlay.className = 't2i-settings-overlay';

        const panel = document.createElement('div');
        panel.className = 't2i-settings-panel';

        panel.innerHTML = `
            <h2>文本转图片设置</h2>
            <div class="t2i-settings-group">
                <label>字体大小 (px)</label>
                <input type="number" id="t2i-font-size" value="${config.fontSize}">

                <label>字体家族</label>
                <input type="text" id="t2i-font-family" value="${config.fontFamily}">

                <label>内边距 (px)</label>
                <input type="number" id="t2i-padding" value="${config.padding}">

                <label>最大宽度 (px)</label>
                <input type="number" id="t2i-max-width" value="${config.maxWidth}">

                <label>行高</label>
                <input type="number" id="t2i-line-height" value="${config.lineHeight}" step="0.1">

                <label>背景颜色</label>
                <input type="color" id="t2i-bg-color" value="${config.backgroundColor}">

                <label>文本颜色</label>
                <input type="color" id="t2i-text-color" value="${config.textColor}">

                <label>圆角大小 (px)</label>
                <input type="number" id="t2i-border-radius" value="${config.borderRadius}">

                <label>快捷键</label>
                <input type="text" id="t2i-shortcut-key" value="${config.shortcutKey}">

                <label>修饰键</label>
                <select id="t2i-shortcut-modifier">
                    <option value="alt" ${config.shortcutModifier === 'alt' ? 'selected' : ''}>Alt</option>
                    <option value="ctrl" ${config.shortcutModifier === 'ctrl' ? 'selected' : ''}>Ctrl</option>
                    <option value="shift" ${config.shortcutModifier === 'shift' ? 'selected' : ''}>Shift</option>
                </select>
            </div>
            <div class="t2i-settings-buttons">
                <button class="t2i-reset-btn">重置默认</button>
                <button class="t2i-cancel-btn">取消</button>
                <button class="t2i-save-btn">保存</button>
            </div>
        `;

        // 保存设置
        const saveButton = panel.querySelector('.t2i-save-btn');
        saveButton.addEventListener('click', () => {
            const newConfig = {
                fontSize: parseInt(panel.querySelector('#t2i-font-size').value),
                fontFamily: panel.querySelector('#t2i-font-family').value,
                padding: parseInt(panel.querySelector('#t2i-padding').value),
                maxWidth: parseInt(panel.querySelector('#t2i-max-width').value),
                lineHeight: parseFloat(panel.querySelector('#t2i-line-height').value),
                backgroundColor: panel.querySelector('#t2i-bg-color').value,
                textColor: panel.querySelector('#t2i-text-color').value,
                borderRadius: parseInt(panel.querySelector('#t2i-border-radius').value),
                shortcutKey: panel.querySelector('#t2i-shortcut-key').value.toLowerCase(),
                shortcutModifier: panel.querySelector('#t2i-shortcut-modifier').value
            };

            config = newConfig;
            GM_setValue('textToImageConfig', newConfig);
            showNotification('设置已保存!', 'success');
            overlay.remove();
        });

        // 取消按钮
        const cancelButton = panel.querySelector('.t2i-cancel-btn');
        cancelButton.addEventListener('click', () => overlay.remove());

        // 重置按钮
        const resetButton = panel.querySelector('.t2i-reset-btn');
        resetButton.addEventListener('click', () => {
            config = { ...DEFAULT_CONFIG };
            GM_setValue('textToImageConfig', config);
            overlay.remove();
            showNotification('设置已重置为默认值!', 'success');
        });

        overlay.appendChild(panel);
        document.body.appendChild(overlay);
    }

    // 获取选中的文本
    function getSelectedText() {
        const activeElement = document.activeElement;
        const selection = window.getSelection().toString().trim();

        if (selection) return selection;

        if (activeElement.tagName === 'TEXTAREA' || activeElement.tagName === 'INPUT') {
            return activeElement.value.substring(
                activeElement.selectionStart,
                activeElement.selectionEnd
            ).trim();
        }

        return '';
    }

    // 优化的文本换行处理
    function wrapText(context, text, maxWidth) {
        const characters = Array.from(text);
        let lines = [];
        let currentLine = '';

        for (let char of characters) {
            const testLine = currentLine + char;
            const metrics = context.measureText(testLine);

            if (metrics.width > maxWidth - (config.padding * 2)) {
                lines.push(currentLine);
                currentLine = char;
            } else {
                currentLine = testLine;
            }
        }

        if (currentLine) {
            lines.push(currentLine);
        }

        return lines;
    }

    // 创建并配置画布
    function setupCanvas(lines) {
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');

        // 设置字体
        context.font = `${config.fontSize}px ${config.fontFamily}`;

        // 计算画布尺寸
        const lineHeight = config.fontSize * config.lineHeight;
        const width = config.maxWidth;
        const height = lines.length * lineHeight + (config.padding * 2);

        // 设置画布尺寸(考虑设备像素比以提高清晰度)
        const scale = window.devicePixelRatio || 1;
        canvas.width = width * scale;
        canvas.height = height * scale;
        canvas.style.width = width + 'px';
        canvas.style.height = height + 'px';

        // 缩放上下文以匹配设备像素比
        context.scale(scale, scale);

        return { canvas, context, lineHeight };
    }

    // 绘制图片
    function drawImage(canvas, context, lines, lineHeight) {
        // 绘制背景
        context.fillStyle = config.backgroundColor;
        context.fillRect(0, 0, canvas.width, canvas.height);

        // 添加圆角
        context.beginPath();
        context.roundRect(0, 0, canvas.width, canvas.height, config.borderRadius);
        context.clip();

        // 添加阴影
        context.shadowColor = config.shadowColor;
        context.shadowBlur = 10;
        context.shadowOffsetX = 0;
        context.shadowOffsetY = 2;

        // 绘制文本
        context.fillStyle = config.textColor;
        context.font = `${config.fontSize}px ${config.fontFamily}`;
        context.textBaseline = 'middle';

        lines.forEach((line, i) => {
            const y = config.padding + (i + 0.5) * lineHeight;
            context.fillText(line, config.padding, y);
        });
    }

    // 显示通知
    function showNotification(message, type = 'info') {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 12px 24px;
            background: ${type === 'success' ? '#4caf50' : type === 'warning' ? '#ff9800' : '#f44336'};
            color: white;
            border-radius: 4px;
            font-size: 14px;
            z-index: 9999;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            animation: fadeInOut 3s ease-in-out;
        `;

        notification.textContent = message;
        document.body.appendChild(notification);

        setTimeout(() => {
            notification.remove();
        }, 3000);
    }

    // 主要事件处理函数
    async function handleKeyPress(event) {
        const isModifierPressed =
            (config.shortcutModifier === 'alt' && event.altKey) ||
            (config.shortcutModifier === 'ctrl' && event.ctrlKey) ||
            (config.shortcutModifier === 'shift' && event.shiftKey);

        if (!(isModifierPressed && event.key.toLowerCase() === config.shortcutKey)) return;

        const selection = getSelectedText();
        if (!selection) {
            showNotification('请先选中文本!', 'warning');
            return;
        }

        try {
            const context = document.createElement('canvas').getContext('2d');
            context.font = `${config.fontSize}px ${config.fontFamily}`;

            const lines = selection.split('\n').flatMap(line =>
                wrapText(context, line, config.maxWidth)
            );

            const { canvas, context: finalContext, lineHeight } = setupCanvas(lines);
            drawImage(canvas, finalContext, lines, lineHeight);

            const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
            await navigator.clipboard.write([
                new ClipboardItem({ 'image/png': blob })
            ]);

            showNotification('图片已复制到剪贴板!', 'success');
        } catch (error) {
            console.error('转换失败:', error);
            showNotification('转换失败,请检查权限或浏览器兼容性。', 'error');
        }
    }

    // 注册菜单命令
    GM_registerMenuCommand('设置', createSettingsPanel);

    // 注册事件监听器
    document.addEventListener('keydown', handleKeyPress);

    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
        @keyframes fadeInOut {
            0% { opacity: 0; transform: translateY(-20px); }
            10% { opacity: 1; transform: translateY(0); }
            90% { opacity: 1; transform: translateY(0); }
            100% { opacity: 0; transform: translateY(-20px); }
        }
    `;
    document.head.appendChild(style);
})();