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/LICENSE +21 -0
- package/README.md +93 -0
- package/bin/avif-gainmap.js +141 -0
- package/docs/API.md +47 -0
- package/docs/BUILDING.md +66 -0
- package/native/avifgainmapresize.c +343 -0
- package/package.json +46 -0
- package/scripts/build-libavif.js +177 -0
- package/scripts/check-prebuilt.js +31 -0
- package/src/index.js +134 -0
- package/src/options.js +139 -0
- package/src/platform.js +89 -0
- package/src/process.js +77 -0
- package/vendor/darwin-arm64/avifgainmapresize +0 -0
- package/vendor/darwin-arm64/avifgainmaputil +0 -0
- package/vendor/darwin-x64/avifgainmapresize +0 -0
- package/vendor/darwin-x64/avifgainmaputil +0 -0
- package/vendor/linux-arm64/avifgainmapresize +0 -0
- package/vendor/linux-arm64/avifgainmaputil +0 -0
- package/vendor/linux-x64/avifgainmapresize +0 -0
- package/vendor/linux-x64/avifgainmaputil +0 -0
- package/vendor/win32-x64/avifgainmapresize.exe +0 -0
- package/vendor/win32-x64/avifgainmaputil.exe +0 -0
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
|
+
};
|
package/src/platform.js
ADDED
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|