Replace entire repo content with correct code from /root/shahikitchen-website/
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* Polish every image in ~/Desktop/ftp-images-new for eye-catching menu quality.
|
||||
* - Sweets: premium white-bg plate compositing (rebuilt from source)
|
||||
* - Dishes: vibrance, contrast, and sharpness boost in-place
|
||||
*/
|
||||
|
||||
import { removeBackground } from '@imgly/background-removal-node';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const OUT_ROOT = path.join(process.env.HOME ?? '', 'Desktop', 'ftp-images-new');
|
||||
const SRC_ROOT = path.join(__dirname, '..', 'ftp-images');
|
||||
const SWEETS_MARKER = `${path.sep}sweets${path.sep}`;
|
||||
|
||||
const CANVAS_W = 1600;
|
||||
const CANVAS_H = 1200;
|
||||
const PLATE_CX = CANVAS_W / 2;
|
||||
const PLATE_CY = CANVAS_H / 2 + 20;
|
||||
const PLATE_R = 440;
|
||||
|
||||
const VIGNETTE_FALLBACK = new Set([
|
||||
'bilder-bp/sweets/namak-paray/bild/IMG_0209.jpg',
|
||||
]);
|
||||
|
||||
const CENTER_CROP_FALLBACK = new Set([
|
||||
'bilder-bp/sweets/patisa/bild/IMG_1400.jpg',
|
||||
]);
|
||||
|
||||
function plateSvg(seed: number): string {
|
||||
const specks: string[] = [];
|
||||
let s = seed + 11;
|
||||
const rand = () => {
|
||||
s = (s * 16807) % 2147483647;
|
||||
return s / 2147483647;
|
||||
};
|
||||
|
||||
const colors = ['#4A7EBB', '#E8843C', '#C4A882', '#6B9E78', '#D4A574', '#9B7CB8'];
|
||||
for (let i = 0; i < 88; i++) {
|
||||
const angle = rand() * Math.PI * 2;
|
||||
const dist = rand() * (PLATE_R - 55);
|
||||
const x = PLATE_CX + Math.cos(angle) * dist;
|
||||
const y = PLATE_CY + Math.sin(angle) * dist;
|
||||
const r = 2.5 + rand() * 10;
|
||||
specks.push(
|
||||
`<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${r.toFixed(1)}" fill="${colors[i % colors.length]}" opacity="0.62"/>`
|
||||
);
|
||||
}
|
||||
|
||||
return `<svg width="${CANVAS_W}" height="${CANVAS_H}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<radialGradient id="plateGrad" cx="50%" cy="40%" r="60%">
|
||||
<stop offset="0%" stop-color="#FFFDF8"/>
|
||||
<stop offset="100%" stop-color="#F0E8DA"/>
|
||||
</radialGradient>
|
||||
<clipPath id="plateClip">
|
||||
<circle cx="${PLATE_CX}" cy="${PLATE_CY}" r="${PLATE_R}"/>
|
||||
</clipPath>
|
||||
<filter id="plateShadow" x="-40%" y="-40%" width="180%" height="180%">
|
||||
<feDropShadow dx="0" dy="14" stdDeviation="28" flood-color="#C8C0B4" flood-opacity="0.28"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#FFFFFF"/>
|
||||
<circle cx="${PLATE_CX}" cy="${PLATE_CY}" r="${PLATE_R + 16}" fill="#FFFFFF" filter="url(#plateShadow)"/>
|
||||
<circle cx="${PLATE_CX}" cy="${PLATE_CY}" r="${PLATE_R + 11}" fill="none" stroke="#C9A227" stroke-width="9"/>
|
||||
<circle cx="${PLATE_CX}" cy="${PLATE_CY}" r="${PLATE_R}" fill="url(#plateGrad)"/>
|
||||
<g clip-path="url(#plateClip)">${specks.join('')}</g>
|
||||
<circle cx="${PLATE_CX}" cy="${PLATE_CY}" r="${PLATE_R - 22}" fill="none" stroke="#E8DCC8" stroke-width="1.5" opacity="0.85"/>
|
||||
<circle cx="${PLATE_CX}" cy="${PLATE_CY}" r="${PLATE_R}" fill="none" stroke="#B8922A" stroke-width="4.5"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function collectImages(dir: string): string[] {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return collectImages(full);
|
||||
if (/\.(jpe?g|png|webp)$/i.test(entry.name)) return [full];
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function findSource(destPath: string): string | null {
|
||||
const rel = path.relative(OUT_ROOT, destPath);
|
||||
const dir = path.dirname(path.join(SRC_ROOT, rel));
|
||||
const base = path.basename(destPath, path.extname(destPath));
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
if (name.startsWith(base) && /\.(jpe?g|png|webp)$/i.test(name)) {
|
||||
return path.join(dir, name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function enhanceFromSource(src: string): Promise<Buffer> {
|
||||
const rotated = sharp(src, { failOn: 'none' }).rotate();
|
||||
const { data, info } = await rotated.toColorspace('srgb').removeAlpha().toBuffer({ resolveWithObject: true });
|
||||
|
||||
const w = info.width;
|
||||
const h = info.height;
|
||||
const aspect = 4 / 3;
|
||||
const sourceAspect = w / h;
|
||||
|
||||
let cropW = w;
|
||||
let cropH = h;
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
if (sourceAspect > aspect) {
|
||||
cropW = Math.round(h * aspect);
|
||||
left = Math.round((w - cropW) / 2);
|
||||
} else if (sourceAspect < aspect) {
|
||||
cropH = Math.round(w / aspect);
|
||||
top = Math.round((h - cropH) / 2);
|
||||
}
|
||||
|
||||
cropW = Math.min(cropW, w - left);
|
||||
cropH = Math.min(cropH, h - top);
|
||||
const minDim = Math.min(cropW, cropH);
|
||||
|
||||
let pipe = sharp(data)
|
||||
.extract({ left, top, width: cropW, height: cropH })
|
||||
.normalize()
|
||||
.modulate({ brightness: 1.05, saturation: 1.22 })
|
||||
.gamma(1.04);
|
||||
|
||||
pipe =
|
||||
minDim < 900
|
||||
? pipe.sharpen({ sigma: 1.1, m1: 0.7, m2: 0.35 })
|
||||
: pipe.sharpen({ sigma: 0.85, m1: 0.55, m2: 0.28 });
|
||||
|
||||
return pipe
|
||||
.resize(CANVAS_W, CANVAS_H, { fit: 'fill', kernel: sharp.kernel.lanczos3 })
|
||||
.jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function cutout(imagePath: string, model: 'small' | 'medium'): Promise<Buffer> {
|
||||
const blob = await removeBackground(imagePath, { model });
|
||||
return Buffer.from(await blob.arrayBuffer());
|
||||
}
|
||||
|
||||
async function cutoutValid(cutout: Buffer): Promise<boolean> {
|
||||
const { data, info } = await sharp(cutout).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||
const w = info.width ?? 1;
|
||||
const h = info.height ?? 1;
|
||||
const x0 = Math.floor(w * 0.2);
|
||||
const y0 = Math.floor(h * 0.2);
|
||||
const x1 = Math.floor(w * 0.8);
|
||||
const y1 = Math.floor(h * 0.8);
|
||||
|
||||
let opaque = 0;
|
||||
let total = 0;
|
||||
for (let y = y0; y < y1; y++) {
|
||||
for (let x = x0; x < x1; x++) {
|
||||
if (data[(y * w + x) * 4 + 3] > 45) opaque++;
|
||||
total++;
|
||||
}
|
||||
}
|
||||
return opaque / total > 0.07;
|
||||
}
|
||||
|
||||
async function beautifyFood(cutout: Buffer, maxW: number, maxH: number): Promise<Buffer> {
|
||||
const meta = await sharp(cutout).metadata();
|
||||
const cw = meta.width ?? 1;
|
||||
const ch = meta.height ?? 1;
|
||||
const scale = Math.min(maxW / cw, maxH / ch, 1);
|
||||
return sharp(cutout)
|
||||
.resize(Math.round(cw * scale), Math.round(ch * scale), { kernel: sharp.kernel.lanczos3 })
|
||||
.modulate({ brightness: 1.04, saturation: 1.18 })
|
||||
.sharpen({ sigma: 0.7, m1: 0.5, m2: 0.25 })
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function stylizeWithCenterCrop(photo: Buffer, index: number): Promise<Buffer> {
|
||||
const meta = await sharp(photo).metadata();
|
||||
const w = meta.width ?? CANVAS_W;
|
||||
const h = meta.height ?? CANVAS_H;
|
||||
const cropW = Math.round(w * 0.58);
|
||||
const cropH = Math.round(h * 0.58);
|
||||
const left = Math.round((w - cropW) / 2);
|
||||
const top = Math.round((h - cropH) / 2);
|
||||
|
||||
const cropped = await sharp(photo)
|
||||
.extract({ left, top, width: cropW, height: cropH })
|
||||
.modulate({ brightness: 1.04, saturation: 1.2 })
|
||||
.sharpen({ sigma: 0.9 })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const maskSvg = `<svg width="${cropW}" height="${cropH}">
|
||||
<defs>
|
||||
<radialGradient id="m" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="white"/>
|
||||
<stop offset="78%" stop-color="white" stop-opacity="0.92"/>
|
||||
<stop offset="100%" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#m)"/>
|
||||
</svg>`;
|
||||
|
||||
const masked = await sharp(cropped)
|
||||
.composite([{ input: await sharp(Buffer.from(maskSvg)).blur(12).png().toBuffer(), blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const fm = await sharp(masked).metadata();
|
||||
const fw = fm.width ?? cropW;
|
||||
const fh = fm.height ?? cropH;
|
||||
const scale = Math.min((PLATE_R * 1.25) / fw, (PLATE_R * 1.05) / fh);
|
||||
const food = await sharp(masked)
|
||||
.resize(Math.round(fw * scale), Math.round(fh * scale), { kernel: sharp.kernel.lanczos3 })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const f2 = await sharp(food).metadata();
|
||||
const posL = Math.round(PLATE_CX - (f2.width ?? 0) / 2);
|
||||
const posT = Math.round(PLATE_CY - (f2.height ?? 0) / 2 + 6);
|
||||
|
||||
const shadow = await sharp(
|
||||
Buffer.from(`<svg width="${CANVAS_W}" height="${CANVAS_H}">
|
||||
<ellipse cx="${PLATE_CX}" cy="${PLATE_CY + (f2.height ?? 0) * 0.16}" rx="${(f2.width ?? 0) * 0.36}" ry="${(f2.height ?? 0) * 0.08}" fill="#8A8278" opacity="0.18"/>
|
||||
</svg>`)
|
||||
)
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
return sharp(Buffer.from(plateSvg(index)))
|
||||
.composite([
|
||||
{ input: shadow, top: 0, left: 0 },
|
||||
{ input: food, top: posT, left: posL },
|
||||
])
|
||||
.jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function stylizeWithVignette(photo: Buffer, index: number): Promise<Buffer> {
|
||||
const plateBuf = await sharp(Buffer.from(plateSvg(index))).png().toBuffer();
|
||||
|
||||
const maskSvg = `<svg width="${CANVAS_W}" height="${CANVAS_H}">
|
||||
<defs>
|
||||
<radialGradient id="f" cx="50%" cy="50%" r="42%">
|
||||
<stop offset="0%" stop-color="white"/>
|
||||
<stop offset="68%" stop-color="white" stop-opacity="0.95"/>
|
||||
<stop offset="100%" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#f)"/>
|
||||
</svg>`;
|
||||
|
||||
const masked = await sharp(photo)
|
||||
.resize(CANVAS_W, CANVAS_H, { fit: 'cover', position: 'centre' })
|
||||
.composite([{ input: await sharp(Buffer.from(maskSvg)).blur(18).png().toBuffer(), blend: 'dest-in' }])
|
||||
.modulate({ brightness: 1.03, saturation: 1.15 })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const fm = await sharp(masked).metadata();
|
||||
const fw = fm.width ?? CANVAS_W;
|
||||
const fh = fm.height ?? CANVAS_H;
|
||||
const scale = Math.min((PLATE_R * 1.35) / fw, (PLATE_R * 1.15) / fh, 0.88);
|
||||
const food = await sharp(masked)
|
||||
.resize(Math.round(fw * scale), Math.round(fh * scale), { kernel: sharp.kernel.lanczos3 })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const f2 = await sharp(food).metadata();
|
||||
const left = Math.round(PLATE_CX - (f2.width ?? 0) / 2);
|
||||
const top = Math.round(PLATE_CY - (f2.height ?? 0) / 2 + 8);
|
||||
|
||||
const shadow = await sharp(
|
||||
Buffer.from(`<svg width="${CANVAS_W}" height="${CANVAS_H}">
|
||||
<ellipse cx="${PLATE_CX}" cy="${PLATE_CY + 58}" rx="${PLATE_R * 0.5}" ry="${PLATE_R * 0.1}" fill="#A09890" opacity="0.18"/>
|
||||
<ellipse cx="${PLATE_CX}" cy="${PLATE_CY + (f2.height ?? 0) * 0.15}" rx="${(f2.width ?? 0) * 0.38}" ry="${(f2.height ?? 0) * 0.08}" fill="#8A8278" opacity="0.16"/>
|
||||
</svg>`)
|
||||
)
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
return sharp(Buffer.from(plateSvg(index)))
|
||||
.composite([
|
||||
{ input: shadow, top: 0, left: 0 },
|
||||
{ input: food, top, left },
|
||||
])
|
||||
.jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function stylizeSweet(destPath: string, rel: string, index: number): Promise<void> {
|
||||
const src = findSource(destPath);
|
||||
if (!src) throw new Error('missing source');
|
||||
|
||||
const enhanced = await enhanceFromSource(src);
|
||||
|
||||
if (CENTER_CROP_FALLBACK.has(rel)) {
|
||||
const out = await stylizeWithCenterCrop(enhanced, index);
|
||||
fs.writeFileSync(destPath, out);
|
||||
return;
|
||||
}
|
||||
|
||||
const useVignette = VIGNETTE_FALLBACK.has(rel);
|
||||
|
||||
if (useVignette) {
|
||||
const out = await stylizeWithVignette(enhanced, index);
|
||||
fs.writeFileSync(destPath, out);
|
||||
return;
|
||||
}
|
||||
|
||||
const tmp = path.join(OUT_ROOT, `.tmp-cut-${index}.jpg`);
|
||||
fs.writeFileSync(tmp, enhanced);
|
||||
|
||||
let cutoutBuf: Buffer | null = null;
|
||||
try {
|
||||
for (const model of ['medium', 'small'] as const) {
|
||||
const attempt = await cutout(tmp, model);
|
||||
if (await cutoutValid(attempt)) {
|
||||
cutoutBuf = attempt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
||||
}
|
||||
|
||||
if (!cutoutBuf) {
|
||||
const out = await stylizeWithVignette(enhanced, index);
|
||||
fs.writeFileSync(destPath, out);
|
||||
return;
|
||||
}
|
||||
|
||||
const plateBuf = await sharp(Buffer.from(plateSvg(index))).png().toBuffer();
|
||||
const food = await beautifyFood(cutoutBuf, PLATE_R * 1.42, PLATE_R * 1.18);
|
||||
const fm = await sharp(food).metadata();
|
||||
const fw = fm.width ?? 1;
|
||||
const fh = fm.height ?? 1;
|
||||
const left = Math.round(PLATE_CX - fw / 2);
|
||||
const top = Math.round(PLATE_CY - fh / 2 + 6);
|
||||
|
||||
const shadow = await sharp(
|
||||
Buffer.from(`<svg width="${CANVAS_W}" height="${CANVAS_H}">
|
||||
<ellipse cx="${PLATE_CX}" cy="${PLATE_CY + fh * 0.18}" rx="${fw * 0.4}" ry="${fh * 0.09}" fill="#8A8278" opacity="0.2"/>
|
||||
</svg>`)
|
||||
)
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const out = await sharp(plateBuf)
|
||||
.composite([
|
||||
{ input: shadow, top: 0, left: 0 },
|
||||
{ input: food, top, left },
|
||||
])
|
||||
.sharpen({ sigma: 0.35, m1: 0.3, m2: 0.15 })
|
||||
.jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||
.toBuffer();
|
||||
|
||||
fs.writeFileSync(destPath, out);
|
||||
}
|
||||
|
||||
async function polishDish(imagePath: string): Promise<void> {
|
||||
const tmp = `${imagePath}.polish.jpg`;
|
||||
await sharp(imagePath, { failOn: 'none' })
|
||||
.rotate()
|
||||
.toColorspace('srgb')
|
||||
.normalize()
|
||||
.modulate({ brightness: 1.04, saturation: 1.16 })
|
||||
.gamma(1.03)
|
||||
.sharpen({ sigma: 0.75, m1: 0.5, m2: 0.25 })
|
||||
.jpeg({ quality: 94, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||
.toFile(tmp);
|
||||
|
||||
fs.renameSync(tmp, imagePath);
|
||||
}
|
||||
|
||||
async function polishPoster(imagePath: string): Promise<void> {
|
||||
const tmp = `${imagePath}.polish.jpg`;
|
||||
await sharp(imagePath, { failOn: 'none' })
|
||||
.rotate()
|
||||
.toColorspace('srgb')
|
||||
.normalize()
|
||||
.modulate({ brightness: 1.02, saturation: 1.1 })
|
||||
.sharpen({ sigma: 0.5 })
|
||||
.jpeg({ quality: 95, mozjpeg: true })
|
||||
.toFile(tmp);
|
||||
fs.renameSync(tmp, imagePath);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const onlyArg = process.argv.find((a) => a.startsWith('--only='));
|
||||
const onlyFilter = onlyArg?.slice('--only='.length);
|
||||
|
||||
let images = collectImages(OUT_ROOT).sort();
|
||||
if (onlyFilter) {
|
||||
images = images.filter((p) => path.relative(OUT_ROOT, p).includes(onlyFilter));
|
||||
}
|
||||
let sweets = 0;
|
||||
let dishes = 0;
|
||||
let posters = 0;
|
||||
|
||||
console.log(`Polishing ${images.length} images in ${OUT_ROOT}\n`);
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
const rel = path.relative(OUT_ROOT, img);
|
||||
try {
|
||||
if (rel.includes(SWEETS_MARKER)) {
|
||||
await stylizeSweet(img, rel, i);
|
||||
sweets++;
|
||||
console.log(` ✓ sweet ${rel}`);
|
||||
} else if (rel.includes('/others/')) {
|
||||
await polishPoster(img);
|
||||
posters++;
|
||||
console.log(` ✓ poster ${rel}`);
|
||||
} else {
|
||||
await polishDish(img);
|
||||
dishes++;
|
||||
console.log(` ✓ dish ${rel}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` ✗ ${rel}: ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nDone: ${sweets} sweets restyled, ${dishes} dishes polished, ${posters} posters`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user