monograph: use fallback fonts when rendering og image

This commit is contained in:
Abdullah Atta
2024-11-26 16:22:43 +05:00
parent 54d6a5c307
commit 8f7fd9d8ee
6 changed files with 212 additions and 13 deletions

View File

@@ -6,4 +6,5 @@ node_modules
.dev.vars
.wrangler
/output
/output
/fonts

View File

@@ -23,32 +23,56 @@ import { ThemeDark } from "@notesnook/theme";
import path from "path";
import { fileURLToPath } from "url";
import { split } from "canvas-hypertxt";
import { readFile } from "fs/promises";
export type OGMetadata = { title: string; description: string; date: string };
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT = path.join(__dirname, "../../");
const fontMap = JSON.parse(
await readFile(path.join(ROOT, "fonts", "fonts.json"), "utf-8")
);
// Register fonts
const OpenSans = path.join(
__dirname,
import.meta.env.DEV
? "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
: "../../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
);
const OpenSansBold = path.join(
__dirname,
import.meta.env.DEV
? "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
: "../../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
);
console.log("OpenSans", GlobalFonts.registerFromPath(OpenSans, "OpenSans"));
console.log(
"registering",
"OpenSansBold",
GlobalFonts.registerFromPath(OpenSansBold, "OpenSansBold")
);
const fontFamilies = {
regular: ["OpenSans"],
bold: ["OpenSansBold"]
};
for (const font of fontMap) {
const id = (font.name + font.weight).replace(/ /g, "");
const result = GlobalFonts.registerFromPath(
path.resolve(ROOT, font.path),
id
);
if (!result)
throw new Error(
`Failed to register font: ${id} at ${path.resolve(ROOT, font.path)}`
);
if (font.weight === "600") fontFamilies.bold.push(id);
else fontFamilies.regular.push(id);
console.log("registering", id, result);
}
const cache = new LRUCache<string, Buffer>({
ttl: 1000 * 60 * 60 * 24,
ttlAutopurge: true
@@ -64,6 +88,9 @@ const logo = loadImage(
: path.resolve(__dirname, "../../client/logo.svg")
);
const boldFontFamily = fontFamilies.bold.join(",");
const regularFontFamily = fontFamilies.regular.join(",");
export async function makeImage(metadata: OGMetadata, cacheKey: string) {
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!;
@@ -106,12 +133,12 @@ export async function makeImage(metadata: OGMetadata, cacheKey: string) {
// Draw title
ctx.fillStyle = theme.primary.heading;
ctx.font = "600 64px OpenSansBold";
ctx.font = `600 64px ${boldFontFamily}`;
let y = PADDING + 105;
const titleLines = split(
ctx as any,
metadata.title,
"600 64px OpenSansBold",
`600 64px ${boldFontFamily}`,
WIDTH - PADDING * 2,
true
);
@@ -122,7 +149,7 @@ export async function makeImage(metadata: OGMetadata, cacheKey: string) {
// Draw description
ctx.fillStyle = theme.primary.paragraph;
ctx.font = "30px OpenSans";
ctx.font = `30px ${regularFontFamily}`;
const description = Buffer.from(
metadata.description || "",
"base64"
@@ -130,7 +157,7 @@ export async function makeImage(metadata: OGMetadata, cacheKey: string) {
const descLines = split(
ctx as any,
description,
"30px OpenSans",
`30px ${regularFontFamily}`,
WIDTH - PADDING * 2,
true
).slice(0, 4);

View File

@@ -53,6 +53,7 @@
"@types/react-dom": "^18.3.0",
"@types/react-modal": "^3.16.3",
"autoprefixer": "^10.4.20",
"google-fonts-helper": "^3.6.0",
"postcss": "^8.4.47",
"vite": "^5.4.8",
"vite-plugin-arraybuffer": "^0.0.8",
@@ -4439,6 +4440,13 @@
"node": ">=6"
}
},
"node_modules/destr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
"integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -5474,6 +5482,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/google-fonts-helper": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/google-fonts-helper/-/google-fonts-helper-3.6.0.tgz",
"integrity": "sha512-ReantWd/l8dedKqTYjvqaQ55rAl/rbRqWL5VXHNXtGwIhMX4N8VNA7V19drr7xiv5G3pzlYID0K4FauvGqnWEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"deepmerge": "^4.3.1",
"hookable": "^5.5.3",
"ofetch": "^1.3.4",
"ufo": "^1.5.3"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -5661,6 +5682,13 @@
"react-is": "^16.7.0"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"license": "MIT"
},
"node_modules/hosted-git-info": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz",
@@ -7910,6 +7938,13 @@
"node": ">= 0.6"
}
},
"node_modules/node-fetch-native": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz",
"integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==",
"dev": true,
"license": "MIT"
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -8059,6 +8094,18 @@
"integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==",
"license": "MIT"
},
"node_modules/ofetch": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz",
"integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==",
"dev": true,
"license": "MIT",
"dependencies": {
"destr": "^2.0.3",
"node-fetch-native": "^1.6.4",
"ufo": "^1.5.4"
}
},
"node_modules/ohash": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz",

View File

@@ -5,9 +5,10 @@
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"build": "npm run fonts && remix vite:build",
"dev": "remix vite:dev",
"typecheck": "tsc",
"fonts": "node scripts/font-loader.mjs",
"start": "bun server.ts"
},
"dependencies": {
@@ -56,6 +57,7 @@
"@types/react-dom": "^18.3.0",
"@types/react-modal": "^3.16.3",
"autoprefixer": "^10.4.20",
"google-fonts-helper": "^3.6.0",
"postcss": "^8.4.47",
"vite": "^5.4.8",
"vite-plugin-arraybuffer": "^0.0.8",

View File

@@ -0,0 +1,121 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createWriteStream, existsSync, writeFileSync } from "fs";
import { constructURL } from "google-fonts-helper";
import { Writable } from "stream";
import { mkdir } from "fs/promises";
const FONTS = [
{ locale: "ja-JP", slug: "Noto+Sans+JP", name: "Noto Sans JP" },
{ locale: "ko-KR", slug: "Noto+Sans+KR", name: "Noto Sans KR" },
{ locale: "zh-CN", slug: "Noto+Sans+SC", name: "Noto Sans SC" },
{ locale: "zh-TW", slug: "Noto+Sans+TC", name: "Noto Sans TC" },
{ locale: "zh-HK", slug: "Noto+Sans+HK", name: "Noto Sans HK" },
{ locale: "th-TH", slug: "Noto+Sans+Thai", name: "Noto Sans Thai" },
{ locale: "bn-IN", slug: "Noto+Sans+Bengali", name: "Noto Sans Bengali" },
{ locale: "ar-AR", slug: "Noto+Sans+Arabic", name: "Noto Sans Arabic" },
{ locale: "ta-IN", slug: "Noto+Sans+Tamil", name: "Noto Sans Tamil" },
{ locale: "ml-IN", slug: "Noto+Sans+Malayalam", name: "Noto Sans Malayalam" },
{ locale: "he-IL", slug: "Noto+Sans+Hebrew", name: "Noto Sans Hebrew" },
{ locale: "te-IN", slug: "Noto+Sans+Telugu", name: "Noto Sans Telugu" },
{
locale: "devanagari",
slug: "Noto+Sans+Devanagari",
name: "Noto Sans Devanagari"
},
{ locale: "kannada", slug: "Noto+Sans+Kannada", name: "Noto Sans Kannada" }
// { locale: "symbol", slug: "Noto+Sans+Symbols", name: "Noto Sans Symbols" },
// {
// locale: "symbol",
// slug: "Noto+Sans+Symbols+2",
// name: "Noto Sans Symbols 2"
// },
// { locale: "math", slug: "Noto+Sans+Math", name: "Noto Sans Math" }
];
async function loadFonts(dir) {
const families = {};
for (const font of FONTS) {
families[font.name] = [400, 600];
}
const css = await (
await fetch(constructURL({ families }), {
headers: {
// Make sure it returns TTF.
"User-Agent":
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1"
}
})
).text();
const fontFaces = css
.split("@font-face")
.filter(Boolean)
.map((fontFace) => {
const src = fontFace.match(
/src: url\((.+)\) format\('(opentype|truetype)'\)/
);
const family = fontFace.match(/font-family: '(.+)'/);
const weight = fontFace.match(/font-weight: (\d+)/);
const style = fontFace.match(/font-style: (\w+)/);
return {
src: src[1],
family: family[1],
weight: weight[1],
style: style[1]
};
});
await mkdir("fonts", { recursive: true });
const fontMap = [];
for (const face of fontFaces) {
console.log("Downloading", face.family, face.weight);
const font = FONTS.find((font) => font.name === face.family);
if (!font) continue;
const fileName = `${font.slug}+${face.weight}.ttf`;
const path = `fonts/${fileName}`;
if (existsSync(path)) continue;
const fileStream = Writable.toWeb(
createWriteStream(path, {
autoClose: true,
emitClose: true,
flush: true
})
);
await (await fetch(face.src)).body.pipeTo(fileStream);
fontMap.push({
name: font.name,
locale: font.locale,
weight: face.weight,
path
});
}
writeFileSync("fonts/fonts.json", JSON.stringify(fontMap, null, 2));
console.log("Done");
}
loadFonts();

View File

@@ -68,7 +68,8 @@ export default defineConfig(({ isSsrBuild }) => ({
isSsrBuild ? viteStaticCopy({
targets: [
{ src: "./server.ts", dest: "../../" },
{ src: "./app/assets", dest: "../" }
{ src: "./app/assets", dest: "../" },
{ src: "./fonts", dest: "../" }
]
}) : undefined
],