image-guard 5.2.0 → 5.2.2
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 +14 -0
- package/README.md +4 -4
- package/bin/image-guard.test.js +18 -3
- package/package.json +1 -1
- package/src/utils.js +10 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.2] - 2026-02-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
* Updated HEIC dry run to report actual file sizes instead of omitting size data
|
|
12
|
+
|
|
13
|
+
## [5.2.1] - 2026-02-17
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
* HEIC-to-AVIF conversion now uses lossy encoding (quality 80) instead of lossless, producing files smaller than the HEIC originals
|
|
18
|
+
* Removed redundant lossless compression pass after HEIC conversion
|
|
19
|
+
* Documented Display P3 to sRGB color space mapping during HEIC conversion
|
|
20
|
+
|
|
7
21
|
## [5.2.0] - 2026-02-17
|
|
8
22
|
|
|
9
23
|
### Added
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
(This project was based on [sum.cumo’s imagemin-merlin](https://github.com/sumcumo/imagemin-merlin). [Changes are documented](https://github.com/sumcumo/imagemin-merlin/compare/master...j9t:master), and include this README. Image Guard supports additional file formats—WebP, AVIF, and HEIC/HEIF—, comes with improved code and documentation, and is being maintained. For this reason, it’s [not based on any Imagemin packages](https://meiert.com/blog/image-guard-4/) anymore.)
|
|
6
6
|
|
|
7
|
-
Image Guard takes care of near-lossless compression of your images, to help you avoid bloat in your repositories. It makes it convenient and as safe as possible to automatically compress PNG, JPG, GIF, WebP, and AVIF images. It can also convert HEIC/HEIF files to AVIF
|
|
7
|
+
Image Guard takes care of near-lossless compression of your images, to help you avoid bloat in your repositories. It makes it convenient and as safe as possible to automatically compress PNG, JPG, GIF, WebP, and AVIF images. It can also convert HEIC/HEIF files to AVIF.
|
|
8
8
|
|
|
9
9
|
It’s convenient because setup is simple. Run it right away—done. Or install, run, add hook—done.
|
|
10
10
|
|
|
@@ -111,7 +111,7 @@ npm pkg set scripts.postprepare="grep -qxF 'npx image-guard --staged' .husky/pre
|
|
|
111
111
|
|
|
112
112
|
* `--staged` (recommended with automated use) triggers a mode that watches PNG, JPG, GIF, WebP, and AVIF files (and HEIC/HEIF, when `--heic-to-avif` is used) in `git diff` and only processes those files—that approach makes Image Guard more efficient in operation.
|
|
113
113
|
|
|
114
|
-
* `--heic-to-avif` converts HEIC/HEIF files to
|
|
114
|
+
* `--heic-to-avif` converts HEIC/HEIF files to AVIF. Because HEIC sources are already lossy-compressed (HEVC), the conversion uses lossy AVIF encoding (quality 80) to produce files smaller than the originals at near-identical visual quality. Original HEIC/HEIF files are deleted after conversion by default. (Note: The conversion maps Display P3 colors—common on iPhones—to sRGB, which may result in slightly less vivid colors.)
|
|
115
115
|
|
|
116
116
|
- `--keep-heic` (used with `--heic-to-avif`) preserves the original HEIC/HEIF files instead of deleting them after conversion.
|
|
117
117
|
|
|
@@ -140,7 +140,7 @@ Roughly like this:
|
|
|
140
140
|
* Green: The image file has been compressed.
|
|
141
141
|
* Cyan: The image file has been converted (HEIC/HEIF to AVIF).
|
|
142
142
|
* White (light gray): The image file has not been changed.
|
|
143
|
-
* Blue: The image file had already been compressed more
|
|
143
|
+
* Blue: The image file had already been compressed more effectively than the new result, and was therefore skipped, too.
|
|
144
144
|
|
|
145
145
|
Tip: Use `--quiet` to suppress these per‑file lines and keep only the final summary.
|
|
146
146
|
|
|
@@ -148,7 +148,7 @@ Tip: Use `--quiet` to suppress these per‑file lines and keep only the final su
|
|
|
148
148
|
|
|
149
149
|
Image Guard is a Node script that uses [sharp](https://www.npmjs.com/package/sharp) under the hood.
|
|
150
150
|
|
|
151
|
-
Automated compression works by monitoring whether a given [change list](https://webglossary.info/terms/change-list/) includes any PNGs, JPGs, GIFs, WebPs, or AVIFs (and HEIC/HEIF when opted in). It’s initiated by a Git hook. Only those images are compressed where there is an improvement. HEIC conversion uses [heic-decode](https://www.npmjs.com/package/heic-decode) for patent-free decoding and sharp for AVIF encoding. The processed images can then be committed to the underlying repository.
|
|
151
|
+
Automated compression works by monitoring whether a given [change list](https://webglossary.info/terms/change-list/) includes any PNGs, JPGs, GIFs, WebPs, or AVIFs (and HEIC/HEIF when opted in). It’s initiated by a Git hook. Only those images are compressed where there is an improvement. HEIC conversion uses [heic-decode](https://www.npmjs.com/package/heic-decode) for patent-free decoding and sharp for lossy AVIF encoding (quality 80). The processed images can then be committed to the underlying repository.
|
|
152
152
|
|
|
153
153
|
Through this approach, though glossed over here, Image Guard makes up for what’s missing or complicated in other packages, namely easy, near-riskless, automatable, resource-friendly in-repo optimization.
|
|
154
154
|
|
package/bin/image-guard.test.js
CHANGED
|
@@ -415,7 +415,12 @@ describe('Image Guard', () => {
|
|
|
415
415
|
|
|
416
416
|
// AVIF should be created by the conversion
|
|
417
417
|
assert.strictEqual(fs.existsSync(avifFile), true, 'AVIF file should be created')
|
|
418
|
-
|
|
418
|
+
const avifSize = fs.statSync(avifFile).size
|
|
419
|
+
assert.ok(avifSize > 0, 'AVIF file should not be empty')
|
|
420
|
+
|
|
421
|
+
// Lossy AVIF should not be larger than the HEIC source
|
|
422
|
+
const heicSize = fs.statSync(path.join(testFolder, 'test.heic')).size
|
|
423
|
+
assert.ok(avifSize <= heicSize, `AVIF (${avifSize}) should not be larger than HEIC (${heicSize})`)
|
|
419
424
|
|
|
420
425
|
// Original HEIC should be deleted
|
|
421
426
|
assert.strictEqual(fs.existsSync(heicFile), false, 'Original HEIC should be deleted')
|
|
@@ -446,19 +451,26 @@ describe('Image Guard', () => {
|
|
|
446
451
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
447
452
|
})
|
|
448
453
|
|
|
449
|
-
test('Dry run does not convert HEIC files', () => {
|
|
454
|
+
test('Dry run does not convert HEIC files but reports sizes', () => {
|
|
450
455
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-guard-heic-dry-'))
|
|
451
456
|
const tempTestFolder = path.join(tempDir, 'test')
|
|
452
457
|
copyFiles(testFolder, tempTestFolder)
|
|
453
458
|
|
|
459
|
+
// Remove pre-existing AVIF so the conversion path is exercised
|
|
460
|
+
const avifFile = path.join(tempTestFolder, 'test.avif')
|
|
461
|
+
if (fs.existsSync(avifFile)) {
|
|
462
|
+
fs.unlinkSync(avifFile)
|
|
463
|
+
}
|
|
464
|
+
|
|
454
465
|
const heicFile = path.join(tempTestFolder, 'test.heic')
|
|
455
466
|
const heicBefore = fs.statSync(heicFile)
|
|
456
467
|
const filesBefore = fs.readdirSync(tempTestFolder).sort()
|
|
457
468
|
|
|
458
469
|
const originalCwd = process.cwd()
|
|
470
|
+
let output = ''
|
|
459
471
|
try {
|
|
460
472
|
process.chdir(tempDir)
|
|
461
|
-
execSync(`node "${imageGuardScript}" --heic-to-avif --dry`, { stdio: 'pipe' })
|
|
473
|
+
output = execSync(`node "${imageGuardScript}" --heic-to-avif --dry`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
462
474
|
} finally {
|
|
463
475
|
process.chdir(originalCwd)
|
|
464
476
|
}
|
|
@@ -472,6 +484,9 @@ describe('Image Guard', () => {
|
|
|
472
484
|
const filesAfter = fs.readdirSync(tempTestFolder).sort()
|
|
473
485
|
assert.deepStrictEqual(filesAfter, filesBefore)
|
|
474
486
|
|
|
487
|
+
// Dry run should report size data for HEIC conversion
|
|
488
|
+
assert.match(output, /Converted.*test\.heic.*KB.*→.*KB/i, 'Dry run should report HEIC conversion sizes')
|
|
489
|
+
|
|
475
490
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
476
491
|
})
|
|
477
492
|
|
package/package.json
CHANGED
package/src/utils.js
CHANGED
|
@@ -133,7 +133,7 @@ const compression = async (filename, dry, quiet = false) => {
|
|
|
133
133
|
} else if (fileSizeAfter > fileSizeBefore) {
|
|
134
134
|
color = 'blue'
|
|
135
135
|
status = 'Skipped'
|
|
136
|
-
details = 'already compressed more
|
|
136
|
+
details = 'already compressed more effectively'
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
logMessage(`${status} ${filename} (${details})`, dry, color, quiet)
|
|
@@ -227,30 +227,27 @@ const conversion = async (filename, dry, keepOriginal, quiet = false) => {
|
|
|
227
227
|
const inputBuffer = await fs.promises.readFile(filename)
|
|
228
228
|
const { width, height, data } = await decode({ buffer: inputBuffer })
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return 0
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Encode as lossless AVIF (pass the decoded view directly to avoid extra-byte issues)
|
|
230
|
+
// Encode as lossy AVIF—HEIC sources are already lossy (HEVC), so lossless
|
|
231
|
+
// re-encoding would inflate file sizes without any quality benefit
|
|
236
232
|
await sharp(data, { raw: { width, height, channels: 4 } })
|
|
237
|
-
.toFormat('avif', {
|
|
233
|
+
.toFormat('avif', { quality: 80, effort: 5 })
|
|
238
234
|
.toFile(avifPath)
|
|
239
235
|
|
|
240
236
|
const fileSizeAfter = await size(avifPath)
|
|
241
237
|
|
|
242
238
|
logMessage(`Converted ${filename} → ${path.basename(avifPath)} (${sizeReadable(fileSizeBefore)} → ${sizeReadable(fileSizeAfter)})`, dry, 'cyan', quiet)
|
|
243
239
|
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
if (dry) {
|
|
241
|
+
await retryFileOperation(() => fs.promises.unlink(avifPath))
|
|
242
|
+
return 0
|
|
243
|
+
}
|
|
246
244
|
|
|
247
|
-
// Delete original HEIC/HEIF file unless
|
|
245
|
+
// Delete original HEIC/HEIF file unless `--keep-heic` is set
|
|
248
246
|
if (!keepOriginal) {
|
|
249
247
|
await retryFileOperation(() => fs.promises.unlink(filename))
|
|
250
248
|
}
|
|
251
249
|
|
|
252
|
-
|
|
253
|
-
return saved
|
|
250
|
+
return fileSizeAfter < fileSizeBefore ? fileSizeBefore - fileSizeAfter : 0
|
|
254
251
|
|
|
255
252
|
} catch (err) {
|
|
256
253
|
// Clean up partial AVIF if it was created
|