Skip to content

Library Usage

pixopt is a Python library as well as a CLI. Every CLI feature is exposed through a typed, public Python API under the pixopt package, so you can build custom pipelines, web backends, automation scripts, and MCP integrations.

All examples below use the exact import and parameter names exported by pixopt. You can import most helpers directly from the top-level package:

import pixopt
from pixopt import (
    optimize_image,
    optimize_directory,
    batch_optimize,
    OutputFormat,
    FitMode,
    Anchor,
    PlaceholderType,
    WatermarkPosition,
    SpriteLayout,
    NextGenFormat,
    EXIFGroup,
)

Note

Functions are typed with pathlib.Path support. You can pass strings or Path objects for all file paths.


Common types

Enum Values Notes
OutputFormat AUTO, JPEG, PNG, WEBP, AVIF, ORIGINAL AUTO infers from the output path or source. ORIGINAL keeps the source format.
FitMode down, cover, contain, fill How the image is resized within max_width / max_height.
Anchor center, top, bottom, left, right, top-left, top-right, bottom-left, bottom-right, face Anchor point for cover / contain cropping.
WatermarkPosition top-left, top-right, bottom-left, bottom-right, center Position of text or image watermarks.
SpriteLayout grid, horizontal, vertical Layout strategy for sprite sheets.
NextGenFormat JXL, WEBP2 Next-generation output formats.
PlaceholderType color, lqip, blurhash Placeholder generation modes.

Basic optimization

optimize_image() is the core single-image API. It handles resizing, format conversion, quality, metadata stripping, EXIF filtering, and backups in one call.

from pixopt import optimize_image, OutputFormat

result = optimize_image(
    "photo.jpg",
    "photo_optimized.webp",
    max_width=1600,
    quality=85,
    strip_metadata=True,
    output_format=OutputFormat.WEBP,
)

print(f"Saved {result.savings_percent:.1f}%")
print(f"Output: {result.output_path}")
print(f"Original: {result.human_original_size}")
print(f"Optimized: {result.human_optimized_size}")
print(f"Dimensions: {result.width}x{result.height}")

Result object

OptimizationResult provides source_path, output_path, original_size, optimized_size, savings_bytes, savings_percent, width, height, format, metadata_removed, success, and error.

Selective EXIF preservation

By default strip_metadata=True removes all EXIF. You can keep specific groups with keep_exif_groups:

from pixopt import optimize_image, EXIFGroup

result = optimize_image(
    "photo.jpg",
    "photo_optimized.jpg",
    max_width=1600,
    quality=85,
    strip_metadata=True,
    keep_exif_groups={EXIFGroup.ORIENTATION, EXIFGroup.CAMERA, EXIFGroup.DATE},
)

Warning

keep_exif_groups only takes effect when strip_metadata=True. It uses the piexif groups defined in pixopt.exif.EXIFGroup.

Cropping and fit modes

from pixopt import optimize_image, OutputFormat, FitMode, Anchor

# 16:9 cover crop centered on the image
result = optimize_image(
    "photo.jpg",
    "banner.webp",
    max_width=1920,
    max_height=1080,
    aspect_ratio=(16, 9),
    fit=FitMode.COVER,
    anchor=Anchor.CENTER,
    output_format=OutputFormat.WEBP,
    quality=80,
)

Batch and directory processing

Directory optimization

optimize_directory() scans a directory and returns a list of OptimizationResult objects.

from pixopt import optimize_directory, OutputFormat


def progress(info):
    print(f"{info.current}/{info.total}: {info.current_file.name} -> {info.success}")


results = optimize_directory(
    "./images",
    "./optimized",
    recursive=True,
    extensions={".jpg", ".jpeg", ".png"},
    max_width=1200,
    quality=80,
    output_format=OutputFormat.WEBP,
    backup_dir="./backups",
    min_size_bytes=1024,
    on_progress=progress,
)

successful = [r for r in results if r.success]
print(f"Processed {len(successful)}/{len(results)} files")

Note

output_dir=None overwrites images in-place. You must set overwrite=True explicitly when you want optimize_image() to overwrite the source.

Batch optimization from a list

batch_optimize() accepts an iterable of paths and writes all outputs into a single directory. It returns a BatchReport.

from pathlib import Path
from pixopt import batch_optimize, OutputFormat

paths = [Path("a.jpg"), Path("b.png"), Path("c.webp")]
report = batch_optimize(
    paths,
    "./optimized",
    max_width=800,
    quality=80,
    output_format=OutputFormat.WEBP,
)

print(report.to_dict())
print(f"Total: {report.total_files}")
print(f"Succeeded: {report.succeeded}, Failed: {report.failed}")
print(f"Savings: {report.human_total_savings} ({report.total_savings_percent:.1f}%)")

Tip

batch_optimize is ideal when you already have a list of files. optimize_directory is ideal when you want to discover files on disk.


Format conversion

Convert to a different format without resizing or quality changes. The simplest approach is optimize_image() with output_format:

from pixopt import optimize_image, OutputFormat

result = optimize_image(
    "icon.png",
    "icon.webp",
    output_format=OutputFormat.WEBP,
    lossless=True,
)

You can also use the convenience wrapper change_extension():

from pixopt import change_extension, OutputFormat

result = change_extension(
    "photo.png",
    "photo.avif",
    output_format=OutputFormat.AVIF,
    quality=80,
)

Note

change_extension() forwards all other parameters to optimize_image(). If output is omitted, set overwrite=True to overwrite the source.


Lossless compression

Use lossless=True for UI assets, icons, screenshots, and graphics where pixel-perfect fidelity is required:

from pixopt import optimize_image, OutputFormat

# WEBP lossless
result = optimize_image(
    "ui_asset.png",
    "ui_asset.webp",
    output_format=OutputFormat.WEBP,
    lossless=True,
)

# PNG lossless (default PNG behavior)
result_png = optimize_image(
    "screenshot.png",
    "screenshot_optimized.png",
    output_format=OutputFormat.PNG,
)

Note

lossless is only honored by PNG and WEBP. JPEG does not support lossless compression.


Adaptive quality (target file size)

find_quality_for_target_size() performs a binary search in memory to find the JPEG/WEBP quality that lands closest to a target byte size.

from PIL import Image
from pixopt import optimize_image, OutputFormat
from pixopt.adaptive_quality import find_quality_for_target_size

with Image.open("photo.jpg") as img:
    img.load()
    quality = find_quality_for_target_size(
        img,
        "JPEG",
        target_size=50 * 1024,
        min_quality=60,
        max_quality=95,
        tolerance=0.05,
        max_iterations=8,
    )

result = optimize_image(
    "photo.jpg",
    "photo_50kb.jpg",
    output_format=OutputFormat.JPEG,
    quality=quality,
)

print(f"Achieved quality: {quality}")
print(f"Final size: {result.human_optimized_size}")

Warning

target_size is in bytes. The algorithm returns the highest quality that keeps the encoded file at or below the target size within the tolerance.


Placeholders

Generate tiny placeholders for lazy-loading with generate_placeholder():

from pixopt.placeholder import generate_placeholder, PlaceholderType

# Dominant color as a CSS hex string
color = generate_placeholder("photo.jpg", placeholder_type=PlaceholderType.COLOR)
# -> "#3f7a8c"

# LQIP: tiny base64 blurred preview
lqip = generate_placeholder(
    "photo.jpg",
    placeholder_type=PlaceholderType.LQIP,
    lqip_size=32,
    lqip_quality=20,
)
# -> "data:image/jpeg;base64,/9j/4AAQ..."

# Blurhash-like compact string
blurhash = generate_placeholder("photo.jpg", placeholder_type=PlaceholderType.BLURHASH)
# -> "LEHV6nWB2yk8pyo0adR*.7kCMdnj"

Tip

LQIP is ideal for immediate visual feedback while images load. Blurhash is ideal when your frontend already has a blurhash decoder.


Smart format detection

detect_optimal_format() analyzes image content and recommends the most efficient output format.

from pixopt.smart_format import (
    detect_optimal_format,
    is_photo,
    has_transparency,
    count_unique_colors,
)
from pixopt.models import OutputFormat

fmt = detect_optimal_format("photo.jpg")
# -> OutputFormat.WEBP for photographs

fmt = detect_optimal_format("icon.png")
# -> OutputFormat.WEBP or OutputFormat.PNG for graphics

fmt = detect_optimal_format(
    "logo_with_alpha.png",
    allow_lossless=True,
    allow_lossy=True,
)

You can also run the individual heuristics directly:

from PIL import Image

with Image.open("photo.jpg") as img:
    print(f"Is photo: {is_photo(img)}")
    print(f"Has alpha: {has_transparency(img)}")
    print(f"Unique colors: {count_unique_colors(img)}")
Content Default recommendation
Photographs OutputFormat.WEBP
Graphics / illustrations with few colors OutputFormat.WEBP or OutputFormat.PNG
Transparent images OutputFormat.WEBP (or OutputFormat.PNG if lossless only)
Animated images OutputFormat.WEBP

Responsive srcset generation

generate_srcset_images() creates multiple resized variants for srcset attributes.

from pixopt.srcset_generator import generate_srcset_images

variants = generate_srcset_images(
    "hero.jpg",
    "./responsive",
    widths=[320, 640, 1024, 1920],
    output_format="WEBP",
    quality=80,
)

for v in variants:
    print(f"{v.width}px -> {v.size_bytes} bytes ({v.output_path.name})")

This produces files like:

  • hero-320w.webp
  • hero-640w.webp
  • hero-1024w.webp
  • hero-1920w.webp

Use them in HTML:

<img
  srcset="hero-320w.webp 320w,
          hero-640w.webp 640w,
          hero-1024w.webp 1024w,
          hero-1920w.webp 1920w"
  sizes="(max-width: 600px) 320px,
         (max-width: 1000px) 640px,
         1920px"
  src="hero-1920w.webp"
  alt="Hero image"
/>

Note

Widths larger than the original image are skipped automatically. Results are sorted by width ascending.


Backup and min-size filter

Protect originals and skip already-optimized files:

from pixopt import optimize_image

result = optimize_image(
    "photo.jpg",
    "photo_optimized.jpg",
    quality=75,
    backup_dir="./backups",
    min_size_bytes=10240,
)

if not result.output_path.exists() and result.error:
    print(f"Skipped: {result.error}")

Warning

When min_size_bytes is set and the source is already smaller, no output file is created. result.success is still True but result.error explains that the file was skipped.


Favicon generation

convert_to_favicon() creates a multi-resolution .ico file.

from pixopt import convert_to_favicon

result = convert_to_favicon(
    "logo.png",
    "favicon.ico",
    sizes=[16, 32, 48],
    background=(255, 255, 255),
    keep_transparency=True,
)

print(f"Favicon created: {result.output_path}")

Tip

The default sizes when sizes is omitted are [16, 32, 48, 64, 128, 256]. The largest size determines the width and height reported in the result.


Visual comparison

generate_comparison_html() builds a self-contained HTML page with an interactive before/after slider.

from pixopt import generate_comparison_html

html_path = generate_comparison_html(
    before_path="photo.jpg",
    after_path="photo_optimized.jpg",
    output_html="comparison.html",
    title="Original vs. Optimized",
)

print(f"Comparison saved to: {html_path}")

Note

The generated HTML embeds both images as base64 data URIs. Images over the internal base64 limit will raise an error; optimize them first if needed.


Watermarking

Text watermarks

from pixopt import add_text_watermark, WatermarkPosition

result = add_text_watermark(
    "photo.jpg",
    "photo_watermarked.jpg",
    "(c) 2025 My Company",
    position=WatermarkPosition.BOTTOM_RIGHT,
    opacity=0.5,
    padding=20,
    font_size=48,
    color=(255, 255, 255),
)

print(f"Watermarked: {result.output_path} ({result.width}x{result.height})")

Image watermarks

from pixopt import add_image_watermark, WatermarkPosition

result = add_image_watermark(
    "photo.jpg",
    "photo_logo.jpg",
    "logo.png",
    position=WatermarkPosition.BOTTOM_RIGHT,
    opacity=0.5,
    padding=20,
    scale=0.2,
)

Tip

For image watermarks, use a PNG with an alpha channel for the best results. scale is relative to the base image width.


Sprite sheets

Sprite sheet

create_sprite() combines multiple images into a single sheet.

from pixopt import create_sprite, SpriteLayout

result = create_sprite(
    images=["icon1.png", "icon2.png", "icon3.png"],
    output="sprites.png",
    cell_width=64,
    cell_height=64,
    columns=3,
    layout=SpriteLayout.GRID,
    padding=2,
    fmt="PNG",
)

print(f"Sprite: {result.output}")
print(f"Dimensions: {result.width}x{result.height}")
print(f"Slots: {len(result.slots)}")

Contact sheet

create_contact_sheet() generates a grid of thumbnails with filename labels.

from pixopt import create_contact_sheet

result = create_contact_sheet(
    images=["a.jpg", "b.jpg", "c.jpg"],
    output="contact_sheet.png",
    cell_width=200,
    cell_height=200,
    padding=10,
    label_height=20,
    fmt="PNG",
)

Note

SpriteResult includes slots with index, source, x, y, width, and height for each placed image.


Asset bundles

generate_asset_bundle() generates a complete set of web assets from one source image in a single call.

from pixopt import generate_asset_bundle, BundleOptions

options = BundleOptions(
    hero_width=1920,
    thumbnail_width=300,
    og_width=1200,
    og_height=630,
    favicon_sizes=[16, 32, 48, 128, 256],
    srcset_widths=[320, 640, 960, 1280, 1920],
    palette_n=6,
    quality=85,
    lqip_size=32,
    lqip_quality=20,
    generate_hero=True,
    generate_thumbnail=True,
    generate_og=True,
    generate_favicon=True,
    generate_srcset=True,
    generate_lqip=True,
    generate_blurhash=True,
    generate_palette=True,
    generate_dominant_color=True,
)

bundle = generate_asset_bundle(
    "hero.jpg",
    "./assets",
    options=options,
)

print(bundle.to_dict())
print(f"LQIP: {bundle.lqip_data_uri[:50]}...")
print(f"Dominant color: {bundle.dominant_color}")
print(f"Palette: {[c.hex for c in bundle.palette.colors]}")

Tip

AssetBundle returns individual OptimizationResult objects for hero, thumbnail, og_image, and favicon, plus a list of SrcsetImage objects and placeholders.


PDF export

pixopt can convert PDF pages to images and images to a PDF. PDF support requires the optional PyMuPDF dependency (pip install pixopt[pdf]).

PDF to images

from pixopt import pdf_to_images

result = pdf_to_images(
    "document.pdf",
    "./pdf_pages",
    dpi=150,
    fmt="PNG",
    prefix="page",
)

print(f"Pages: {result.total_pages}")
for page in result.pages:
    print(f"  {page.page_number}: {page.width}x{page.height} -> {page.output_path}")

Images to PDF

from pixopt import images_to_pdf

result = images_to_pdf(
    ["cover.jpg", "page1.jpg", "page2.jpg"],
    "output.pdf",
    title="My Document",
)

print(f"PDF written: {result.output} ({result.page_count} pages)")

Warning

pdf_to_images() returns a PdfImportResult with success=False when PyMuPDF is not installed. images_to_pdf() uses Pillow and does not require PyMuPDF.


Next-gen formats (JXL / WebP 2)

The pixopt.nextgen module supports JPEG XL and WebP 2 when the underlying Pillow plugins are available.

from pixopt import (
    detect_format_support,
    is_format_supported,
    convert_to_nextgen,
    NextGenFormat,
)

# Check what is available
support = detect_format_support()
for info in support.formats:
    print(f"{info.format}: supported={info.supported}, note={info.note}")

# Convert to JXL, falling back to WEBP if not supported
result = convert_to_nextgen(
    "photo.jpg",
    "photo.jxl",
    fmt=NextGenFormat.JXL,
    quality=85,
    fallback=True,
    fallback_format="WEBP",
)

print(f"Format: {result.format}")
print(f"Fallback used: {result.fallback}")
print(f"Savings: {result.savings_percent:.1f}%")

Note

convert_to_nextgen() gracefully falls back to fallback_format when the requested next-gen format is not installed.


Duplicates detection

Scan a directory for duplicates

from pixopt import scan_duplicates

report = scan_duplicates(
    "./gallery",
    algorithm="phash",
    threshold=5,
    recursive=True,
    hash_size=8,
)

print(f"Scanned: {report.total_files}")
for group in report.duplicate_groups:
    print(f"Group {group.hash_hex}: {group.count} files")
    for f in group.files:
        print(f"  - {f}")

Compute hashes manually

from pixopt import phash, dhash, ahash, hamming_distance, compute_hash

h1 = compute_hash("image1.jpg", algorithm="phash", hash_size=8)
h2 = compute_hash("image2.jpg", algorithm="phash", hash_size=8)

distance = hamming_distance(h1.hash_hex, h2.hash_hex)
print(f"Hamming distance: {distance}")

Find duplicates from a list of hashes

from pathlib import Path
from pixopt import compute_hash, find_duplicates

files = ["image1.jpg", "image2.jpg", "image3.jpg"]
hashes = [compute_hash(f, algorithm="phash", hash_size=8) for f in files]

groups = find_duplicates(hashes, threshold=5)
for group in groups:
    print(f"Hash: {group.hash_hex}")
    for f in group.files:
        print(f"  - {f}")

Tip

Use threshold=0 in scan_duplicates() or find_duplicates() for exact-duplicate detection. A higher threshold catches near-duplicates.


Directory scanning

scan_directory() returns a structured inventory of a directory with aggregate statistics.

from pixopt import scan_directory

report = scan_directory(
    "./images",
    recursive=True,
    extensions={".jpg", ".jpeg", ".png", ".webp"},
)

print(f"Total files: {report.total_files}")
print(f"Valid images: {report.valid_images}, Errors: {report.errors}")
print(f"Total size: {report.human_total_size}")
print(f"Largest: {report.largest_file} ({report.largest_size} bytes)")
print("Format breakdown:", report.formats)

for entry in report.entries:
    print(f"{entry.file_path.name}: {entry.width}x{entry.height} {entry.format}")

Format benchmark

benchmark_formats() encodes an image across multiple formats and quality levels and recommends the smallest successful variant.

from pixopt import benchmark_formats

result = benchmark_formats(
    "photo.jpg",
    qualities=[50, 60, 70, 80, 90],
    formats=["JPEG", "WEBP", "AVIF", "PNG"],
)

for variant in result.variants:
    print(f"{variant.label}: {variant.size_bytes} bytes, saved {variant.savings_percent:.1f}%")

print(f"Recommended: {result.recommended_format} @ q{result.recommended_quality}")
print(f"Recommended size: {result.human_recommended_size}")

Note

AVIF is only included if a compatible Pillow plugin (for example pillow-avif-plugin or pillow-heif) is installed.


Color palette

extract_palette() returns the dominant colors of an image.

from pixopt import extract_palette

result = extract_palette("photo.jpg", n=6)

for swatch in result.colors:
    print(f"{swatch.hex} {swatch.rgb} {swatch.percent}%")

Tip

n must be between 1 and 32. Colors are sorted by frequency, with the most dominant color first.


Base64 and bytes I/O

All image optimization also works in-memory. This is useful for web servers and MCP servers that cannot touch disk.

Optimize raw bytes

from pixopt import optimize_bytes, OutputFormat

with open("photo.jpg", "rb") as f:
    data = f.read()

result = optimize_bytes(
    data,
    max_width=1200,
    quality=80,
    output_format=OutputFormat.WEBP,
)

print(f"Original: {result.human_original_size}")
print(f"Optimized: {result.human_optimized_size}")
print(f"Success: {result.success}")

# result.data is the optimized image as bytes
open("photo_optimized.webp", "wb").write(result.data)

Optimize base64 strings

import base64
from pixopt import optimize_base64

with open("photo.jpg", "rb") as f:
    b64 = base64.b64encode(f.read()).decode("ascii")

result = optimize_base64(
    b64,
    max_width=1200,
    quality=80,
    output_format=OutputFormat.WEBP,
)

if result.success:
    print(f"Output base64 length: {len(result.base64)}")

PIL Image helpers

from PIL import Image
from pixopt import image_to_bytes, image_to_base64, bytes_to_image, base64_to_image

with Image.open("photo.jpg") as img:
    img.load()
    data = image_to_bytes(img, fmt="WEBP", quality=80)
    b64 = image_to_base64(img, fmt="JPEG", quality=85)

img_from_bytes = bytes_to_image(data)
img_from_b64 = base64_to_image(b64)

Note

BytesResult and Base64Result include original_size, optimized_size, savings_bytes, savings_percent, width, and height.


Async API

pixopt exposes async variants of the core functions. CPU-bound work is dispatched to a background thread with asyncio.to_thread so the event loop stays responsive.

async optimize image

import asyncio
from pixopt import async_optimize_image, OutputFormat


async def main():
    result = await async_optimize_image(
        "photo.jpg",
        "photo_optimized.webp",
        max_width=1600,
        quality=85,
        output_format=OutputFormat.WEBP,
    )
    print(f"Saved: {result.savings_percent:.1f}%")


asyncio.run(main())

async batch optimize

import asyncio
from pixopt import async_batch_optimize, OutputFormat


async def main():
    report = await async_batch_optimize(
        ["a.jpg", "b.png", "c.webp"],
        "./optimized",
        max_width=1200,
        quality=80,
        output_format=OutputFormat.WEBP,
        max_concurrency=4,
    )
    print(f"Processed {report.succeeded}/{report.total_files}")


asyncio.run(main())

async optimize base64

import asyncio
import base64
from pixopt import async_optimize_base64


async def main():
    with open("photo.jpg", "rb") as f:
        b64 = base64.b64encode(f.read()).decode("ascii")

    result = await async_optimize_base64(
        b64,
        max_width=1200,
        quality=80,
    )
    print(f"Base64 success: {result.success}")


asyncio.run(main())

Tip

The async API mirrors the sync API. Other async helpers include async_optimize_bytes, async_inspect_image, async_scan_directory, and async_scan_duplicates.


Advanced: Custom pipelines

Combine multiple features into custom automation scripts. The Pipeline class lets you build fluent, chainable workflows.

Manual pipeline with the optimizer

from pathlib import Path
from pixopt import optimize_image
from pixopt.smart_format import detect_optimal_format

src_dir = Path("./uploads")
out_dir = Path("./optimized")
out_dir.mkdir(parents=True, exist_ok=True)

results = []
for src in src_dir.rglob("*"):
    if not src.is_file():
        continue

    rel = src.relative_to(src_dir)
    fmt = detect_optimal_format(src)

    result = optimize_image(
        src,
        out_dir / rel.with_suffix(f".{fmt.value}"),
        max_width=1600,
        quality=85,
        output_format=fmt,
        strip_metadata=True,
        backup_dir="./uploads_backup",
    )
    results.append(result)

successes = [r for r in results if r.success]
if successes:
    avg = sum(r.savings_percent for r in successes) / len(successes)
    print(f"Processed {len(successes)}/{len(results)} files, avg savings {avg:.1f}%")

Using the Pipeline class

from pixopt import Pipeline

result = (
    Pipeline()
    .open("photo.jpg")
    .auto_orient()
    .resize(max_width=1200, fit="cover", aspect_ratio="16:9")
    .watermark_text("(c) pixopt", position="bottom-right", opacity=0.5)
    .optimize(quality=85, output_format="WEBP")
    .save("photo_optimized.webp")
    .run()
)

print(f"Saved to {result.output_path} ({result.size_bytes} bytes)")
print(f"Steps: {result.steps_executed}")

You can also chain an image watermark:

from pixopt import Pipeline

result = (
    Pipeline()
    .open("photo.jpg")
    .resize(max_width=1600)
    .watermark_image("logo.png", position="bottom-right", scale=0.2, opacity=0.5)
    .optimize(quality=85, output_format="WEBP")
    .save("photo_watermarked.webp")
    .run()
)

Note

Pipeline.optimize() accepts OutputFormat or a string. Pipeline.run() returns a PipelineResult with output_path, width, height, format, size_bytes, and steps_executed.


Error handling

Most pixopt functions return a result object with success and error fields rather than raising exceptions for image-level failures. File-not-found and validation errors still raise.

from pixopt import optimize_image

result = optimize_image("corrupt.jpg", "out.webp")
if not result.success:
    print(f"Failed: {result.error}")

Warning

Always check result.success in batch, directory, bytes, and base64 workflows. Exceptions are reserved for programmer errors, path traversal attempts, and resource-limit violations.