libavif-with-gainmap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const { normalizeConvertOptions } = require('./options');
8
+ const { AvifGainMapError, runFile } = require('./process');
9
+ const {
10
+ SUPPORTED_PLATFORM_KEYS,
11
+ TOOL_GAINMAP_RESIZE,
12
+ TOOL_GAINMAP_UTIL,
13
+ assertToolAvailable,
14
+ getPlatformKey,
15
+ resolveTool
16
+ } = require('./platform');
17
+
18
+ function asPath(name, value) {
19
+ if (typeof value !== 'string' || value.length === 0) {
20
+ throw new TypeError(`${name} must be a non-empty file path.`);
21
+ }
22
+ return value;
23
+ }
24
+
25
+ function pushOption(args, flag, value) {
26
+ if (value !== undefined && value !== null) {
27
+ args.push(flag, String(value));
28
+ }
29
+ }
30
+
31
+ function buildConvertArgs(input, output, options, forResize) {
32
+ const args = ['convert'];
33
+ pushOption(args, '--qcolor', forResize ? options.intermediateQuality : options.quality);
34
+ pushOption(
35
+ args,
36
+ '--qgain-map',
37
+ forResize ? options.intermediateGainMapQuality : options.gainMapQuality
38
+ );
39
+ pushOption(args, '--speed', options.speed);
40
+ pushOption(args, '--jobs', options.jobs);
41
+ pushOption(args, '--depth', options.depth);
42
+ pushOption(args, '--yuv', options.yuv);
43
+ pushOption(args, '--cicp', options.cicp);
44
+ pushOption(args, '--clli', options.clli);
45
+ if (options.swapBase) {
46
+ args.push('--swap-base');
47
+ }
48
+ args.push(input, output);
49
+ return args;
50
+ }
51
+
52
+ function buildResizeArgs(input, output, options) {
53
+ const args = [input, output];
54
+ pushOption(args, '--qcolor', options.quality);
55
+ pushOption(args, '--qgain-map', options.gainMapQuality);
56
+ pushOption(args, '--speed', options.speed);
57
+ pushOption(args, '--jobs', options.jobs);
58
+
59
+ const size = options.size;
60
+ if (size) {
61
+ pushOption(args, '--width', size.width);
62
+ pushOption(args, '--height', size.height);
63
+ pushOption(args, '--max-width', size.maxWidth);
64
+ pushOption(args, '--max-height', size.maxHeight);
65
+ }
66
+ return args;
67
+ }
68
+
69
+ async function convertJpegGainMap(input, output, rawOptions = {}) {
70
+ input = asPath('input', input);
71
+ output = asPath('output', output);
72
+
73
+ const options = normalizeConvertOptions(rawOptions);
74
+ const utilPath = assertToolAvailable(
75
+ resolveTool(TOOL_GAINMAP_UTIL, options),
76
+ TOOL_GAINMAP_UTIL
77
+ );
78
+ const resizePath = options.size
79
+ ? assertToolAvailable(resolveTool(TOOL_GAINMAP_RESIZE, options), TOOL_GAINMAP_RESIZE)
80
+ : null;
81
+
82
+ let tempDir;
83
+ let intermediate = output;
84
+ try {
85
+ if (resizePath) {
86
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'avif-gainmap-'));
87
+ intermediate = path.join(tempDir, 'converted.avif');
88
+ }
89
+
90
+ // Resize is a second AVIF encode, so the intermediate conversion defaults to lossless.
91
+ const convertArgs = buildConvertArgs(input, intermediate, options, Boolean(resizePath));
92
+ const convert = await runFile(utilPath, convertArgs, options);
93
+
94
+ let resize = null;
95
+ if (resizePath) {
96
+ const resizeArgs = buildResizeArgs(intermediate, output, options);
97
+ resize = await runFile(resizePath, resizeArgs, options);
98
+ }
99
+
100
+ return {
101
+ convert,
102
+ input: path.resolve(input),
103
+ output: path.resolve(output),
104
+ resize,
105
+ resized: Boolean(resize),
106
+ tempDir: options.keepTemp ? tempDir : undefined
107
+ };
108
+ } finally {
109
+ if (tempDir && !options.keepTemp) {
110
+ await fs.rm(tempDir, { force: true, recursive: true });
111
+ }
112
+ }
113
+ }
114
+
115
+ async function runAvifGainMapUtil(args, rawOptions = {}) {
116
+ if (!Array.isArray(args)) {
117
+ throw new TypeError('args must be an array.');
118
+ }
119
+ const options = normalizeConvertOptions(rawOptions);
120
+ const utilPath = assertToolAvailable(
121
+ resolveTool(TOOL_GAINMAP_UTIL, options),
122
+ TOOL_GAINMAP_UTIL
123
+ );
124
+ return runFile(utilPath, args.map(String), options);
125
+ }
126
+
127
+ module.exports = {
128
+ AvifGainMapError,
129
+ SUPPORTED_PLATFORM_KEYS,
130
+ convert: convertJpegGainMap,
131
+ convertJpegGainMap,
132
+ getPlatformKey,
133
+ runAvifGainMapUtil
134
+ };
package/src/options.js ADDED
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const os = require('node:os');
4
+
5
+ const DEFAULT_QUALITY = 80;
6
+ const DEFAULT_GAIN_MAP_QUALITY = 60;
7
+ const DEFAULT_SPEED = 6;
8
+
9
+ function toInteger(name, value) {
10
+ if (typeof value === 'number' && Number.isInteger(value)) {
11
+ return value;
12
+ }
13
+ if (typeof value === 'string' && /^-?\d+$/.test(value.trim())) {
14
+ return Number(value);
15
+ }
16
+ throw new TypeError(`${name} must be an integer.`);
17
+ }
18
+
19
+ function integerInRange(name, value, min, max) {
20
+ const number = toInteger(name, value);
21
+ if (number < min || number > max) {
22
+ throw new RangeError(`${name} must be between ${min} and ${max}.`);
23
+ }
24
+ return number;
25
+ }
26
+
27
+ function optionalRange(name, value, min, max, fallback) {
28
+ if (value === undefined || value === null) {
29
+ return fallback;
30
+ }
31
+ return integerInRange(name, value, min, max);
32
+ }
33
+
34
+ function positiveInteger(name, value) {
35
+ const number = toInteger(name, value);
36
+ if (number < 1) {
37
+ throw new RangeError(`${name} must be greater than 0.`);
38
+ }
39
+ return number;
40
+ }
41
+
42
+ function optionalChoice(name, value, choices) {
43
+ if (value === undefined || value === null) {
44
+ return undefined;
45
+ }
46
+ const normalized = String(value);
47
+ if (!choices.includes(normalized)) {
48
+ throw new RangeError(`${name} must be one of: ${choices.join(', ')}.`);
49
+ }
50
+ return normalized;
51
+ }
52
+
53
+ function normalizeJobs(value) {
54
+ if (value === undefined || value === null) {
55
+ return undefined;
56
+ }
57
+ if (value === 'all') {
58
+ return typeof os.availableParallelism === 'function'
59
+ ? os.availableParallelism()
60
+ : Math.max(1, os.cpus().length);
61
+ }
62
+ return positiveInteger('jobs', value);
63
+ }
64
+
65
+ function normalizeSize(options) {
66
+ const width = options.width === undefined ? undefined : positiveInteger('width', options.width);
67
+ const height = options.height === undefined ? undefined : positiveInteger('height', options.height);
68
+ const maxWidth =
69
+ options.maxWidth === undefined ? undefined : positiveInteger('maxWidth', options.maxWidth);
70
+ const maxHeight =
71
+ options.maxHeight === undefined ? undefined : positiveInteger('maxHeight', options.maxHeight);
72
+
73
+ if ((width || height) && (maxWidth || maxHeight)) {
74
+ throw new Error('Use width/height or maxWidth/maxHeight, not both.');
75
+ }
76
+ if (!width && !height && !maxWidth && !maxHeight) {
77
+ return null;
78
+ }
79
+
80
+ return { width, height, maxWidth, maxHeight };
81
+ }
82
+
83
+ function normalizeConvertOptions(options = {}) {
84
+ const quality = optionalRange('quality', options.quality ?? options.qcolor, 0, 100, DEFAULT_QUALITY);
85
+ const gainMapQuality = optionalRange(
86
+ 'gainMapQuality',
87
+ options.gainMapQuality ?? options.qgainMap,
88
+ 0,
89
+ 100,
90
+ DEFAULT_GAIN_MAP_QUALITY
91
+ );
92
+
93
+ return {
94
+ binDir: options.binDir,
95
+ cicp: options.cicp,
96
+ clli: options.clli,
97
+ cwd: options.cwd,
98
+ depth:
99
+ options.depth === undefined
100
+ ? undefined
101
+ : Number(optionalChoice('depth', options.depth, ['8', '10', '12'])),
102
+ env: options.env,
103
+ gainMapQuality,
104
+ intermediateGainMapQuality: optionalRange(
105
+ 'intermediateGainMapQuality',
106
+ options.intermediateGainMapQuality,
107
+ 0,
108
+ 100,
109
+ 100
110
+ ),
111
+ intermediateQuality: optionalRange(
112
+ 'intermediateQuality',
113
+ options.intermediateQuality,
114
+ 0,
115
+ 100,
116
+ 100
117
+ ),
118
+ jobs: normalizeJobs(options.jobs),
119
+ keepTemp: Boolean(options.keepTemp),
120
+ platformKey: options.platformKey,
121
+ quality,
122
+ signal: options.signal,
123
+ size: normalizeSize(options),
124
+ speed: optionalRange('speed', options.speed, 0, 10, DEFAULT_SPEED),
125
+ swapBase: Boolean(options.swapBase),
126
+ toolPaths: options.toolPaths,
127
+ verbose: Boolean(options.verbose),
128
+ yuv: optionalChoice('yuv', options.yuv, ['auto', '444', '422', '420', '400'])
129
+ };
130
+ }
131
+
132
+ module.exports = {
133
+ DEFAULT_GAIN_MAP_QUALITY,
134
+ DEFAULT_QUALITY,
135
+ DEFAULT_SPEED,
136
+ normalizeConvertOptions,
137
+ normalizeJobs,
138
+ normalizeSize
139
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const TOOL_GAINMAP_UTIL = 'avifgainmaputil';
7
+ const TOOL_GAINMAP_RESIZE = 'avifgainmapresize';
8
+ const TOOL_NAMES = Object.freeze([TOOL_GAINMAP_UTIL, TOOL_GAINMAP_RESIZE]);
9
+
10
+ const SUPPORTED_PLATFORM_KEYS = Object.freeze([
11
+ 'darwin-arm64',
12
+ 'darwin-x64',
13
+ 'linux-arm64',
14
+ 'linux-x64',
15
+ 'win32-x64'
16
+ ]);
17
+
18
+ function packageRoot() {
19
+ return path.resolve(__dirname, '..');
20
+ }
21
+
22
+ function getPlatformKey({ platform = process.platform, arch = process.arch } = {}) {
23
+ const key = `${platform}-${arch}`;
24
+ if (!SUPPORTED_PLATFORM_KEYS.includes(key)) {
25
+ throw new Error(
26
+ `Unsupported platform ${key}. Supported platforms: ${SUPPORTED_PLATFORM_KEYS.join(', ')}`
27
+ );
28
+ }
29
+ return key;
30
+ }
31
+
32
+ function executableName(tool, platformKey = getPlatformKey()) {
33
+ if (!TOOL_NAMES.includes(tool)) {
34
+ throw new Error(`Unknown native tool "${tool}".`);
35
+ }
36
+ return platformKey.startsWith('win32-') ? `${tool}.exe` : tool;
37
+ }
38
+
39
+ function vendorDir(platformKey = getPlatformKey()) {
40
+ return path.join(packageRoot(), 'vendor', platformKey);
41
+ }
42
+
43
+ function resolveTool(tool, options = {}) {
44
+ if (!TOOL_NAMES.includes(tool)) {
45
+ throw new Error(`Unknown native tool "${tool}".`);
46
+ }
47
+
48
+ const platformKey = options.platformKey || getPlatformKey();
49
+ const toolPaths = options.toolPaths || {};
50
+ if (toolPaths[tool]) {
51
+ return path.resolve(toolPaths[tool]);
52
+ }
53
+
54
+ const envName =
55
+ tool === TOOL_GAINMAP_UTIL ? 'AVIF_GAINMAPUTIL_PATH' : 'AVIF_GAINMAPRESIZE_PATH';
56
+ if (process.env[envName]) {
57
+ return path.resolve(process.env[envName]);
58
+ }
59
+
60
+ const binDir = options.binDir || process.env.AVIF_GAINMAP_BIN_DIR;
61
+ if (binDir) {
62
+ return path.resolve(binDir, executableName(tool, platformKey));
63
+ }
64
+
65
+ return path.join(vendorDir(platformKey), executableName(tool, platformKey));
66
+ }
67
+
68
+ function assertToolAvailable(toolPath, tool) {
69
+ if (!fs.existsSync(toolPath)) {
70
+ throw new Error(
71
+ `${tool} binary was not found at ${toolPath}. ` +
72
+ 'Run the GitHub Actions release workflow, npm run build:libavif, or set AVIF_GAINMAP_BIN_DIR.'
73
+ );
74
+ }
75
+ return toolPath;
76
+ }
77
+
78
+ module.exports = {
79
+ SUPPORTED_PLATFORM_KEYS,
80
+ TOOL_GAINMAP_RESIZE,
81
+ TOOL_GAINMAP_UTIL,
82
+ TOOL_NAMES,
83
+ assertToolAvailable,
84
+ executableName,
85
+ getPlatformKey,
86
+ packageRoot,
87
+ resolveTool,
88
+ vendorDir
89
+ };
package/src/process.js ADDED
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+
5
+ class AvifGainMapError extends Error {
6
+ constructor(message, detail = {}) {
7
+ super(message);
8
+ this.name = 'AvifGainMapError';
9
+ this.command = detail.command;
10
+ this.args = detail.args;
11
+ this.exitCode = detail.exitCode;
12
+ this.signal = detail.signal;
13
+ this.stdout = detail.stdout || '';
14
+ this.stderr = detail.stderr || '';
15
+ if (detail.cause) {
16
+ this.cause = detail.cause;
17
+ }
18
+ }
19
+ }
20
+
21
+ function runFile(command, args, options = {}) {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(command, args, {
24
+ cwd: options.cwd,
25
+ env: { ...process.env, ...options.env },
26
+ signal: options.signal,
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ windowsHide: true
29
+ });
30
+
31
+ let stdout = '';
32
+ let stderr = '';
33
+
34
+ child.stdout.on('data', (chunk) => {
35
+ stdout += chunk.toString();
36
+ if (options.verbose) {
37
+ process.stdout.write(chunk);
38
+ }
39
+ });
40
+ child.stderr.on('data', (chunk) => {
41
+ stderr += chunk.toString();
42
+ if (options.verbose) {
43
+ process.stderr.write(chunk);
44
+ }
45
+ });
46
+
47
+ child.on('error', (cause) => {
48
+ reject(
49
+ new AvifGainMapError(`Failed to start ${command}: ${cause.message}`, {
50
+ args,
51
+ cause,
52
+ command,
53
+ stderr,
54
+ stdout
55
+ })
56
+ );
57
+ });
58
+
59
+ child.on('close', (exitCode, signal) => {
60
+ const result = { args, command, exitCode, signal, stderr, stdout };
61
+ if (exitCode === 0) {
62
+ resolve(result);
63
+ return;
64
+ }
65
+ reject(
66
+ new AvifGainMapError(`${command} exited with code ${exitCode}.`, {
67
+ ...result
68
+ })
69
+ );
70
+ });
71
+ });
72
+ }
73
+
74
+ module.exports = {
75
+ AvifGainMapError,
76
+ runFile
77
+ };
Binary file