diff --git a/apps/monograph/app/components/monograph-chat.tsx b/apps/monograph/app/components/monograph-chat.tsx index 1bbc943d6..f671d9ad1 100644 --- a/apps/monograph/app/components/monograph-chat.tsx +++ b/apps/monograph/app/components/monograph-chat.tsx @@ -62,7 +62,7 @@ export function MonographChat({ sx }: SxProp) { borderRadius: 10, width: "100%" }} - src={`${PUBLIC_URL}/api/og.png?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023`} + src={`${PUBLIC_URL}/api/og.jpg?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023`} /> & { content: string }; export const meta: MetaFunction = ({ data }) => { if (!data || !data.metadata || !data.monograph) return []; - const imageUrl = `${PUBLIC_URL}/api/og.png?${new URLSearchParams({ + const imageUrl = `${PUBLIC_URL}/api/og.jpg?${new URLSearchParams({ title: data?.metadata?.title || "", description: data?.metadata?.fullDescription ? Buffer.from(data.metadata.fullDescription, "utf-8").toString("base64") diff --git a/apps/monograph/app/routes/api.og.ts b/apps/monograph/app/routes/api.og.ts index dc80c5ad6..5bc3b9a95 100644 --- a/apps/monograph/app/routes/api.og.ts +++ b/apps/monograph/app/routes/api.og.ts @@ -21,6 +21,6 @@ import { LoaderFunctionArgs } from "@remix-run/node"; export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); - url.pathname += ".png"; + url.pathname += ".jpg"; return Response.redirect(url, 308); } diff --git a/apps/monograph/app/routes/api.og[.jpg].ts b/apps/monograph/app/routes/api.og[.jpg].ts new file mode 100644 index 000000000..484e25834 --- /dev/null +++ b/apps/monograph/app/routes/api.og[.jpg].ts @@ -0,0 +1,52 @@ +/* +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 { LoaderFunctionArgs } from "@remix-run/node"; +import { makeImage } from "../utils/generate-og-image.server"; +import { formatDate } from "@notesnook/core"; + +export async function loader({ request }: LoaderFunctionArgs) { + const url = new URL(request.url); + const title = url.searchParams.get("title") || "Not found"; + const description = url.searchParams.get("description") || ""; + const date = + url.searchParams.get("date") || + formatDate(new Date(), { + type: "date-time", + dateFormat: "YYYY-MM-DD", + timeFormat: "24-hour" + }); + + return new Response( + await makeImage( + { + date, + description, + title + }, + url.search + ), + { + status: 200, + headers: { + "Content-Type": "image/jpeg" + } + } + ); +} diff --git a/apps/monograph/app/routes/api.og[.png].ts b/apps/monograph/app/routes/api.og[.png].ts index cb81d6a93..3c92c8bcf 100644 --- a/apps/monograph/app/routes/api.og[.png].ts +++ b/apps/monograph/app/routes/api.og[.png].ts @@ -18,35 +18,9 @@ along with this program. If not, see . */ import { LoaderFunctionArgs } from "@remix-run/node"; -import { makeImage } from "../utils/generate-og-image.server"; -import { formatDate } from "@notesnook/core"; export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); - const title = url.searchParams.get("title") || "Not found"; - const description = url.searchParams.get("description") || ""; - const date = - url.searchParams.get("date") || - formatDate(new Date(), { - type: "date-time", - dateFormat: "YYYY-MM-DD", - timeFormat: "24-hour" - }); - - return new Response( - await makeImage( - { - date, - description, - title - }, - url.search - ), - { - status: 200, - headers: { - "Content-Type": "image/png" - } - } - ); + url.pathname = url.pathname.replace(/.png/, ".jpg"); + return Response.redirect(url, 308); } diff --git a/apps/monograph/app/utils/generate-og-image.server.ts b/apps/monograph/app/utils/generate-og-image.server.ts new file mode 100644 index 000000000..c42cfe365 --- /dev/null +++ b/apps/monograph/app/utils/generate-og-image.server.ts @@ -0,0 +1,147 @@ +/* +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"; + +export type OGMetadata = { title: string; description: string; date: string }; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 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" +); +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" +); + +console.log("OpenSans", GlobalFonts.registerFromPath(OpenSans, "OpenSans")); +console.log( + "OpenSansBold", + GlobalFonts.registerFromPath(OpenSansBold, "OpenSansBold") +); + +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") +); + +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 OpenSansBold"; + let y = PADDING + 105; + const titleLines = split( + ctx as any, + metadata.title, + "600 64px OpenSansBold", + 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 OpenSans"; + const description = Buffer.from( + metadata.description || "", + "base64" + ).toString("utf-8"); + const descLines = split( + ctx as any, + description, + "30px OpenSans", + 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; +} diff --git a/apps/monograph/app/utils/generate-og-image.server.tsx b/apps/monograph/app/utils/generate-og-image.server.tsx deleted file mode 100644 index 15ac025eb..000000000 --- a/apps/monograph/app/utils/generate-og-image.server.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* -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 satori from "satori"; -import { ThemeDark } from "@notesnook/theme"; -import sharp from "sharp"; -import fontRegular from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf?arraybuffer"; -import fontBold from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf?arraybuffer"; -import {} from "lru-cache"; -import { Readable } from "node:stream"; -import { LRUCache } from "lru-cache"; - -export type OGMetadata = { title: string; description: string; date: string }; - -const cache = new LRUCache({ - ttl: 1000 * 60 * 60 * 24, - ttlAutopurge: true -}); - -export async function makeImage(metadata: OGMetadata, cacheKey: string) { - const theme = ThemeDark.scopes.base; - - console.time("satori"); - let svg = cache.get(cacheKey); - - if (!svg) { - svg = await satori( -
-
-

- {metadata.date} -

-

- {metadata.title} -

-

- {Buffer.from(metadata.description || "", "base64").toString( - "utf-8" - )} -

-
-
- - - - - - - - - - - - -
-

- Notesnook Monograph -

-

- Anonymous, secure, and encrypted note sharing with password - protection. -

-
-
-
, - { - width: 1200, - height: 630, - fonts: [ - { - name: "Open Sans", - data: fontRegular!, - weight: 400, - style: "normal" - }, - { - name: "Open Sans", - data: fontBold!, - weight: 600, - style: "normal" - } - ] - } - ); - cache.set(cacheKey, svg); - } - console.timeEnd("satori"); - - return sharp(Buffer.from(svg)).png().toBuffer(); -} diff --git a/apps/monograph/package.json b/apps/monograph/package.json index 22c770ffa..9eabe06cf 100644 --- a/apps/monograph/package.json +++ b/apps/monograph/package.json @@ -17,6 +17,7 @@ "@lingui/core": "4.11.4", "@lingui/react": "4.11.4", "@mdi/js": "^7.4.47", + "@napi-rs/canvas": "0.1.59", "@notesnook/core": "file:../../packages/core", "@notesnook/crypto": "file:../../packages/crypto", "@notesnook/editor": "file:../../packages/editor", @@ -28,7 +29,7 @@ "@sagi.io/workers-kv": "^0.0.14", "@theme-ui/components": "^0.16.2", "@theme-ui/core": "^0.16.2", - "buffer": "^6.0.3", + "canvas-hypertxt": "^1.0.3", "comlink": "^4.4.1", "date-fns": "^4.1.0", "html-to-text": "^9.0.5", @@ -42,10 +43,7 @@ "react-turnstile": "^1.1.4", "refractor": "^4.8.1", "remix-utils": "^7.7.0", - "satori": "^0.11.1", - "sharp": "^0.33.5", "slugify": "^1.6.6", - "svg2png-wasm": "^1.4.1", "zustand": "^4.5.5" }, "devDependencies": { diff --git a/apps/monograph/public/logo.svg b/apps/monograph/public/logo.svg new file mode 100644 index 000000000..c7f54a7e1 --- /dev/null +++ b/apps/monograph/public/logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + \ No newline at end of file