|
1 | 1 | import path from "node:path"; |
| 2 | +import { fileURLToPath } from "node:url"; |
| 3 | +import { mkdir, readFile, writeFile } from "node:fs/promises"; |
2 | 4 | import { Plugin } from "vite"; |
3 | 5 | import sharp from "sharp"; |
4 | 6 | import convert, { RGB } from "color-convert" |
| 7 | +import { createFont, woff2 } from "fonteditor-core"; |
5 | 8 | import { findBrandingFile, findLogoFile } from "./brandingManifest"; |
6 | 9 | import { getThemeFile } from "./theme"; |
7 | 10 |
|
8 | 11 | type Env = Record<string, string|null|undefined>; |
9 | 12 |
|
| 13 | +const fontsConfTemplate = `<?xml version="1.0"?> |
| 14 | +<!DOCTYPE fontconfig SYSTEM "fonts.dtd"> |
| 15 | +<fontconfig> |
| 16 | + <dir prefix="relative">./</dir> |
| 17 | + <config></config> |
| 18 | +</fontconfig> |
| 19 | +`; |
| 20 | + |
| 21 | +/** |
| 22 | + * Sets up a self-contained font environment: |
| 23 | + * 1. Converts a WOFF2 font to TTF for Node rendering. |
| 24 | + * 2. Writes the font and a minimal Fontconfig XML to a project directory. |
| 25 | + * 3. Sets FONTCONFIG_PATH so rendering libraries can find the font. |
| 26 | + * |
| 27 | + * The reason for converting from WOFF2 to TTF is so we can keep using the |
| 28 | + * `@fontsource/inter` package and not need to bundle font files. |
| 29 | + */ |
| 30 | +async function setupFontsEnvironment(baseDir: string) { |
| 31 | + const fontsConfDir = path.resolve(baseDir, "fonts"); |
| 32 | + const inputFontFiles = [ |
| 33 | + import.meta.resolve("@fontsource/inter/files/inter-latin-600-normal.woff2"), |
| 34 | + ]; |
| 35 | + |
| 36 | + await woff2.init(); |
| 37 | + await mkdir(fontsConfDir, { recursive: true }); |
| 38 | + |
| 39 | + for (const input of inputFontFiles) { |
| 40 | + const inputBuffer = await readFile(fileURLToPath(input)); |
| 41 | + |
| 42 | + const font = createFont(inputBuffer, { |
| 43 | + type: "woff2", |
| 44 | + hinting: true, |
| 45 | + kerning: true, |
| 46 | + }); |
| 47 | + |
| 48 | + const outputBuffer = font.write({ |
| 49 | + type: "ttf", |
| 50 | + hinting: true, |
| 51 | + kerning: true, |
| 52 | + }); |
| 53 | + |
| 54 | + await writeFile(path.join(fontsConfDir, path.basename(input)), outputBuffer as Buffer); |
| 55 | + } |
| 56 | + |
| 57 | + await writeFile(path.join(fontsConfDir, "fonts.conf"), fontsConfTemplate), |
| 58 | + |
| 59 | + process.env.FONTCONFIG_PATH = fontsConfDir; |
| 60 | +} |
| 61 | + |
10 | 62 | function splitTitle(title: string, maxLength: number): string[] { |
11 | 63 | const lines: string[] = []; |
12 | 64 | let remainingTitle = title; |
@@ -69,20 +121,11 @@ type ImageTemplateProps = { |
69 | 121 | text: string; |
70 | 122 | } |
71 | 123 | } |
72 | | - |
73 | 124 | const imageTemplate = ({ title, logoB64, colors }: ImageTemplateProps) => ` |
74 | 125 | <svg width="1200" height="628" viewBox="0 0 1200 628" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
75 | | - <style> |
76 | | - .header { |
77 | | - dominant-baseline: hanging; |
78 | | - } |
79 | | - .footer { |
80 | | - dominant-baseline: alphabetic; |
81 | | - } |
82 | | - </style> |
83 | 126 | <rect x="0" y="0" width="1200" height="628" fill="${colors.background}" /> |
84 | 127 | ${logoTemplate(logoB64, 250)} |
85 | | - <g fill="${colors.text}" font-family="Arial" font-weight="bold"> |
| 128 | + <g fill="${colors.text}" font-family="Inter SemiBold"> |
86 | 129 | ${titleTemplate(title)} |
87 | 130 | </g> |
88 | 131 | </svg> |
@@ -202,6 +245,9 @@ export function MetadataImagePlugin(env: Env): Plugin { |
202 | 245 | return { |
203 | 246 | name: "metadata-image-plugin", |
204 | 247 |
|
| 248 | + async configResolved(config) { |
| 249 | + await setupFontsEnvironment(config.cacheDir); |
| 250 | + }, |
205 | 251 | async configureServer(server) { |
206 | 252 | let image = await generateMetadataImage(env); |
207 | 253 |
|
|
0 commit comments