Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
839 commits
Select commit Hold shift + click to select a range
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
e5b58b7
Merge branch 'main' into develop
ajslater Jul 5, 2026
965bb04
Fix repeated "Optimizing formats" banner with -W and planted .picopt.…
ajslater Jul 5, 2026
bd0ba5d
bump version to 6.6.1
ajslater Jul 5, 2026
c3eb8b0
bump news
ajslater Jul 5, 2026
d373db6
Fix animated WebP optimization failing on zero-duration frames
ajslater Jul 6, 2026
b707ca0
update deps
ajslater Jul 6, 2026
1771f7f
Clear basedpyright typecheck warnings
ajslater Jul 6, 2026
8458275
fix news formattign
ajslater Jul 6, 2026
b71aa1b
Reduce cyclomatic complexity of four rank-C functions
ajslater Jul 6, 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
20 changes: 14 additions & 6 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# 📰 Picopt News

## v6.6.1

### Fixes

- Animated WebP images with zero-duration frames no longer fail to optimize.
- The `Optimizing formats` summary is no longer repeated once per directory when
using `-W` or per-directory `.picopt.yaml` files.

## v6.6.0

### Fixes
Expand Down Expand Up @@ -91,7 +99,7 @@
- Faster metadata extraction
- Defer animated image duration doublecheck to only WebP animated and only at
handler time. Speeds up other animated images.
- Use rich_argparse to format cli help.
- Use rich\_argparse to format cli help.

## v6.4.0

Expand Down Expand Up @@ -233,7 +241,7 @@
- Support converting losslessly converting MPO to JPEG (choose primary
image)
- Internal oxipng replaces external optipng for png compression.
- Better support for preserving EXIF, ICC_PROFILE, & XMP data across
- Better support for preserving EXIF, ICC\_PROFILE, & XMP data across
optimization and conversion.
- `--destroy-metadata` option becomes `--strip-metadata`
- `--near-lossless` option for lossless WebP.
Expand Down Expand Up @@ -276,7 +284,7 @@

## v3.3.1

- Upstream treestamps fixes crashes and check_config option.
- Upstream treestamps fixes crashes and check\_config option.

## v3.3.0

Expand Down Expand Up @@ -376,7 +384,7 @@
- EPub support. ePub contents are never converted.
- TIFF support
- Timestamps
- Timestamps are now kept in .picopt_treestamps.yaml files.
- Timestamps are now kept in .picopt\_treestamps.yaml files.
- Picopt will convert and clean up old style timestamps.
- Timestamps are now recorded after optimizing every image for each image
individually instead of directories, preserving progress.
Expand Down Expand Up @@ -426,7 +434,7 @@
## v1.6.3

- Don't use pngout on 16 bit pngs because it crashes
- Fix broken option -D --optimize_after
- Fix broken option -D --optimize\_after

## v1.6.2

Expand Down Expand Up @@ -488,7 +496,7 @@

- Print found picopt timestamps
- Lots of linting
- Fix jpeg_multithread flag
- Fix jpeg\_multithread flag
- moved tests out of picopt module

## v1.3.2
Expand Down
42 changes: 39 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"eslint-plugin-security": "^4.0.1",
"eslint-plugin-sonarjs": "^4.1.0",
"eslint-plugin-toml": "^1.4.0",
"eslint-plugin-unicorn": "^70.0.0",
"eslint-plugin-unicorn": "^71.0.0",
"eslint-plugin-yml": "^3.5.0",
"prettier": "^3.9.4",
"prettier-plugin-nginx": "^1.0.3",
Expand Down
6 changes: 4 additions & 2 deletions picopt/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,8 @@ def _build_config(
args: Namespace | None = None,
dir_config_files: tuple[Path, ...] = (),
modname: str = PROGRAM_NAME,
*,
print_summary: bool = False,
) -> Configuration:
"""
Build a fully-layered, normalized confuse Configuration.
Expand Down Expand Up @@ -421,7 +423,7 @@ def _build_config(
self._set_after(config_program)
self._set_memory_limit(config_program)
self._set_timestamps(config_program)
self.set_format_handler_map(config_program)
self.set_format_handler_map(config_program, print_summary=print_summary)
return config

@staticmethod
Expand All @@ -434,7 +436,7 @@ def get_config(
self, args: Namespace | None = None, modname: str = PROGRAM_NAME
) -> PicoptSettings:
"""Get the validated, frozen settings layered from defaults/env/args."""
config = self._build_config(args, modname=modname)
config = self._build_config(args, modname=modname, print_summary=True)
# Validate (via _config_to_settings) before persisting so a bad
# command line can't poison a config file. get_config passes no
# directory files, so -w output round-trips via -C unchanged.
Expand Down
75 changes: 47 additions & 28 deletions picopt/config/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,32 +136,18 @@ def _set_format_handler_stages(
if stages is not None:
handler_stages[handler_cls] = stages

def set_format_handler_map(self, config: Subview) -> None:
"""Probe handlers for the requested formats and store availability."""
all_format_strs = self._get_config_set(config, "formats", "extra_formats")
config["formats"].set(tuple(sorted(all_format_strs)))
convert_to = self._get_config_set(config, "convert_to")
# Write the upcased lists back so template validation accepts
# lowercase user input for -x and -c exactly as it does for -f.
if extra_formats := self._get_config_set(config, "extra_formats"):
config["extra_formats"].set(tuple(sorted(extra_formats)))
if convert_to:
config["convert_to"].set(tuple(sorted(convert_to)))

disabled_list: list[str] | None = config["disable_programs"].get(list)
disabled_program_names = (
frozenset(disabled_list) if disabled_list else frozenset()
)

handler_stages: dict[type[Handler], tuple[Tool, ...]] = {}
for handler_cls in _enabled_handler_classes(all_format_strs):
self._set_format_handler_stages(
handler_cls, handler_stages, disabled_program_names
)
# Build the verbose-output summary with the routing layer's own
# decision function so the log can never drift from what the walk
# actually does. repack=True for archives applies the same
# convert/CAN_PACK gates the repack pass will.
def _log_formats_summary(
self,
verbose: int,
all_format_strs: frozenset[str],
convert_to: frozenset[str],
handler_stages: dict[type[Handler], tuple[Tool, ...]],
) -> None:
"""Log the run-wide "Optimizing formats" banner."""
# Build the summary with the routing layer's own decision function
# so the log can never drift from what the walk actually does.
# repack=True for archives applies the same convert/CAN_PACK gates
# the repack pass will.
handled_format_strs: set[str] = set()
convert_format_strs: dict[str, set[str]] = {}
routes = registry.routes_by_format()
Expand All @@ -184,7 +170,40 @@ def set_format_handler_map(self, config: Subview) -> None:
convert_format_strs.setdefault(picked.OUTPUT_FORMAT_STR, set()).add(
file_format.format_str
)
self._print_formats_config(verbose, handled_format_strs, convert_format_strs)

def set_format_handler_map(
self, config: Subview, *, print_summary: bool = False
) -> None:
"""Probe handlers for the requested formats and store availability."""
all_format_strs = self._get_config_set(config, "formats", "extra_formats")
config["formats"].set(tuple(sorted(all_format_strs)))
convert_to = self._get_config_set(config, "convert_to")
# Write the upcased lists back so template validation accepts
# lowercase user input for -x and -c exactly as it does for -f.
if extra_formats := self._get_config_set(config, "extra_formats"):
config["extra_formats"].set(tuple(sorted(extra_formats)))
if convert_to:
config["convert_to"].set(tuple(sorted(convert_to)))

disabled_list: list[str] | None = config["disable_programs"].get(list)
disabled_program_names = (
frozenset(disabled_list) if disabled_list else frozenset()
)

handler_stages: dict[type[Handler], tuple[Tool, ...]] = {}
for handler_cls in _enabled_handler_classes(all_format_strs):
self._set_format_handler_stages(
handler_cls, handler_stages, disabled_program_names
)
config["computed"]["handler_stages"].set(handler_stages)

verbose: int = config["verbose"].get(int)
self._print_formats_config(verbose, handled_format_strs, convert_format_strs)
# The banner is a run-wide summary. Emit it only for the top-level
# build, never for the per-directory .picopt.yaml resolutions that
# rebuild this config once per directory during the walk (which, with
# -W or any planted .picopt.yaml, would repeat it for every directory).
if print_summary:
verbose: int = config["verbose"].get(int)
self._log_formats_summary(
verbose, all_format_strs, convert_to, handler_stages
)
40 changes: 31 additions & 9 deletions picopt/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,30 +221,52 @@ def pick_route_handler(
"""
if file_format is None:
return None
handler_cls: type[Handler] | None = None
if convert and (not file_format.archive or repack):
handler_cls = _pick_convert_handler(convert_chain, convert_to, handler_stages)
handler_cls = _pick_convert_handler(
convert_chain,
convert_to,
handler_stages,
file_format,
convert=convert,
repack=repack,
)
if (
handler_cls is None
and native is not None
and is_pipeline_available(native, handler_stages)
):
handler_cls = native
if (
repack
and handler_cls is not None
and not (issubclass(handler_cls, ContainerHandler) and handler_cls.CAN_PACK)
):
if repack and not _can_pack_repack(handler_cls):
handler_cls = None
return handler_cls


def _can_pack_repack(handler_cls: type[Handler] | None) -> bool:
"""Report whether the handler is a container that can pack itself back up."""
return (
handler_cls is not None
and issubclass(handler_cls, ContainerHandler)
and handler_cls.CAN_PACK
)


def _pick_convert_handler(
convert_chain: tuple[type[Handler], ...],
convert_to: frozenset[str],
handler_stages: Mapping,
file_format: FileFormat,
*,
convert: bool,
repack: bool,
) -> type[Handler] | None:
"""First convert candidate the user asked for whose pipeline is available."""
"""
First convert candidate the user asked for whose pipeline is available.

Returns ``None`` when conversion doesn't apply at all: the caller didn't
request it, or it's an archive outside the repack pass (archives only
convert while repacking).
"""
if not convert or (file_format.archive and not repack):
return None
for candidate in convert_chain:
if candidate.OUTPUT_FORMAT_STR not in convert_to:
continue
Expand Down
10 changes: 7 additions & 3 deletions picopt/plugins/base/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,17 @@ def _cleanup_after_optimize_get_return_data(
final_data_buffer.close()
return return_data

def _should_replace(self, bytes_in: int, bytes_out: int) -> bool:
"""Report whether the new buffer is a real improvement worth keeping."""
if self.config.dry_run or bytes_out <= 0:
return False
return bytes_out < bytes_in or self.config.bigger

def _cleanup_after_optimize(self, final_data_buffer: BinaryIO) -> ReportStats:
"""Replace the old file with the better one or discard the new wasteful one."""
bytes_in = self.path_info.bytes_in()
bytes_out = self._get_buffer_len(final_data_buffer)
replaced = not self.config.dry_run and (
bytes_out > 0 and (bytes_out < bytes_in or self.config.bigger)
)
replaced = self._should_replace(bytes_in, bytes_out)
return_data = self._cleanup_after_optimize_get_return_data(
final_data_buffer, replaced=replaced
)
Expand Down
5 changes: 4 additions & 1 deletion picopt/plugins/webp/animated.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ def img2webp_args(self) -> tuple[str, ...]:
if frame_path is None:
continue
if frame_duration is not None:
out += ["-d", str(frame_duration)]
# img2webp rejects durations <= 0 ("Invalid negative
# duration"). Some sources (e.g. scraped animated WebPs)
# carry 0ms frame durations, so clamp to its 1ms minimum.
out += ["-d", str(max(1, int(frame_duration)))]
out.append(str(frame_path))
return tuple(out)

Expand Down
53 changes: 27 additions & 26 deletions picopt/walk/skip.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,43 +63,44 @@ def _log_skip(self, reason: str, path_info: PathInfo, *, warn: bool) -> None:
self._reporter.stats.record_skipped()
self._reporter.progress.mark_skipped()

def _is_skippable(self, path_info: PathInfo) -> bool:
"""Handle things that are not optimizable files."""
reason = ""
warn = False

def _skip_reason_fs(self, path_info: PathInfo) -> tuple[str, bool]:
"""Disk checks, done last for performance and skipped inside archives."""
path = path_info.path
if self._in_archive or not path:
return "", False
if not path.exists():
return "not found", True
fs_stat = path_info.stat()
if fs_stat is not None and not (
S_ISREG(fs_stat.st_mode) or S_ISDIR(fs_stat.st_mode)
):
# FIFOs, sockets, device nodes: opening a FIFO blocks forever.
return "not a regular file", False
return "", False

# File types
def _skip_reason(self, path_info: PathInfo) -> tuple[str, bool]:
"""Return ``(reason, warn)`` for an unoptimizable file, else ``("", False)``."""
path = path_info.path
basename = Path(path_info.name()).name
if not self._config.symlinks and path and path.is_symlink():
reason = "symlink"
elif basename == DIR_CONFIG_FILENAME:
reason = "picopt config"
elif basename in self._TIMESTAMPS_FILENAMES:
return "symlink", False
if basename == DIR_CONFIG_FILENAME:
return "picopt config", False
if basename in self._TIMESTAMPS_FILENAMES:
legacy = "legacy " if basename == OLD_TIMESTAMPS_NAME else ""
reason = f"{legacy}timestamp"
elif is_path_ignored(
return f"{legacy}timestamp", False
if is_path_ignored(
self._config,
path_info.archive_pseudo_path(),
ignore_case=not path_info.is_case_sensitive,
):
reason = "ignored"
elif not self._in_archive and path and not path.exists():
# Check disk last for performance
reason = "not found"
warn = True
elif (
not self._in_archive
and path
and (fs_stat := path_info.stat()) is not None
and not (S_ISREG(fs_stat.st_mode) or S_ISDIR(fs_stat.st_mode))
):
# FIFOs, sockets, device nodes: opening a FIFO blocks forever.
reason = "not a regular file"
return "ignored", False
return self._skip_reason_fs(path_info)

def _is_skippable(self, path_info: PathInfo) -> bool:
"""Handle things that are not optimizable files."""
reason, warn = self._skip_reason(path_info)
self._log_skip(reason, path_info, warn=warn)

return bool(reason)

def _clean_up_working_files(self, path: Path) -> None:
Expand Down
26 changes: 20 additions & 6 deletions picopt/walk/walk.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,24 @@ def _walk_top_path(self, top_path: Path, scheduler: Scheduler) -> None:
)
self.walk_file(path_info, scheduler)

@staticmethod
def _count_stops_here(
settings: PicoptSettings,
path: Path,
name: str,
*,
is_symlink: bool,
is_dir: bool,
) -> bool:
"""Whether the walk would not recurse into ``path`` (mirror walk_file)."""
return bool(
not settings.recurse
or (not settings.symlinks and is_symlink)
or name in WalkSkipper.SKIP_FILENAMES
or not is_dir
or is_path_ignored(settings, path, ignore_case=False)
)

def _count(
self,
top_path: Path,
Expand All @@ -372,12 +390,8 @@ def _count(
settings = self._dirconfig.get_settings(
top_path, path if is_dir else path.parent
)
if (
not settings.recurse
or (not settings.symlinks and is_symlink)
or name in WalkSkipper.SKIP_FILENAMES
or not is_dir
or is_path_ignored(settings, path, ignore_case=False)
if self._count_stops_here(
settings, path, name, is_symlink=is_symlink, is_dir=is_dir
):
return 1
try:
Expand Down
Loading