79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
type SharpLike = (input?: unknown, options?: unknown) => { png: () => { toBuffer: () => Promise<Buffer> } };
|
|
|
|
let sharpFn: SharpLike | null = null;
|
|
async function getSharp(): Promise<SharpLike> {
|
|
if (sharpFn) return sharpFn;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const mod: any = await import('sharp');
|
|
sharpFn = (mod?.default || mod) as SharpLike;
|
|
return sharpFn;
|
|
}
|
|
|
|
const PUBLIC_DIR = path.join(process.cwd(), 'public');
|
|
|
|
async function fetchBytes(url: string): Promise<Uint8Array> {
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
return new Uint8Array(await res.arrayBuffer());
|
|
}
|
|
|
|
async function readBytesFromPublic(localPath: string): Promise<Uint8Array> {
|
|
const abs = path.join(PUBLIC_DIR, localPath.replace(/^\//, ''));
|
|
return new Uint8Array(fs.readFileSync(abs));
|
|
}
|
|
|
|
function transformLogoSvgToPrintBlack(svg: string): string {
|
|
return svg
|
|
.replace(/fill\s*:\s*white/gi, 'fill:#0E2A47')
|
|
.replace(/fill\s*=\s*"white"/gi, 'fill="#0E2A47"')
|
|
.replace(/fill\s*=\s*'white'/gi, "fill='#0E2A47'");
|
|
}
|
|
|
|
async function toPngBytes(inputBytes: Uint8Array, inputHint: string): Promise<Uint8Array> {
|
|
const ext = (path.extname(inputHint).toLowerCase() || '').replace('.', '');
|
|
if (ext === 'png') return inputBytes;
|
|
|
|
if (ext === 'svg' && /\/media\/logo\.svg$/i.test(inputHint)) {
|
|
const svg = Buffer.from(inputBytes).toString('utf8');
|
|
inputBytes = new Uint8Array(Buffer.from(transformLogoSvgToPrintBlack(svg), 'utf8'));
|
|
}
|
|
|
|
const sharp = await getSharp();
|
|
return new Uint8Array(await sharp(Buffer.from(inputBytes)).png().toBuffer());
|
|
}
|
|
|
|
function toDataUrlPng(bytes: Uint8Array): string {
|
|
return `data:image/png;base64,${Buffer.from(bytes).toString('base64')}`;
|
|
}
|
|
|
|
export async function loadImageAsPngDataUrl(src: string | null): Promise<string | null> {
|
|
if (!src) return null;
|
|
try {
|
|
if (src.startsWith('/')) {
|
|
const bytes = await readBytesFromPublic(src);
|
|
const png = await toPngBytes(bytes, src);
|
|
return toDataUrlPng(png);
|
|
}
|
|
const bytes = await fetchBytes(src);
|
|
const png = await toPngBytes(bytes, src);
|
|
return toDataUrlPng(png);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function loadQrAsPngDataUrl(data: string): Promise<string | null> {
|
|
try {
|
|
const safe = encodeURIComponent(data);
|
|
const url = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${safe}`;
|
|
const bytes = await fetchBytes(url);
|
|
const png = await toPngBytes(bytes, url);
|
|
return toDataUrlPng(png);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|