139 lines
4.4 KiB
JavaScript
139 lines
4.4 KiB
JavaScript
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import { spawn } from 'node:child_process';
|
|
import { once } from 'node:events';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
|
|
const root = resolve(import.meta.dirname, '..');
|
|
const outputPath = resolve(root, 'media/hdr-wordmark.png');
|
|
const width = 2200;
|
|
const height = 500;
|
|
|
|
function run(command, args, options = {}) {
|
|
const child = spawn(command, args, options);
|
|
return once(child, 'close').then(([code]) => {
|
|
if (code !== 0) throw new Error(`${command} exited with code ${code}`);
|
|
});
|
|
}
|
|
|
|
function capture(command, args) {
|
|
return new Promise((resolveBuffer, reject) => {
|
|
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'inherit'] });
|
|
const chunks = [];
|
|
child.stdout.on('data', (chunk) => chunks.push(chunk));
|
|
child.on('error', reject);
|
|
child.on('close', (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(`${command} exited with code ${code}`));
|
|
return;
|
|
}
|
|
resolveBuffer(Buffer.concat(chunks));
|
|
});
|
|
});
|
|
}
|
|
|
|
const tempDir = await mkdtemp(join(tmpdir(), 'hdr-wordmark-'));
|
|
const svgPath = join(tempDir, 'wordmark.svg');
|
|
const maskPath = join(tempDir, 'wordmark-mask.png');
|
|
|
|
const svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
<defs>
|
|
<filter id="blur-strong" x="-20%" y="-70%" width="140%" height="240%">
|
|
<feGaussianBlur stdDeviation="38"/>
|
|
</filter>
|
|
<filter id="blur-soft" x="-20%" y="-70%" width="140%" height="240%">
|
|
<feGaussianBlur stdDeviation="16"/>
|
|
</filter>
|
|
</defs>
|
|
<g font-family="Arial Black, Helvetica, Arial, sans-serif" font-size="220" font-weight="900" letter-spacing="-9" text-anchor="middle">
|
|
<text x="1100" y="326" fill="#00faff" stroke="#00faff" stroke-width="48" opacity="0.96" filter="url(#blur-strong)">HDR Web Plugin</text>
|
|
<text x="1114" y="336" fill="#ff31ef" stroke="#ff31ef" stroke-width="30" opacity="0.74" filter="url(#blur-strong)">HDR Web Plugin</text>
|
|
<text x="1086" y="316" fill="#2f76ff" stroke="#2f76ff" stroke-width="22" opacity="0.62" filter="url(#blur-soft)">HDR Web Plugin</text>
|
|
<text x="1100" y="326" fill="#ffffff" stroke="#ffffff" stroke-width="18" opacity="0.72" filter="url(#blur-soft)">HDR Web Plugin</text>
|
|
<text x="1094" y="322" fill="#eaffff" stroke="#00faff" stroke-width="12" opacity="1" filter="url(#blur-soft)">HDR Web Plugin</text>
|
|
<text x="1100" y="326" fill="#ffffff" stroke="#ffffff" stroke-width="7">HDR Web Plugin</text>
|
|
</g>
|
|
</svg>`;
|
|
|
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
await writeFile(svgPath, svg, 'utf8');
|
|
|
|
await run('/usr/bin/sips', [
|
|
'-s',
|
|
'format',
|
|
'png',
|
|
svgPath,
|
|
'--out',
|
|
maskPath
|
|
], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
|
|
const rgba = await capture('ffmpeg', [
|
|
'-hide_banner',
|
|
'-loglevel',
|
|
'error',
|
|
'-i',
|
|
maskPath,
|
|
'-f',
|
|
'rawvideo',
|
|
'-pix_fmt',
|
|
'rgba',
|
|
'pipe:1'
|
|
]);
|
|
|
|
const frame = Buffer.alloc(width * height * 8);
|
|
for (let index = 0; index < width * height; index += 1) {
|
|
const src = index * 4;
|
|
const dst = index * 8;
|
|
const alpha = rgba[src + 3] / 255;
|
|
const red = alpha === 0 ? 0 : rgba[src] / 255;
|
|
const green = alpha === 0 ? 0 : rgba[src + 1] / 255;
|
|
const blue = alpha === 0 ? 0 : rgba[src + 2] / 255;
|
|
const channels = [red, green, blue];
|
|
|
|
for (let channel = 0; channel < 3; channel += 1) {
|
|
const lifted = Math.pow(channels[channel], 0.42);
|
|
const value = Math.min(65535, Math.round(lifted * 65535));
|
|
frame.writeUInt16LE(value, dst + channel * 2);
|
|
}
|
|
|
|
const alphaValue = Math.min(65535, Math.round(Math.pow(alpha, 0.42) * 65535));
|
|
frame.writeUInt16LE(alphaValue, dst + 6);
|
|
}
|
|
|
|
const encode = spawn('ffmpeg', [
|
|
'-y',
|
|
'-f',
|
|
'rawvideo',
|
|
'-pix_fmt',
|
|
'rgba64le',
|
|
'-s:v',
|
|
`${width}x${height}`,
|
|
'-i',
|
|
'pipe:0',
|
|
'-vf',
|
|
'setparams=range=full:color_primaries=bt2020:color_trc=smpte2084:colorspace=gbr',
|
|
'-frames:v',
|
|
'1',
|
|
'-update',
|
|
'1',
|
|
'-c:v',
|
|
'png',
|
|
'-pix_fmt',
|
|
'rgba64be',
|
|
'-pred',
|
|
'mixed',
|
|
'-color_primaries',
|
|
'bt2020',
|
|
'-color_trc',
|
|
'smpte2084',
|
|
outputPath
|
|
], { stdio: ['pipe', 'inherit', 'inherit'] });
|
|
|
|
encode.stdin.end(frame);
|
|
const [encodeCode] = await once(encode, 'close');
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
if (encodeCode !== 0) throw new Error(`ffmpeg png exited with code ${encodeCode}`);
|
|
|
|
console.log(`Wrote ${outputPath}`);
|