您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
导入本地字体(.ttf,.otf,.woff,.woff2格式)来更换网页字体,避免依赖第三方
// ==UserScript== // @name 【更换网页字体】 // @namespace https://greasyfork.org/ // @version 250823 // @description 导入本地字体(.ttf,.otf,.woff,.woff2格式)来更换网页字体,避免依赖第三方 // @author You // @license MIT // @run-at document-start // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== (function() { 'use strict'; const main = () => { // 默认字体配置 const defaultFont = { name: 'serif(默认)', fontFamily: 'serif', isDefault: true }; const fontData = GM_getValue('fontData', { fonts: [defaultFont], currentFont: defaultFont.name, isTextStroke: false, isTextShadow: true }); // 创建样式元素:一个用于字体加载,一个用于通用样式 const createStyleElement = (elementId) => { let styleElement = document.getElementById(elementId); if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = elementId; document.head.appendChild(styleElement); } return styleElement; }; const fontFaceStyleElement = createStyleElement('font-face-style'); const commonStyleElement = createStyleElement('font-common-style'); // 缓存已加载的字体Blob URL const cachedFontBlobUrls = {}; // 更新通用样式规则(粗体、阴影等) const updateCommonStyles = () => { const selectedFont = fontData.fonts.find(font => font.name === fontData.currentFont); if (!selectedFont) return; const cssRules = `body *{ font-family: '${selectedFont.fontFamily}'; ${fontData.isTextStroke ? '-webkit-text-stroke: 0.5px;' : ''} ${fontData.isTextShadow ? 'text-shadow: 0 0 0.2px rgba(0, 0, 0, 0.9), 1px 1px 3px rgba(0, 0, 0, 0.2);' : ''} }`; commonStyleElement.textContent = cssRules; }; // 处理字体文件更新 const updateFontFaces = (selectedFont) => { // 内置字体不需要@font-face规则 if (!selectedFont || !selectedFont.storageKey) { fontFaceStyleElement.textContent = ''; updateCommonStyles(); return; } // 如果已有缓存,直接使用 const fontBlobUrl = cachedFontBlobUrls[selectedFont.storageKey] || ''; if (fontBlobUrl) { const fontFaceCss = buildFontFaceCSS( selectedFont.fontFamily, fontBlobUrl, selectedFont.format ); fontFaceStyleElement.textContent = fontFaceCss; updateCommonStyles(); return; } const fontChunks = GM_getValue(`font_${selectedFont.storageKey}_chunks`, []); const totalChunks = GM_getValue(`font_${selectedFont.storageKey}_total`, 0); if (fontChunks.length === totalChunks) { Promise.all(fontChunks.map(index => GM_getValue(`font_${selectedFont.storageKey}_chunk_${index}`))) .then(base64Chunks => { const base64Data = base64Chunks.join(''); const blob = base64ToBlob(base64Data, selectedFont.mimeType); const fontBlobUrl = URL.createObjectURL(blob); // 缓存URL避免重复加载 cachedFontBlobUrls[selectedFont.storageKey] = fontBlobUrl; const fontFaceCss = buildFontFaceCSS( selectedFont.fontFamily, fontBlobUrl, selectedFont.format ); fontFaceStyleElement.textContent = fontFaceCss; updateCommonStyles(); }); } }; // 构建字体face CSS规则 const buildFontFaceCSS = (fontFamily, fontUrl, fontFormat) => { return `@font-face { font-family: '${fontFamily}'; src: url(${fontUrl}) format('${fontFormat}'); }`; }; // 创建字体控制面板UI const createFontPanel = () => { // 创建遮罩层 const overlay = document.createElement('div'); overlay.style = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 99998;`; const panel = document.createElement('div'); panel.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 99999; min-width: 300px;`; // 标题 const panelTitle = document.createElement('h3'); panelTitle.textContent = '字体设置'; panelTitle.style = 'text-align: center;'; panel.appendChild(panelTitle); // 字体列表容器 const fontListContainer = document.createElement('div'); fontListContainer.style = 'margin: 20px 0;'; panel.appendChild(fontListContainer); // 渲染字体列表 const renderFontList = () => { fontListContainer.innerHTML = ''; fontData.fonts.forEach(font => { const fontItem = document.createElement('div'); fontItem.style = 'margin: 8px 0; display: flex; align-items: center; cursor: pointer; padding: 4px 8px; border-radius: 4px;'; // 为选中的字体添加背景色 if (font.name === fontData.currentFont) { fontItem.style.backgroundColor = '#e0e0e0'; // 选中字体的背景色 } // 选择指示器 const selectIndicator = document.createElement('span'); selectIndicator.textContent = font.name === fontData.currentFont ? '●' : '○'; selectIndicator.style = 'width: 1em; font-size: 16px; cursor: pointer;'; selectIndicator.onclick = (e) => { e.stopPropagation(); fontData.currentFont = font.name; updateFontFaces(font); renderFontList(); }; // 字体名称 const fontName = document.createElement('span'); fontName.textContent = font.name; fontName.style = 'flex-grow: 1; cursor: pointer; text-align: center;'; fontName.className = 'font-name'; fontName.onclick = () => { fontData.currentFont = font.name; updateFontFaces(font); renderFontList(); }; fontItem.appendChild(selectIndicator); fontItem.appendChild(fontName); // 删除按钮 const deleteButton = document.createElement('button'); if (font.isDefault) { deleteButton.textContent = ' '; deleteButton.style = 'width: 1em; background: none; border: none; border-radius: 4px; padding: 2px 8px; cursor: pointer;'; } else { deleteButton.textContent = '×'; deleteButton.style = 'width: 1em; color: #ff4444; background: none; border: none; border-radius: 4px; cursor: pointer;'; deleteButton.onclick = (e) => { e.stopPropagation(); if (confirm(`确定要删除字体 "${font.name}" 吗?`)) { handleDeleteFont(font); } }; } fontItem.appendChild(deleteButton); fontListContainer.appendChild(fontItem); }); }; // 删除字体 const handleDeleteFont = (font) => { fontData.fonts = fontData.fonts.filter(f => f.name !== font.name); // 如果删除的是当前字体,切换到第一个字体 if (fontData.currentFont === font.name) { fontData.currentFont = fontData.fonts[0].name; } // 清理存储数据 if (font.storageKey) { const fontChunks = GM_getValue(`font_${font.storageKey}_chunks`, []); fontChunks.forEach((_, i) => GM_deleteValue(`font_${font.storageKey}_chunk_${i}`)); GM_deleteValue(`font_${font.storageKey}_chunks`); GM_deleteValue(`font_${font.storageKey}_total`); // 清除缓存 if (cachedFontBlobUrls[font.storageKey]) { URL.revokeObjectURL(cachedFontBlobUrls[font.storageKey]); delete cachedFontBlobUrls[font.storageKey]; } } renderFontList(); GM_setValue('fontData', fontData); updateFontFaces(fontData.fonts.find(f => f.name === fontData.currentFont)); }; // 创建开关控件 const createToggle = (label, key, onChange) => { const container = document.createElement('div'); Object.assign(container.style, { display: 'flex', justifyContent: 'center', alignItems: 'center', margin: '20px 0', cursor: 'pointer' }); const indicator = document.createElement('span'); indicator.textContent = fontData[key] ? '●' : '○'; indicator.style.marginRight = '5px'; const labelElement = document.createElement('span'); labelElement.textContent = label; container.appendChild(indicator); container.appendChild(labelElement); container.addEventListener('click', () => { fontData[key] = !fontData[key]; indicator.textContent = fontData[key] ? '●' : '○'; onChange(); }); return container; }; // 添加开关 panel.appendChild(createToggle('加粗', 'isTextStroke', updateCommonStyles)); panel.appendChild(createToggle('阴影', 'isTextShadow', updateCommonStyles)); // 导入字体按钮 const importButton = document.createElement('button'); importButton.textContent = '导入本地字体'; importButton.style = 'display: block; margin: 20px auto; padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;'; importButton.onclick = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.ttf,.otf,.woff,.woff2'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.onchange = async (e) => { const file = e.target.files[0]; if (file) { const originalName = file.name.replace(/\.[^/.]+$/, ""); let newName = originalName; // 检查是否已存在相同的字体(文件名和文件大小) const existingFont = fontData.fonts.find(f => f.originalFileName === file.name && f.fileSize === file.size); if (existingFont) { alert(`字体 "${originalName}" 已存在,无需重复导入。`); document.body.removeChild(fileInput); return; } // 检查字体名称是否重复 let index = 2; while (fontData.fonts.some(f => f.name === newName)) { newName = `${originalName}(${index})`; index++; } // 读取文件 const reader = new FileReader(); reader.onload = () => { const result = reader.result; const base64Data = result.split(',')[1]; const mimeType = result.split(',')[0].split(':')[1]; const storageKey = 'font_' + Date.now(); // 分块存储数据 const chunkSize = 500000; const chunks = []; for (let i = 0; i < base64Data.length; i += chunkSize) { const chunk = base64Data.substring(i, i + chunkSize); GM_setValue(`font_${storageKey}_chunk_${chunks.length}`, chunk); chunks.push(chunk); } // 存储分块信息 GM_setValue(`font_${storageKey}_chunks`, chunks.map((_, i) => i)); GM_setValue(`font_${storageKey}_total`, chunks.length); // 添加新字体 fontData.fonts.push({ name: newName, fontFamily: newName, originalFileName: file.name, mimeType: mimeType, storageKey: storageKey, format: getFontFormat(file.name), fileSize: file.size }); fontData.currentFont = newName; GM_setValue('fontData', fontData); updateFontFaces(fontData.fonts[fontData.fonts.length - 1]); renderFontList(); }; reader.readAsDataURL(file); } document.body.removeChild(fileInput); }; fileInput.click(); }; panel.appendChild(importButton); // 保存按钮 const buttonContainer = document.createElement('div'); buttonContainer.style = 'display: flex; justify-content: center; gap: 10px;'; const saveButton = document.createElement('button'); saveButton.textContent = '保存设置'; saveButton.style = 'padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;'; saveButton.onclick = () => { GM_setValue('fontData', fontData); document.body.removeChild(overlay); }; buttonContainer.appendChild(saveButton); panel.appendChild(buttonContainer); overlay.appendChild(panel); // 点击遮罩层关闭设置面板 overlay.onclick = e => { if (e.target === overlay) document.body.removeChild(overlay); }; document.body.appendChild(overlay); renderFontList(); }; // Base64转Blob对象 const base64ToBlob = (base64String, mimeType) => { const byteCharacters = atob(base64String); const byteArrays = []; for (let i = 0; i < byteCharacters.length; i += 512) { const slice = byteCharacters.slice(i, i + 512); const byteNumbers = new Array(slice.length); for (let j = 0; j < slice.length; j++) { byteNumbers[j] = slice.charCodeAt(j); } byteArrays.push(new Uint8Array(byteNumbers)); } return new Blob(byteArrays, { type: mimeType }); }; // 获取字体格式 const getFontFormat = (fileName) => { const ext = fileName.split('.').pop().toLowerCase(); return { 'ttf': 'truetype', 'otf': 'opentype', 'woff': 'woff', 'woff2': 'woff2' }[ext] || 'truetype'; }; // 注册菜单命令 GM_registerMenuCommand('🎨 字体设置', createFontPanel); GM_registerMenuCommand('⚙️ 查看配置', () => alert(JSON.stringify(fontData, null, 2))); GM_registerMenuCommand('🔄 重新加载', main); // 初始化字体 updateFontFaces(fontData.fonts.find(font => font.name === fontData.currentFont)); }; main(); })();