Skip to content

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)