import { Router, Request, Response } from 'express'; import fs from 'fs'; import path from 'path'; import https from 'https'; const FONTS_DIR = path.join( __dirname, '..', '..', '..', 'storage', 'fonts' ); const GOOGLE_FONTS_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; const ALL_WEIGHTS = [ 100, 200, 300, 400, 500, 600, 700, 800, 900 ]; // ─── types ─────────────────────────────────────────────────────────────────── interface FontMeta { family: string; weights: string[]; italics: string[]; } // ─── helpers ───────────────────────────────────────────────────────────────── function fontsDir( family: string ): string { return path.join( FONTS_DIR, family.toLowerCase() ); } function readMeta( family: string ): FontMeta | null { const p = path.join( fontsDir( family ), 'meta.json' ); if ( !fs.existsSync( p ) ) return null; return JSON.parse( fs.readFileSync( p, 'utf8' ) ); } function writeMeta( meta: FontMeta ): void { fs.writeFileSync( path.join( fontsDir( meta.family ), 'meta.json' ), JSON.stringify( meta, null, 2 ) ); } function fontFileExists( family: string, weight: string, italic: boolean ): boolean { const name = italic ? `${weight}-italic.woff2` : `${weight}.woff2`; return fs.existsSync( path.join( fontsDir( family ), name ) ); } function buildFontFace( displayFamily: string, weight: string, italic: boolean ): string { const style = italic ? 'italic' : 'normal'; const fileName = italic ? `${weight}-italic.woff2` : `${weight}.woff2`; const url = `/fonts/${displayFamily.toLowerCase()}/${fileName}`; return [ '@font-face {', ` font-family: '${displayFamily}';`, ` font-style: ${style};`, ` font-weight: ${weight};`, ` src: url('${url}') format('woff2');`, ' font-display: swap;', '}', ].join( '\n' ); } function httpsGet( url: string, headers: Record = {} ): Promise { return new Promise( ( resolve, reject ) => { const req = https.get( url, { headers }, res => { if ( res.statusCode >= 300 && res.statusCode < 400 && res.headers.location ) { resolve( httpsGet( res.headers.location, headers ) ); return; } let data = ''; res.on( 'data', chunk => { data += chunk; } ); res.on( 'end', () => resolve( data ) ); } ); req.on( 'error', reject ); } ); } function httpsGetBuffer( url: string ): Promise { return new Promise( ( resolve, reject ) => { const req = https.get( url, res => { if ( res.statusCode >= 300 && res.statusCode < 400 && res.headers.location ) { resolve( httpsGetBuffer( res.headers.location ) ); return; } const chunks: Buffer[] = []; res.on( 'data', chunk => { chunks.push( chunk ); } ); res.on( 'end', () => resolve( Buffer.concat( chunks ) ) ); } ); req.on( 'error', reject ); } ); } // ─── public router ──────────────────────────────────────────────────────────── // // GET /get-font — returns a CSS file with @font-face rules // // Modes (mutually exclusive): // ?family=barlow → all files in directory // ?family=barlow&weights=400,700 → those weights + auto-italic if exists // ?family=barlow&variations=400,400 italic → exactly those variations export const publicFontsRouter = Router(); publicFontsRouter.get( '/get-font', ( req: Request, res: Response ): void => { const familyParam = ( req.query.family as string | undefined )?.trim(); if ( !familyParam ) { res.status( 400 ).send( '/* family param required */' ); return; } const meta = readMeta( familyParam ); if ( !meta ) { res.status( 404 ).send( `/* font family '${familyParam}' not found */` ); return; } const blocks: string[] = []; if ( req.query.variations ) { const vars = ( req.query.variations as string ) .split( ',' ) .map( v => v.trim().toLowerCase() ); for ( const v of vars ) { const italic = v.endsWith( ' italic' ); const weight = italic ? v.slice( 0, -7 ).trim() : v; if ( fontFileExists( meta.family, weight, italic ) ) blocks.push( buildFontFace( meta.family, weight, italic ) ); } } else if ( req.query.weights ) { const weights = ( req.query.weights as string ) .split( ',' ) .map( w => w.trim().toLowerCase() ); for ( const w of weights ) { if ( fontFileExists( meta.family, w, false ) ) blocks.push( buildFontFace( meta.family, w, false ) ); if ( fontFileExists( meta.family, w, true ) ) blocks.push( buildFontFace( meta.family, w, true ) ); } } else { for ( const w of meta.weights ) blocks.push( buildFontFace( meta.family, w, false ) ); for ( const w of meta.italics ) blocks.push( buildFontFace( meta.family, w, true ) ); } res .set( 'Content-Type', 'text/css; charset=utf-8' ) .set( 'Cache-Control', 'public, max-age=86400' ) .send( blocks.join( '\n\n' ) ); } ); // ─── api router (auth-gated, mounted at /api/fonts) ────────────────────────── export const apiFontsRouter = Router(); // GET / — list all available font families apiFontsRouter.get( '/', ( _req: Request, res: Response ): void => { if ( !fs.existsSync( FONTS_DIR ) ) { res.json( [] ); return; } const families = fs.readdirSync( FONTS_DIR, { withFileTypes: true } ) .filter( d => d.isDirectory() ) .map( d => { const meta = readMeta( d.name ); return meta ?? { family: d.name, weights: [], italics: [] }; } ); res.json( families ); } ); // POST /download — fetch from Google Fonts and save to storage apiFontsRouter.post( '/download', async ( req: Request, res: Response ): Promise => { const { family, weights } = req.body as { family: string; weights?: number[] }; if ( !family || typeof family !== 'string' ) { res.status( 400 ).json( { error: 'family is required' } ); return; } const requestedWeights = ( weights && weights.length > 0 ) ? weights : ALL_WEIGHTS; const wghts = requestedWeights.map( w => `0,${w};1,${w}` ).join( ';' ); const googleUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent( family )}:ital,wght@${wghts}&display=swap`; let css: string; try { css = await httpsGet( googleUrl, { 'User-Agent': GOOGLE_FONTS_UA } ); } catch ( err ) { res.status( 502 ).json( { error: 'Failed to reach Google Fonts', detail: String( err ) } ); return; } if ( css.trim() === '' ) { res.status( 404 ).json( { error: `Font family '${family}' not found on Google Fonts` } ); return; } const faceRegex = /@font-face\s*\{([^}]+)\}/g; const urlRegex = /src:\s*url\(([^)]+)\)/; const weightRx = /font-weight:\s*(\d+)/; const styleRx = /font-style:\s*(normal|italic)/; const dir = fontsDir( family ); fs.mkdirSync( dir, { recursive: true } ); const meta: FontMeta = { family, weights: [], italics: [] }; const downloaded: string[] = []; let match: RegExpExecArray | null; while ( ( match = faceRegex.exec( css ) ) !== null ) { const block = match[1]; const urlMatch = urlRegex.exec( block ); const weightMatch = weightRx.exec( block ); const styleMatch = styleRx.exec( block ); if ( !urlMatch || !weightMatch || !styleMatch ) continue; const fontUrl = urlMatch[1].trim(); const weight = weightMatch[1]; const italic = styleMatch[1] === 'italic'; const fileName = italic ? `${weight}-italic.woff2` : `${weight}.woff2`; try { const buffer = await httpsGetBuffer( fontUrl ); fs.writeFileSync( path.join( dir, fileName ), buffer ); downloaded.push( fileName ); if ( italic ) { if ( !meta.italics.includes( weight ) ) meta.italics.push( weight ); } else { if ( !meta.weights.includes( weight ) ) meta.weights.push( weight ); } } catch { // skip individual file failures } } meta.weights.sort( ( a, b ) => Number( a ) - Number( b ) ); meta.italics.sort( ( a, b ) => Number( a ) - Number( b ) ); writeMeta( meta ); res.json( { ok: true, family, downloaded } ); } );