/** * @deprecated VTracer dégrade couleurs/cadrage — ne plus utiliser en prod. * Préférer : pnpm run brand:header-icon && pnpm run brand:raster * * Pipeline legacy : PNG source → découpes (sharp) → SVG (VTracer CLI). * Prérequis : `cargo install vtracer` ou binaire sur PATH / variable VTRACER_PATH. */ import { execFileSync } from "node:child_process" import { existsSync, copyFileSync } from "node:fs" import { mkdirSync, mkdtempSync, rmSync } from "node:fs" import { tmpdir } from "node:os" import { join, dirname } from "node:path" import { fileURLToPath } from "node:url" import sharp from "sharp" const __dirname = dirname(fileURLToPath(import.meta.url)) const root = join(__dirname, "..") const brandDir = join(root, "public/brand") const VT_ARGS_BALANCED = [ "--mode", "spline", "--filter_speckle", "5", "--gradient_step", "11", "--color_precision", "6", "--path_precision", "2", ] const VT_ARGS_MARK = [ "--mode", "spline", "--filter_speckle", "6", "--gradient_step", "12", "--color_precision", "5", "--path_precision", "2", ] function findSourcePng() { const a = join(brandDir, "ultimail-original.png") const b = join(brandDir, "utlimail-original.png") if (existsSync(a)) return a if (existsSync(b)) return b throw new Error( "Missing public/brand/ultimail-original.png (or legacy utlimail-original.png). Drop the logo PNG there." ) } function findVtracer() { if (process.env.VTRACER_PATH && existsSync(process.env.VTRACER_PATH)) { return process.env.VTRACER_PATH } const home = process.env.HOME || "" const fromCargo = join(home, ".cargo/bin/vtracer") if (existsSync(fromCargo)) return fromCargo try { const p = execFileSync("command", ["-v", "vtracer"], { encoding: "utf8" }).trim().split("\n")[0] if (p) return p } catch { /* ignore */ } throw new Error( "vtracer not found. Install: cargo install vtracer --version 0.6.5 OR set VTRACER_PATH to the binary." ) } function vtracer(bin, inputPng, outputSvg, extraCliArgs) { execFileSync(bin, ["--input", inputPng, "--output", outputSvg, ...extraCliArgs], { stdio: "inherit", }) } async function main() { mkdirSync(brandDir, { recursive: true }) const src = findSourcePng() const canonical = join(brandDir, "ultimail-original.png") if (src !== canonical) { copyFileSync(src, canonical) } const vbin = findVtracer() const tmp = mkdtempSync(join(tmpdir(), "um-vec-")) try { const base = sharp(canonical) const meta = await base.metadata() const W = meta.width ?? 0 const H = meta.height ?? 0 if (!W || !H) throw new Error("Could not read image dimensions") const splitY = Math.min(H - 40, Math.max(80, Math.round(H * 0.52))) const topStrip = join(tmp, "strip-icon.png") const botStrip = join(tmp, "strip-text.png") await base.clone().extract({ left: 0, top: 0, width: W, height: splitY }).png().toFile(topStrip) await base.clone().extract({ left: 0, top: splitY, width: W, height: H - splitY }).png().toFile(botStrip) const side = Math.min(W, splitY) const left = Math.max(0, Math.floor((W - side) / 2)) const topCrop = Math.max(0, Math.floor((splitY - side) / 2)) const markSrc = join(tmp, "mark-square-src.png") await sharp(topStrip) .extract({ left, top: topCrop, width: side, height: side }) .resize(512, 512, { fit: "contain", position: "center", background: { r: 255, g: 255, b: 255, alpha: 1 }, }) .flatten({ background: "#ffffff" }) .png() .toFile(markSrc) const iconH = 240 const textH = 160 const iconBuf = await sharp(topStrip) .resize({ height: iconH, fit: "inside" }) .flatten({ background: "#ffffff" }) .toBuffer() const textBuf = await sharp(botStrip) .resize({ height: textH, fit: "inside" }) .flatten({ background: "#ffffff" }) .toBuffer() const iw = (await sharp(iconBuf).metadata()).width ?? 0 const ih = (await sharp(iconBuf).metadata()).height ?? 0 const tw = (await sharp(textBuf).metadata()).width ?? 0 const th = (await sharp(textBuf).metadata()).height ?? 0 const padX = 48 const gap = 56 const canvasW = iw + tw + padX * 2 + gap const canvasH = Math.max(ih, th) + 32 const horizRaster = join(tmp, "horizontal-layout.png") await sharp({ create: { width: canvasW, height: canvasH, channels: 3, background: { r: 255, g: 255, b: 255 }, }, }) .composite([ { input: iconBuf, left: padX, top: Math.round((canvasH - ih) / 2) }, { input: textBuf, left: padX + iw + gap, top: Math.round((canvasH - th) / 2) }, ]) .png() .toFile(horizRaster) const stackedSvg = join(brandDir, "ultimail-wordmark-stacked.svg") const horizontalSvg = join(brandDir, "ultimail-wordmark-horizontal.svg") const markSvgTmp = join(tmp, "mark.svg") console.log("VTracer: full stacked logo…") vtracer(vbin, canonical, stackedSvg, VT_ARGS_BALANCED) console.log("VTracer: horizontal composite…") vtracer(vbin, horizRaster, horizontalSvg, VT_ARGS_BALANCED) console.log("VTracer: icon mark (square)…") vtracer(vbin, markSrc, markSvgTmp, VT_ARGS_MARK) const publicMark = join(root, "public/ultimail-mark.svg") copyFileSync(markSvgTmp, publicMark) console.log("Header icon + favicons (raster transparent)…") execFileSync(process.execPath, [join(__dirname, "emit-ultimail-header-icon.mjs")], { cwd: root, stdio: "inherit", }) console.log("Done:", { stackedSvg, horizontalSvg, publicMark }) } finally { rmSync(tmp, { recursive: true, force: true }) } } main().catch((e) => { console.error(e) process.exit(1) })