Version history:
================


---
v.2.2.0 - 2026-10-25
 * Security: minimum Pillow is now 13.0.0, which fixes five vulnerabilities
   in Pillow 12.3.0 and older. The critical one, CVE-2026-112403, could be
   reached by optimize-images: a crafted lossless JPEG downscaled with
   -mw/-mh (also when converting, or through max_w/max_h in the API) could
   overflow a heap buffer. CVE-2026-112404 and CVE-2026-112897 (crafted
   TIFF files) could only be reached through -ca with a TIFF file given an
   image extension, or through convert_image_data(); CVE-2026-112397 and
   CVE-2026-112896 affect Pillow features optimize-images does not use.
   Users who cannot upgrade optimize-images yet are also protected by
   upgrading Pillow alone (pip install -U Pillow); optimize-images 2.1.0
   was tested with Pillow 13.0.0. Python 3.13t users now need to build
   Pillow from source, since Pillow 13 has no wheels for that Python.
 * Behaviour change (JPEG): smaller JPEG files by default. New
   -jt/--jpeg-tables option (JPEG, conversion to JPEG): 'auto' (the default),
   'standard' or 'mozjpeg3'. 'mozjpeg3' encodes with the default quantization
   table of the mozjpeg encoder (index 3), for brightness and colour, scaled
   from the same quality value; only the table changes, not the encoder, and
   the output is still a baseline JPEG. 'auto' uses 'standard' for JPEG files
   already saved with the standard tables at about the quality used (such as
   files optimized before by optimize-images), so they are not compressed a
   second time, and 'mozjpeg3' for everything else; with --ssim-min, a file
   whose mozjpeg3 result scores below the threshold is encoded again with the
   standard tables before the original is kept. On the project's test
   images, 'auto' gives JPEG files about 12% smaller in total (median
   SSIMULACRA 2 loss of 0.2 points) and PNG to JPEG conversions about 3%
   smaller, with the same quality. 'standard' restores the previous output
   exactly. CMYK JPEGs are not affected. Public API (backwards compatible):
   keyword-only jpeg_tables argument in optimize_single_image(),
   optimize_image_data() and convert_image_data(), and a matching
   PublicBatchOptions field (default 'auto'); unknown names raise ValueError.
 * New SSIM quality control: the -ssm/--ssim-min THRESHOLD option (JPEG,
   WebP, conversion; value between 0.0 and 1.0) computes the structural
   similarity between the image as it was just before the final lossy
   encoding (after all transforms) and the optimized or converted result,
   and keeps the original file whenever the score falls below the threshold.
   The check is independent of the size comparison, so it also applies when
   the comparison is disabled with -nc.
 * New --show-ssim / --no-show-ssim options (JPEG, WebP, conversion)
   control the SSIM score in the per-file report. The score is shown
   automatically whenever --ssim-min is used; with --no-show-ssim it is
   hidden for optimized files, but a file rejected because its score fell
   below the threshold is still reported with the score and the threshold.
 * SSIM computation relies on the optional scikit-image package (install it
   with: pip install scikit-image, or use the new [ssim] extra: pip install
   optimize-images[ssim]). Without it, --ssim-min stops the run early with a
   clear error message, and --show-ssim prints a warning once and shows no
   scores.
 * Files rejected by the SSIM threshold are reported with their score and the
   threshold, so they stay distinguishable from files skipped because they
   were not smaller.
 * Measured across 224 photographs with the default encoder, scores range
   0.929-1.0 (median 0.981): 0.92 keeps everything a default run produces,
   while 0.95 rejects about 2% of the files (roughly 2.5% of the space
   savings). Computing the score can dominate the run time on very large
   images, and it is inert for PNG files that are not converted (their
   in-place encoding is lossless, score always 1.0000).
 * For images with transparency, the score is computed over the background
   color (-bg/-hbg, white by default), so invisible RGB under transparent
   pixels no longer lowers it.
 * The score uses scikit-image's structural_similarity with its default
   uniform 7x7 window (the parameters are pinned explicitly, so the scores do
   not change if scikit-image changes its defaults). Scores are slightly
   higher (about +0.004) than tools using the Gaussian window of the original
   paper; the recommended thresholds were calibrated with this implementation.
 * Faster start: numpy and scikit-image are loaded only when SSIM scores
   are computed, and the process-pool and package-metadata modules only when
   they are used. On the test machine (macOS, Python 3.11-3.15),
   optimize-images --version takes about 60-90 ms instead of 140-270 ms, a
   single image is optimized 38-47% faster, and importing the library is
   2.5-4.5 times faster.
 * Faster JPEG optimization: the automatic quality search downsizes its
   400x400 working copy in two steps (Pillow's reducing_gap), about 11%
   faster with -jobs 1 on Python 3.12 and later. The chosen quality can be one
   step lower on a few photos (1 to 3 of 80 test photos: 2-4% smaller, SSIM
   lower by 0.002 at most).
 * Embedded ICC colour profiles are now kept when optimizing and
   converting JPEG, WebP and PNG files (profiles equivalent to sRGB
   are removed). Files with a wide-gamut profile may be slightly
   larger, but their colours no longer change.
 * Dropped Python 3.10 (end of life since 2026-10-01); Python 3.11
   is now the minimum. Python 3.15 is supported. The [ssim] extra now
   also declares numpy.
 * Public API (backwards compatible): optimize_single_image(),
   optimize_image_data() and convert_image_data() gained keyword-only ssim_min
   and show_ssim arguments; PublicBatchOptions gained the matching ssim_min and
   show_ssim fields; PublicTaskResult reports the computed score in a new ssim
   field (None when SSIM was not computed; with ssim_min alone, files rejected
   by the size check are not scored; show_ssim always returns the score). The
   public entry points raise ValueError for an ssim_min outside 0.0-1.0.
 * SSIM: identical (lossless) outputs are scored 1.0 without running
   the comparison, and batches that compute SSIM scores run at most
   8 images at a time by default (much faster on macOS and Windows,
   where the default used up to 64 and 32 threads respectively).
 * convert_image_data() and the other conversion entry points probe the
   available target formats once per process instead of on every call (3-5%
   faster when converting small images in memory).
 * Removed the unused optimize_images/watch.py module. Directory watching
   has been served by the public API (optimize_images.api.watch_directory)
   since v1.4; the -wd/--watch-directory option is unaffected.
 * Behaviour change (JPEG): -q / quality is now honoured without fast mode.
   Previously, -q was ignored for in-place JPEG optimization unless -fm was
   also given. Now, when a quality is explicitly provided, it is always used
   (with or without fast mode). Without -q, quality is chosen automatically,
   except with -fm and in conversions to JPEG/AVIF, which use 80. Public API
   quality now defaults to None (previously 80), so 'not given' is
   distinguishable from quality=80.
 * Ctrl-C now stops a batch promptly and safely: files not started yet are
   left untouched, files in progress are finished first, and no traceback is
   shown. Before, the whole batch kept running to the end on macOS and
   Windows, and with worker processes (the default on Linux) an interrupted
   worker could leave an image truncated to 0 bytes in place of the original.
   Public API: stopping optimize_as_batch_stream() early (break, an exception
   or Ctrl-C) now also cancels the files not started yet.
 * Watch mode (-wd) is more reliable. Files renamed into place (a temporary
   file renamed when complete, as browsers and many editors save) are now
   processed; before, they were ignored. A new file that reuses the name of
   an earlier one is processed again; before, each name was processed only
   once per session. The copy written by -ca/-cb and already processed files
   that are only renamed are no longer optimized a second time. A file is
   processed only after it has not changed for half a second, instead of 10
   ms, so files copied slowly are no longer read half-written and skipped. To
   tell a finished file from one still being written, watch mode measures the
   file system's clock when it starts, by creating and removing a small
   temporary file in the folder. An error while handling one file no longer
   ends the watch silently: it is shown and watching goes on. Public API: the
   same applies to watch_directory(), where an exception raised by on_result
   is printed to standard error with its traceback, and a watcher that stops
   unexpectedly raises the new OIWatchStoppedError instead of waiting
   forever.
 * Running out of memory (for instance with a high -jobs value on big
   images), or an image above Pillow's pixel limit, no longer aborts the
   whole batch with an error: the file is skipped, its original is kept, the
   report says why, and the final summary suggests a lower -jobs value when
   memory ran out (also while computing SSIM scores). Results expose the
   reason in a new error field.
 * -rc no longer turns the transparent areas of PNG images opaque (or,
   with partial transparency, makes them more transparent): fully
   transparent pixels stay transparent, using one palette entry that
   counts toward -mc, and half-transparent pixels keep an approximate
   transparency. -rc is also much faster on palette images.
 * Palette PNG optimization now keeps every colour (it used to
   re-quantize the image) and is much faster. The palette and its
   transparency are stored compactly (only the colours used). Palette
   PNGs may therefore shrink less than before, because their colours
   are no longer changed.
 * The CLI no longer crashes when a file name cannot be written in the
   output encoding (e.g. Windows output redirected to a file or pipe);
   such characters are shown as "?". Unicode icons and the path ellipsis
   fall back to ASCII when the output cannot display them.
 * Animated PNG files (APNG) are now left untouched, like animated WebP
   files, instead of being flattened to their first frame. (Camera MPO files
   are still optimized as their primary image, as before.)
 * Converting a 16-bit grayscale PNG (Pillow modes I;16, I;16B, I;16L and I)
   to another format now scales the samples to 8 bits (each value divided by
   256) instead of clipping every value above 255 to white.
 * Converting a CMYK image (typically a JPEG) to a format that does not
   support CMYK, such as PNG or WebP, now converts it to RGB first instead of
   failing with an error.
 * setup.py reads README.md as UTF-8, so building the package no longer
   depends on the system's text encoding.

---
v.2.1.0 - 2026-06-26
 * Added WebP support: existing WebP files are now optimized in place, just
   like PNG and JPEG. Animated WebP files are left untouched.
 * New WebP encoding options: -wq (quality), -wl/--webp-lossless, and -wm
   (method / compression effort).
 * Format conversion was generalized. The new -cf/--convert-to FORMAT option
   chooses the output format; the available targets depend on the codecs
   compiled into the Pillow build in use (typically jpeg, png, webp, avif and
   jpeg2000). The default target remains JPEG, so existing behaviour is kept
   unless the option is used.
 * -ca/--convert-all now converts images of any source format (not only PNG)
   to the chosen target. -cb/--convert-big stays specific to big photographic
   PNGs.
 * Conversion now honours the size comparison just like in-place optimization:
   the converted file is kept only when it actually turns out smaller, unless
   the comparison is disabled with -nc. (Previously, converting forced the
   write regardless of size.)
 * New image inspection: inspect_image(path) returns an ImageMetadata object
   with the image's intrinsic properties (format, mode, dimensions, alpha,
   palette size, progressive/interlaced flags, frame count, DPI, ICC profile
   presence and description, and EXIF). EXIF comes grouped by its standardized
   IFD sections - image (main IFD), camera (Exif sub-IFD) and gps - with raw
   values. A companion helper, format_exif(), optionally renders those values
   for display using standardized EXIF semantics (units like f/1.8 and 50 mm,
   enumerations like Orientation, and combined GPS coordinates). A new
   -i/--info CLI option prints this report for a single image and exits;
   it must be used on its own, with only the image path.
 * Public API (backwards compatible): optimize_single_image() gained
   keyword-only arguments convert_to, webp_quality, webp_lossless and
   webp_method, all with defaults; PublicBatchOptions gained the matching
   fields. New discovery helpers - available_input_formats(),
   available_output_formats() and format_capabilities() - report what the
   running Pillow build can read, write and convert to, so third-party apps can
   populate their own menus. PublicTaskResult is unchanged.
 * Public API: new optimize_image_data(data) optimizes an image given as bytes
   and returns the optimized bytes plus a PublicTaskResult, for callers that
   hold images as binary data rather than files (CMS, object storage,
   databases) and cannot provide a file path. It keeps the original format and
   reuses the same per-format processing as the file-based optimizer, so the
   behaviour (resizing, transparency, EXIF, dynamic quality and the size
   comparison) is identical; format conversion is out of scope for this entry
   point.
 * Public API: new convert_image_data(data, to=...) is the in-memory
   counterpart of format conversion: it converts image bytes to another
   available format (e.g. PNG to WebP) and returns the converted bytes plus the
   resulting format, reusing the same converter as the file-based workflow. As
   with the file conversion, the result is kept only when it is actually
   smaller (unless the comparison is disabled); converting to the source format
   just optimizes it (no conversion); multi-frame sources are left unchanged.
 * UPGRADE NOTE: because WebP is now a supported format, running this tool on a
   folder will also re-encode any WebP files it finds (destructively, in
   place), where previously those files were ignored. Note also the conversion
   size-comparison change described above. As always, keep a backup of the
   originals before processing.

---
v.2.0.0 - 2026-05-26
 * Removed dependency on piexif, since Pillow supports EXIF metadata (thanks to
   @varnav for the alert on this).
 * Refactored some code to make it easier to import optimize-images as a
   package in your python projects, without having to execute the CLI app.
 * Changes in requirements to allow newer versions of Python and dependencies.
 * Removed support for older python versions, keeping only active support
   versions.
 * Initial, experimental support for free threaded python, which seems
   significantly faster (tests and feedback are always very welcome).
 * Removed support for Pythonista/iOS, since it is no longer maintained.
 * Added some basic tests.

---
v.1.5.1 - 2022-04-18
 * New console output options: --quiet, --only-summary, and --only-progress
   (kindly contributed by derenix@Github).
 * Changes in requirements to allow newer versions of Python and dependencies.

---
v.1.5.0 - 2021-05-29
 * Replaced temporary files with in-memory buffers which, at the expense of a
   bit higher RAM usage, prevents unnecessary I/O and may result in faster
   performance when working with slower disks.
 * Added information about the running environment to the -v/--version CLI
   argument, so that you can check which python version is being used, as well
   as some information about required and optional third-party packages.
 * Added a minimum space saving threshold (1% per file). Files are only replaced
   if the space saved is at least 1% of the original file size. Contributed by
   varnav@GitHub.
 * Added a check for the presence of the required package Piexif.
 * Added new methods intended to allow optimize-images to be imported and used
   as a package. The initial refactoring was kindly contributed by Tharindu N.
   (truethari@GitHub)
 * Added custom exceptions to better support the public API.

---
v.1.4.0 - 2020-11-01
 * New feature: watch a directory for changes and optimize any new image file as
   soon as it is created (no multiprocessing for now, sorry). This feature is
   not compatible with Pythonista for iOS, since it requires a third-party
   package called Watchdog, which is currently not available on Pythonista).
 * Long running tasks, like optimizing recursively a folder containing a large
   number of image files, or watching for new files a a directory, can now be
   interrupted by the user with CTRL+C. Instead of an ugly traceback, the final
   report is presented with the usual statistics for the images that were
   processed.
 * Added the ability to specify the number of tasks to run simultaneously, so
   that you can optionally limit the number of processor cores being used by
   this application and keep in reserve some CPU power for any other tasks.

---
v.1.3.6 - 2020-08-02
 * Fixed a UnicodeEncodeError that would occur in some environments when
   printing Emoji to screen (Windows, Haiku, maybe others).
 * Fixed a strange multiprocessing error that apparently only occured on... you
   guessed (yeah, Windows, we are looking at you!...).
 * Improved stability on format detection.
   
---
v.1.3.5 - 2020-04-07
 * Fixed a small bug in unsupported image format treatment.
 * Added experimental support for MPO images, which are now treated as JPEG
   image files (if multiple pictures are present in one MPO file, only the first
   one will be processed).

---
v.1.3.4 - 2020-02-19
 * Ignore unsupported image formats, contributed by Petro (liashchynskyi@GitHub).
 * Added brief instructions (and a script for macOS) for Pillow-SIMD
   installation, as a faster alternative to Pillow.
 
---
v.1.3.3 - 2019-08-06
 * Any temporary files (e.g.: '~temp~filename.jpg') created by this utility
   are now ignored, to avoid stepping on its own feet. As a side effect, if
   you have the strange habit of naming your image files ~temp~something.xxx,
   they will also be ignored. So, please get a better naming scheme until we
   come out with a better solution. ;-)
 * Trying to run optimize-images without Pillow installed now results in a
   simple message being printed out to the screen, instead of a full exception
   traceback.

---
v.1.3.2 - 2019-06-27
 * Added a symbol legend to the beginning of the optimization report, so that
   the user knows what each symbol means.
 * Fixed a UnicodeEncodeError that would occur in some environments when
   printing Emoji to screen.

---
v.1.3.1 - 2019-05-03
 * Fixed a PermissionError that would occur when trying to optimize a single
   image in the current directory without explicitly specifying its path.
 * Added basic Exception handling for piexif related instructions. This is a
   temporary solution that should silence, for now, some errors related to
   issue #3 but we should still check if we can provide a more specific
   treatment for those piexif exceptions. We still have a few TODO items
   related to issue #3. Let's hope they can be addressed in the next release.

---
v.1.3 - 2018-10-10
 * Added dynamic (variable) JPG quality setting.
 * Some code cleaning.

---
v.1.2 - 2018-08-27 - Finally available at PyPI.org!
 * Adding a new big feature: "convert big PNG files to JPG format"
 * Added also an option to convert all PNG files to JPEG format.
 * Added a new option to set background color while doing any PNG operations
   that remove transparency (convert big and reduce colors).
 * Adding an option to set background color in hexadecimal mode (like in HTML).
 * Added an option to convert to grayscale.
 * Added an option to ignore file size comparison between original and
   processed files, allowing to always save anyway the processed version.
 * Added new palette rebuild step for indexed color images (creates images that
   may then be compressed more efficiently using other tools).
 * Added a new "fast mode", that currently skips the (also recently
   implemented) palette rebuild step.
 * Changed the transparency removal algorithm to something better.
 * Created some global constants for iOS console and thread settings.
 * Created a global constant for JPEG quality and raised the default to 80.
 * Adjustments in the downsizing code to better produce the expected behavior.
 * Refactored module into a full package and created the `setup.py` file.
 * Added Portuguese version of the documentation.
 * Other minor changes.

---
v.1.1.1 - 2018-06-24
 * Bug fix in single image optimization.

---
v.1.1 - 2018-06-24
 * Added new options to allow downsizing of images to a maximum width and/or
   height before applying any other optimization (guess what? It really makes
   a huge difference in file sizes…).
 * Added a new option to keep EXIF data in JPEG images (by default, it will
   be discarded).
 * Added a new CLI argument to display current version.
 * Added a new CLI argument to display a list of the currently supported image
   formats.
 * Added some bigger JPEG photos with EXIF to text-images.zip.

---
v.1.0 - 2018-06-20 (First public release)
 * No special dependencies, besides PIL/Pillow (this enables full compatibility
   with iPhones and iPads running Pythonista 3)
 * Basic CLI argument parsing, with separate groups of arguments for JPEG and
   PNG images.
 * Multiprocessing support on multicore and multiprocessor computers for really
   fast operation.
 * Multithreading support on Pythonista/iOS, for slightly improved performance
   (too bad, multiprocessing is currently not supported on that environment).
 * Supports optimization of both folders and individual files.
 * Option to avoid recursion into subdirectories (by default, it will recurse).
 * Option for manually setting JPEG quality (by default, 70%).
 * Option for reducing the number of colors by applying adaptive palette to PNG
   images (by default, 256 colors).
 * Option for manually setting a different maximum number of colors while
   reducing colors.