ultisuite-client/scripts/rasterize-ultimail-brand.mjs
R3D347HR4Y 5304790ed5
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(auth): enhance session management and identity provider settings
- Added SessionGuard component to manage session expiration and online status.
- Updated AuthProvider to streamline session fetching and handling.
- Introduced IdentityProvidersSection for managing OAuth, SAML, and LDAP identity providers.
- Implemented identity provider guides for easier configuration.
- Enhanced mail settings with infinite scroll option for improved user experience.
- Updated global styles and layout components for better consistency across the application.
2026-06-09 09:36:46 +02:00

162 lines
5.1 KiB
JavaScript

/**
* Exporte les déclinaisons brand (PNG + JPG) depuis les masters raster :
* - public/brand/ultimail-original.png (stacked + bandeau texte)
* - public/brand/ultimail-header-icon.png (mark / horizontal)
*
* Ne vectorise pas : les SVG VTracer dégradent couleurs et cadrage.
*
* pnpm run brand:raster
*/
import { existsSync } from "node:fs"
import { dirname, join } 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")
function findOriginalPng() {
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).")
}
function headerIconPath() {
const p = join(brandDir, "ultimail-header-icon.png")
if (!existsSync(p)) {
throw new Error("Missing public/brand/ultimail-header-icon.png — run: pnpm run brand:header-icon")
}
return p
}
/** Wordmark empilé fond transparent : picto couleurs d'origine, texte éclairci (sans invert/hue). */
async function buildStackedDark(originalPath) {
const { data, info } = await sharp(originalPath)
.resize({
width: 800,
height: 800,
fit: "contain",
background: { r: 255, g: 255, b: 255, alpha: 1 },
})
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })
for (let i = 0; i < data.length; i += 4) {
const r = data[i]
const g = data[i + 1]
const b = data[i + 2]
if (r >= 235 && g >= 235 && b >= 235) {
data[i + 3] = 0
continue
}
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const chroma = max - min
// Texte / traits neutres sombres → gris clair lisible sur fond dark.
if (chroma < 40 && max < 200) {
data[i] = 245
data[i + 1] = 245
data[i + 2] = 247
}
data[i + 3] = 255
}
const out = join(brandDir, "ultimail-wordmark-stacked-dark.png")
await sharp(Buffer.from(data), {
raw: { width: info.width, height: info.height, channels: 4 },
})
.png({ compressionLevel: 9 })
.toFile(out)
console.log("Wrote ultimail-wordmark-stacked-dark.png")
}
async function writePngJpg(input, outBase, w, h, bg = "#ffffff") {
const resize = {
width: w,
height: h,
fit: "contain",
background: { r: 255, g: 255, b: 255, alpha: 1 },
}
const pngOut = join(brandDir, `${outBase}.png`)
const jpgOut = join(brandDir, `${outBase}.jpg`)
await sharp(input).resize(resize).png({ compressionLevel: 9 }).toFile(pngOut)
await sharp(input).resize(resize).flatten({ background: bg }).jpeg({ quality: 92, mozjpeg: true }).toFile(jpgOut)
console.log(`Wrote ${outBase}.png / .jpg`)
}
/** Bande basse du master ≈ wordmark « Ultimail ». */
async function extractWordmarkTextStrip(originalPath) {
const meta = await sharp(originalPath).metadata()
const W = meta.width ?? 0
const H = meta.height ?? 0
if (!W || !H) throw new Error("Could not read original dimensions")
const splitY = Math.min(H - 32, Math.max(80, Math.round(H * 0.62)))
const strip = await sharp(originalPath)
.extract({ left: 0, top: splitY, width: W, height: H - splitY })
.flatten({ background: "#ffffff" })
.png()
.toBuffer()
// Deux appels sharp séparés : évite un bug sharp (trim en chaîne après autres exports).
return sharp(strip).trim({ threshold: 12 }).png().toBuffer()
}
async function buildHorizontalWordmark(originalPath, headerIcon) {
const iconH = 240
const iconBuf = await sharp(headerIcon)
.resize({ height: iconH, fit: "inside" })
.png()
.toBuffer()
const textBuf = await extractWordmarkTextStrip(originalPath)
const textResized = await sharp(textBuf)
.resize({ height: Math.round(iconH * 0.72), fit: "inside" })
.png()
.toBuffer()
const iw = (await sharp(iconBuf).metadata()).width ?? 0
const ih = (await sharp(iconBuf).metadata()).height ?? 0
const tw = (await sharp(textResized).metadata()).width ?? 0
const th = (await sharp(textResized).metadata()).height ?? 0
const padX = 48
const gap = 56
const canvasW = iw + tw + padX * 2 + gap
const canvasH = Math.max(ih, th) + 32
return 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: textResized, left: padX + iw + gap, top: Math.round((canvasH - th) / 2) },
])
.png()
.toBuffer()
}
async function main() {
const original = findOriginalPng()
const headerIcon = headerIconPath()
const horizontalBuf = await buildHorizontalWordmark(original, headerIcon)
await writePngJpg(headerIcon, "ultimail-mark", 256, 256)
await writePngJpg(original, "ultimail-wordmark-stacked", 800, 800)
await buildStackedDark(original)
await writePngJpg(horizontalBuf, "ultimail-wordmark-horizontal", 1600, 460)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})