Compressor
blanket.Compressor provides optional lossless and quality-bounded lossy
optimization for individual Image.save() calls. Both search alternative
encodings and keep the smallest qualifying result, including the normal save.
from blanket import Image
from blanket.Compressor import LosslessImageCompressor
compressor = LosslessImageCompressor(effort=7)
with Image.open("input.png") as image:
image.save("optimized.png", compressor=compressor)
image.convert("RGB").save("optimized.jpg", quality=90, compressor=compressor)
image.save("optimized.jxl", lossless=True, compressor=compressor)
image.save("optimized.heic", lossless=True, compressor=compressor)
See formats and save options for codec limits.
API
LosslessImageCompressor(*, effort=7)
Construct a reusable compressor configuration. effort must be an integer
from 1 through 10, inclusive. Non-integers, including booleans, raise
TypeError; integers outside that range raise ValueError.
The read-only .effort property returns the configured value. The compressor
does not expose a separate compress() method: pass it through the
compressor keyword to Image.save().
from io import BytesIO
from blanket import Image
from blanket.Compressor import LosslessImageCompressor
image = Image.new("RGB", (128, 128), (12, 31, 79))
output = BytesIO()
image.save(output, "PNG", compress_level=6, compressor=LosslessImageCompressor(effort=3))
encoded = output.getvalue()
An explicit format is needed for buffers. Files can use their filename
extension to select the format. compressor=None uses normal saving; other
objects passed as compressor raise TypeError.
The same compressor can be used for multiple images and formats. Optimization does not change the input image's pixels, mode, bit depth, or metadata.
LossyImageCompressor(*, max_rmse=2.0, effort=7)
Search for smaller encodings while bounding additional sample error relative to the normal save with the same options. The normal save's existing loss is not included in this bound. This measures pixel differences, not perceptual similarity or error relative to an opened image's original file.
from blanket.Compressor import LossyImageCompressor
compressor = LossyImageCompressor(max_rmse=2.0, effort=7)
image.save("smaller.png", compressor=compressor)
image.convert("RGB").save("smaller.jpg", quality=90, compressor=compressor)
max_rmse must be a finite number from 0 through 255. Booleans and nonnumeric
values raise TypeError; out-of-range values raise ValueError. effort
accepts integers from 1 through 10 with the same validation as the lossless
compressor. Both properties are read-only.
For each candidate, decoded samples are normalized to the 0–255 scale, grayscale is expanded to RGB, and the root mean square error is computed over all RGB samples. Hidden RGB beneath transparent pixels is included. Alpha must match exactly; it is excluded from the error average. Each candidate is compared to the fixed normal-save reference, never to the previous winner. Dimensions are preserved. High-bit-depth codec samples use the same normalized error scale without first rounding to eight bits.
| Format | Lossy search |
|---|---|
| 8-bit non-indexed PNG | Use histogram-guided color quantization, then optimize the best trial losslessly; alpha is unchanged |
| JPEG, WebP, JPEG XL, AVIF, HEIF/HEIC | Adaptively search lower quality settings using decoded error; optimize the winning JPEG's entropy coding |
| Indexed or high-bit-depth PNG, other formats | Retain the lossless compressor's behavior |
The search retains the normal save, with lossless optimization applied before
searching except for JPEG, which optimizes entropy coding after choosing quality.
At most effort quantization or quality trials are encoded. PNG evaluates
quantization bin widths from 256 down to 2 using a color histogram.
Trials include uniform rounding and mapping each
occupied bin to its rounded weighted mean, minimizing squared error within
that bin. Uniform rounding retains regular sample spacing for PNG filters.
Invalid and duplicate tables are skipped before encoding. Ordinary PNG saves
rank the eligible trials;
only the smallest trial receives the full lossless optimization search.
Codec searches start one quality point below the requested quality and double
the quality drop until a trial exceeds the error bound. They then bisect the
bracketed range according to measured error. They stop when the
range is exhausted or the trial budget is reached. Codec error and encoded
size need not be monotonic, so this is a heuristic, not a global optimum.
Every accepted candidate satisfies the error bound independently.
Codec save(..., effort=...) retains its ordinary meaning independently of
the compressor's search effort.
max_rmse=0 uses lossless optimization only. Explicit lossless=True also
disables additional loss for codecs supporting that save option. The smallest
qualifying result wins, so output is never larger than the normal save.
Results may remain identical when no smaller candidate meets the bound.
As with the lossless compressor, the source is unchanged, metadata follows
ordinary save behavior, and the configuration is reusable across saves.
What “lossless” means
The reference is a normal save with the same save options. That encoding always remains a candidate, so optimization cannot increase its encoded size. Only strictly smaller candidates replace the current result; ties retain the earlier encoding.
This guarantee applies to the requested save, not to the original file:
- JPEG still applies the requested quality and subsampling during the normal save. Optimization preserves that save's DCT coefficients.
- Lossy JXL and HEIF saves still introduce their normal loss. Optimization adds no further sample changes.
- HEIF's RGB/YUV conversion can change color samples even with
lossless=True; candidates must match the normal save's decoded samples. - Saving an opened image encodes its current pixels. The compressor does not reuse or transcode the original file's bitstream, and does not guarantee a smaller file than that original.
Metadata follows ordinary save behavior. The compressor does not add metadata retention; Blanket currently saves pixels without carrying through source EXIF, XMP, or ICC metadata.
Format-specific behavior
| Format | Optimization | Input depths |
|---|---|---|
| PNG | Filter and DEFLATE searches, exact channel reductions, packed grayscale, transparency keys, and exact palettes | 8-bit L, RGB, and RGBA |
| JPEG | Optimized Huffman tables; progressive scans at effort 3 and above | 8-bit L and RGB |
| JPEG XL | Encoder effort search, with exact decoded-sample verification | 8, 10, 12, and 16 bits |
| HEIF / HEIC | x265 preset search, with exact decoded-sample verification | 8, 10, and 12 bits |
High-bit-depth PNG, indexed PNG, and other output formats use their normal encoders. Passing a compressor does not expand a format's supported modes or bit depths; for example, JPEG still requires conversion from RGBA to RGB.
PNG
PNG optimization preserves every sample, including RGB values beneath fully transparent pixels. It can remove opaque alpha channels, reduce gray RGB images to grayscale, and store gray RGBA images as grayscale-plus-alpha. Reopening an optimized PNG may therefore report a different mode; convert it to the original mode to compare pixels.
Exact grayscale packing uses 1-bit samples for values 0/255, 2-bit samples for multiples of 85, and 4-bit samples for multiples of 17. Wider representations remain candidates because filtering can make them smaller.
An exact palette can represent up to 256 distinct colors, including alpha. The search tries packed indices and, at higher effort, alternative palette orders. Transparent entries can be moved earlier to shorten the transparency chunk. Efforts 8–10 also try wider index depths; this adds encoding work but can improve compression of images with long runs of palette colors. No colors are quantized or discarded.
A single transparency key is considered only when alpha is binary, all transparent pixels have the same RGB color, and no opaque pixel has that color. Nonzero keys in 2-/4-bit grayscale are avoided for Pillow compatibility.
Higher efforts try more row filters, including minimum-entropy filtering. Efforts 9–10 also search additional DEFLATE levels.
JPEG
All efforts try optimized Huffman coding. Effort 3 and above also tries progressive scans. These transformations preserve DCT coefficients, quantization tables, subsampling, dimensions, and partial edge blocks. Arithmetic coding is not used.
JPEG XL and HEIF/HEIC
JXL searches encoder efforts 1 through the compressor's effort, skipping the
setting already used by the normal save. The codec's save(..., effort=...)
option controls that normal save; the compressor's .effort controls the
additional search. The normal save remains eligible even when its codec
effort exceeds the compressor's effort.
HEIF searches x265 presets from ultrafast through the selected effort:
medium is 6, slow is 7, and placebo is 10. The normal save uses medium,
which is not repeated. Other HEVC plugins retain their own preset defaults.
Each smaller candidate is decoded and checked against the normal save's dimensions, mode, bit depth, and every sample, including alpha and hidden colors. A candidate with any difference is discarded. For lossy saves, the search also tries lossless encoding of the normal save's decoded pixels; those candidates must pass the same checks.
Choosing effort
Lower effort performs fewer trials; higher effort spends more time searching. The best tradeoff depends on image content and format. A higher setting is not a promise of further size reduction, and JPEG uses the same two candidates at efforts 3–10. Use the compression benchmark to choose a setting for your images and latency budget.
Reference
LosslessImageCompressor
LosslessImageCompressor(*, effort: int = 7)
Configure optimization for image.save(..., compressor=...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
effort
|
int
|
Optimization effort from 1 (fastest) to 10 (most thorough). Controls PNG filter search, JPEG scan coding, JXL effort search, and HEIF/HEIC encoder preset search. Higher settings can be slow. |
7
|
Eight-bit PNG saves try grayscale and opaque-alpha reductions and multiple row filters, including minimum-entropy filtering. Exact palettes use packed indices and try frequency and color ordering. Grayscale samples try packed 1-, 2-, and 4-bit storage when exact; compatible binary alpha tries a PNG transparency key. Efforts 9-10 also search DEFLATE levels. The smallest encoding wins, including the normal baseline, without quantizing colors or discarding RGB values in transparent pixels. The image's mode and pixels stay unchanged; reopening a saved PNG may report a reduced mode. Size relative to the original source file is not guaranteed. JPEG saves optimize Huffman tables and, at effort 3 or higher, also try progressive scans without changing the normal save's DCT coefficients. JXL and HEIF/HEIC saves search encoder settings, including high-bit-depth images. Smaller candidates are accepted only when their decoded samples exactly match the normal save. For lossy saves, this also tries lossless encoding of the normal save's decoded pixels.
Optimization introduces no additional loss relative to the requested save; it does not turn a lossy save into a pixel-exact copy of the input image. Opened images are encoded from their current pixels, not their source bitstream. Quality and lossless settings retain their normal meaning. High-bit-depth and indexed PNG and other formats keep normal encoding. Metadata follows normal save behavior. The object is reusable and affects only the save it is passed to, without modifying the image.
Configure LosslessImageCompressor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
effort
|
int
|
Integer search effort from 1 (fastest) through 10 (most thorough); booleans are rejected. |
7
|
Examples:
from blanket import Image
image = Image.new("RGB", (8, 8), (40, 100, 180))
from io import BytesIO
from blanket.Compressor import LosslessImageCompressor
compressor = LosslessImageCompressor(effort=1)
output = BytesIO()
image.save(output, format="PNG", compressor=compressor)
effort
property
effort: int
Configured search effort.
Examples:
from blanket import Image
image = Image.new("RGB", (8, 8), (40, 100, 180))
from io import BytesIO
from blanket.Compressor import LosslessImageCompressor
compressor = LosslessImageCompressor(effort=1)
print(compressor.effort)
LossyImageCompressor
LossyImageCompressor(*, max_rmse: float = 2.0, effort: int = 7)
Search for smaller saves within an additional decoded-pixel error limit.
max_rmse is the maximum RGB root-mean-square error relative to the
normal save, on a 0-255 scale (including for high-bit-depth images).
Alpha must match exactly. Hidden RGB values count toward the error.
This is a sample error bound, not a perceptual quality guarantee.
effort from 1 through 10 controls the number of candidate trials.
PNG uses histogram-guided color quantization for 8-bit non-indexed images.
JPEG, WebP, JPEG XL, AVIF, and HEIF/HEIC adaptively search lower codec
quality settings using the measured error of each trial.
Explicit lossless=True disables additional loss. Other formats and
indexed or high-bit-depth PNG retain lossless optimization behavior.
The normal save remains eligible, so output cannot grow relative to it.
The source image is unchanged and this configuration is reusable.
Configure LossyImageCompressor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_rmse
|
float
|
Maximum additional RGB root mean square error relative to the normal save, from 0 through 255. Alpha must match exactly. |
2.0
|
effort
|
int
|
Integer search effort from 1 (fastest) through 10 (most thorough); booleans are rejected. |
7
|
Examples:
from blanket import Image
image = Image.new("RGB", (8, 8), (40, 100, 180))
from io import BytesIO
from blanket.Compressor import LossyImageCompressor
compressor = LossyImageCompressor(effort=1)
output = BytesIO()
image.save(output, format="PNG", compressor=compressor)
max_rmse
property
max_rmse: float
Configured maximum additional RGB error.
Examples:
from blanket import Image
image = Image.new("RGB", (8, 8), (40, 100, 180))
from io import BytesIO
from blanket.Compressor import LossyImageCompressor
compressor = LossyImageCompressor(effort=1)
print(compressor.max_rmse)
effort
property
effort: int
Configured search effort.
Examples:
from blanket import Image
image = Image.new("RGB", (8, 8), (40, 100, 180))
from io import BytesIO
from blanket.Compressor import LossyImageCompressor
compressor = LossyImageCompressor(effort=1)
print(compressor.effort)