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