diff options
Diffstat (limited to 'scripts/woff2')
| -rw-r--r-- | scripts/woff2/assets/LiberationSans-Regular-2048.ttf | bin | 0 -> 898548 bytes | |||
| -rw-r--r-- | scripts/woff2/assets/LiberationSans-Regular.ttf | bin | 0 -> 864800 bytes | |||
| -rw-r--r-- | scripts/woff2/assets/NotoEmoji-Regular-2048.ttf | bin | 0 -> 896640 bytes | |||
| -rw-r--r-- | scripts/woff2/assets/NotoEmoji-Regular.ttf | bin | 0 -> 862680 bytes | |||
| -rw-r--r-- | scripts/woff2/assets/Xiaolai-Regular.ttf | bin | 0 -> 21745444 bytes | |||
| -rw-r--r-- | scripts/woff2/woff2-esbuild-plugins.js | 251 | ||||
| -rw-r--r-- | scripts/woff2/woff2-vite-plugins.js | 105 |
7 files changed, 356 insertions, 0 deletions
diff --git a/scripts/woff2/assets/LiberationSans-Regular-2048.ttf b/scripts/woff2/assets/LiberationSans-Regular-2048.ttf Binary files differnew file mode 100644 index 0000000..ff514cd --- /dev/null +++ b/scripts/woff2/assets/LiberationSans-Regular-2048.ttf diff --git a/scripts/woff2/assets/LiberationSans-Regular.ttf b/scripts/woff2/assets/LiberationSans-Regular.ttf Binary files differnew file mode 100644 index 0000000..006f615 --- /dev/null +++ b/scripts/woff2/assets/LiberationSans-Regular.ttf diff --git a/scripts/woff2/assets/NotoEmoji-Regular-2048.ttf b/scripts/woff2/assets/NotoEmoji-Regular-2048.ttf Binary files differnew file mode 100644 index 0000000..608947b --- /dev/null +++ b/scripts/woff2/assets/NotoEmoji-Regular-2048.ttf diff --git a/scripts/woff2/assets/NotoEmoji-Regular.ttf b/scripts/woff2/assets/NotoEmoji-Regular.ttf Binary files differnew file mode 100644 index 0000000..f68173b --- /dev/null +++ b/scripts/woff2/assets/NotoEmoji-Regular.ttf diff --git a/scripts/woff2/assets/Xiaolai-Regular.ttf b/scripts/woff2/assets/Xiaolai-Regular.ttf Binary files differnew file mode 100644 index 0000000..c8673de --- /dev/null +++ b/scripts/woff2/assets/Xiaolai-Regular.ttf diff --git a/scripts/woff2/woff2-esbuild-plugins.js b/scripts/woff2/woff2-esbuild-plugins.js new file mode 100644 index 0000000..19ebafc --- /dev/null +++ b/scripts/woff2/woff2-esbuild-plugins.js @@ -0,0 +1,251 @@ +const fs = require("fs"); +const path = require("path"); +const { execSync } = require("child_process"); +const which = require("which"); +const wawoff = require("wawoff2"); +const { Font } = require("fonteditor-core"); + +/** + * 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(``); + } + }); + }, + }; +}; diff --git a/scripts/woff2/woff2-vite-plugins.js b/scripts/woff2/woff2-vite-plugins.js new file mode 100644 index 0000000..c2ed881 --- /dev/null +++ b/scripts/woff2/woff2-vite-plugins.js @@ -0,0 +1,105 @@ +// define `EXCALIDRAW_ASSET_PATH` as a SSOT +const OSS_FONTS_CDN = "/"; +const OSS_FONTS_FALLBACK = "/"; + +/** + * Custom vite plugin for auto-prefixing `EXCALIDRAW_ASSET_PATH` woff2 fonts in `excalidraw-app`. + * + * @returns {import("vite").PluginOption} + */ +module.exports.woff2BrowserPlugin = () => { + let isDev; + + return { + name: "woff2BrowserPlugin", + enforce: "pre", + config(_, { command }) { + isDev = command === "serve"; + }, + transform(code, id) { + // using copy / replace as fonts defined in the `.css` don't have to be manually copied over (vite/rollup does this automatically), + // but at the same time can't be easily prefixed with the `EXCALIDRAW_ASSET_PATH` only for the `excalidraw-app` + if (!isDev && id.endsWith("/excalidraw/fonts/fonts.css")) { + return `/* WARN: The following content is generated during excalidraw-app build */ + + @font-face { + font-family: "Assistant"; + src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-Regular.woff2) + format("woff2"), + url(./Assistant-Regular.woff2) format("woff2"); + font-weight: 400; + style: normal; + display: swap; + } + + @font-face { + font-family: "Assistant"; + src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-Medium.woff2) + format("woff2"), + url(./Assistant-Medium.woff2) format("woff2"); + font-weight: 500; + style: normal; + display: swap; + } + + @font-face { + font-family: "Assistant"; + src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-SemiBold.woff2) + format("woff2"), + url(./Assistant-SemiBold.woff2) format("woff2"); + font-weight: 600; + style: normal; + display: swap; + } + + @font-face { + font-family: "Assistant"; + src: url(${OSS_FONTS_CDN}fonts/Assistant/Assistant-Bold.woff2) + format("woff2"), + url(./Assistant-Bold.woff2) format("woff2"); + font-weight: 700; + style: normal; + display: swap; + }`; + } + + if (!isDev && id.endsWith("excalidraw-app/index.html")) { + return code.replace( + "<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->", + `<script> + // use app-relative asset paths in prod + window.EXCALIDRAW_ASSET_PATH = [ + "${OSS_FONTS_CDN}", + "${OSS_FONTS_FALLBACK}", + ]; + </script> + + <!-- Preload all default fonts to avoid swap on init --> + <link + rel="preload" + href="${OSS_FONTS_CDN}fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2" + as="font" + type="font/woff2" + crossorigin="anonymous" + /> + <!-- For Nunito only preload the latin range, which should be good enough for now --> + <link + rel="preload" + href="${OSS_FONTS_CDN}fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2" + as="font" + type="font/woff2" + crossorigin="anonymous" + /> + <link + rel="preload" + href="${OSS_FONTS_CDN}fonts/ComicShanns/ComicShanns-Regular-279a7b317d12eb88de06167bd672b4b4.woff2" + as="font" + type="font/woff2" + crossorigin="anonymous" + /> + `, + ); + } + }, + }; +}; |
