/*
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 .
*/
import { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas";
import { LRUCache } from "lru-cache";
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/" : "../../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/" : "../../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({
ttl: 1000 * 60 * 60 * 24,
ttlAutopurge: true
});
const WIDTH = 1200;
const HEIGHT = 630;
const PADDING = 50;
const QUALITY = 80;
const logo = loadImage(
import.meta.env.DEV
? path.resolve(__dirname, "../../public/logo.svg")
: 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)!;
}
console.time("canvas");
const theme = ThemeDark.scopes.base;
const canvas = createCanvas(WIDTH, HEIGHT);
const ctx = canvas.getContext("2d");
// Background
ctx.fillStyle = theme.primary.background;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Bottom border
ctx.fillStyle = "#008837";
ctx.fillRect(0, HEIGHT - 10, WIDTH, 10);
// Draw logo
ctx.drawImage(await logo, PADDING, HEIGHT - PADDING - 85, 80, 80);
// Draw bottom text
ctx.fillStyle = theme.primary.heading;
ctx.font = "600 32px OpenSansBold";
ctx.fillText("Notesnook Monograph", PADDING + 95, HEIGHT - PADDING - 55);
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.fillText(
"Anonymous, secure, and encrypted note sharing with password protection.",
PADDING + 95,
HEIGHT - PADDING - 19
);
// Draw date
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.fillText(metadata.date, PADDING, PADDING + 25);
// Draw title
ctx.fillStyle = theme.primary.heading;
ctx.font = `600 64px ${boldFontFamily}`;
let y = PADDING + 105;
const titleLines = split(
ctx as any,
metadata.title,
`600 64px ${boldFontFamily}`,
WIDTH - PADDING * 2,
true
);
for (const line of titleLines) {
ctx.fillText(line, PADDING, y);
y += 60;
}
// Draw description
ctx.fillStyle = theme.primary.paragraph;
ctx.font = `30px ${regularFontFamily}`;
const description = Buffer.from(
metadata.description || "",
"base64"
).toString("utf-8");
const descLines = split(
ctx as any,
description,
`30px ${regularFontFamily}`,
WIDTH - PADDING * 2,
true
).slice(0, 4);
for (const line of descLines) {
ctx.fillText(line, PADDING, y);
y += 40;
}
const buffer = canvas.toBuffer("image/jpeg", QUALITY);
console.timeEnd("canvas");
cache.set(cacheKey, buffer);
return buffer;
}