Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
*
!lib/
!.optimiztrc.cjs
!cli.js
!convert.js
!index.js
!LICENSE
!optimize.js
!package*.json
!package.json
!package-lock.json
!packages/
!packages/optimizt/
!packages/optimizt/**
!packages/optimizt-sharp/
!packages/optimizt-sharp/**
!packages/optimizt-guetzli/
!packages/optimizt-guetzli/**
!packages/optimizt-gifsicle/
!packages/optimizt-gifsicle/**
packages/optimizt-sharp/dist/
packages/optimizt/vendor/
node_modules/
coverage/
5 changes: 4 additions & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#!/bin/sh

npx lint-staged
# lint-staged resolves each workspace's eslint.config.js from its own directory,
# so it must run once per workspace rather than from the repository root.
npx lint-staged --cwd packages/optimizt
npx lint-staged --cwd packages/optimizt-sharp
2 changes: 1 addition & 1 deletion .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/sh

npm test && npm run license-check
npm run check
35 changes: 31 additions & 4 deletions .github/workflows/ci-nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,38 @@ permissions:
contents: read

jobs:
provenance:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
persist-credentials: false
- id: changes
run: |
if git diff --name-only "${{ github.event.pull_request.base.sha }}...HEAD" | grep -Eq '^packages/optimizt-(gifsicle|guetzli)/'; then
echo "required=true" >> "$GITHUB_OUTPUT"
else
echo "required=false" >> "$GITHUB_OUTPUT"
fi
- name: Use Node.js
if: steps.changes.outputs.required == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24.18.0
- if: steps.changes.outputs.required == 'true'
run: npm ci
- if: steps.changes.outputs.required == 'true'
run: npm run provenance:setup
- if: steps.changes.outputs.required == 'true'
run: npm run provenance:verify

tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [22.22.1, latest]
node-version: [24.18.0, latest]
os: [
ubuntu-latest, # x64
ubuntu-24.04-arm, # arm64
Expand All @@ -22,7 +49,7 @@ jobs:
]
include:
- os: windows-latest
node-version: 22.22.1
node-version: 24.18.0
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
Expand All @@ -34,5 +61,5 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Validate packages
run: npm run check
4 changes: 2 additions & 2 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
id: check
run: |
LATEST_VERSION=$(npm view . version)
CURRENT_VERSION=$(node -p "require('./package.json').version")
CURRENT_VERSION=$(node -p "require('./packages/optimizt/package.json').version")
if [ "$LATEST_VERSION" != "$CURRENT_VERSION" ]; then
echo "Version changed from $LATEST_VERSION to $CURRENT_VERSION"
echo "VERSION_CHANGED=true" >> "$GITHUB_OUTPUT"
Expand All @@ -39,4 +39,4 @@ jobs:
run: npm ci
- name: Publish to npm
if: ${{ steps.check.outputs.VERSION_CHANGED == 'true' }}
run: npm publish --access public
run: npm publish --workspace @343dev/optimizt --access public
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
coverage/
node_modules/
.cache/
.DS_Store
packages/optimizt-sharp/dist/
packages/optimizt/vendor/

artifacts/
10 changes: 6 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ WORKDIR /app

COPY . .

ENV NODE_ENV="production"

RUN npm ci \
&& npm link \
RUN npm ci --include=dev \
&& npm run build \
&& npm prune --omit=dev \
&& npm link --workspace @343dev/optimizt \
&& npm cache clean --force

ENV NODE_ENV="production"

WORKDIR /src

ENTRYPOINT ["optimizt"]
186 changes: 9 additions & 177 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,185 +1,17 @@
# @343dev/optimizt
# Optimizt monorepo

<img align="right" width="176" height="176"
alt="Optimizt logo: OK hand sign with Mona Lisa image between the fingers"
src="./docs/logo.png">
This repository contains four workspaces:

[![NPM Downloads](https://img.shields.io/npm/dw/%40343dev%2Foptimizt)](https://www.npmjs.com/package/@343dev/optimizt)
[![npm](https://img.shields.io/npm/v/@343dev/optimizt.svg)](https://www.npmjs.com/package/@343dev/optimizt)
[![Docker](https://img.shields.io/docker/v/343dev/optimizt?label=Docker)](https://hub.docker.com/r/343dev/optimizt)

Optimizt is a command-line tool for compressing images and converting them to AVIF and WebP.

It compresses PNG, JPEG, GIF, and SVG files. You can process individual files or entire directories, including their subdirectories.

## Why Optimizt?

I built Optimizt while working on images for the web. I kept switching between tools to compress images and convert formats. I wanted one command-line tool for both.

## Usage

Install Optimizt:

```sh
npm install -g @343dev/optimizt
```

Optimize an image in place:

```sh
optimizt path/to/picture.jpg
```

This command can replace the original file. To keep it, use `--output` with a separate, existing directory.

## Command-line options

| Option | Description |
| --- | --- |
| `--avif` | Create AVIF versions of images. |
| `--webp` | Create WebP versions of images. |
| `-f, --force` | Replace existing AVIF and WebP versions. |
| `-l, --lossless` | Use the lossless profile. JPEG compression is still lossy. |
| `-v, --verbose` | Show detailed output, including skipped files. |
| `-c, --config <path>` | Use a custom configuration file instead of the bundled defaults. |
| `-o, --output <path>` | Write results to an existing directory. |
| `-p, --prefix <text>` | Add a prefix to output file names. |
| `-s, --suffix <text>` | Add a suffix to output file names. |
| `-V, --version` | Show the version. |
| `-h, --help` | Show help. |

## Examples

```bash
# optimize a single image
optimizt path/to/picture.jpg

# optimize multiple images with the lossless profile
optimizt --lossless path/to/picture.jpg path/to/another/picture.png

# recursively create AVIF and WebP versions for all images in a directory
optimizt --avif --webp path/to/directory

# recursively optimize JPEG files in the current directory
find . -iname \*.jpg -exec optimizt {} +
```

## Lossy and lossless modes

### Lossy (default)

Optimizt uses lossy compression to reduce file size, with some loss of image quality. SVG settings are the same in both modes.

### Lossless (`--lossless`)

With the bundled settings:

- **AVIF/WebP/PNG/GIF**: Uses lossless compression.
- **JPEG**: Uses [Guetzli](https://github.com/google/guetzli) for higher-quality lossy compression. Despite the profile name, JPEG compression is not lossless, and repeated optimization may reduce quality.
- **SVG**: Uses the same settings as the default mode.

Files may be larger than those produced by the lossy profile.

## How files are written

Optimizt writes each result to a temporary file in the destination directory and synchronizes it to disk. It then replaces the destination file in one step, so other programs see either the old file or the complete new file, never a partially written image.

Optimizt does not restore files it has already replaced if the run fails or stops.

- When replacing a file, Optimizt keeps its permissions. It also keeps the file's owner if the operating system allows it.
- Optimizt does not replace files with multiple hard links, because the other links would still point to the old file. Use a separate output path instead.
- When optimizing a symbolic link in place, Optimizt replaces the file it points to and keeps the link itself.
- When converting an image through a symbolic link, Optimizt writes the AVIF or WebP version next to the link. Use `--output` to write it to a different directory.

> [!NOTE]
> Atomic replacement protects against partially written files. It does not guarantee that the directory entry itself survives sudden power loss, and it does not preserve file timestamps.

## Configuration

Optimizt uses:

- [sharp](https://github.com/lovell/sharp) for [JPEG](https://sharp.pixelplumbing.com/api-output#jpeg), [PNG](https://sharp.pixelplumbing.com/api-output#png), [WebP](https://sharp.pixelplumbing.com/api-output#webp), and [AVIF](https://sharp.pixelplumbing.com/api-output#avif).
- [svgo](https://github.com/svg/svgo) for SVG.
- [gifsicle](https://github.com/kohler/gifsicle) for GIF.

For JPEG in the lossless profile, Optimizt uses [Guetzli](https://github.com/google/guetzli) instead of sharp for the final compression step.

See [.optimiztrc.cjs](./.optimiztrc.cjs) for the default settings. For available options and accepted values, check the documentation for each image processor linked above.

Use `--config path/to/.optimiztrc.cjs` to load your own settings instead of the bundled configuration. Without `--config`, Optimizt looks for `.optimiztrc.cjs` in the current directory, then in each parent directory. It uses the first file it finds, or the bundled defaults if it finds none.

> [!WARNING]
> `.optimiztrc.cjs` is executable code and runs with your permissions. This applies whether Optimizt finds the file automatically or you select it with `--config`. Use Optimizt only in repositories you trust.
- [`@343dev/optimizt`](packages/optimizt) — the published Optimizt command-line image optimizer.
- [`@343dev/optimizt-sharp`](packages/optimizt-sharp) — a private build workspace that reproducibly generates the WASM-only Sharp distribution vendored into Optimizt.
- [`@343dev/optimizt-guetzli`](packages/optimizt-guetzli) — the private Guetzli WebAssembly build and provenance workspace.
- [`@343dev/optimizt-gifsicle`](packages/optimizt-gifsicle) — the private Gifsicle WebAssembly build and provenance workspace.

## Development

After cloning the repository, enable Git hooks once:

```sh
npm run enable-git-hooks
npm ci
npm run check
```

This tells Git to use the hooks stored in the [.githooks](./.githooks) directory.

## Docker

### Pre-built image

```bash
# pull the latest image
docker pull 343dev/optimizt

# pull a specific version
docker pull 343dev/optimizt:9.0.2
```

### Build the image

```bash
# clone the repository
git clone https://github.com/343dev/optimizt.git
cd optimizt

# build the image
docker build --tag 343dev/optimizt .
```

Alternatively:

```bash
# build directly from GitHub
# ignores .dockerignore (see: https://github.com/docker/cli/issues/2827)
docker build --tag 343dev/optimizt https://github.com/343dev/optimizt.git
```

### Run the container

```bash
# mount the current directory at /src in the container
docker run --rm --user "$(id -u):$(id -g)" --volume "$(pwd):/src" 343dev/optimizt --webp ./image.png
```

## Integrations

Use these guides to run Optimizt from your editor or GitHub Actions:

- [JetBrains IDEs](./docs/jetbrains.md)
- [Visual Studio Code](./docs/vscode.md)
- [Sublime Text 3](./docs/sublime-text.md)
- [GitHub Actions](./docs/github.md)

## Articles

- [anuwong.com](https://anuwong.com/blog/2023-08-21-save-tons-of-gbs-with-optimizt/). An article in Thai about compressing files before uploading them.
- [Linux Format, Issue 277 (July 2021)](https://www.linuxformat.com/archives?issue=277#:~:text=Kitchen%20Tales%2C%20zFRAG%2C-,Optimizt,-and%20SingleFileZ.). An article about reducing image file sizes with Optimizt.

## Credits

[Igor Garybaldi](http://pandabanda.com/) created the logo.

## Other projects

- [harold](https://github.com/343dev/harold) compares frontend project bundle sizes between snapshots from the command line.
- [jailbot](https://github.com/343dev/jailbot) wraps Docker containers and automatically mounts filesystem paths.
- [markdown-lint](https://github.com/343dev/markdown-lint) checks Markdown style with Prettier, Remark, and Typograf.
- [languagetool-node](https://github.com/343dev/languagetool-node) checks spelling and grammar from the command line with LanguageTool.
`npm run build` downloads the exact upstream Sharp tarball pinned by `packages/optimizt-sharp/package.json`, verifies its integrity, generates the wrapper, and copies all three codec runtimes into the ignored `packages/optimizt/vendor` directory. Only `@343dev/optimizt` is published.
2 changes: 1 addition & 1 deletion docs/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '22.22.1'
node-version: '24.18.0'

- run: npm install --global @343dev/optimizt

Expand Down
Loading
Loading