Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
830 commits
Select commit Hold shift + click to select a range
34aca28
keep the color tester
ajslater Apr 29, 2025
0bf307c
v5.0.0-a0 (#87)
ajslater May 1, 2025
31cde36
v5.0.0
ajslater May 1, 2025
698f4b9
Merge branch 'main' into develop
ajslater May 1, 2025
152d2d1
move to basedpyright. much linting
ajslater May 4, 2025
a67c150
distribute with typing-extensions
ajslater May 5, 2025
af564fb
add pyright option
ajslater May 5, 2025
db47fd6
use tarfile isdir()
ajslater May 15, 2025
8261a0f
update deps. update py7zr-rc3. use 'x' mode in py7zr. remove unused p…
ajslater May 15, 2025
e1d28ef
update deps
ajslater Jun 10, 2025
67edc16
lint
ajslater Jun 10, 2025
c05b2cd
fix bad durations in gifs
ajslater Jun 10, 2025
07d7f66
Merge branch 'main' into develop
ajslater Jun 10, 2025
4df737d
Squashed commit of the following:
ajslater Jun 11, 2025
6008984
update deps
ajslater Jun 11, 2025
7607cdd
Squashed commit of the following:
ajslater Jun 12, 2025
c298e40
update deps. version 5.1.0
ajslater Jun 12, 2025
b4993a8
move sharp_yuv to webp out of base. gif2webp doesn't have it
ajslater Jun 12, 2025
a7db646
different gif size for linux
ajslater Jun 12, 2025
c183c95
Merge branch 'main' into develop
ajslater Jun 12, 2025
05634e2
update deps. lint for ruff 0.12.0
ajslater Jun 18, 2025
03125fa
fix verbosity
ajslater Jun 18, 2025
db895de
bump version
ajslater Jun 18, 2025
0102b47
Merge branch 'main' into develop
ajslater Jun 18, 2025
a793e0b
update deps
ajslater Jun 26, 2025
f15c80c
Squashed commit of the following:
ajslater Jun 26, 2025
12a4d6d
bump news
ajslater Jun 26, 2025
87dd6d2
remove mozjpeg from build.Dockerfile
ajslater Jun 26, 2025
9c696c8
Merge branch 'main' into develop
ajslater Jun 26, 2025
2d75c4c
mkdocs
ajslater Jul 18, 2025
b791de8
use dry run lockfile for updated
ajslater Jul 18, 2025
8ea942c
configure mkdocs properly
ajslater Jul 18, 2025
0205bfa
tilde versions for deps
ajslater Jul 18, 2025
13aa16a
add html section to readme
ajslater Jul 18, 2025
42db67c
update deps
ajslater Jul 25, 2025
3f60ea5
update deps
ajslater Jul 30, 2025
c9c6821
revise project urls
ajslater Jul 30, 2025
6b3785b
update deps add ty. typecheck with ty
ajslater Aug 6, 2025
2056625
uv_build
ajslater Aug 9, 2025
f8573fd
update scripts, pyproject, eslint, depts, fix lints
ajslater Aug 24, 2025
ce762d9
update deps
ajslater Aug 24, 2025
f2db131
update deps ane eslint config
ajslater Sep 13, 2025
757069f
update deps
ajslater Oct 9, 2025
74dcf7c
doesn't work with python 3.14
ajslater Oct 10, 2025
34a2990
fix builder dockerfile
ajslater Oct 10, 2025
49bb2de
copy debian sources on build properly for ci
ajslater Oct 10, 2025
6f70ef8
format
ajslater Oct 10, 2025
a79df7b
fix tests for new versions of external programs on darwin and somewha…
ajslater Oct 10, 2025
617bab3
fix formatting and use python 3.13 for build
ajslater Oct 10, 2025
0fb68a8
format
ajslater Oct 11, 2025
d821573
fix linux test file sizes
ajslater Oct 11, 2025
c336fa9
more test size changes for linux
ajslater Oct 11, 2025
8062c1f
remove test subdirs
ajslater Oct 11, 2025
0a2c35b
fix webp sizes in tests for linux
ajslater Oct 11, 2025
dbadc02
try to fix pre optimized webp on linux again
ajslater Oct 11, 2025
56ec580
fix sizes again
ajslater Oct 11, 2025
daa53df
one more size
ajslater Oct 11, 2025
aeadb55
Merge branch 'main' into develop
ajslater Oct 11, 2025
c0089a3
update deps
ajslater Oct 15, 2025
81bf34f
update deps
ajslater Oct 15, 2025
d00fa0f
add ty to makefile
ajslater Oct 15, 2025
640a7e1
delete deprecated pillow code
ajslater Oct 27, 2025
59f0670
support python 3.14, remove old pyright ignores
ajslater Oct 27, 2025
a0a5de1
bump version and news
ajslater Oct 27, 2025
fb66fbe
Merge branch 'main' into develop
ajslater Oct 27, 2025
1bfdde5
update deps
ajslater Nov 3, 2025
0d5fe8a
update deps
ajslater Nov 6, 2025
93515ed
update deps
ajslater Nov 8, 2025
104e02b
try to get duration info from detect format and PIL
ajslater Nov 8, 2025
b90d616
use webpmux for webp animated
ajslater Nov 8, 2025
9563570
update deps, update pytest config
ajslater Nov 9, 2025
cba2a31
change container test names
ajslater Nov 9, 2025
951fe2c
fix PIL jpeg conversion size for mpo test
ajslater Nov 9, 2025
1d67030
replace old test animated webp with one without dropped frames
ajslater Nov 9, 2025
449e7cd
enable im2webpanimatedlossless permanently enable webpumx, adjust fil…
ajslater Nov 9, 2025
35dd24c
fix adding info to correct handler clases constructor
ajslater Nov 9, 2025
a9e2371
add icecream importer
ajslater Nov 9, 2025
f1a9feb
bump news and version
ajslater Nov 9, 2025
4463b60
more news
ajslater Nov 9, 2025
d377684
simplify run_ext_fs for webp no longer needing output_path. compact a…
ajslater Nov 9, 2025
d5c891e
change webp arg order
ajslater Nov 9, 2025
0528786
use python secure tmpdir for webp temporary files
ajslater Nov 9, 2025
c9f4948
fix news version number
ajslater Nov 9, 2025
a028fad
fix test webp sizesfor linux
ajslater Nov 9, 2025
801fcd6
Merge branch 'main' into develop
ajslater Nov 9, 2025
71bcf40
update deps
ajslater Nov 23, 2025
d798acb
fix method and variable speling
ajslater Dec 1, 2025
8c22066
type checking fixes
ajslater Dec 1, 2025
0b13666
reduct complexity
ajslater Dec 1, 2025
d659d5d
complexipy & dep groups
ajslater Dec 19, 2025
ae0842d
reduce complexity of create handler even more
ajslater Dec 19, 2025
f484521
clean now cleans all caches
ajslater Dec 19, 2025
dbbc9be
update deps
ajslater Dec 19, 2025
2df0862
fix image format detection
ajslater Dec 20, 2025
296d77e
isolate handler external command tuples for debugging
ajslater Dec 20, 2025
d42dc18
fix extracting info from images
ajslater Dec 20, 2025
d4a314a
fix conversion to 7zr
ajslater Jan 13, 2026
eba8708
update deps
ajslater Jan 13, 2026
1cdec72
remove unused ty ignores
ajslater Jan 13, 2026
4dcd6b1
ty ignores
ajslater Jan 13, 2026
f063a91
update deps
ajslater Jan 21, 2026
0a248ee
update deps
ajslater Jan 26, 2026
98b1eb2
update deps
ajslater Jan 26, 2026
aa12963
update deps
ajslater Jan 29, 2026
1a56304
turn some const sets into tuples for type compatibility
ajslater Jan 30, 2026
a6b9589
use tuple instead of list
ajslater Jan 30, 2026
7361176
type get_config method. remove useless warning raise
ajslater Jan 30, 2026
ca8f27f
update deps
ajslater Jan 31, 2026
fa4194f
fix attrdict type
ajslater Jan 31, 2026
a423013
update deps
ajslater Feb 1, 2026
00e9fed
fix mpo test
ajslater Feb 1, 2026
ab0b41e
bump version and pin confuse, bump news
ajslater Feb 1, 2026
58cc71e
fix test script with test dependency group
ajslater Feb 1, 2026
76c2cb7
fix lint script with group
ajslater Feb 1, 2026
f7ec6bd
Merge branch 'main' into develop
ajslater Feb 1, 2026
f27c5aa
change confuse version specifier
ajslater Feb 1, 2026
0e309c9
use dockerfmt. update deps. ruff converts inline ifs to ors
ajslater Feb 10, 2026
178636b
update build dockerfile
ajslater Feb 10, 2026
6197292
update deps and add eslint plugins
ajslater Feb 10, 2026
b6e95cb
add mbake
ajslater Feb 10, 2026
494e171
remove unused ruff noqas
ajslater Feb 13, 2026
cf2ec87
Add types with pyre-check
ajslater Feb 13, 2026
a8fd71b
infer types. bump version and news. add py.typed
ajslater Feb 14, 2026
6e21b1a
Squashed commit of the following:
ajslater Feb 15, 2026
52f507f
update devenv and deps
ajslater Feb 15, 2026
7446ce1
update devenv
ajslater Feb 16, 2026
af8f417
update devs and devenv
ajslater Feb 16, 2026
a22eadb
update devenv
ajslater Feb 16, 2026
3ec7ade
add option for ignore archive entry mtimes
ajslater Feb 16, 2026
2866691
change option name
ajslater Feb 17, 2026
b8c0518
update devenv add missing ci script
ajslater Feb 17, 2026
dd08d8f
fix script hashbang
ajslater Feb 17, 2026
cf62dcc
move docker-compose to compose remove ignored version
ajslater Feb 17, 2026
2a6983c
try running script with simpler sh
ajslater Feb 17, 2026
739d518
add debug command for circlecir persist
ajslater Feb 17, 2026
56a9c39
chown dist before store
ajslater Feb 17, 2026
24fa5e6
speling
ajslater Feb 17, 2026
d384d30
Merge branch 'main' into develop
ajslater Feb 17, 2026
16d9325
update devenv
ajslater Feb 25, 2026
3963900
remove cruft
ajslater Feb 25, 2026
00cc86a
update makefile
ajslater Feb 25, 2026
7f574a7
update deps
ajslater Feb 26, 2026
b2f0c27
update devenv
ajslater Mar 2, 2026
87ef503
update devenv
ajslater Mar 12, 2026
9132a50
update devenv
ajslater Mar 14, 2026
a3fd72c
update devenv
ajslater Mar 14, 2026
0d66974
update devevn
ajslater Mar 15, 2026
ca108f9
update devenv
ajslater Mar 22, 2026
ddbe5af
update devenv & deps
ajslater Mar 31, 2026
101a9c6
update deps
ajslater Apr 1, 2026
26ee70f
update devevn & deps
ajslater Apr 8, 2026
6b5e88b
Squashed commit of the following:
ajslater Apr 8, 2026
036638b
dockerfile becomes builder
ajslater Apr 8, 2026
ba48a21
update devenv
ajslater Apr 8, 2026
84757ef
Squashed commit of the following:
ajslater Apr 10, 2026
9b12ef5
add cancelled if to gha steps
ajslater Apr 10, 2026
9a18e57
fix gha file cancelled() function
ajslater Apr 10, 2026
dbf52cd
install tool for tests
ajslater Apr 10, 2026
9212076
update devenv
ajslater Apr 10, 2026
73bb1df
Merge branch 'main' into develop
ajslater Apr 10, 2026
3242a0c
remove circleci remnamnts
ajslater Apr 10, 2026
13d110b
format dockerfile
ajslater Apr 10, 2026
86ee7e4
add a set_do_repack() method for the upcoming scheduler to use
ajslater Apr 10, 2026
456d0d2
for safety have buffered objects write to the tempfile before replaci…
ajslater Apr 10, 2026
275ce8b
Scheduler (#103)
ajslater Apr 12, 2026
b534686
Merge branch 'main' into develop
ajslater Apr 12, 2026
ad78506
treestamps 3.0
ajslater Apr 13, 2026
31ee42f
bump version and news
ajslater Apr 13, 2026
ea1d70d
Merge branch 'main' into develop
ajslater Apr 13, 2026
1756b57
update devenv. convert to bun
ajslater Apr 16, 2026
96d1a1b
Righttyper typing & corrections (#106)
ajslater Apr 16, 2026
413bf20
fix bun install on docker and gha compose
ajslater Apr 19, 2026
1091b2e
remove package-lock.json
ajslater Apr 19, 2026
f6404e4
update devenv
ajslater Apr 19, 2026
3ebaeb2
only copy bun deps first for dockerfile
ajslater Apr 19, 2026
95cf1d8
update devenv
ajslater Apr 19, 2026
edcae13
update devenv
ajslater Apr 19, 2026
e3dd03d
Fix BufferedReader pickling error when timestamps file exists (#107)
ajslater Apr 20, 2026
d86fcc7
print exception traces
ajslater Apr 20, 2026
34c626a
update deps
ajslater Apr 20, 2026
a15203b
bump news and version
ajslater Apr 20, 2026
6bad1b5
install poppler utils in docker for pdf tests
ajslater Apr 20, 2026
c682b2a
update deps
ajslater Apr 20, 2026
79d5389
format dockerfile
ajslater Apr 20, 2026
93f101a
add bunx support
ajslater Apr 20, 2026
b8a0a8c
bump version and news to v6.2.0
ajslater Apr 20, 2026
923bc72
try with svgo in packages
ajslater Apr 21, 2026
4abe8d8
Merge branch 'main' into develop
ajslater Apr 21, 2026
4188151
Squashed commit of the following:
ajslater Apr 29, 2026
ffccaf2
fix pyright warnings
ajslater Apr 29, 2026
34b22a0
ty ignore
ajslater Apr 29, 2026
e3e725f
update treestamps
ajslater Apr 29, 2026
6b2f6fd
Merge branch 'main' into develop
ajslater Apr 29, 2026
cc0187e
fix MPO error for 6.3.1
ajslater Apr 29, 2026
51305ac
Migrate to confuse 2.2.0 by replacing AttrDict with PicoptSettings (#…
ajslater Apr 29, 2026
d3b1f36
Drop dead isinstance check; split Scheduler._submit_ready_job (#116)
ajslater Apr 29, 2026
79320eb
update deps
ajslater Apr 29, 2026
8dfdfaa
bump version and news to 6.4.0
ajslater Apr 29, 2026
bb4ebea
Merge branch 'main' into develop
ajslater Apr 29, 2026
afde2b6
update devenv
ajslater May 4, 2026
70f810d
Format argparse help with rich-argparse (#118)
ajslater May 5, 2026
0c95ec7
bump version and news to 6.5.0
ajslater May 5, 2026
72cdfe3
typecheck
ajslater May 5, 2026
c1fcddb
Defer animated WebP duration extraction to handler (#119)
ajslater May 5, 2026
4925375
bump news for animated deferall
ajslater May 5, 2026
c708dc0
Cache file header bytes on PathInfo for detectors (#120)
ajslater May 5, 2026
f77d701
bump news
ajslater May 5, 2026
4210bb2
Single Image.open in _extract_image_info; read metadata before verify…
ajslater May 5, 2026
cc7a519
bump news
ajslater May 5, 2026
6c94267
Merge branch 'main' into develop
ajslater May 5, 2026
7c0c320
Merge branch 'main' into develop
ajslater May 5, 2026
122834a
update devenv and fix typechecking
ajslater May 22, 2026
fa1cf05
remove use of prettier sh plugin'
ajslater May 22, 2026
01074bd
fix data loss when converting without external tools
ajslater May 27, 2026
7e415f0
doctor: show platform-specific install hints for missing tools
ajslater May 27, 2026
138efa9
decomplexify doctor
ajslater May 27, 2026
3c45b4b
update deps
ajslater May 27, 2026
5733bf1
bump version and news
ajslater May 27, 2026
840d71d
update devenv. remove old prettier sh plugin
ajslater May 27, 2026
322bde1
Merge branch 'main' into develop
ajslater May 27, 2026
b84c49b
replace pyoxipng with oxipng-pybind (#124)
ajslater May 28, 2026
f07d3b7
update deps
ajslater May 28, 2026
2600dcf
bump news and version to v6.5.2
ajslater May 28, 2026
828004f
Merge branch 'main' into develop
ajslater May 28, 2026
057bd9c
update deps
ajslater Jun 25, 2026
407a643
update deps
ajslater Jul 1, 2026
e22370c
Add memory-aware scheduling to prevent OOM on large archives
ajslater Jul 1, 2026
358aead
update deps and format
ajslater Jul 1, 2026
28ccb7a
Fix basedpyright and ty diagnostics
ajslater Jul 1, 2026
d3c1131
update devenv
ajslater Jul 1, 2026
3d8bf23
remove pytest-gitignore
ajslater Jul 2, 2026
05b880c
Fix critical data-loss bugs in file replacement and archive repacking
ajslater Jul 2, 2026
ed8ffb5
Reduce _select_pipeline_for_handler cognitive complexity
ajslater Jul 2, 2026
2128b45
Fix scheduler and walk error-path correctness
ajslater Jul 2, 2026
07f7029
Preserve archive metadata fidelity through repack
ajslater Jul 2, 2026
5d8be7a
Fix config layering, validation, and doctor accounting
ajslater Jul 2, 2026
9fda386
Fix webp lossless detection, png depth parsing, and handler edge cases
ajslater Jul 2, 2026
05c1f45
Make PDF and SVG optimization safer
ajslater Jul 2, 2026
2df08ac
Survive symlink loops and unreadable directories
ajslater Jul 2, 2026
65f6324
Revive in-archive entry skipping and timestamp consumption
ajslater Jul 3, 2026
482e802
Improve scheduler-thread and archive throughput
ajslater Jul 3, 2026
c86d469
Extract dir timestamping, wire staging cleanup, untangle reporter dis…
ajslater Jul 5, 2026
22cebb4
Split the webp plugin into a package
ajslater Jul 5, 2026
2a6e7ed
Deduplicate handler routing and document the PathInfo contract
ajslater Jul 5, 2026
d163ce6
Memoize tool probes
ajslater Jul 5, 2026
44ecc57
Add per-directory .picopt.yaml config files and config-writing flags
ajslater Jul 5, 2026
a2bcd82
Add fail-fast, cancellation, idempotency, and pixel-identity tests
ajslater Jul 5, 2026
136a435
Update NEWS for v6.6.0
ajslater Jul 5, 2026
b301776
Add missing v6.6.0 news items
ajslater Jul 5, 2026
2ee5a9e
version 6.6.0
ajslater Jul 5, 2026
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
76 changes: 76 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,81 @@
# 📰 Picopt News

## v6.6.0

### Fixes

- Files are now replaced atomically: a crash, kill, or full disk mid-write can
no longer destroy the original image.
- Archive members whose optimization fails are kept in the repacked archive;
previously they were silently dropped.
- Nested archives (e.g. a CBZ inside a ZIP) now optimize; previously they always
errored.
- Archives keep their comments and member order through repack; tar symlinks and
hardlinks survive; 7z member times are preserved; pre-1980 member times no
longer crash conversion; legacy non-UTF8 zip member names are repaired.
- Archive conversions compress each member by content; converting a rar, tar, or
7z no longer leaves every member stored uncompressed.
- Files that fail to optimize are retried on the next timestamped run;
previously directory timestamps covered them and they were skipped forever.
- One corrupt file inside an archive (e.g. a decompression bomb) no longer
aborts the entire run.
- Boolean options set in config files or environment variables are no longer
ignored.
- Requested archive conversions (e.g. CBR to CBZ) now happen even when the
contents are already optimized.
- Lossless WebPs with large metadata inside archives are no longer misdetected
as lossy.
- Owner-password-restricted PDFs keep their encryption; optimized PDF images
keep their color transform; more signed PDFs are detected and refused.
- svgo no longer strips viewBox, and keeps title/desc metadata unless
`--strip-metadata` is given.
- Conversions discarded for being bigger (and dry runs) are reported as skips,
not errors.
- `--preserve` no longer fails as a non-root user when it cannot change
ownership; it still restores permissions and modification time.
- Temporary files and animated-WebP frame directories are cleaned up when a tool
fails or a run is cancelled, instead of leaking to disk.
- `picopt doctor` no longer reports missing tools and exits nonzero on healthy
installs.
- Symlink loops, unreadable directories, and FIFOs no longer hang or crash the
walk.
- Bad `--memory-limit` and `--after` values abort with clean error messages;
timezone-aware `--after` values are honored; lowercase format lists are
accepted for `-x` and `-c`.
- `-I`/`--no-default-ignores` without `-i` no longer aborts at startup.
- Existing timestamps are invalidated once after upgrading because the timestamp
config check gained new keys; the first `-t` run re-examines already-optimized
trees.

### Features

- Per-directory `.picopt.yaml` config files: pin any setting to a directory
tree. Deeper directories win; environment variables and the command line still
override. Editing one re-processes its tree on timestamped runs.
- Write your invoked options to config files: `-w` to the user config, `-W` to a
`.picopt.yaml` in each target directory, `--write-config-file PATH` to an
explicit path.
- Memory-aware scheduling: picopt now estimates the peak memory of large
archives and limits how many run at once so big libraries (e.g. multi-GB comic
archives) no longer exhaust RAM and get the process OOM-killed. Tune the
budget with the new `--memory-limit` option (e.g. `--memory-limit 8G`), which
reads as an approximate peak-memory target; the default is two-thirds of
detected RAM.
- Incremental archive re-optimization: when a timestamped archive changes,
members older than its timestamp are skipped instead of re-optimized; `-E`
disables the member check. Timestamp files found inside archives are consumed
and removed on repack.

### Performance

- Archive member format detection runs in parallel workers instead of
serializing the scheduler.
- Solid 7z archives extract in one pass instead of decompressing from the start
for every member.
- The progress pre-scan is skipped in quiet mode, less image data is shipped
between processes, and scheduling under a memory limit no longer rescans the
whole queue on every completion.

## v6.5.2

- Update to latest oxipng v10
Expand Down
76 changes: 67 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,9 @@ When configured to convert GIFS to WebP, Animated GIFs are converted to WebP
with the [gif2webp](https://developers.google.com/speed/webp/docs/gif2webp)
binary if it exists. It is normally distributed as part of the webp package.

#### Experimental option

This experimental options is activated with an environment variable.

Set `PICOPT_ENABLE_IMG2WEBP=1` to make picopt optimize animated WebPs and
convert animated PNGs to webp with the
[img2webp](https://developers.google.com/speed/webp/docs/img2webp) binary. In my
experiments img2webp has performed worse than picopt's custom algorithm using
PIL & cwebp
Animated WebPs are optimized (and animated PNGs converted to WebP) with the
[img2webp](https://developers.google.com/speed/webp/docs/img2webp) binary when
it is available, falling back to picopt's internal algorithm using PIL & cwebp.

### SVG

Expand Down Expand Up @@ -243,6 +237,54 @@ Or you can install svgo with npm:
npm install -G svgo
```

## ⚙️ Configuration

Picopt layers configuration from these sources, lowest to highest priority:
packaged defaults, your user config file, a `-C CONFIG` file, per-directory
`.picopt.yaml` files (see below), environment variables, and command line
options. Every config file uses the same envelope:

<!-- eslint-skip -->

```yaml
picopt:
recurse: true
convert_to: [WEBP, CBZ]
```

### Per-directory `.picopt.yaml` files

A `.picopt.yaml` placed inside a target directory applies to that directory and
everything under it. Files are discovered from each processed file's directory
up to the command line target root (never above it), and deeper directories win
over shallower ones. Environment variables and command line options still win
over every directory file.

<!-- eslint-skip -->

```sh
printf 'picopt:\n convert_to: [CBZ]\n' > comics/.picopt.yaml
picopt -rx CBR,CBZ . # CBRs under comics/ convert; siblings don't
```

Any config key is accepted and validated, but run-scoped keys — `dry_run`,
`list_only`, `timestamps`, `after`, `jobs`, `memory_limit`, `fail_fast`,
`fail_fast_container`, `verbose`, and `paths` — are governed by the run-level
value; setting them in a directory file has no per-directory effect. When
timestamps (`-t`) are enabled, editing, adding, or removing any `.picopt.yaml`
re-processes its tree on the next run.

### Writing config files

- `-w` / `--write-config` merges the options you invoked into your user config
file and then runs normally.
- `-W` / `--write-dir-config` writes them into a `.picopt.yaml` in each target
directory (a file target's parent).
- `--write-config-file PATH` writes them to an explicit path.

Existing keys and comments in the target file survive the merge. Run-mode flags
(`-d`, `-L`, verbosity) and the target paths are never persisted.

## ⌨️ Use Examples

Optimize all JPEG files in a directory:
Expand Down Expand Up @@ -288,6 +330,22 @@ timestamps.
picopt -rStc CBZ,WEBP -x TIFF,CBR,CBZ /Volumes/Media/Comics
```

Optimize the same comic library, but cap memory use so multi-GB archives don't
exhaust RAM:

<!-- eslint-skip -->

```sh
picopt -rStc CBZ,WEBP -x TIFF,CBR,CBZ --memory-limit 6G /Volumes/Media/Comics
```

picopt holds each archive plus its decompressed pages in memory while
optimizing, so large archives spread across many parallel workers can use a lot
of RAM. `--memory-limit` is an approximate peak-memory target (default:
two-thirds of detected RAM) that bounds how many large archives run at once — a
single archive bigger than the whole budget still runs, on its own — and `-j`
caps the number of parallel workers.

Optimize all files, but only JPEG format files:

<!-- eslint-skip -->
Expand Down
115 changes: 115 additions & 0 deletions bin/benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Benchmark picopt wall-clock time and peak RSS over synthetic corpora.

Builds corpora from the test fixtures (an archive-heavy one and an
image-heavy one), runs picopt as a subprocess, and reports wall time and
the peak resident set size of the whole process tree.

Usage:
uv run python bin/benchmark.py [--archives N] [--images N] [--keep]
"""

import argparse
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
from tempfile import mkdtemp

REPO_ROOT = Path(__file__).parent.parent
FIXTURES = REPO_ROOT / "tests" / "test_files"
CONTAINER_SRCS = ("test_cbz.cbz",)
IMAGE_SRCS = (
"test_png.png",
"test_jpg.jpg",
"test_gif.gif",
"test_webp_lossless.webp",
"test_animated_gif.gif",
)
# /usr/bin/time peak-RSS line: bytes on darwin (-l), kilobytes on linux (-v).
_IS_DARWIN = sys.platform == "darwin"
_TIME_ARGS = ("/usr/bin/time", "-l" if _IS_DARWIN else "-v")
_RSS_RE = re.compile(
r"^\s*(\d+)\s+maximum resident set size"
if _IS_DARWIN
else r"Maximum resident set size[^:]*:\s*(\d+)",
re.MULTILINE,
)
_RSS_DIVISOR = 1024 * 1024 if _IS_DARWIN else 1024


def _build_corpus(root: Path, n_archives: int, n_images: int) -> None:
archive_dir = root / "archives"
archive_dir.mkdir(parents=True)
for i in range(n_archives):
src = FIXTURES / "containers" / CONTAINER_SRCS[i % len(CONTAINER_SRCS)]
shutil.copy(src, archive_dir / f"comic_{i:04d}.cbz")
image_dir = root / "images"
image_dir.mkdir(parents=True)
for i in range(n_images):
src = FIXTURES / "images" / IMAGE_SRCS[i % len(IMAGE_SRCS)]
shutil.copy(src, image_dir / f"img_{i:04d}{src.suffix}")


def _run(args: tuple[str, ...], target: Path) -> tuple[float, int, int]:
"""Run picopt over target; return (seconds, peak_rss_mib, returncode)."""
start = time.perf_counter()
proc = subprocess.run( # noqa: S603
(
*_TIME_ARGS,
sys.executable,
"-c",
"from picopt.cli import main; main()",
*args,
str(target),
),
capture_output=True,
cwd=REPO_ROOT,
check=False,
)
elapsed = time.perf_counter() - start
stderr = proc.stderr.decode(errors="replace")
match = _RSS_RE.search(stderr)
peak_mib = int(match.group(1)) // _RSS_DIVISOR if match else -1
if proc.returncode:
print(proc.stdout.decode(errors="replace")[-2000:])
print(stderr[-2000:])
return elapsed, peak_mib, proc.returncode


def main() -> None:
"""Build corpora and run the benchmark scenarios."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--archives", type=int, default=100)
parser.add_argument("--images", type=int, default=300)
parser.add_argument(
"--keep", action="store_true", help="Keep the corpus dir afterwards"
)
opts = parser.parse_args()

root = Path(mkdtemp(prefix="picopt-bench-"))
_build_corpus(root, opts.archives, opts.images)
print(f"corpus: {root} ({opts.archives} archives, {opts.images} images)")

scenarios = (
("archives cold", ("-rx", "CBZ,PNG,JPEG,GIF,WEBP"), root / "archives"),
("images cold", ("-rx", "PNG,JPEG,GIF,WEBP"), root / "images"),
("images quiet", ("-q", "-rx", "PNG,JPEG,GIF,WEBP"), root / "images"),
)
try:
print(f"{'scenario':<16} {'seconds':>8} {'peak MiB':>9} {'rc':>3}")
for name, args, target in scenarios:
elapsed, peak, rc = _run(args, target)
print(f"{name:<16} {elapsed:>8.2f} {peak:>9} {rc:>3}")
finally:
if opts.keep:
print(f"kept: {root}")
else:
shutil.rmtree(root, ignore_errors=True)


if __name__ == "__main__":
main()
12 changes: 6 additions & 6 deletions bin/roman.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def iter_files(path_strs: Sequence[str], spec: PathSpec) -> Generator[Path]:
for path_str in path_strs:
path = Path(path_str)
if not path.exists():
print(f"👎 Path does not exist: {path}", file=sys.stderr) # noqa: T201
print(f"👎 Path does not exist: {path}", file=sys.stderr)
sys.exit(2)

root = path.resolve()
Expand Down Expand Up @@ -163,13 +163,13 @@ def _parse_ignore_file(args: Namespace) -> PathSpec:
if args.ignore_file:
ignore_path = Path(args.ignore_file)
if not ignore_path.is_file():
print(f"👎 Can't read ignore file: {ignore_path}", file=sys.stderr) # noqa: T201
print(f"👎 Can't read ignore file: {ignore_path}", file=sys.stderr)
sys.exit(2)

try:
return build_ignore_spec(ignore_path)
except OSError as exc:
print(f"👎 Failed to parse ignore file: {exc}", file=sys.stderr) # noqa: T201
print(f"👎 Failed to parse ignore file: {exc}", file=sys.stderr)
sys.exit(2)


Expand All @@ -188,17 +188,17 @@ def main() -> None:
continue

if not has_description_comment(line2):
print(f"🔪 {path}") # noqa: T201
print(f"🔪 {path}")
offenders.append(path)

if offenders:
print( # noqa: T201
print(
f"\n{len(offenders)} script(s) missing a description comment.",
file=sys.stderr,
)
sys.exit(1)

print("👍") # noqa: T201
print("👍")


if __name__ == "__main__":
Expand Down
Loading