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 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 (opt-in via `--heic-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 lossless AVIF. The conversion decodes HEIC/HEIF pixel data and re-encodes it as lossless AVIF, then runs the result through Image Guard’s compression pipeline. Original HEIC/HEIF files are deleted after conversion by default.
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 aggressively than the new result, and was therefore skipped, too.
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
 
@@ -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
- assert.ok(fs.statSync(avifFile).size > 0, 'AVIF file should not be empty')
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
@@ -46,5 +46,5 @@
46
46
  "test": "node --test bin/image-guard.test.js"
47
47
  },
48
48
  "type": "module",
49
- "version": "5.2.0"
49
+ "version": "5.2.2"
50
50
  }
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 aggressively'
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
- if (dry) {
231
- logMessage(`Converted ${filename} → ${path.basename(avifPath)}`, dry, 'cyan', quiet)
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', { effort: 5, lossless: true })
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
- // Compress the resulting AVIF through the standard compression pipeline
245
- const saved = await compression(avifPath, dry, quiet)
240
+ if (dry) {
241
+ await retryFileOperation(() => fs.promises.unlink(avifPath))
242
+ return 0
243
+ }
246
244
 
247
- // Delete original HEIC/HEIF file unless --keep-heic is set (deferred until after compression succeeds)
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
- // Return compression savings only (conversion size change is informational)
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