185 lines
5.6 KiB
JavaScript
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)
|
|
})
|