Greasy Fork

Greasy Fork is available in English.

网页字体替换

该脚本允许你将所有网页的字体替换为你本地的任意字体

当前为 2025-12-04 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         网页字体替换
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  该脚本允许你将所有网页的字体替换为你本地的任意字体
// @author       Kyurin
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @noframes
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) return;

    const CONFIG = {
        CHUNK_SIZE: 1024 * 1024,
        DB_PREFIX: "FONT_DATA_",
        META_KEY: "FONT_META",
        CUSTOM_FAMILY: "UserLocalFont"
    };

    function injectGlobalStyles(blobUrl) {
        let css = "";

        // 1. Font Name Hijacking (劫持列表)
        const hijackList = [
            // X (Twitter)
            "TwitterChirp", "TwitterChirpExtendedHeavy", "Chirp",

            // ArXiv & Academic
            "Latin Modern Roman", "Computer Modern", "LinLibertine", "Lucida Grande",

            // Modern UI
            "Inter", "Inter var", "Inter Tight",
            "Google Sans", "Google Sans Text",
            "Roboto", "San Francisco", "Segoe UI",
            "system-ui", "ui-sans-serif", "-apple-system", "BlinkMacSystemFont", "sans-serif",

            // Web Standards
            "Helvetica Neue", "Helvetica", "Arial", "Verdana", "Tahoma",
            "Open Sans", "Fira Sans", "Ubuntu",

            // CJK
            "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑",
            "Heiti SC", "SimHei", "SimSun", "Noto Sans SC", "Source Han Sans SC",
            
            // Reddit & Others
            "IBM Plex Sans", "Reddit Sans", "Noto Sans"
        ];

        hijackList.forEach(name => {
            css += `@font-face { font-family: '${name}'; src: url('${blobUrl}'); font-display: swap; }`;
        });

        css += `@font-face { font-family: '${CONFIG.CUSTOM_FAMILY}'; src: url('${blobUrl}'); font-display: swap; }`;

        // 2. Tag-based & Attribute-based Overrides (Reddit 修复核心)
        const targetSelectors = [
            // 基础标签
            "body", "p", "article", "section", "blockquote",
            "h1", "h2", "h3", "h4", "h5", "h6",
            "li", "dt", "dd", "th", "td",
            "b", "strong",

            // 导航与链接 (针对 X 的侧边栏)
            "nav",
            "[role='link']",
            "[role='button']",
            "[role='menuitem']",

            // 文本特征属性 (X 的推文和菜单大量使用 dir="auto")
            "[dir='auto']",
            "[dir='ltr']",
            "[lang]"
        ];

        css += `
            ${targetSelectors.join(", ")} {
                font-family: "${CONFIG.CUSTOM_FAMILY}", "TwitterChirp", "Inter", "Microsoft YaHei", sans-serif !important;
            }
            input, textarea, select {
                font-family: "${CONFIG.CUSTOM_FAMILY}", sans-serif !important;
            }
        `;

        // 3. Bilibili Subtitle Fix (新增:B站字幕修复)
        css += `
            .bpx-player-subtitle-panel-text,
            .bpx-player-subtitle-wrap span,
            .bilibili-player-video-subtitle {
                font-family: "${CONFIG.CUSTOM_FAMILY}", sans-serif !important;
            }
        `;

        // 4. CSS Variables Injection
        css += `
            :root, html, body {
                --text-font-family: "${CONFIG.CUSTOM_FAMILY}", sans-serif !important;
                --font-family-twitter: "${CONFIG.CUSTOM_FAMILY}", sans-serif !important;
                --font-sans: "${CONFIG.CUSTOM_FAMILY}", sans-serif !important;
                --font-serif: "${CONFIG.CUSTOM_FAMILY}", serif !important;
            }
            .ltx_text, .ltx_title, .ltx_abstract, .ltx_font_bold {
                 font-family: "${CONFIG.CUSTOM_FAMILY}", sans-serif !important;
            }
        `;

        // 5. Exclusion & Protection (图标与代码保护)
        const monoFonts = [
            "monospace", "ui-monospace", "Consolas", "Courier New", "Menlo",
            "Monaco", "Space Mono", "Roboto Mono", "Fira Code", "JetBrains Mono"
        ];

        monoFonts.forEach(name => {
            css += `@font-face { font-family: '${name}'; src: local('monospace'), local('Courier New'); }`;
        });

        css += `
            pre, code, kbd, samp, .monaco-editor, .code-block, textarea.code {
                font-family: "Space Mono", "Consolas", monospace !important;
                font-variant-ligatures: none;
            }
        `;

        // Icon Protection (SVGs and Icon Fonts)
        css += `
            [class*="material-symbols"], [class*="material-icons"],
            .material-icons, i, em, .icon,
            [class*="icon"], [class*="fa-"], [class*="fas"], [class*="fab"],
            b[class*="icon"], strong[class*="icon"],
            .ltx_icon, .ltx_svg_icon,

            /* X (Twitter) 图标保护 */
            svg, svg * {
                font-family: 'Material Symbols Outlined', 'Material Icons', FontAwesome, initial !important;
                font-weight: normal;
                font-style: normal;
            }

            /* Math Protection */
            .MathJax, .MathJax *, .mjx-container, .mjx-container *,
            .ltx_Math, .ltx_equation, .ltx_equation *, math, math * {
                font-family: "Latin Modern Math", serif !important;
            }
        `;

        if (typeof GM_addStyle !== 'undefined') {
            GM_addStyle(css);
        } else {
            const styleEl = document.createElement('style');
            styleEl.innerHTML = css;
            document.head.appendChild(styleEl);
        }

        setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
    }

    const Storage = {
        save: function(file) {
            const reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = e => {
                const base64 = e.target.result.split(',')[1];
                const totalChunks = Math.ceil(base64.length / CONFIG.CHUNK_SIZE);
                this.clear();
                try {
                    for (let i = 0; i < totalChunks; i++) {
                        GM_setValue(`${CONFIG.DB_PREFIX}${i}`, base64.slice(i * CONFIG.CHUNK_SIZE, (i + 1) * CONFIG.CHUNK_SIZE));
                    }
                    GM_setValue(CONFIG.META_KEY, { name: file.name, type: file.type, totalChunks: totalChunks });
                    alert("✅ 字体上传成功。");
                    location.reload();
                } catch (err) {
                    alert("❌ 保存失败:空间不足。");
                }
            };
        },
        load: function() {
            return new Promise((resolve, reject) => {
                const meta = GM_getValue(CONFIG.META_KEY);
                if (!meta) { resolve(null); return; }
                setTimeout(() => {
                    try {
                        const chunks = [];
                        for (let i = 0; i < meta.totalChunks; i++) {
                            const chunk = GM_getValue(`${CONFIG.DB_PREFIX}${i}`);
                            if (chunk) chunks.push(chunk);
                        }
                        if (chunks.length !== meta.totalChunks) throw new Error("Corrupted data");
                        
                        // 兼容处理:尝试使用 fetch 转换 Base64 (比 atob 更稳定支持中文大文件)
                        fetch(`data:${meta.type};base64,${chunks.join('')}`)
                            .then(res => res.blob())
                            .then(blob => resolve(blob))
                            .catch(() => {
                                // 降级回退到旧的解码方式
                                const byteStr = atob(chunks.join(''));
                                const bytes = new Uint8Array(byteStr.length);
                                for (let i = 0; i < byteStr.length; i++) bytes[i] = byteStr.charCodeAt(i);
                                resolve(new Blob([bytes], {type: meta.type}));
                            });
                    } catch (e) { reject(e); }
                }, 0);
            });
        },
        clear: function() {
            GM_listValues().forEach(k => {
                if (k.startsWith(CONFIG.DB_PREFIX) || k === CONFIG.META_KEY) GM_deleteValue(k);
            });
        }
    };

    function init() {
        GM_registerMenuCommand("📂 上传字体文件", () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.style.display = 'none';
            input.accept = ".ttf,.otf,.woff,.woff2";
            input.onchange = e => { if(e.target.files[0]) Storage.save(e.target.files[0]); };
            document.body.appendChild(input);
            input.click();
            document.body.removeChild(input);
        });
        GM_registerMenuCommand("🗑️ 恢复默认", () => {
            if(confirm("确定恢复默认吗?")) { Storage.clear(); location.reload(); }
        });
        Storage.load().then(blob => {
            if(blob) injectGlobalStyles(URL.createObjectURL(blob));
        }).catch(e => console.error("FontLoader Error:", e));
    }

    init();
})();