【更换网页字体】

导入本地字体(.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();
})();