| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 | const { execSync } = require("child_process");const fs = require("fs");const path = require("path");const { Font } = require("fonteditor-core");const wawoff = require("wawoff2");const which = require("which");/** * Custom esbuild plugin to: * 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs) * 2. convert all the imported fonts (including those from cdn) at build time into .ttf (since Resvg does not support woff2, neither inlined dataurls - https://github.com/RazrFalcon/resvg/issues/541) *    - merging multiple woff2 into one ttf (for same families with different unicode ranges) *    - deduplicating glyphs due to the merge process *    - merging fallback font for each *    - printing out font metrics * * @returns {import("esbuild").Plugin} */module.exports.woff2ServerPlugin = (options = {}) => {  return {    name: "woff2ServerPlugin",    setup(build) {      const fonts = new Map();      build.onResolve({ filter: /\.woff2$/ }, (args) => {        const resolvedPath = path.resolve(args.resolveDir, args.path);        return {          path: resolvedPath,          namespace: "woff2ServerPlugin",        };      });      build.onLoad(        { filter: /.*/, namespace: "woff2ServerPlugin" },        async (args) => {          let woff2Buffer;          if (path.isAbsolute(args.path)) {            // read local woff2 as a buffer (WARN: `readFileSync` does not work!)            woff2Buffer = await fs.promises.readFile(args.path);          } else {            throw new Error(`Font path has to be absolute! "${args.path}"`);          }          // google's brotli decompression into snft          const snftBuffer = new Uint8Array(            await wawoff.decompress(woff2Buffer),          ).buffer;          // load font and store per fontfamily & subfamily cache          let font;          try {            font = Font.create(snftBuffer, {              type: "ttf",              hinting: true,              kerning: true,            });          } catch {            // if loading as ttf fails, try to load as otf            font = Font.create(snftBuffer, {              type: "otf",              hinting: true,              kerning: true,            });          }          const fontFamily = font.data.name.fontFamily;          const subFamily = font.data.name.fontSubFamily;          if (!fonts.get(fontFamily)) {            fonts.set(fontFamily, {});          }          if (!fonts.get(fontFamily)[subFamily]) {            fonts.get(fontFamily)[subFamily] = [];          }          // store the snftbuffer per subfamily          fonts.get(fontFamily)[subFamily].push(font);          // inline the woff2 as base64 for server-side use cases          // NOTE: "file" loader is broken in commonjs and "dataurl" loader does not produce correct ur          return {            contents: `data:font/woff2;base64,${woff2Buffer.toString(              "base64",            )}`,            loader: "text",          };        },      );      build.onEnd(async () => {        const { outdir } = options;        if (!outdir) {          return;        }        const outputDir = path.resolve(outdir);        const isFontToolsInstalled = await which("fonttools", {          nothrow: true,        });        if (!isFontToolsInstalled) {          console.error(            `Skipped TTF generation: install "fonttools" first in order to generate TTF fonts!\nhttps://github.com/fonttools/fonttools`,          );          return;        }        const xiaolaiPath = path.resolve(          __dirname,          "./assets/Xiaolai-Regular.ttf",        );        const emojiPath = path.resolve(          __dirname,          "./assets/NotoEmoji-Regular.ttf",        );        // need to use the same em size as built-in fonts, otherwise pyftmerge throws (modified manually with font forge)        const emojiPath_2048 = path.resolve(          __dirname,          "./assets/NotoEmoji-Regular-2048.ttf",        );        const liberationPath = path.resolve(          __dirname,          "./assets/LiberationSans-Regular.ttf",        );        // need to use the same em size as built-in fonts, otherwise pyftmerge throws (modified manually with font forge)        const liberationPath_2048 = path.resolve(          __dirname,          "./assets/LiberationSans-Regular-2048.ttf",        );        const xiaolaiFont = Font.create(fs.readFileSync(xiaolaiPath), {          type: "ttf",        });        const emojiFont = Font.create(fs.readFileSync(emojiPath), {          type: "ttf",        });        const liberationFont = Font.create(fs.readFileSync(liberationPath), {          type: "ttf",        });        const sortedFonts = Array.from(fonts.entries()).sort(          ([family1], [family2]) => (family1 > family2 ? 1 : -1),        );        // for now we are interested in the regular families only        for (const [family, { Regular }] of sortedFonts) {          if (family.includes("Xiaolai")) {            // don't generate ttf for Xiaolai, as we have it hardcoded as one ttf            continue;          }          const baseFont = Regular[0];          const tempPaths = Regular.map((_, index) =>            path.resolve(outputDir, `temp_${family}_${index}.ttf`),          );          for (const [index, font] of Regular.entries()) {            // tempFileNames            if (!fs.existsSync(outputDir)) {              fs.mkdirSync(outputDir, { recursive: true });            }            // write down the buffer            fs.writeFileSync(tempPaths[index], font.write({ type: "ttf" }));          }          const mergedFontPath = path.resolve(outputDir, `${family}.ttf`);          const fallbackFontsPaths = [];          const shouldIncludeXiaolaiFallback = family.includes("Excalifont");          if (shouldIncludeXiaolaiFallback) {            fallbackFontsPaths.push(xiaolaiPath);          }          // add liberation as fallback to all fonts, so that unknown characters are rendered similarly to how browser renders them (Helvetica, Arial, etc.)          if (baseFont.data.head.unitsPerEm === 2048) {            fallbackFontsPaths.push(emojiPath_2048, liberationPath_2048);          } else {            fallbackFontsPaths.push(emojiPath, liberationPath);          }          // drop Vertical related metrics, otherwise it does not allow us to merge the fonts          // vhea (Vertical Header Table)          // vmtx (Vertical Metrics Table)          execSync(            `pyftmerge --drop-tables=vhea,vmtx --output-file="${mergedFontPath}" "${tempPaths.join(              '" "',            )}" "${fallbackFontsPaths.join('" "')}"`,          );          // cleanup          for (const path of tempPaths) {            fs.rmSync(path);          }          // yeah, we need to read the font again (:          const mergedFont = Font.create(fs.readFileSync(mergedFontPath), {            type: "ttf",            kerning: true,            hinting: true,          });          const getNameField = (field) => {            const base = baseFont.data.name[field];            const xiaolai = xiaolaiFont.data.name[field];            const emoji = emojiFont.data.name[field];            const liberation = liberationFont.data.name[field];            // liberation font            return shouldIncludeXiaolaiFallback              ? `${base} & ${xiaolai} & ${emoji} & ${liberation}`              : `${base} & ${emoji} & ${liberation}`;          };          mergedFont.set({            ...mergedFont.data,            name: {              ...mergedFont.data.name,              copyright: getNameField("copyright"),              licence: getNameField("licence"),            },          });          fs.rmSync(mergedFontPath);          fs.writeFileSync(mergedFontPath, mergedFont.write({ type: "ttf" }));          const { ascent, descent } = baseFont.data.hhea;          console.info(`Generated "${family}"`);          if (Regular.length > 1) {            console.info(              `- by merging ${Regular.length} woff2 fonts and related fallback fonts`,            );          }          console.info(            `- with metrics ${baseFont.data.head.unitsPerEm}, ${ascent}, ${descent}`,          );          console.info(``);        }      });    },  };};
 |