optimo 0.0.11 → 0.0.12
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/README.md +9 -2
- package/bin/help.js +2 -1
- package/bin/index.js +13 -1
- package/package.json +2 -2
- package/src/index.js +89 -159
- package/src/magick.js +128 -0
- package/src/util.js +52 -1
package/README.md
CHANGED
|
@@ -17,14 +17,16 @@ npx -y optimo public/media # for a directory
|
|
|
17
17
|
npx -y optimo public/media/banner.png # for a file
|
|
18
18
|
npx -y optimo public/media/banner.png -f jpeg # convert + optimize
|
|
19
19
|
npx -y optimo public/media/banner.png -r 50% # resize + optimize
|
|
20
|
+
npx -y optimo public/media/banner.png -r 100kB # resize to max file size
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
##
|
|
23
|
+
## Highlights
|
|
23
24
|
|
|
25
|
+
- Metadata stripping.
|
|
24
26
|
- Compression-first per format.
|
|
25
|
-
- Metadata stripping (`-strip`) where applicable.
|
|
26
27
|
- Format-specific tuning for stronger size reduction.
|
|
27
28
|
- Safety guard: if optimized output is not smaller, original file is kept.
|
|
29
|
+
- Resizing supports percentage values (`50%`) and max file size targets (`100kB`, `2MB`).
|
|
28
30
|
|
|
29
31
|
## Programmatic API
|
|
30
32
|
|
|
@@ -39,6 +41,11 @@ await optimo.file('/absolute/path/image.jpg', {
|
|
|
39
41
|
onLogs: console.log
|
|
40
42
|
})
|
|
41
43
|
|
|
44
|
+
await optimo.file('/absolute/path/image.jpg', {
|
|
45
|
+
resize: '100kB',
|
|
46
|
+
onLogs: console.log
|
|
47
|
+
})
|
|
48
|
+
|
|
42
49
|
// optimize a dir recursively
|
|
43
50
|
const result = await optimo.dir('/absolute/path/images')
|
|
44
51
|
|
package/bin/help.js
CHANGED
|
@@ -10,7 +10,7 @@ Usage
|
|
|
10
10
|
Options
|
|
11
11
|
-d, --dry-run Show what would be optimized without making changes
|
|
12
12
|
-f, --format Convert output format (e.g. jpeg, webp, avif)
|
|
13
|
-
-r, --resize Resize
|
|
13
|
+
-r, --resize Resize by percentage (50%) or target max size (100kB)
|
|
14
14
|
|
|
15
15
|
Examples
|
|
16
16
|
$ optimo image.jpg
|
|
@@ -18,4 +18,5 @@ Examples
|
|
|
18
18
|
$ optimo image.jpg -d
|
|
19
19
|
$ optimo image.png -f jpeg
|
|
20
20
|
$ optimo image.png -r 50%
|
|
21
|
+
$ optimo image.png -r 100kB
|
|
21
22
|
`)
|
package/bin/index.js
CHANGED
|
@@ -18,6 +18,18 @@ async function main () {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
const input = argv._[0]
|
|
21
|
+
let resize = argv.resize
|
|
22
|
+
|
|
23
|
+
if (resize !== undefined && resize !== null) {
|
|
24
|
+
const unitToken = argv._[1]
|
|
25
|
+
if (
|
|
26
|
+
unitToken &&
|
|
27
|
+
/^[kmg]?b$/i.test(unitToken) &&
|
|
28
|
+
/^\d*\.?\d+$/.test(String(resize))
|
|
29
|
+
) {
|
|
30
|
+
resize = `${resize}${unitToken}`
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
|
|
22
34
|
if (!input) {
|
|
23
35
|
console.log(require('./help'))
|
|
@@ -34,7 +46,7 @@ async function main () {
|
|
|
34
46
|
await fn(input, {
|
|
35
47
|
dryRun: argv['dry-run'],
|
|
36
48
|
format: argv.format,
|
|
37
|
-
resize
|
|
49
|
+
resize,
|
|
38
50
|
onLogs: logger
|
|
39
51
|
})
|
|
40
52
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "optimo",
|
|
3
3
|
"description": "The no-brainer ImageMagick CLI for optimizing images",
|
|
4
4
|
"homepage": "https://github.com/kikobeats/optimo",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.12",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js"
|
|
8
8
|
},
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"clean": "rm -rf node_modules",
|
|
63
63
|
"contributors": "(git-authors-cli && finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
64
64
|
"coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
|
|
65
|
-
"lint": "standard
|
|
65
|
+
"lint": "standard",
|
|
66
66
|
"postrelease": "pnpm release:tags && pnpm release:github && (ci-publish || npm publish --access=public)",
|
|
67
67
|
"pretest": "pnpm lint",
|
|
68
68
|
"release": "pnpm release:version && pnpm release:changelog && pnpm release:commit && pnpm release:tag",
|
package/src/index.js
CHANGED
|
@@ -1,169 +1,93 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { stat, unlink, rename, readdir } = require('node:fs/promises')
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
const path = require('node:path')
|
|
6
6
|
const $ = require('tinyspawn')
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const { magickPath, magickFlags } = require('./magick')
|
|
9
9
|
const { yellow, gray, green } = require('./colors')
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'-sampling-factor',
|
|
19
|
-
'4:2:0',
|
|
20
|
-
'-define',
|
|
21
|
-
'jpeg:optimize-coding=true',
|
|
22
|
-
'-define',
|
|
23
|
-
'jpeg:dct-method=float',
|
|
24
|
-
'-quality',
|
|
25
|
-
'80',
|
|
26
|
-
'-interlace',
|
|
27
|
-
'Plane'
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
/*
|
|
31
|
-
* PNG preset (maximum deflate effort):
|
|
32
|
-
* - Strip metadata payloads.
|
|
33
|
-
* - Use highest compression level with explicit strategy/filter tuning.
|
|
34
|
-
*/
|
|
35
|
-
const MAGICK_PNG_FLAGS = [
|
|
36
|
-
'-strip',
|
|
37
|
-
'-define',
|
|
38
|
-
'png:compression-level=9',
|
|
39
|
-
'-define',
|
|
40
|
-
'png:compression-strategy=1',
|
|
41
|
-
'-define',
|
|
42
|
-
'png:compression-filter=5'
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
/*
|
|
46
|
-
* GIF preset (animation optimization):
|
|
47
|
-
* - Coalesce frames before layer optimization to maximize delta compression.
|
|
48
|
-
* - OptimizePlus is more aggressive for animated GIF size reduction.
|
|
49
|
-
*/
|
|
50
|
-
const MAGICK_GIF_FLAGS = ['-strip', '-coalesce', '-layers', 'OptimizePlus']
|
|
51
|
-
|
|
52
|
-
/*
|
|
53
|
-
* WebP preset (compression-first):
|
|
54
|
-
* - Use a strong encoder method and preserve compatibility with lossy output.
|
|
55
|
-
*/
|
|
56
|
-
const MAGICK_WEBP_FLAGS = [
|
|
57
|
-
'-strip',
|
|
58
|
-
'-define',
|
|
59
|
-
'webp:method=6',
|
|
60
|
-
'-define',
|
|
61
|
-
'webp:thread-level=1',
|
|
62
|
-
'-quality',
|
|
63
|
-
'80'
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
/*
|
|
67
|
-
* AVIF preset (compression-first):
|
|
68
|
-
* - Slow encoder speed for stronger compression.
|
|
69
|
-
*/
|
|
70
|
-
const MAGICK_AVIF_FLAGS = [
|
|
71
|
-
'-strip',
|
|
72
|
-
'-define',
|
|
73
|
-
'heic:speed=1',
|
|
74
|
-
'-quality',
|
|
75
|
-
'50'
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
/*
|
|
79
|
-
* HEIC/HEIF preset (compression-first):
|
|
80
|
-
* - Slow encoder speed for stronger compression.
|
|
81
|
-
*/
|
|
82
|
-
const MAGICK_HEIC_FLAGS = [
|
|
83
|
-
'-strip',
|
|
84
|
-
'-define',
|
|
85
|
-
'heic:speed=1',
|
|
86
|
-
'-quality',
|
|
87
|
-
'75'
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
/*
|
|
91
|
-
* JPEG XL preset (compression-first):
|
|
92
|
-
* - Use max effort where supported.
|
|
93
|
-
*/
|
|
94
|
-
const MAGICK_JXL_FLAGS = ['-strip', '-define', 'jxl:effort=9', '-quality', '75']
|
|
95
|
-
|
|
96
|
-
/*
|
|
97
|
-
* SVG preset:
|
|
98
|
-
* - Keep optimization minimal to avoid destructive transformations.
|
|
99
|
-
*/
|
|
100
|
-
const MAGICK_SVG_FLAGS = ['-strip']
|
|
101
|
-
|
|
102
|
-
/*
|
|
103
|
-
* Generic preset for any other format:
|
|
104
|
-
* - Keep it broadly safe across decoders while still reducing size.
|
|
105
|
-
*/
|
|
106
|
-
const MAGICK_GENERIC_FLAGS = ['-strip']
|
|
107
|
-
|
|
108
|
-
const magickPath = (() => {
|
|
109
|
-
try {
|
|
110
|
-
return execSync('which magick', {
|
|
111
|
-
stdio: ['pipe', 'pipe', 'ignore']
|
|
112
|
-
})
|
|
113
|
-
.toString()
|
|
114
|
-
.trim()
|
|
115
|
-
} catch {
|
|
116
|
-
return false
|
|
117
|
-
}
|
|
118
|
-
})()
|
|
119
|
-
|
|
120
|
-
const percentage = (partial, total) =>
|
|
121
|
-
(((partial - total) / total) * 100).toFixed(1)
|
|
122
|
-
|
|
123
|
-
const normalizeFormat = format => {
|
|
124
|
-
if (!format) return null
|
|
125
|
-
|
|
126
|
-
const normalized = String(format).trim().toLowerCase().replace(/^\./, '')
|
|
127
|
-
if (normalized === 'jpg') return 'jpeg'
|
|
128
|
-
if (normalized === 'tif') return 'tiff'
|
|
129
|
-
|
|
130
|
-
return normalized
|
|
131
|
-
}
|
|
11
|
+
const {
|
|
12
|
+
formatBytes,
|
|
13
|
+
formatLog,
|
|
14
|
+
normalizeFormat,
|
|
15
|
+
parseResize,
|
|
16
|
+
percentage
|
|
17
|
+
} = require('./util')
|
|
132
18
|
|
|
133
19
|
const getOutputPath = (filePath, format) => {
|
|
134
20
|
const normalizedFormat = normalizeFormat(format)
|
|
135
21
|
if (!normalizedFormat) return filePath
|
|
136
|
-
|
|
137
22
|
const parsed = path.parse(filePath)
|
|
138
23
|
return path.join(parsed.dir, `${parsed.name}.${normalizedFormat}`)
|
|
139
24
|
}
|
|
140
25
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
26
|
+
const runMagick = async ({
|
|
27
|
+
filePath,
|
|
28
|
+
optimizedPath,
|
|
29
|
+
flags,
|
|
30
|
+
resizePercentage = null
|
|
31
|
+
}) => {
|
|
32
|
+
const magickArgs = [
|
|
33
|
+
filePath,
|
|
34
|
+
...(resizePercentage ? ['-resize', resizePercentage] : []),
|
|
35
|
+
...flags,
|
|
36
|
+
optimizedPath
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
await $('magick', magickArgs)
|
|
40
|
+
return (await stat(optimizedPath)).size
|
|
152
41
|
}
|
|
153
42
|
|
|
154
|
-
const
|
|
155
|
-
|
|
43
|
+
const optimizeForMaxSize = async ({
|
|
44
|
+
filePath,
|
|
45
|
+
optimizedPath,
|
|
46
|
+
flags,
|
|
47
|
+
maxSize
|
|
48
|
+
}) => {
|
|
49
|
+
const resultByScale = new Map()
|
|
50
|
+
|
|
51
|
+
const measureScale = async scale => {
|
|
52
|
+
if (resultByScale.has(scale)) return resultByScale.get(scale)
|
|
53
|
+
const resizePercentage = scale === 100 ? null : `${scale}%`
|
|
54
|
+
const size = await runMagick({
|
|
55
|
+
filePath,
|
|
56
|
+
optimizedPath,
|
|
57
|
+
flags,
|
|
58
|
+
resizePercentage
|
|
59
|
+
})
|
|
60
|
+
resultByScale.set(scale, size)
|
|
61
|
+
return size
|
|
62
|
+
}
|
|
156
63
|
|
|
157
|
-
const
|
|
158
|
-
|
|
64
|
+
const fullSize = await measureScale(100)
|
|
65
|
+
if (fullSize <= maxSize) {
|
|
66
|
+
return fullSize
|
|
67
|
+
}
|
|
159
68
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
69
|
+
const minScaleSize = await measureScale(1)
|
|
70
|
+
if (minScaleSize > maxSize) {
|
|
71
|
+
return minScaleSize
|
|
164
72
|
}
|
|
165
73
|
|
|
166
|
-
|
|
74
|
+
let low = 1
|
|
75
|
+
let high = 100
|
|
76
|
+
let bestScale = 1
|
|
77
|
+
|
|
78
|
+
while (high - low > 1) {
|
|
79
|
+
const mid = Math.floor((low + high) / 2)
|
|
80
|
+
const size = await measureScale(mid)
|
|
81
|
+
|
|
82
|
+
if (size <= maxSize) {
|
|
83
|
+
low = mid
|
|
84
|
+
bestScale = mid
|
|
85
|
+
} else {
|
|
86
|
+
high = mid
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return resultByScale.get(bestScale)
|
|
167
91
|
}
|
|
168
92
|
|
|
169
93
|
const file = async (
|
|
@@ -174,32 +98,38 @@ const file = async (
|
|
|
174
98
|
throw new Error('ImageMagick is not installed')
|
|
175
99
|
}
|
|
176
100
|
const outputPath = getOutputPath(filePath, outputFormat)
|
|
177
|
-
const
|
|
178
|
-
const flags =
|
|
101
|
+
const resizeConfig = parseResize(resize)
|
|
102
|
+
const flags = magickFlags(outputPath)
|
|
179
103
|
|
|
180
104
|
const optimizedPath = `${outputPath}.optimized`
|
|
181
105
|
const isConverting = outputPath !== filePath
|
|
182
106
|
|
|
183
107
|
let originalSize
|
|
184
|
-
|
|
185
|
-
const magickArgs = [
|
|
186
|
-
filePath,
|
|
187
|
-
...(resizePercentage ? ['-resize', resizePercentage] : []),
|
|
188
|
-
...flags,
|
|
189
|
-
optimizedPath
|
|
190
|
-
]
|
|
108
|
+
let optimizedSize
|
|
191
109
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
110
|
+
try {
|
|
111
|
+
originalSize = (await stat(filePath)).size
|
|
112
|
+
|
|
113
|
+
if (resizeConfig?.mode === 'max-size') {
|
|
114
|
+
optimizedSize = await optimizeForMaxSize({
|
|
115
|
+
filePath,
|
|
116
|
+
optimizedPath,
|
|
117
|
+
flags,
|
|
118
|
+
maxSize: resizeConfig.value
|
|
119
|
+
})
|
|
120
|
+
} else {
|
|
121
|
+
optimizedSize = await runMagick({
|
|
122
|
+
filePath,
|
|
123
|
+
optimizedPath,
|
|
124
|
+
flags,
|
|
125
|
+
resizePercentage: resizeConfig?.value
|
|
126
|
+
})
|
|
127
|
+
}
|
|
196
128
|
} catch {
|
|
197
129
|
onLogs(formatLog('[unsupported]', yellow, filePath))
|
|
198
130
|
return { originalSize: 0, optimizedSize: 0 }
|
|
199
131
|
}
|
|
200
132
|
|
|
201
|
-
const optimizedSize = (await stat(optimizedPath)).size
|
|
202
|
-
|
|
203
133
|
if (!isConverting && optimizedSize >= originalSize) {
|
|
204
134
|
await unlink(optimizedPath)
|
|
205
135
|
onLogs(formatLog('[optimized]', gray, filePath))
|
package/src/magick.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const { execSync } = require('child_process')
|
|
5
|
+
|
|
6
|
+
const magickPath = (() => {
|
|
7
|
+
try {
|
|
8
|
+
return execSync('which magick', {
|
|
9
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
10
|
+
})
|
|
11
|
+
.toString()
|
|
12
|
+
.trim()
|
|
13
|
+
} catch {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
})()
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* JPEG preset (compression-first):
|
|
20
|
+
* - Favor smaller output via chroma subsampling and optimized Huffman coding.
|
|
21
|
+
* - Progressive scan improves perceived loading on the web.
|
|
22
|
+
*/
|
|
23
|
+
const MAGICK_JPEG_FLAGS = [
|
|
24
|
+
'-strip',
|
|
25
|
+
'-sampling-factor',
|
|
26
|
+
'4:2:0',
|
|
27
|
+
'-define',
|
|
28
|
+
'jpeg:optimize-coding=true',
|
|
29
|
+
'-define',
|
|
30
|
+
'jpeg:dct-method=float',
|
|
31
|
+
'-quality',
|
|
32
|
+
'80',
|
|
33
|
+
'-interlace',
|
|
34
|
+
'Plane'
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* PNG preset (maximum deflate effort):
|
|
39
|
+
* - Strip metadata payloads.
|
|
40
|
+
* - Use highest compression level with explicit strategy/filter tuning.
|
|
41
|
+
*/
|
|
42
|
+
const MAGICK_PNG_FLAGS = [
|
|
43
|
+
'-strip',
|
|
44
|
+
'-define',
|
|
45
|
+
'png:compression-level=9',
|
|
46
|
+
'-define',
|
|
47
|
+
'png:compression-strategy=1',
|
|
48
|
+
'-define',
|
|
49
|
+
'png:compression-filter=5'
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* GIF preset (animation optimization):
|
|
54
|
+
* - Coalesce frames before layer optimization to maximize delta compression.
|
|
55
|
+
* - OptimizePlus is more aggressive for animated GIF size reduction.
|
|
56
|
+
*/
|
|
57
|
+
const MAGICK_GIF_FLAGS = ['-strip', '-coalesce', '-layers', 'OptimizePlus']
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* WebP preset (compression-first):
|
|
61
|
+
* - Use a strong encoder method and preserve compatibility with lossy output.
|
|
62
|
+
*/
|
|
63
|
+
const MAGICK_WEBP_FLAGS = [
|
|
64
|
+
'-strip',
|
|
65
|
+
'-define',
|
|
66
|
+
'webp:method=6',
|
|
67
|
+
'-define',
|
|
68
|
+
'webp:thread-level=1',
|
|
69
|
+
'-quality',
|
|
70
|
+
'80'
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* AVIF preset (compression-first):
|
|
75
|
+
* - Slow encoder speed for stronger compression.
|
|
76
|
+
*/
|
|
77
|
+
const MAGICK_AVIF_FLAGS = [
|
|
78
|
+
'-strip',
|
|
79
|
+
'-define',
|
|
80
|
+
'heic:speed=1',
|
|
81
|
+
'-quality',
|
|
82
|
+
'50'
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* HEIC/HEIF preset (compression-first):
|
|
87
|
+
* - Slow encoder speed for stronger compression.
|
|
88
|
+
*/
|
|
89
|
+
const MAGICK_HEIC_FLAGS = [
|
|
90
|
+
'-strip',
|
|
91
|
+
'-define',
|
|
92
|
+
'heic:speed=1',
|
|
93
|
+
'-quality',
|
|
94
|
+
'75'
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* JPEG XL preset (compression-first):
|
|
99
|
+
* - Use max effort where supported.
|
|
100
|
+
*/
|
|
101
|
+
const MAGICK_JXL_FLAGS = ['-strip', '-define', 'jxl:effort=9', '-quality', '75']
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* SVG preset:
|
|
105
|
+
* - Keep optimization minimal to avoid destructive transformations.
|
|
106
|
+
*/
|
|
107
|
+
const MAGICK_SVG_FLAGS = ['-strip']
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* Generic preset for any other format:
|
|
111
|
+
* - Keep it broadly safe across decoders while still reducing size.
|
|
112
|
+
*/
|
|
113
|
+
const MAGICK_GENERIC_FLAGS = ['-strip']
|
|
114
|
+
|
|
115
|
+
const magickFlags = filePath => {
|
|
116
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
117
|
+
if (ext === '.jpg' || ext === '.jpeg') return MAGICK_JPEG_FLAGS
|
|
118
|
+
if (ext === '.png') return MAGICK_PNG_FLAGS
|
|
119
|
+
if (ext === '.gif') return MAGICK_GIF_FLAGS
|
|
120
|
+
if (ext === '.webp') return MAGICK_WEBP_FLAGS
|
|
121
|
+
if (ext === '.avif') return MAGICK_AVIF_FLAGS
|
|
122
|
+
if (ext === '.heic' || ext === '.heif') return MAGICK_HEIC_FLAGS
|
|
123
|
+
if (ext === '.jxl') return MAGICK_JXL_FLAGS
|
|
124
|
+
if (ext === '.svg') return MAGICK_SVG_FLAGS
|
|
125
|
+
return MAGICK_GENERIC_FLAGS
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { magickPath, magickFlags }
|
package/src/util.js
CHANGED
|
@@ -18,7 +18,58 @@ const formatLog = (plainStatus, colorize, filePath) => {
|
|
|
18
18
|
return `${colorize(paddedPlainStatus)} ${gray(filePath)}`
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const percentage = (partial, total) =>
|
|
22
|
+
(((partial - total) / total) * 100).toFixed(1)
|
|
23
|
+
|
|
24
|
+
const normalizeFormat = format => {
|
|
25
|
+
if (!format) return null
|
|
26
|
+
const normalized = String(format).trim().toLowerCase().replace(/^\./, '')
|
|
27
|
+
if (normalized === 'jpg') return 'jpeg'
|
|
28
|
+
if (normalized === 'tif') return 'tiff'
|
|
29
|
+
return normalized
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parseResize = resize => {
|
|
33
|
+
if (resize === undefined || resize === null || resize === '') return null
|
|
34
|
+
|
|
35
|
+
const raw = String(resize).trim()
|
|
36
|
+
const normalized = raw.toLowerCase().replace(/\s+/g, '')
|
|
37
|
+
|
|
38
|
+
const maxSizeMatch = normalized.match(/^(\d*\.?\d+)(b|kb|mb|gb)$/)
|
|
39
|
+
if (maxSizeMatch) {
|
|
40
|
+
const units = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 }
|
|
41
|
+
const value = Number(maxSizeMatch[1])
|
|
42
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
'Resize max size must be greater than 0 (e.g. 100kB, 2MB)'
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
mode: 'max-size',
|
|
50
|
+
value: Math.floor(value * units[maxSizeMatch[2]])
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const percentage = raw.replace(/%$/, '')
|
|
55
|
+
const value = Number(percentage)
|
|
56
|
+
|
|
57
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
58
|
+
throw new TypeError(
|
|
59
|
+
'Resize must be a percentage (e.g. 50 or 50%) or max size (e.g. 100kB)'
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
mode: 'percentage',
|
|
65
|
+
value: `${value}%`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
module.exports = {
|
|
22
70
|
formatBytes,
|
|
23
|
-
formatLog
|
|
71
|
+
formatLog,
|
|
72
|
+
normalizeFormat,
|
|
73
|
+
parseResize,
|
|
74
|
+
percentage
|
|
24
75
|
}
|