ultisuite-client/scripts/vectorize-ultimail-brand.mjs
2026-05-15 17:40:17 +02:00

185 lines
5.6 KiB
JavaScript

/**
* Pipeline : PNG source → découpes (sharp) → SVG (VTracer CLI).
*
* Prérequis : `cargo install vtracer` ou binaire sur PATH / variable VTRACER_PATH.
* Source attendue : public/brand/ultimail-original.png (sinon public/brand/utlimail-original.png).
*/
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)
})