aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/woff2
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/woff2')
-rw-r--r--scripts/woff2/assets/LiberationSans-Regular-2048.ttfbin0 -> 898548 bytes
-rw-r--r--scripts/woff2/assets/LiberationSans-Regular.ttfbin0 -> 864800 bytes
-rw-r--r--scripts/woff2/assets/NotoEmoji-Regular-2048.ttfbin0 -> 896640 bytes
-rw-r--r--scripts/woff2/assets/NotoEmoji-Regular.ttfbin0 -> 862680 bytes
-rw-r--r--scripts/woff2/assets/Xiaolai-Regular.ttfbin0 -> 21745444 bytes
-rw-r--r--scripts/woff2/woff2-esbuild-plugins.js251
-rw-r--r--scripts/woff2/woff2-vite-plugins.js105
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
new file mode 100644
index 0000000..ff514cd
--- /dev/null
+++ b/scripts/woff2/assets/LiberationSans-Regular-2048.ttf
Binary files differ
diff --git a/scripts/woff2/assets/LiberationSans-Regular.ttf b/scripts/woff2/assets/LiberationSans-Regular.ttf
new file mode 100644
index 0000000..006f615
--- /dev/null
+++ b/scripts/woff2/assets/LiberationSans-Regular.ttf
Binary files differ
diff --git a/scripts/woff2/assets/NotoEmoji-Regular-2048.ttf b/scripts/woff2/assets/NotoEmoji-Regular-2048.ttf
new file mode 100644
index 0000000..608947b
--- /dev/null
+++ b/scripts/woff2/assets/NotoEmoji-Regular-2048.ttf
Binary files differ
diff --git a/scripts/woff2/assets/NotoEmoji-Regular.ttf b/scripts/woff2/assets/NotoEmoji-Regular.ttf
new file mode 100644
index 0000000..f68173b
--- /dev/null
+++ b/scripts/woff2/assets/NotoEmoji-Regular.ttf
Binary files differ
diff --git a/scripts/woff2/assets/Xiaolai-Regular.ttf b/scripts/woff2/assets/Xiaolai-Regular.ttf
new file mode 100644
index 0000000..c8673de
--- /dev/null
+++ b/scripts/woff2/assets/Xiaolai-Regular.ttf
Binary files differ
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"
+ />
+ `,
+ );
+ }
+ },
+ };
+};