image-guard 5.2.2 → 5.2.4
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/CHANGELOG.md +23 -1
- package/NOTICE.md +7 -0
- package/README.md +7 -21
- package/bin/image-guard.sh +15 -0
- package/bin/image-guard.test.js +26 -8
- package/package.json +5 -5
- package/src/index.js +30 -22
- package/src/utils.js +46 -39
- package/NOTICE +0 -6
- /package/{LICENSE → LICENSE.md} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ Starting with version 5.2.0, all notable changes to Image Guard are documented i
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [5.2.4] - 2026-03-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
* Changed bin entry from a Node.js script to a shell wrapper that locates node from common version managers (nvm, Volta, asdf, mise) and package managers (Homebrew) before invocation, resolving hook failures in GUI Git clients that don’t inherit the shell’s PATH
|
|
12
|
+
- **Migration:** If you set up hooks, replace `npx image-guard --staged` with `./node_modules/.bin/image-guard --staged` in your `.githooks/pre-commit` or `.husky/pre-commit`.
|
|
13
|
+
|
|
14
|
+
## [5.2.3] - 2026-02-21
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* Improved corrupt-file error detection to cover WebP and AVIF formats, normalize message casing before matching, and avoid false positives from the overly broad `Invalid` string match
|
|
19
|
+
* Warned when a `.bak` file is left behind after a failed file replacement
|
|
20
|
+
* Removed a dead code branch in result processing
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
* Moved to compression and conversion running in parallel (non-overlapping file sets), reducing wall-clock time when `--heic-to-avif` is active
|
|
25
|
+
* Reduced directory traversals from two to one in non-staged mode when `--heic-to-avif` is active
|
|
26
|
+
* Extracted shared `MAX_FILE_SIZE` constant; aligned `node:fs` and `node:path` import style in `utils.js`
|
|
27
|
+
* Extended test coverage for corrupt file detection and reporting
|
|
28
|
+
|
|
7
29
|
## [5.2.2] - 2026-02-17
|
|
8
30
|
|
|
9
31
|
### Fixed
|
|
@@ -14,7 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
14
36
|
|
|
15
37
|
### Changed
|
|
16
38
|
|
|
17
|
-
* HEIC-to-AVIF conversion
|
|
39
|
+
* Adjusted HEIC-to-AVIF conversion to use lossy instead of lossless encoding (quality 80), to produce files smaller than the HEIC originals but still at high quality
|
|
18
40
|
* Removed redundant lossless compression pass after HEIC conversion
|
|
19
41
|
* Documented Display P3 to sRGB color space mapping during HEIC conversion
|
|
20
42
|
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Image Guard
|
|
2
|
+
|
|
3
|
+
Copyright 2022 [Jens Oliver Meiert](https://meiert.com/)
|
|
4
|
+
|
|
5
|
+
The initial developer of imagemin is [the imagemin team](https://github.com/orgs/imagemin/people).
|
|
6
|
+
|
|
7
|
+
The initial developer of imagemin-merlin (aka Merlin) is [sum.cumo](https://web.archive.org/web/20191208211414/https://www.sumcumo.com/). Copyright 2019 sum.cumo GmbH.
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Native Git hooks are simpler to set up and don’t require additional dependenci
|
|
|
64
64
|
mkdir -p .githooks;\
|
|
65
65
|
cat > .githooks/pre-commit << 'EOF'
|
|
66
66
|
#!/bin/sh
|
|
67
|
-
|
|
67
|
+
./node_modules/.bin/image-guard --staged
|
|
68
68
|
EOF
|
|
69
69
|
chmod +x .githooks/pre-commit;\
|
|
70
70
|
git config core.hooksPath .githooks;\
|
|
@@ -72,7 +72,7 @@ git add .githooks/pre-commit;\
|
|
|
72
72
|
git commit -m "feat: add Git pre-commit hook for Image Guard";\
|
|
73
73
|
npm pkg set scripts.postprepare="mkdir -p .githooks && cat > .githooks/pre-commit << 'EOF'
|
|
74
74
|
#\!/bin/sh
|
|
75
|
-
|
|
75
|
+
./node_modules/.bin/image-guard --staged
|
|
76
76
|
EOF
|
|
77
77
|
chmod +x .githooks/pre-commit && git config core.hooksPath .githooks"
|
|
78
78
|
```
|
|
@@ -82,21 +82,21 @@ chmod +x .githooks/pre-commit && git config core.hooksPath .githooks"
|
|
|
82
82
|
If you already use [Husky](https://typicode.github.io/husky/), run the following commands in your project root (you can copy and execute them at once):
|
|
83
83
|
|
|
84
84
|
```console
|
|
85
|
-
grep -qxF "
|
|
85
|
+
grep -qxF "./node_modules/.bin/image-guard --staged" .husky/pre-commit || printf '\n./node_modules/.bin/image-guard --staged' >> .husky/pre-commit;\
|
|
86
86
|
git add .husky/pre-commit;\
|
|
87
87
|
git commit -m "feat: add Husky pre-commit hook for Image Guard";\
|
|
88
|
-
npm pkg set scripts.postprepare="grep -qxF '
|
|
88
|
+
npm pkg set scripts.postprepare="grep -qxF './node_modules/.bin/image-guard --staged' .husky/pre-commit || printf '\\n./node_modules/.bin/image-guard --staged' >> .husky/pre-commit"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
If you don’t use Husky
|
|
91
|
+
If you don’t yet but want to use Husky, run the following commands from your project root:
|
|
92
92
|
|
|
93
93
|
```console
|
|
94
94
|
npm i -D husky;\
|
|
95
95
|
npx husky init;\
|
|
96
|
-
echo "
|
|
96
|
+
echo "./node_modules/.bin/image-guard --staged" > .husky/pre-commit;\
|
|
97
97
|
git add .husky/pre-commit;\
|
|
98
98
|
git commit -m "feat: add Husky pre-commit hook for Image Guard";\
|
|
99
|
-
npm pkg set scripts.postprepare="grep -qxF '
|
|
99
|
+
npm pkg set scripts.postprepare="grep -qxF './node_modules/.bin/image-guard --staged' .husky/pre-commit || printf '\\n./node_modules/.bin/image-guard --staged' >> .husky/pre-commit"
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
(The `postprepare` script ensures that the hook is added to the repository whenever someone installs the package.)
|
|
@@ -117,20 +117,6 @@ npm pkg set scripts.postprepare="grep -qxF 'npx image-guard --staged' .husky/pre
|
|
|
117
117
|
|
|
118
118
|
* `--quiet` suppresses per‑file logs and prints only the final summary (plus errors). This reduces console noise and speeds up runs in CI and Git hooks.
|
|
119
119
|
|
|
120
|
-
### Troubleshooting
|
|
121
|
-
|
|
122
|
-
#### “npx: command not found”
|
|
123
|
-
|
|
124
|
-
If Git hooks fail with “npx: command not found,” make sure to install (`npm i -D image-guard`) and to refer to the binary directly in the `pre-commit` hook (and, not detailed here, also in the `postprepare` script):
|
|
125
|
-
|
|
126
|
-
```console
|
|
127
|
-
#!/bin/sh
|
|
128
|
-
export PATH="$PWD/node_modules/.bin:$PATH"
|
|
129
|
-
./node_modules/.bin/image-guard --staged
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
This issue can arise in GUI Git clients (VS Code, GitHub Desktop, etc.) or with Node version managers, as these environments may not inherit your shell's `PATH`/Node environment. This affects any tool using npx in hooks.
|
|
133
|
-
|
|
134
120
|
## What Does the Output Look Like?
|
|
135
121
|
|
|
136
122
|
Roughly like this:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
if ! command -v node > /dev/null 2>&1; then
|
|
3
|
+
[ -s "${NVM_DIR:-$HOME/.nvm}/nvm.sh" ] && . "${NVM_DIR:-$HOME/.nvm}/nvm.sh"
|
|
4
|
+
[ -d "$HOME/.volta/bin" ] && export PATH="$HOME/.volta/bin:$PATH"
|
|
5
|
+
[ -d "$HOME/.asdf/shims" ] && export PATH="$HOME/.asdf/shims:$PATH"
|
|
6
|
+
[ -d "$HOME/.local/share/mise/shims" ] && export PATH="$HOME/.local/share/mise/shims:$PATH"
|
|
7
|
+
[ -d "/opt/homebrew/bin" ] && export PATH="/opt/homebrew/bin:$PATH"
|
|
8
|
+
[ -d "/usr/local/bin" ] && export PATH="/usr/local/bin:$PATH"
|
|
9
|
+
[ -d "/home/linuxbrew/.linuxbrew/bin" ] && export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
|
|
10
|
+
fi
|
|
11
|
+
if ! command -v node > /dev/null 2>&1; then
|
|
12
|
+
echo "Image Guard: Node not found. Install Node.js via nvm, Volta, asdf, mise, or Homebrew." >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
exec node "$(dirname "$0")/../image-guard/bin/image-guard.js" "$@"
|
package/bin/image-guard.test.js
CHANGED
|
@@ -141,7 +141,7 @@ describe('Image Guard', () => {
|
|
|
141
141
|
assert.strictEqual(allCompressed, true)
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
-
test('
|
|
144
|
+
test('Ensure files are not modified in dry run', () => {
|
|
145
145
|
const originalStats = fs.readdirSync(testFolderGit).sort().map(file => {
|
|
146
146
|
const filePath = path.join(testFolderGit, file)
|
|
147
147
|
return { file, stats: fs.statSync(filePath) }
|
|
@@ -298,7 +298,7 @@ describe('Image Guard', () => {
|
|
|
298
298
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
299
299
|
})
|
|
300
300
|
|
|
301
|
-
test('
|
|
301
|
+
test('Ensure quiet mode suppresses per-file logs but keeps summary', () => {
|
|
302
302
|
// Prepare isolated temp directory with test images
|
|
303
303
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-quiet-'))
|
|
304
304
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
@@ -320,7 +320,7 @@ describe('Image Guard', () => {
|
|
|
320
320
|
assert.strictEqual(!(/Compressed|Skipped/.test(stdout)), true)
|
|
321
321
|
})
|
|
322
322
|
|
|
323
|
-
test('
|
|
323
|
+
test('Ensure dry and quiet runs leave no artifacts and do not mutate files', () => {
|
|
324
324
|
// Use isolated temp directory
|
|
325
325
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-dry-quiet-'))
|
|
326
326
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
@@ -368,7 +368,7 @@ describe('Image Guard', () => {
|
|
|
368
368
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
369
369
|
})
|
|
370
370
|
|
|
371
|
-
test('
|
|
371
|
+
test('Ensure no .bak files remain after normal compression', () => {
|
|
372
372
|
// Prepare isolated temp directory with test images
|
|
373
373
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-bak-'))
|
|
374
374
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
@@ -428,7 +428,7 @@ describe('Image Guard', () => {
|
|
|
428
428
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
429
429
|
})
|
|
430
430
|
|
|
431
|
-
test('`--keep-heic` preserves original HEIC file', () => {
|
|
431
|
+
test('Ensure `--keep-heic` preserves original HEIC file', () => {
|
|
432
432
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-keep-heic-'))
|
|
433
433
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
434
434
|
copyFiles(testFolder, tempTestFolder)
|
|
@@ -451,7 +451,7 @@ describe('Image Guard', () => {
|
|
|
451
451
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
452
452
|
})
|
|
453
453
|
|
|
454
|
-
test('
|
|
454
|
+
test('Ensure dry run does not convert HEIC files but reports sizes', () => {
|
|
455
455
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-heic-dry-'))
|
|
456
456
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
457
457
|
copyFiles(testFolder, tempTestFolder)
|
|
@@ -490,7 +490,7 @@ describe('Image Guard', () => {
|
|
|
490
490
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
491
491
|
})
|
|
492
492
|
|
|
493
|
-
test('HEIC files are ignored without `--heic-to-avif` flag', () => {
|
|
493
|
+
test('Ensure HEIC files are ignored without `--heic-to-avif` flag', () => {
|
|
494
494
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-heic-noflag-'))
|
|
495
495
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
496
496
|
copyFiles(testFolder, tempTestFolder)
|
|
@@ -514,7 +514,25 @@ describe('Image Guard', () => {
|
|
|
514
514
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
515
515
|
})
|
|
516
516
|
|
|
517
|
-
test('
|
|
517
|
+
test('Skip and report corrupt files', () => {
|
|
518
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-corrupt-'))
|
|
519
|
+
const tempTestFolder = path.join(tempDir, 'test')
|
|
520
|
+
copyFiles(testFolder, tempTestFolder)
|
|
521
|
+
|
|
522
|
+
const originalCwd = process.cwd()
|
|
523
|
+
let output = ''
|
|
524
|
+
try {
|
|
525
|
+
process.chdir(tempDir)
|
|
526
|
+
output = execSync(`node "${imageGuardScript}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
527
|
+
} finally {
|
|
528
|
+
process.chdir(originalCwd)
|
|
529
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
assert.match(output, /Skipped.*test#corrupt\.gif.*corrupt file/i)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('Ensure `--keep-heic` without `--heic-to-avif` issues warning', () => {
|
|
518
536
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-keep-warn-'))
|
|
519
537
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
520
538
|
copyFiles(testFolder, tempTestFolder)
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Jens Oliver Meiert",
|
|
3
3
|
"bin": {
|
|
4
|
-
"image-guard": "bin/image-guard.
|
|
5
|
-
"imagemin-guard": "bin/image-guard.
|
|
6
|
-
"imagemin-merlin": "bin/image-guard.
|
|
4
|
+
"image-guard": "bin/image-guard.sh",
|
|
5
|
+
"imagemin-guard": "bin/image-guard.sh",
|
|
6
|
+
"imagemin-merlin": "bin/image-guard.sh"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"globby": "^16.1.0",
|
|
10
10
|
"heic-decode": "^2.1.0",
|
|
11
11
|
"sharp": "^0.34.5",
|
|
12
|
-
"simple-git": "^3.
|
|
12
|
+
"simple-git": "^3.31.1"
|
|
13
13
|
},
|
|
14
14
|
"description": "Near-lossless image compressor that’s easy to use and easy to automate",
|
|
15
15
|
"devDependencies": {
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"test": "node --test bin/image-guard.test.js"
|
|
47
47
|
},
|
|
48
48
|
"type": "module",
|
|
49
|
-
"version": "5.2.
|
|
49
|
+
"version": "5.2.4"
|
|
50
50
|
}
|
package/src/index.js
CHANGED
|
@@ -97,11 +97,7 @@ export async function runImageGuard() {
|
|
|
97
97
|
let addedKB = 0
|
|
98
98
|
for (const r of results) {
|
|
99
99
|
if (r.status === 'fulfilled') {
|
|
100
|
-
|
|
101
|
-
addedKB += r.value
|
|
102
|
-
} else {
|
|
103
|
-
hadFailures = true
|
|
104
|
-
}
|
|
100
|
+
addedKB += r.value
|
|
105
101
|
} else {
|
|
106
102
|
hadFailures = true
|
|
107
103
|
const reason = r.reason && r.reason.message ? r.reason.message : String(r.reason)
|
|
@@ -192,32 +188,44 @@ export async function runImageGuard() {
|
|
|
192
188
|
const diffOutput = await git.raw(['diff', '--name-only', '--cached', '--diff-filter=ACMRT'])
|
|
193
189
|
const stagedFiles = diffOutput.split('\n').map(s => s.trim()).filter(Boolean)
|
|
194
190
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
// Collect compression and conversion candidates in parallel
|
|
192
|
+
const [compressionFiles, conversionFiles] = await Promise.all([
|
|
193
|
+
filterStagedFiles(stagedFiles, fileTypes),
|
|
194
|
+
doConversion ? filterStagedFiles(stagedFiles, convertTypes) : Promise.resolve([])
|
|
195
|
+
])
|
|
196
|
+
totalFiles += compressionFiles.length + conversionFiles.length
|
|
197
|
+
|
|
198
|
+
// Run compression and conversion in parallel (non-overlapping file sets)
|
|
199
|
+
const [compFailed, convFailed] = await Promise.all([
|
|
200
|
+
compress(compressionFiles, argv.dry),
|
|
201
|
+
convert(conversionFiles, argv.dry, argv['keep-heic'])
|
|
202
|
+
])
|
|
203
|
+
hadFailures = compFailed || convFailed
|
|
205
204
|
} catch (err) {
|
|
206
205
|
console.error(err)
|
|
207
206
|
hadFailures = true
|
|
208
207
|
}
|
|
209
208
|
} else {
|
|
210
|
-
|
|
211
|
-
const
|
|
209
|
+
// Single directory traversal for all relevant types
|
|
210
|
+
const allFiles = await findFiles(getFilePattern(argv.ignore, allTypes))
|
|
211
|
+
|
|
212
|
+
const compExts = new Set(fileTypes)
|
|
213
|
+
const compressionFiles = allFiles.filter(f => compExts.has(path.extname(f).slice(1).toLowerCase()))
|
|
212
214
|
totalFiles += compressionFiles.length
|
|
213
|
-
hadFailures = await compress(compressionFiles, argv.dry)
|
|
214
215
|
|
|
215
216
|
if (doConversion) {
|
|
216
|
-
const
|
|
217
|
-
const conversionFiles =
|
|
217
|
+
const convExts = new Set(convertTypes)
|
|
218
|
+
const conversionFiles = allFiles.filter(f => convExts.has(path.extname(f).slice(1).toLowerCase()))
|
|
218
219
|
totalFiles += conversionFiles.length
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
|
|
221
|
+
// Run compression and conversion in parallel (non-overlapping file sets)
|
|
222
|
+
const [compFailed, convFailed] = await Promise.all([
|
|
223
|
+
compress(compressionFiles, argv.dry),
|
|
224
|
+
convert(conversionFiles, argv.dry, argv['keep-heic'])
|
|
225
|
+
])
|
|
226
|
+
hadFailures = compFailed || convFailed
|
|
227
|
+
} else {
|
|
228
|
+
hadFailures = await compress(compressionFiles, argv.dry)
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
|
package/src/utils.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// This file, which had been forked from imagemin-merlin, was modified for image-guard: https://github.com/sumcumo/imagemin-merlin/compare/master...j9t:master
|
|
2
2
|
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import path from 'path'
|
|
3
|
+
import { promises as fs } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
5
|
import sharp from 'sharp'
|
|
6
6
|
import { styleText } from 'node:util'
|
|
7
7
|
import decode from 'heic-decode'
|
|
8
8
|
|
|
9
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100 MB
|
|
10
|
+
|
|
9
11
|
const logMessage = (message, dry, color = 'yellow', quiet = false) => {
|
|
10
12
|
if (quiet) return
|
|
11
13
|
const prefix = dry ? 'Dry run: ' : ''
|
|
@@ -38,9 +40,7 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
38
40
|
return 0
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (fileSizeBefore > maxFileSize) {
|
|
43
|
+
if (fileSizeBefore > MAX_FILE_SIZE) {
|
|
44
44
|
logMessage(`Skipped ${filename} (file too large: ${sizeReadable(fileSizeBefore)})`, dry, 'yellow', quiet)
|
|
45
45
|
return 0
|
|
46
46
|
}
|
|
@@ -114,17 +114,17 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
114
114
|
details = `${sizeReadable(fileSizeBefore)} → ${sizeReadable(fileSizeAfter)}`
|
|
115
115
|
if (!dry) {
|
|
116
116
|
// Only now create a backup and replace the original
|
|
117
|
-
await retryFileOperation(() => fs.
|
|
117
|
+
await retryFileOperation(() => fs.copyFile(filename, filenameBackup))
|
|
118
118
|
// Prefer atomic rename when possible
|
|
119
119
|
try {
|
|
120
|
-
await retryFileOperation(() => fs.
|
|
120
|
+
await retryFileOperation(() => fs.rename(tempFilePath, filename))
|
|
121
121
|
// Temp file was renamed (consumed)
|
|
122
122
|
tempConsumed = true
|
|
123
123
|
replacementSucceeded = true
|
|
124
124
|
} catch {
|
|
125
125
|
// Fallback to copy when rename across devices isn’t possible
|
|
126
|
-
await retryFileOperation(() => fs.
|
|
127
|
-
await retryFileOperation(() => fs.
|
|
126
|
+
await retryFileOperation(() => fs.copyFile(tempFilePath, filename))
|
|
127
|
+
await retryFileOperation(() => fs.unlink(tempFilePath))
|
|
128
128
|
// Temp file explicitly removed after copy
|
|
129
129
|
tempConsumed = true
|
|
130
130
|
replacementSucceeded = true
|
|
@@ -139,17 +139,7 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
139
139
|
logMessage(`${status} ${filename} (${details})`, dry, color, quiet)
|
|
140
140
|
|
|
141
141
|
if (dry) {
|
|
142
|
-
|
|
143
|
-
return 0
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Clean up temp file only when it wasn’t consumed
|
|
147
|
-
if (!tempConsumed) {
|
|
148
|
-
try {
|
|
149
|
-
await retryFileOperation(() => fs.promises.unlink(tempFilePath))
|
|
150
|
-
} catch (err) {
|
|
151
|
-
if (err.code !== 'ENOENT') throw err
|
|
152
|
-
}
|
|
142
|
+
return 0 // Temp file cleaned up in finally
|
|
153
143
|
}
|
|
154
144
|
|
|
155
145
|
if (fileSizeAfter === 0) {
|
|
@@ -160,15 +150,17 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
160
150
|
|
|
161
151
|
} catch (err) {
|
|
162
152
|
|
|
163
|
-
// Check if this is a file corruption error
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
153
|
+
// Check if this is a file corruption error (sharp/libvips error messages)
|
|
154
|
+
const msg = err.message ? err.message.toLowerCase() : ''
|
|
155
|
+
if (msg.includes('corrupt header') ||
|
|
156
|
+
msg.includes('unexpected end of') ||
|
|
157
|
+
msg.includes('invalid image') ||
|
|
158
|
+
msg.includes('gifload:') ||
|
|
159
|
+
msg.includes('pngload:') ||
|
|
160
|
+
msg.includes('jpegload:') ||
|
|
161
|
+
msg.includes('webpload:') ||
|
|
162
|
+
msg.includes('avifload:')
|
|
163
|
+
) {
|
|
172
164
|
logMessage(`Skipped ${filename} (corrupt file)`, dry, 'yellow', quiet)
|
|
173
165
|
} else {
|
|
174
166
|
console.error(styleText('red', `Error compressing ${filename}:`), err)
|
|
@@ -177,15 +169,32 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
177
169
|
|
|
178
170
|
} finally {
|
|
179
171
|
|
|
172
|
+
// Clean up temp file if it wasn’t consumed (covers dry-run, error, and no-improvement paths)
|
|
173
|
+
if (!tempConsumed) {
|
|
174
|
+
try {
|
|
175
|
+
await retryFileOperation(() => fs.unlink(tempFilePath))
|
|
176
|
+
} catch {
|
|
177
|
+
// Best-effort cleanup—ignore all errors to avoid masking the original error
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
180
181
|
// If backup created (i.e., only in improvement path), try to remove it
|
|
181
182
|
if (!dry && replacementSucceeded) {
|
|
182
183
|
try {
|
|
183
|
-
await retryFileOperation(() => fs.
|
|
184
|
+
await retryFileOperation(() => fs.unlink(filenameBackup))
|
|
184
185
|
} catch (err) {
|
|
185
186
|
if (err.code !== 'ENOENT') {
|
|
186
187
|
console.warn(styleText('yellow', `Failed to delete backup file ${filenameBackup}:`), err)
|
|
187
188
|
}
|
|
188
189
|
}
|
|
190
|
+
} else if (!dry && !replacementSucceeded) {
|
|
191
|
+
// If a backup was created but replacement failed, warn so the user can recover it
|
|
192
|
+
try {
|
|
193
|
+
await fs.access(filenameBackup)
|
|
194
|
+
console.warn(styleText('yellow', `Replacement failed for ${filename}; backup preserved at ${filenameBackup}`))
|
|
195
|
+
} catch {
|
|
196
|
+
// No backup exists—nothing to warn about
|
|
197
|
+
}
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
}
|
|
@@ -199,9 +208,7 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
199
208
|
return 0
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (fileSizeBefore > maxFileSize) {
|
|
211
|
+
if (fileSizeBefore > MAX_FILE_SIZE) {
|
|
205
212
|
logMessage(`Skipped ${filename} (file too large: ${sizeReadable(fileSizeBefore)})`, dry, 'yellow', quiet)
|
|
206
213
|
return 0
|
|
207
214
|
}
|
|
@@ -215,7 +222,7 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
215
222
|
|
|
216
223
|
// Avoid overwriting an existing AVIF file
|
|
217
224
|
try {
|
|
218
|
-
await fs.
|
|
225
|
+
await fs.access(avifPath)
|
|
219
226
|
logMessage(`Skipped ${filename} (${path.basename(avifPath)} already exists)`, dry, 'yellow', quiet)
|
|
220
227
|
return 0
|
|
221
228
|
} catch {
|
|
@@ -224,7 +231,7 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
224
231
|
|
|
225
232
|
try {
|
|
226
233
|
// Decode HEIC/HEIF to raw pixel data
|
|
227
|
-
const inputBuffer = await fs.
|
|
234
|
+
const inputBuffer = await fs.readFile(filename)
|
|
228
235
|
const { width, height, data } = await decode({ buffer: inputBuffer })
|
|
229
236
|
|
|
230
237
|
// Encode as lossy AVIF—HEIC sources are already lossy (HEVC), so lossless
|
|
@@ -238,13 +245,13 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
238
245
|
logMessage(`Converted ${filename} → ${path.basename(avifPath)} (${sizeReadable(fileSizeBefore)} → ${sizeReadable(fileSizeAfter)})`, dry, 'cyan', quiet)
|
|
239
246
|
|
|
240
247
|
if (dry) {
|
|
241
|
-
await retryFileOperation(() => fs.
|
|
248
|
+
await retryFileOperation(() => fs.unlink(avifPath))
|
|
242
249
|
return 0
|
|
243
250
|
}
|
|
244
251
|
|
|
245
252
|
// Delete original HEIC/HEIF file unless `--keep-heic` is set
|
|
246
253
|
if (!keepOriginal) {
|
|
247
|
-
await retryFileOperation(() => fs.
|
|
254
|
+
await retryFileOperation(() => fs.unlink(filename))
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
return fileSizeAfter < fileSizeBefore ? fileSizeBefore - fileSizeAfter : 0
|
|
@@ -252,7 +259,7 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
252
259
|
} catch (err) {
|
|
253
260
|
// Clean up partial AVIF if it was created
|
|
254
261
|
try {
|
|
255
|
-
await fs.
|
|
262
|
+
await retryFileOperation(() => fs.unlink(avifPath))
|
|
256
263
|
} catch (cleanupErr) {
|
|
257
264
|
if (cleanupErr.code !== 'ENOENT') {
|
|
258
265
|
console.warn(styleText('yellow', `Failed to clean up ${avifPath}:`), cleanupErr)
|
|
@@ -272,7 +279,7 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
272
279
|
}
|
|
273
280
|
|
|
274
281
|
const size = async (file) => {
|
|
275
|
-
const stats = await fs.
|
|
282
|
+
const stats = await fs.stat(file)
|
|
276
283
|
return stats.size
|
|
277
284
|
}
|
|
278
285
|
|
package/NOTICE
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
Image Guard
|
|
2
|
-
Copyright 2022 Jens Oliver Meiert (https://meiert.com/)
|
|
3
|
-
|
|
4
|
-
The initial developer of imagemin is the imagemin team (https://github.com/orgs/imagemin/people).
|
|
5
|
-
|
|
6
|
-
The initial developer of imagemin-merlin (aka Merlin) is sum.cumo (https://web.archive.org/web/20191208211414/https://www.sumcumo.com/). Copyright 2019 sum.cumo GmbH.
|
/package/{LICENSE → LICENSE.md}
RENAMED
|
File without changes
|