Skip to content

API Reference

Core optimization

optimize_image(source: Path | str, output: Path | str | None = None, *, max_width: int | None = None, max_height: int | None = None, quality: int = 85, strip_metadata: bool = True, output_format: OutputFormat = OutputFormat.AUTO, keep_aspect_ratio: bool = True, progressive: bool = True, optimize: bool = True, overwrite: bool = False, lossless: bool = False, backup_dir: Path | str | None = None, min_size_bytes: int | None = None) -> OptimizationResult

Optimize a single image.

Parameters:

Name Type Description Default
source Path | str

Path to the source image.

required
output Path | str | None

Path for the optimized image. If None, overwrites source (if overwrite=True).

None
max_width int | None

Maximum width in pixels. None means no resize.

None
max_height int | None

Maximum height in pixels. None means no resize.

None
quality int

JPEG/WEBP quality (1-100). Higher is better quality, larger file.

85
strip_metadata bool

Remove EXIF and other metadata.

True
output_format OutputFormat

Target format. AUTO infers from output path or original.

AUTO
keep_aspect_ratio bool

Maintain aspect ratio when resizing.

True
progressive bool

Use progressive JPEG encoding.

True
optimize bool

Enable Pillow optimization flags.

True
overwrite bool

Allow overwriting the source file when output is None.

False
lossless bool

Use lossless compression for PNG/WEBP. Ignored for JPEG.

False
backup_dir Path | str | None

Directory to copy the original file into before processing.

None
min_size_bytes int | None

Skip files already smaller than this threshold (bytes).

None

Returns:

Type Description
OptimizationResult

OptimizationResult with details of the operation.

Source code in src/pixopt/optimizer.py
def optimize_image(
    source: Path | str,
    output: Path | str | None = None,
    *,
    max_width: int | None = None,
    max_height: int | None = None,
    quality: int = 85,
    strip_metadata: bool = True,
    output_format: OutputFormat = OutputFormat.AUTO,
    keep_aspect_ratio: bool = True,
    progressive: bool = True,
    optimize: bool = True,
    overwrite: bool = False,
    lossless: bool = False,
    backup_dir: Path | str | None = None,
    min_size_bytes: int | None = None,
) -> OptimizationResult:
    """Optimize a single image.

    Args:
        source: Path to the source image.
        output: Path for the optimized image. If None, overwrites source (if overwrite=True).
        max_width: Maximum width in pixels. None means no resize.
        max_height: Maximum height in pixels. None means no resize.
        quality: JPEG/WEBP quality (1-100). Higher is better quality, larger file.
        strip_metadata: Remove EXIF and other metadata.
        output_format: Target format. AUTO infers from output path or original.
        keep_aspect_ratio: Maintain aspect ratio when resizing.
        progressive: Use progressive JPEG encoding.
        optimize: Enable Pillow optimization flags.
        overwrite: Allow overwriting the source file when output is None.
        lossless: Use lossless compression for PNG/WEBP. Ignored for JPEG.
        backup_dir: Directory to copy the original file into before processing.
        min_size_bytes: Skip files already smaller than this threshold (bytes).

    Returns:
        OptimizationResult with details of the operation.
    """
    source_path = Path(source)
    if not source_path.exists():
        return _error_result(source_path, f"File not found: {source_path}")

    original_size = source_path.stat().st_size

    if backup_dir is not None:
        backup = Path(backup_dir)
        backup.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source_path, backup / source_path.name)

    if min_size_bytes is not None and original_size <= min_size_bytes:
        return OptimizationResult(
            source_path=source_path,
            output_path=source_path,
            original_size=original_size,
            optimized_size=original_size,
            savings_bytes=0,
            savings_percent=0.0,
            width=0,
            height=0,
            format="",
            metadata_removed=False,
            success=True,
            error=f"Skipped: file already below {min_size_bytes} bytes",
        )

    if output is None:
        if not overwrite:
            return _error_result(
                source_path,
                "Output path required unless overwrite=True",
                original_size=original_size,
            )
        output_path = source_path
    else:
        output_path = Path(output)

    # Handle SVG files with pure-Python optimizer
    if source_path.suffix.lower() == ".svg":
        return _optimize_svg(source_path, output_path, original_size)

    try:
        with Image.open(source_path) as img:
            img.load()
            output_path, pillow_fmt = resolve_and_adjust_path(
                img, output_path, output_format
            )

            is_animated = getattr(img, "is_animated", False) or getattr(img, "n_frames", 1) > 1
            is_gif_source = (img.format or "").upper() == "GIF"

            if is_animated and is_gif_source and pillow_fmt == "WEBP":
                return _optimize_animated_gif(
                    img,
                    source_path,
                    output_path,
                    original_size,
                    pillow_fmt,
                    max_width=max_width,
                    max_height=max_height,
                    keep_aspect_ratio=keep_aspect_ratio,
                    quality=quality,
                    strip_metadata=strip_metadata,
                    optimize=optimize,
                    lossless=lossless,
                )

            img = convert_mode(img, pillow_fmt)  # type: ignore[assignment]
            img = resize_image(  # type: ignore[assignment]
                img,
                max_width=max_width,
                max_height=max_height,
                keep_aspect_ratio=keep_aspect_ratio,
            )
            new_width, new_height = img.size

            save_kwargs = build_save_kwargs(
                pillow_fmt,
                quality=quality,
                progressive=progressive,
                optimize=optimize,
                strip_metadata=strip_metadata,
                lossless=lossless,
            )
            img = strip_metadata_pillow(img, pillow_fmt)  # type: ignore[assignment]


            output_path.parent.mkdir(parents=True, exist_ok=True)
            img.save(output_path, format=pillow_fmt, **save_kwargs)

        if strip_metadata:
            strip_exif_post_process(output_path, pillow_fmt)

        optimized_size = output_path.stat().st_size
        savings = original_size - optimized_size

        return OptimizationResult(
            source_path=source_path,
            output_path=output_path,
            original_size=original_size,
            optimized_size=optimized_size,
            savings_bytes=savings,
            savings_percent=(savings / original_size * 100) if original_size > 0 else 0.0,
            width=new_width,
            height=new_height,
            format=pillow_fmt,
            metadata_removed=strip_metadata,
            success=True,
        )

    except Exception as exc:
        return _error_result(source_path, str(exc), original_size=original_size, output=output_path)

optimize_directory(source_dir: Path | str, output_dir: Path | str | None = None, *, recursive: bool = False, extensions: Iterable[str] | None = None, backup_dir: Path | str | None = None, min_size_bytes: int | None = None, **kwargs: object) -> list[OptimizationResult]

Optimize all images in a directory.

Parameters:

Name Type Description Default
source_dir Path | str

Directory containing images.

required
output_dir Path | str | None

Destination directory. If None, overwrites in-place.

None
recursive bool

Search subdirectories.

False
extensions Iterable[str] | None

File extensions to process. Defaults to common image types.

None
backup_dir Path | str | None

Directory to copy originals into before processing.

None
min_size_bytes int | None

Skip files already smaller than this threshold (bytes).

None
**kwargs object

Passed to optimize_image.

{}

Returns:

Type Description
list[OptimizationResult]

List of OptimizationResult for each processed file.

Source code in src/pixopt/optimizer.py
def optimize_directory(
    source_dir: Path | str,
    output_dir: Path | str | None = None,
    *,
    recursive: bool = False,
    extensions: Iterable[str] | None = None,
    backup_dir: Path | str | None = None,
    min_size_bytes: int | None = None,
    **kwargs: object,
) -> list[OptimizationResult]:
    """Optimize all images in a directory.

    Args:
        source_dir: Directory containing images.
        output_dir: Destination directory. If None, overwrites in-place.
        recursive: Search subdirectories.
        extensions: File extensions to process. Defaults to common image types.
        backup_dir: Directory to copy originals into before processing.
        min_size_bytes: Skip files already smaller than this threshold (bytes).
        **kwargs: Passed to optimize_image.

    Returns:
        List of OptimizationResult for each processed file.
    """
    src = Path(source_dir)
    results: list[OptimizationResult] = []

    for file_path in discover_images(src, recursive=recursive, extensions=extensions):
        if output_dir is not None:
            rel = file_path.relative_to(src)
            out = Path(output_dir) / rel
        else:
            out = None

        result = optimize_image(
            file_path,
            out,
            overwrite=(output_dir is None),
            backup_dir=backup_dir,
            min_size_bytes=min_size_bytes,
            **kwargs,  # type: ignore[arg-type]
        )
        results.append(result)

    return results

change_extension(source: Path | str, output: Path | str | None = None, *, output_format: OutputFormat = OutputFormat.AUTO, backup_dir: Path | str | None = None, min_size_bytes: int | None = None, **kwargs: object) -> OptimizationResult

Convert an image to a different file format / extension.

This is a thin wrapper around optimize_image focused on format conversion. All other optimization parameters are forwarded.

Parameters:

Name Type Description Default
source Path | str

Path to the source image.

required
output Path | str | None

Destination path. If None, overwrites source (requires overwrite=True).

None
output_format OutputFormat

Target format. Defaults to inferring from output path.

AUTO
backup_dir Path | str | None

Directory to copy originals into before processing.

None
min_size_bytes int | None

Skip files already smaller than this threshold (bytes).

None
**kwargs object

Passed to optimize_image.

{}

Returns:

Type Description
OptimizationResult

OptimizationResult with details of the conversion.

Source code in src/pixopt/optimizer.py
def change_extension(
    source: Path | str,
    output: Path | str | None = None,
    *,
    output_format: OutputFormat = OutputFormat.AUTO,
    backup_dir: Path | str | None = None,
    min_size_bytes: int | None = None,
    **kwargs: object,
) -> OptimizationResult:
    """Convert an image to a different file format / extension.

    This is a thin wrapper around optimize_image focused on format conversion.
    All other optimization parameters are forwarded.

    Args:
        source: Path to the source image.
        output: Destination path. If None, overwrites source (requires overwrite=True).
        output_format: Target format. Defaults to inferring from output path.
        backup_dir: Directory to copy originals into before processing.
        min_size_bytes: Skip files already smaller than this threshold (bytes).
        **kwargs: Passed to optimize_image.

    Returns:
        OptimizationResult with details of the conversion.
    """
    return optimize_image(
        source,
        output,
        output_format=output_format,
        backup_dir=backup_dir,
        min_size_bytes=min_size_bytes,
        **kwargs,  # type: ignore[arg-type]
    )

convert_to_favicon(source: Path | str, output: Path | str | None = None, *, sizes: list[int] | None = None, background: tuple[int, int, int] = (255, 255, 255), keep_transparency: bool = True) -> OptimizationResult

Convert an image to a multi-resolution ICO favicon.

Generates a .ico file containing multiple square resolutions suitable for browser tabs, bookmarks and high-DPI displays.

Parameters:

Name Type Description Default
source Path | str

Path to the source image.

required
output Path | str | None

Output .ico path. If None, uses source name with .ico extension.

None
sizes list[int] | None

List of square sizes to include. Default: [16, 32, 48, 64, 128, 256].

None
background tuple[int, int, int]

RGB fill for transparent images when keep_transparency=False.

(255, 255, 255)
keep_transparency bool

Preserve alpha channel if present.

True

Returns:

Type Description
OptimizationResult

OptimizationResult with details of the operation.

Source code in src/pixopt/optimizer.py
def convert_to_favicon(
    source: Path | str,
    output: Path | str | None = None,
    *,
    sizes: list[int] | None = None,
    background: tuple[int, int, int] = (255, 255, 255),
    keep_transparency: bool = True,
) -> OptimizationResult:
    """Convert an image to a multi-resolution ICO favicon.

    Generates a .ico file containing multiple square resolutions suitable
    for browser tabs, bookmarks and high-DPI displays.

    Args:
        source: Path to the source image.
        output: Output .ico path. If None, uses source name with .ico extension.
        sizes: List of square sizes to include. Default: [16, 32, 48, 64, 128, 256].
        background: RGB fill for transparent images when keep_transparency=False.
        keep_transparency: Preserve alpha channel if present.

    Returns:
        OptimizationResult with details of the operation.
    """
    source_path = Path(source)
    if not source_path.exists():
        return _error_result(source_path, f"File not found: {source_path}")

    original_size = source_path.stat().st_size

    if output is None:
        output_path = source_path.with_suffix(".ico")
    else:
        output_path = Path(output)
        if not output_path.suffix:
            output_path = output_path.with_suffix(".ico")

    chosen_sizes = sizes if sizes is not None else DEFAULT_FAVICON_SIZES.copy()

    try:
        with Image.open(source_path) as img:
            img.load()
            if img.mode not in ("RGB", "RGBA") or img.mode == "RGB":
                img = img.convert("RGBA")  # type: ignore[assignment]

            icons: list[Image.Image] = []
            for size in chosen_sizes:
                copy = img.copy()
                copy = copy.resize((size, size), Resampling.LANCZOS)
                if not keep_transparency:
                    bg = Image.new("RGB", (size, size), background)
                    bg.paste(copy, mask=copy.split()[3])
                    icons.append(bg)
                else:
                    icons.append(copy)

            output_path.parent.mkdir(parents=True, exist_ok=True)
            icons[0].save(
                output_path,
                format="ICO",
                append_images=icons[1:],
            )

        optimized_size = output_path.stat().st_size
        savings = original_size - optimized_size
        max_size = max(chosen_sizes)

        return OptimizationResult(
            source_path=source_path,
            output_path=output_path,
            original_size=original_size,
            optimized_size=optimized_size,
            savings_bytes=savings,
            savings_percent=(savings / original_size * 100) if original_size > 0 else 0.0,
            width=max_size,
            height=max_size,
            format="ICO",
            metadata_removed=True,
            success=True,
        )

    except Exception as exc:
        return _error_result(
            source_path, str(exc), original_size=original_size, output=output_path
        )

Models

OptimizationResult(source_path: Path, output_path: Path, original_size: int, optimized_size: int, savings_bytes: int, savings_percent: float, width: int, height: int, format: str, metadata_removed: bool, success: bool, error: str | None = None) dataclass

Result of an image optimization operation.

Attributes

source_path: Path instance-attribute

output_path: Path instance-attribute

original_size: int instance-attribute

optimized_size: int instance-attribute

savings_bytes: int instance-attribute

savings_percent: float instance-attribute

width: int instance-attribute

height: int instance-attribute

format: str instance-attribute

metadata_removed: bool instance-attribute

success: bool instance-attribute

error: str | None = None class-attribute instance-attribute

human_original_size: str property

human_optimized_size: str property

human_savings: str property

OutputFormat

Bases: str, Enum

Supported output formats.

Attributes

AUTO = 'auto' class-attribute instance-attribute

JPEG = 'jpeg' class-attribute instance-attribute

PNG = 'png' class-attribute instance-attribute

WEBP = 'webp' class-attribute instance-attribute

AVIF = 'avif' class-attribute instance-attribute

ORIGINAL = 'original' class-attribute instance-attribute

Placeholders

generate_placeholder(image_path: Path, *, placeholder_type: PlaceholderType = 'lqip', lqip_size: int = 32, lqip_quality: int = 20) -> str

Generate a placeholder string for an image.

Parameters:

Name Type Description Default
image_path Path

Path to the source image.

required
placeholder_type PlaceholderType

One of 'color', 'lqip', 'blurhash'.

'lqip'
lqip_size int

Max thumbnail dimension for LQIP.

32
lqip_quality int

JPEG quality for LQIP.

20

Returns:

Type Description
str

A CSS color string, base64 data URI, or blurhash string.

Source code in src/pixopt/placeholder.py
def generate_placeholder(
    image_path: Path,
    *,
    placeholder_type: PlaceholderType = "lqip",
    lqip_size: int = 32,
    lqip_quality: int = 20,
) -> str:
    """Generate a placeholder string for an image.

    Args:
        image_path: Path to the source image.
        placeholder_type: One of 'color', 'lqip', 'blurhash'.
        lqip_size: Max thumbnail dimension for LQIP.
        lqip_quality: JPEG quality for LQIP.

    Returns:
        A CSS color string, base64 data URI, or blurhash string.
    """
    with Image.open(image_path) as img:
        img.load()
        if placeholder_type == "color":
            return extract_dominant_color(img)
        if placeholder_type == "lqip":
            return generate_lqip_datauri(img, size=lqip_size, quality=lqip_quality)
        if placeholder_type == "blurhash":
            return generate_blurhash(img)
    return ""

extract_dominant_color(img: Image.Image) -> str

Return the dominant color of an image as a hex CSS string.

Uses a downsample + average approach for accuracy.

Source code in src/pixopt/placeholder.py
def extract_dominant_color(img: Image.Image) -> str:
    """Return the dominant color of an image as a hex CSS string.

    Uses a downsample + average approach for accuracy.
    """
    rgb = img.convert("RGB")
    # Average all pixels by resizing to 1x1 with high-quality filter
    pixel = rgb.resize((1, 1), Image.Resampling.LANCZOS).getpixel((0, 0))
    if not isinstance(pixel, tuple):
        return "#000000"
    return f"#{pixel[0]:02x}{pixel[1]:02x}{pixel[2]:02x}"

generate_lqip_datauri(img: Image.Image, *, size: int = 32, quality: int = 20) -> str

Generate a tiny blurred placeholder image as a base64 data URI.

Parameters:

Name Type Description Default
img Image

Source PIL Image.

required
size int

Maximum dimension of the thumbnail (maintains aspect ratio).

32
quality int

JPEG quality for the tiny image (low = smaller).

20

Returns:

Type Description
str

A base64 data URI string like 'data:image/jpeg;base64,/9j/4AAQ...'.

Source code in src/pixopt/placeholder.py
def generate_lqip_datauri(img: Image.Image, *, size: int = 32, quality: int = 20) -> str:
    """Generate a tiny blurred placeholder image as a base64 data URI.

    Args:
        img: Source PIL Image.
        size: Maximum dimension of the thumbnail (maintains aspect ratio).
        quality: JPEG quality for the tiny image (low = smaller).

    Returns:
        A base64 data URI string like 'data:image/jpeg;base64,/9j/4AAQ...'.
    """
    thumb = img.copy()
    thumb = thumb.convert("RGB")
    thumb.thumbnail((size, size), Image.Resampling.LANCZOS)
    thumb = thumb.filter(ImageFilter.GaussianBlur(radius=2))

    buf = io.BytesIO()
    thumb.save(buf, format="JPEG", quality=quality, optimize=True)
    b64 = base64.b64encode(buf.getvalue()).decode("ascii")
    return f"data:image/jpeg;base64,{b64}"

generate_blurhash(img: Image.Image, *, components_x: int = 4, components_y: int = 3) -> str

Generate a simplified blurhash-like string from an image.

This is a pure-Python approximation that encodes average colors of a grid into a compact base-83 string. It is NOT the official BlurHash algorithm, but produces visually similar short placeholders.

Parameters:

Name Type Description Default
img Image

Source PIL Image.

required
components_x int

Number of horizontal grid cells.

4
components_y int

Number of vertical grid cells.

3

Returns:

Type Description
str

A short blurhash-like string.

Source code in src/pixopt/placeholder.py
def generate_blurhash(img: Image.Image, *, components_x: int = 4, components_y: int = 3) -> str:
    """Generate a simplified blurhash-like string from an image.

    This is a pure-Python approximation that encodes average colors of a
    grid into a compact base-83 string. It is NOT the official BlurHash
    algorithm, but produces visually similar short placeholders.

    Args:
        img: Source PIL Image.
        components_x: Number of horizontal grid cells.
        components_y: Number of vertical grid cells.

    Returns:
        A short blurhash-like string.
    """
    rgb = img.convert("RGB")
    w, h = rgb.size
    cell_w = max(1, w // components_x)
    cell_h = max(1, h // components_y)

    # Size flag (components - 1) each fits in one char
    size_flag = (components_y - 1) * 9 + (components_x - 1)
    parts: list[str] = [_encode_base83(size_flag, 1)]

    for cy in range(components_y):
        for cx in range(components_x):
            x1 = cx * cell_w
            y1 = cy * cell_h
            x2 = min(w, x1 + cell_w)
            y2 = min(h, y1 + cell_h)
            region = rgb.crop((x1, y1, x2, y2))
            data = region.tobytes()
            n = len(data) // 3
            if n == 0:
                r = g = b = 0
            else:
                r = sum(data[i] for i in range(0, len(data), 3)) // n
                g = sum(data[i + 1] for i in range(0, len(data), 3)) // n
                b = sum(data[i + 2] for i in range(0, len(data), 3)) // n
            # Pack RGB into a single base83 value
            packed = (r << 16) | (g << 8) | b
            parts.append(_encode_base83(packed, 4))

    return "".join(parts)

Smart format detection

detect_optimal_format(image_path: Path | str, *, allow_lossy: bool = True, allow_lossless: bool = True, allow_animation: bool = True) -> OutputFormat

Analyze an image and return the most efficient output format.

Rules
  • Transparent image → WEBP (or PNG if lossless only)
  • Animated image → WEBP
  • Photograph with many colors → WEBP (or JPEG if no WEBP)
  • Graphic/UI with few colors → WEBP lossless or PNG
Source code in src/pixopt/smart_format.py
def detect_optimal_format(
    image_path: Path | str,
    *,
    allow_lossy: bool = True,
    allow_lossless: bool = True,
    allow_animation: bool = True,
) -> OutputFormat:
    """Analyze an image and return the most efficient output format.

    Rules:
        - Transparent image → WEBP (or PNG if lossless only)
        - Animated image → WEBP
        - Photograph with many colors → WEBP (or JPEG if no WEBP)
        - Graphic/UI with few colors → WEBP lossless or PNG
    """
    path = Path(image_path)

    with Image.open(path) as img:
        img.load()

        is_animated = (
            getattr(img, "is_animated", False)
            or getattr(img, "n_frames", 1) > 1
        )
        if is_animated and allow_animation:
            return OutputFormat.WEBP

        transparent = has_transparency(img)
        photo = is_photo(img)

        if transparent:
            if allow_lossless:
                return OutputFormat.WEBP
            return OutputFormat.PNG

        if photo and allow_lossy:
            return OutputFormat.WEBP

        if not photo and allow_lossless:
            return OutputFormat.WEBP

        if allow_lossy:
            return OutputFormat.JPEG

        return OutputFormat.PNG

Srcset generation

generate_srcset_images(source: Path | str, output_dir: Path | str, widths: list[int], *, quality: int = 85, output_format: str = 'WEBP', strip_metadata: bool = True, progressive: bool = True, optimize: bool = True, lossless: bool = False) -> list[SrcsetImage]

Generate resized variants of an image for responsive srcset.

Parameters:

Name Type Description Default
source Path | str

Path to the source image.

required
output_dir Path | str

Directory where variants will be saved.

required
widths list[int]

List of target widths in pixels. Each variant will have this width, preserving aspect ratio.

required
quality int

JPEG/WEBP quality (1-100).

85
output_format str

Pillow format string for output (e.g. "WEBP", "JPEG").

'WEBP'
strip_metadata bool

Remove EXIF and other metadata.

True
progressive bool

Use progressive JPEG encoding.

True
optimize bool

Enable Pillow optimizer.

True
lossless bool

Use lossless compression for PNG/WEBP.

False

Returns:

Type Description
list[SrcsetImage]

List of SrcsetImage entries, sorted by width ascending.

Source code in src/pixopt/srcset_generator.py
def generate_srcset_images(
    source: Path | str,
    output_dir: Path | str,
    widths: list[int],
    *,
    quality: int = 85,
    output_format: str = "WEBP",
    strip_metadata: bool = True,
    progressive: bool = True,
    optimize: bool = True,
    lossless: bool = False,
) -> list[SrcsetImage]:
    """Generate resized variants of an image for responsive srcset.

    Args:
        source: Path to the source image.
        output_dir: Directory where variants will be saved.
        widths: List of target widths in pixels. Each variant will have
            this width, preserving aspect ratio.
        quality: JPEG/WEBP quality (1-100).
        output_format: Pillow format string for output (e.g. "WEBP", "JPEG").
        strip_metadata: Remove EXIF and other metadata.
        progressive: Use progressive JPEG encoding.
        optimize: Enable Pillow optimizer.
        lossless: Use lossless compression for PNG/WEBP.

    Returns:
        List of SrcsetImage entries, sorted by width ascending.
    """
    source_path = Path(source)
    out_dir = Path(output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    fmt = _resolve_output_format(output_format)
    results: list[SrcsetImage] = []

    with Image.open(source_path) as img:
        img.load()
        orig_width = img.width
        ext = output_format.lower()
        if ext == "jpeg":
            ext = "jpg"

        for target_width in sorted(set(widths)):
            if target_width > orig_width:
                continue

            suffix = f"-{target_width}w.{ext}"
            out_path = out_dir / (source_path.stem + suffix)

            result = optimize_image(
                source_path,
                out_path,
                max_width=target_width,
                quality=quality,
                strip_metadata=strip_metadata,
                output_format=fmt,
                progressive=progressive,
                optimize=optimize,
                lossless=lossless,
            )

            if result.success:
                results.append(
                    SrcsetImage(
                        width=result.width,
                        output_path=out_path,
                        size_bytes=result.optimized_size,
                    )
                )

    return results

SrcsetImage(width: int, output_path: Path, size_bytes: int) dataclass

A single responsive image variant.

Attributes

width: int instance-attribute

output_path: Path instance-attribute

size_bytes: int instance-attribute

Adaptive quality

find_quality_for_target_size(img: Image.Image, pillow_fmt: str, target_size: int, *, max_width: int | None = None, max_height: int | None = None, keep_aspect_ratio: bool = True, strip_metadata: bool = True, progressive: bool = True, optimize: bool = True, lossless: bool = False, min_quality: int = 1, max_quality: int = 100, tolerance: float = 0.05, max_iterations: int = 8) -> int

Find the JPEG/WEBP quality that produces a file closest to target_size.

Uses binary search over quality (1-100) and measures the actual encoded file size in memory. Returns the quality value that yields a size closest to but not exceeding the target.

Parameters:

Name Type Description Default
img Image

Open PIL Image.

required
pillow_fmt str

Target Pillow format (JPEG or WEBP).

required
target_size int

Target file size in bytes.

required
max_width int | None

Maximum width in pixels, or None.

None
max_height int | None

Maximum height in pixels, or None.

None
keep_aspect_ratio bool

Whether to keep the original aspect ratio.

True
strip_metadata bool

Whether to strip metadata before saving.

True
progressive bool

Whether to use progressive encoding.

True
optimize bool

Whether to optimize the output.

True
lossless bool

Whether to use lossless compression.

False
min_quality int

Lowest quality to try.

1
max_quality int

Highest quality to try.

100
tolerance float

Fractional tolerance around target_size (e.g. 0.05 = 5%).

0.05
max_iterations int

Maximum binary-search iterations.

8

Returns:

Type Description
int

Quality integer (1-100).

Source code in src/pixopt/adaptive_quality.py
def find_quality_for_target_size(
    img: Image.Image,
    pillow_fmt: str,
    target_size: int,
    *,
    max_width: int | None = None,
    max_height: int | None = None,
    keep_aspect_ratio: bool = True,
    strip_metadata: bool = True,
    progressive: bool = True,
    optimize: bool = True,
    lossless: bool = False,
    min_quality: int = 1,
    max_quality: int = 100,
    tolerance: float = 0.05,
    max_iterations: int = 8,
) -> int:
    """Find the JPEG/WEBP quality that produces a file closest to target_size.

    Uses binary search over quality (1-100) and measures the actual encoded
    file size in memory. Returns the quality value that yields a size
    closest to but not exceeding the target.

    Args:
        img: Open PIL Image.
        pillow_fmt: Target Pillow format (JPEG or WEBP).
        target_size: Target file size in bytes.
        max_width: Maximum width in pixels, or None.
        max_height: Maximum height in pixels, or None.
        keep_aspect_ratio: Whether to keep the original aspect ratio.
        strip_metadata: Whether to strip metadata before saving.
        progressive: Whether to use progressive encoding.
        optimize: Whether to optimize the output.
        lossless: Whether to use lossless compression.
        min_quality: Lowest quality to try.
        max_quality: Highest quality to try.
        tolerance: Fractional tolerance around target_size (e.g. 0.05 = 5%).
        max_iterations: Maximum binary-search iterations.

    Returns:
        Quality integer (1-100).
    """
    if pillow_fmt not in ("JPEG", "WEBP"):
        return 85

    working = img.copy()
    working = convert_mode(working, pillow_fmt)
    working = resize_image(
        working,
        max_width=max_width,
        max_height=max_height,
        keep_aspect_ratio=keep_aspect_ratio,
    )

    low = min_quality
    high = max_quality
    best_quality = low
    best_diff = float("inf")

    for _ in range(max_iterations):
        if low > high:
            break
        mid = (low + high) // 2

        buf = BytesIO()
        kwargs = build_save_kwargs(
            pillow_fmt,
            quality=mid,
            progressive=progressive,
            optimize=optimize,
            strip_metadata=strip_metadata,
            lossless=lossless,
        )
        working.save(buf, format=pillow_fmt, **kwargs)
        size = buf.tell()

        diff = abs(size - target_size)
        if diff < best_diff:
            best_diff = diff
            best_quality = mid

        # Within tolerance window?
        if abs(size - target_size) <= target_size * tolerance:
            best_quality = mid
            break

        if size > target_size:
            high = mid - 1
        else:
            low = mid + 1

    return best_quality

Visual comparison

generate_comparison_html(before_path: Path, after_path: Path, output_html: Path, title: str = 'Image Comparison') -> Path

Generate a self-contained HTML file with an interactive before/after slider.

Parameters:

Name Type Description Default
before_path Path

Path to the original image.

required
after_path Path

Path to the optimized image.

required
output_html Path

Path where the HTML file will be saved.

required
title str

Page title displayed above the slider.

'Image Comparison'

Returns:

Type Description
Path

Path to the generated HTML file.

Source code in src/pixopt/html_comparison.py
def generate_comparison_html(
    before_path: Path,
    after_path: Path,
    output_html: Path,
    title: str = "Image Comparison",
) -> Path:
    """Generate a self-contained HTML file with an interactive before/after slider.

    Args:
        before_path: Path to the original image.
        after_path: Path to the optimized image.
        output_html: Path where the HTML file will be saved.
        title: Page title displayed above the slider.

    Returns:
        Path to the generated HTML file.
    """
    before_b64 = _img_to_base64(before_path)
    after_b64 = _img_to_base64(after_path)

    with Image.open(before_path) as img:
        width = img.width

    orig_size = before_path.stat().st_size
    opt_size = after_path.stat().st_size
    savings = orig_size - opt_size
    pct = savings / orig_size * 100 if orig_size > 0 else 0

    def _human(size: int) -> str:
        if size < 1024:
            return f"{size} B"
        if size < 1024 * 1024:
            return f"{size / 1024:.1f} KB"
        return f"{size / (1024 * 1024):.2f} MB"

    meta = (
        f"Original: {_human(orig_size)}  |  "
        f"Optimized: {_human(opt_size)}  |  "
        f"Savings: {_human(savings)} ({pct:.1f}%)"
    )

    html = HTML_TEMPLATE.format(
        title=title,
        meta=meta,
        before_b64=before_b64,
        after_b64=after_b64,
        width=width,
    )

    output_html.parent.mkdir(parents=True, exist_ok=True)
    output_html.write_text(html, encoding="utf-8")
    return output_html