mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-05 14:42:57 +00:00
use submodule
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,252 @@
|
||||
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(``);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
// define `EXCALIDRAW_ASSET_PATH` as a SSOT
|
||||
const OSS_FONTS_CDN = "https://excalidraw.nyc3.cdn.digitaloceanspaces.com/oss/";
|
||||
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>
|
||||
// point into our CDN in prod, fallback to root (excalidraw.com) domain in case of issues
|
||||
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"
|
||||
/>
|
||||
`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user