永中文档在线预览字体加载实现方案

448 阅读3分钟

在线预览文档,如果想使用特定的字体可以通过 @font-face 引用是解决访问用户电脑本地没有安装该字体导致不能按设计样式显示的问题。 由于本地字体缺失,在线预览文档字体显示和原文档不一致,需要前端向后端请求加载缺失的字体。

image.png

实现步骤

  1. 遍历字体目录,在static目录下创建个字体映射表,映射表格式
  2. 字体名1,字体名2...:字体url;字体名1,字体名2...:字体url 备注:冒号左边为字体名称,由于一种字体可能多个名称,字体名称用逗号分隔,冒号右边为服务器字体url,此url新开一个映射接口;每个字体直接用分号分隔;新增字体时重启后端即可刷新映射表

设计思路

a)通过配置控制是否打开字体加载功能

b)后端通过index.json返回每个页面所需要的字体集合

c)前端异步渲染页面时,判断页面字体是否缺失,如果缺失向后端请求加载缺失的字体,字体加载完后重新渲染页面

d)后端需要提供字体映射表(全量或者请求缺失部分)

代码实现

//字体映射表
{
    "等线 Light": "Dengl.ttf",
    "Arabic Typesetting": "arabtype.ttf",
    "CordiaUPC￾": "cordiauz.ttf",
    "Palatino Linotype￾": "palabi.ttf",
    "Tahoma": "tahoma.ttf",
    "Rockwell Condensed": "ROCC____.TTF",
    "Viner Hand ITC": "VINERITC.TTF",
    "Balloon": "Balloon-1.otf",
    "Corbel￾": "corbelz.ttf",
    "DaunPenh": "daunpenh.ttf",
    "Arial￾": "ARIALBI.TTF",
    "Microsoft JhengHei Light": "msjhl.ttc",
    "Gill Sans MT Condensed": "GILC____.TTF",
    "Source Han Serif TC ExtraLight": "思源宋体TC-ExtraLight.otf",
    "MingLiU": "mingliu.ttc",
    "DFPOP1W5-B5": "華康POP1體W5.TTC",
    "Impact": "impact.ttf",
    "Bebas": "bebas.TTF",
    "Stencil": "STENCIL.TTF",
    "Iskoola Pota": "iskpota.ttf",
    "Bookshelf Symbol 7": "BSSYM7.TTF"
    }
class Font {
  fontName: string = '';
  status: FONT_STATUS = FONT_STATUS.UNKNOWN;
  options: any = {};
  constructor(fontName: string, options?: any) {
    this.fontName = fontName;
    this.options = options || {};
    if (this.isAvailable()) {
      this.setStatus(FONT_STATUS.ACTIVE);
      this.loaded();
    }
  }

  isAvailable = (): boolean => {
    const defaultFF = 'Arial';
    if (this.fontName.toLowerCase() == defaultFF.toLowerCase()) {
      return true;
    }
    const e = 'a';
    const d = 100;
    const width = 100,
    height = 100;
    let canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    canvas.width = width;
    canvas.height = height;

    function getImageData(ff: string): Array<string> {
        if (!context) return [];
        if (!ff) return [];
            context.clearRect(0, 0, width, height);
            context.font = d + 'px ' + ff + ', ' + defaultFF;
            context.fillText(e, width / 2, height / 2);
            var k = context.getImageData(0, 0, width, height).data;
            return [].slice.call(k).filter(function (l) {
                return l != 0;
        });
    }

    if (context) {
      context.textAlign = 'center';
      context.fillStyle = 'black';
      context.textBaseline = 'middle';
      let result =
        getImageData(defaultFF).join('') !==
        getImageData(this.fontName).join('');

      if(canvas.remove){
        canvas.remove();
      }else {
        canvas = null;
      }
      return result;
    } else {
      return false;
    }
  };

  setStatus(status: FONT_STATUS): void {
    this.status = status;
  }

  loaded(): void {
    this.options.loaded && this.options.loaded();
  }

  unloaded(): void {
    this.options.unloaded && this.options.unloaded();
  }
}
class Fonts {
  static values: Map<string, Font> = new Map<string, Font>();
  static styleElement: HTMLStyleElement | undefined;
  static fontJson: any;
  static f:any;
  static async bind(fontNames: Array<string>) {
    if (!fontNames) return;
    if (!this.fontJson) {
      this.fontJson = (await axios.get(window.fontsConfig + '/fontsConfig.json')).data;
    }
    const loadFontPromise = (fontName: string) => {
      return new Promise((resolve, reject) => {
        let font = Fonts.values.get(fontName);
        if (!font) {
          font = new Font(fontName, {
            loaded: () => {
              this.f && this.f();
              resolve(font);
            },
            unloaded: () => {
              resolve(font);
            },
          });
          Fonts.values.set(fontName, font);
          Fonts.loadFont(font);
        } else {
          resolve(font);
        }
        return font;
      });
    };
    return Promise.all(fontNames.map((v) => loadFontPromise(v)));
  }

  static async loadFont(font: Font) {
    const rule = await Fonts.createFontFaceRule(font.fontName);
    Fonts.insertRule(rule);
    font.setStatus(FONT_STATUS.LOADING);
    Fonts.check(font, 0);
  }

  static check(font: Font, erroCount: number): void {
    if (font.status == FONT_STATUS.ACTIVE) {
      return;
    }
    if (erroCount > FONT_CHECK_MAX_ERROR_COUNT) {
      font.setStatus(FONT_STATUS.INACTIVE);
      font.unloaded();
      return;
    }
    setTimeout(() => {
      if (font.isAvailable()) {
        font.setStatus(FONT_STATUS.ACTIVE);
        font.loaded();
      } else {
        Fonts.check(font, erroCount++);
      }
    }, FONT_CHECK_INTERVAL);
  }

  static async createFontFaceRule(fontName: string) {
    const fonturl = this.fontJson[fontName];
    // TODO 获取字体地址
    const url = `url('${window.fonts}/${fonturl}');`;
    const rule = `@font-face {font-family:"${fontName}";src:${url}}`;
    console.log(rule);
    return rule;
  }

  static insertRule(rule: string) {
    let styleElement = Fonts.styleElement;
    if (!styleElement) {
      styleElement = Fonts.styleElement = document.createElement('style');
      document.documentElement
        .getElementsByTagName('head')[0]
        .appendChild(styleElement);
    }
    const styleSheet = styleElement.sheet;
    if (styleSheet) {
      styleSheet.insertRule(rule, styleSheet.cssRules.length);
    }
  }

  static addEvents(f:any) {
    this.f =f;
  }
}

//兼容写法
@font-face {
 font-family: 'myFirstFont';
 src: url('YourWebFontName.eot'); /* IE9 Compat Modes */
 src: url('YourWebFontName.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
         url('YourWebFontName.woff') format('woff'), /* Modern Browsers */
         url('YourWebFontName.ttf')  format('truetype'), /* Safari, Android, iOS */
         url('YourWebFontName.svg#YourWebFontName') format('svg'); /* Legacy iOS */
}

扩展:字体路径 local 表示本机地址, url 表示网址(url路径的字体,网页加载时,会自动在服务器上下载字体,因此如果字体文件太大,网页加载会比较慢)。

提示:如果在src上定义了多种字体,是按顺序的候选关系,如果修改了定义的字体或顺序,需要重新打开浏览器才能看到修改后的效果,刷新是无效的。