// ==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 // @downloadURL none // ==/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(); })();