Skip to content

Commit

Permalink
Merge pull request #129 from j9t/refactor/deps-continued
Browse files Browse the repository at this point in the history
Dependency modernization (continued)
  • Loading branch information
j9t authored Oct 9, 2024
2 parents 7537209 + 7ef3e1d commit 6bfa761
Show file tree
Hide file tree
Showing 14 changed files with 1,510 additions and 4,753 deletions.
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Imagemin Guard

(This project has been 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. Imagemin Guard supports two additional file formats—WebP and AVIF—, comes with improved code and documentation, and is being maintained—including automatically updated dependencies.)
(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. Imagemin Guard supports two additional file formats—WebP and AVIF—, comes with improved code and documentation, and is being maintained. For this reason, it’s not based on any imagemin packages anymore.)

Imagemin Guard takes care of lossless compression of your images, to help you avoid bloat in your repositories. It’s an extension of [imagemin](https://www.npmjs.com/package/imagemin) and a fork of [imagemin-merlin (Merlin)](https://github.com/sumcumo/imagemin-merlin) that makes it convenient and safe to automatically compress JPG, PNG, GIF, WebP, and AVIF images.
Imagemin 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 JPG, PNG, GIF, WebP, and AVIF images.

It’s convenient because setup is simple. Install, run, add hook, done.

It’s safe because compression happens _losslessly_. Therefore, no worries about forgetting to compress images, but no worries about sacrificing too much quality, either. (You can take care of additional optimizations by yourself or through other tooling.)
It’s as safe as possible because compression happens losslessly (to be precise, near-lossless for PNG and GIF images). That allows you to stop worrying about forgetting to compress images, but also about sacrificing too much quality. (You can take care of additional optimizations by yourself or through other tooling.)

## Installation and Use

Expand Down Expand Up @@ -45,15 +45,15 @@ git commit -m "feat: add Husky pre-commit hook for Imagemin Guard"

### Parameters

`--dry` allows to run Imagemin Guard in “dry mode.” All changed files can then be inspected under `/tmp/imagemin-guard`.
* `--dry` allows you to run Imagemin Guard in “dry mode.” All changes are shown in the terminal.

`--ignore` allows to specify paths to be ignored (as in `--ignore=example,test`). Multiple paths have to be separated by comma. (Files and paths specified in .gitignore files are generally ignored.)
* `--ignore` allows you to specify paths to be ignored (as in `--ignore=example,test`). Multiple paths must be separated by commas. (Files and paths specified in .gitignore files are generally ignored.)

`--staged` (recommended with automated use) triggers a mode that watches JPG, PNG, GIF, WebP, and AVIF files in `git diff` and only compresses those files—that approach makes Imagemin Guard more efficient in operation.
* `--staged` (recommended with automated use) triggers a mode that watches JPG, PNG, GIF, WebP, and AVIF files in `git diff` and only compresses those files—that approach makes Imagemin Guard more efficient in operation.

## How Does the Output Look Like?
## What Does the Output Look Like?

Roughly like this (the screenshot shows an early version of Merlin):
Roughly like this (the screenshot is still based on an early version of Merlin):

![Screenshot of Imagemin Guard’s predecessor, Merlin, in operation.](https://raw.githubusercontent.com/j9t/imagemin-guard/master/media/output.png)

Expand All @@ -63,33 +63,31 @@ Roughly like this (the screenshot shows an early version of Merlin):

## How Does Imagemin Guard Work?

Imagemin Guard is a Node script that puts a wrapper around [imagemin-cli](https://www.npmjs.com/package/imagemin-cli) and the packages [imagemin-mozjpeg](https://www.npmjs.com/package/imagemin-mozjpeg), [imagemin-optipng](https://www.npmjs.com/package/imagemin-optipng), [imagemin-gifsicle](https://www.npmjs.com/package/imagemin-gifsicle), [imagemin-webp](https://www.npmjs.com/package/imagemin-webp), and [imagemin-avif](https://www.npmjs.com/package/imagemin-avif).
Imagemin Guard is a Node script currently using [sharp](https://www.npmjs.com/package/sharp) and [gifsicle](https://www.npmjs.com/package/gifsicle) under the hood.

Automated compression works by monitoring whether a given change list includes any JPGs, PNGs, GIFs, WebPs, or AVIFs. It’s initiated by a Git hook. Only those images are compressed where there is an improvement. The compressed images can then be committed to the underlying repository.
Automated compression works by monitoring whether a given [change list](https://webglossary.info/terms/change-list/) includes any JPGs, PNGs, GIFs, WebPs, or AVIFs. It’s initiated by a Git hook. Only those images are compressed where there is an improvement. The compressed images can then be committed to the underlying repository.

Through this approach, though still glossed over here, Imagemin Guard makes up for what’s missing or complicated in imagemin and related packages, namely easy, riskless, automated, resource-friendly in-repo optimization.
Through this approach, though still glossed over here, Imagemin Guard makes up for what’s missing or complicated in other packages, namely easy, near-riskless, automatable, resource-friendly in-repo optimization.

## Why Use Imagemin Guard?

(This is a paraphrased remainder of earlier documentation, left in case it makes anything more clear ☺️)

You _can_ use Imagemin Guard if you need a simple, automatable, robust solution to compress images and to keep the compressed results in your repository (instead of only in the production environment).

That last piece is important, as Imagemin Guard compresses losslessly, so there’s no risk that images suffer from quality issues after processing. With this kind of defensive base compression, there’s no reason, and only advantages, to feed back compressed graphics into the respective source repository.
That last piece is important, as Imagemin Guard compresses near-losslessly, so there’s little risk that images suffer from quality issues after processing. With this kind of defensive base compression, there’s no reason, and only advantages, to feed back compressed graphics into the respective source repository.

## What Does Imagemin Guard _Not_ Do?

Imagemin Guard is no substitute for image fine-tuning and micro-optimization. That is difficult to do in an automated fashion, because this type of compression requires [balancing quality and performance](https://meiert.com/en/blog/understanding-image-compression/) and is context-dependent. In its most extreme form, when maximum quality at maximum performance is required from each graphic, micro-optimization is even hard to do manually.
Imagemin Guard is no substitute for image fine-tuning and micro-optimization. That’s difficult to do in an automated fashion, because this type of compression requires [balancing quality and performance](https://meiert.com/en/blog/understanding-image-compression/) and is context-dependent. In its most extreme form, when maximum quality at maximum performance is required from each graphic, micro-optimization is even challenging to do manually.

The point is: Micro-optimization still needs to be taken care of through other means, whether manually or through tools (well including other packages from the [imagemin family](https://github.com/imagemin)). Imagemin Guard just solves the problem that images are checked in or go live that are not compressed _at all_.

## What’s Next?

Following [Merlin](https://github.com/sumcumo/imagemin-merlin), which Imagemin Guard is based on, new features may include the option to configure the underlying imagemin plugins (somewhat prepared but not completed yet), or supporting projects in which the project’s .git folder is not at the same level as its package.json (at the moment, automatic mode doesn’t work in these cases).
There are a few ideas, like adding light SVG support, or ensuring compatibility with projects in which the project’s .git folder is not at the same level as its package.json (currently, automatic mode doesn’t work in these cases).

Also, as some imagemin packages are not maintained at the moment, it may be useful or necessary to change to a different compression solution, like [Sqoosh](https://github.com/GoogleChromeLabs/squoosh). The situation is being monitored. Ideally, any change here will only happen under the hood.

Thoughts or suggestions? Please [file an issue](https://github.com/j9t/imagemin-guard/issues/new) or send a pull request (some code still needs care). Thank you!
Feedback is appreciated: Please [file an issue](https://github.com/j9t/imagemin-guard/issues/new) or send a pull request. Thank you!

## License

Expand Down
46 changes: 37 additions & 9 deletions bin/imagemin-guard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const testFolder = path.join(__dirname, '../media/test')
const testFolderGit = path.join(__dirname, '../media/test-git')
const imageminGuardScript = path.join(__dirname, '../bin/imagemin-guard.js')
// Crutch to avoid files like .DS_Store to sneak in
// @@ Consolidate with package, to keep image definitions DRY
// @@ Consolidate with package, to keep image definitions DRY (once there’s better Jest ESM support?)
const allowedFileTypes = ['avif', 'gif', 'jpg', 'jpeg', 'png', 'webp']

// Function to copy files
Expand All @@ -22,20 +22,31 @@ function copyFiles(srcDir, destDir) {
}

// Function to check if images are compressed
const ignoreFiles = ['test#corrupt.gif']

function areImagesCompressed(dir) {
const uncompressedFiles = []
const allCompressed = fs.readdirSync(dir).every(file => {
if (ignoreFiles.includes(file)) {
// console.info(`Ignoring file: ${file}`)
return true
}
const ext = path.extname(file).slice(1)
if (!allowedFileTypes.includes(ext)) return true
const filePath = path.join(dir, file)
const originalFilePath = path.join(testFolder, file)
const originalStats = fs.statSync(originalFilePath)
const compressedStats = fs.statSync(filePath)
const isCompressed = compressedStats.size < originalStats.size
if (!isCompressed) {
uncompressedFiles.push(file)
try {
const originalStats = fs.statSync(originalFilePath)
const compressedStats = fs.statSync(filePath)
const isCompressed = compressedStats.size < originalStats.size
if (!isCompressed) {
uncompressedFiles.push(file)
}
return isCompressed
} catch (err) {
console.warn(`Skipping corrupt file: ${file}`)
return true
}
return isCompressed
})
return { allCompressed, uncompressedFiles }
}
Expand All @@ -55,7 +66,7 @@ function areImagesAlreadyCompressed(dir) {

describe('Imagemin Guard', () => {
beforeAll(() => {
// Backup original images
// Back up original images
copyFiles(testFolder, testFolderGit)
})

Expand All @@ -74,7 +85,6 @@ describe('Imagemin Guard', () => {
// Verify images are compressed
const { allCompressed, uncompressedFiles } = areImagesCompressed(testFolderGit)
if (uncompressedFiles.length > 0) {
// @@ Ensure all compressed files are listed
console.log('The following files were not compressed:', uncompressedFiles.join(', '))
}
expect(allCompressed).toBe(true)
Expand Down Expand Up @@ -106,4 +116,22 @@ describe('Imagemin Guard', () => {
}
expect(allCompressed).toBe(true)
})

test('Do not modify files in dry run', () => {
const originalStats = fs.readdirSync(testFolderGit).map(file => {
const filePath = path.join(testFolderGit, file)
return { file, stats: fs.statSync(filePath) }
})
execSync(`node ${imageminGuardScript} --dry`)
const newStats = fs.readdirSync(testFolderGit).map(file => {
const filePath = path.join(testFolderGit, file)
return { file, stats: fs.statSync(filePath) }
})
originalStats.forEach((original, index) => {
const newFile = newStats[index]
expect(newFile.file).toStrictEqual(original.file)
expect(newFile.stats.size).toStrictEqual(original.stats.size)
expect(newFile.stats.mtime).toStrictEqual(original.stats.mtime)
})
})
})
Binary file modified media/output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added media/test/test#corrupt.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added media/test/test.JPEG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified media/test/test.avif
Binary file not shown.
Binary file modified media/test/test.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified media/test/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified media/test/test.webp
Binary file not shown.
Loading

0 comments on commit 6bfa761

Please sign in to comment.