DocsDocumentation

Nitro Image

Fast, chainable image processing for web apps and SaaS.

Fast, friendly image processing for Python web apps and SaaS. Nitro Image wraps Pillow with a chainable, lazy-evaluated pipeline so you can resize, convert, optimize, and generate responsive image sets with one fluent call.

from nitro_img import Image

Image("photo.jpg").resize(800).webp(quality=80).save("photo.webp")

Features

  • Chainable API - Fluent, readable pipelines instead of verbose PIL boilerplate
  • Lazy Execution - Operations queue up and only run on output (.save(), .to_bytes(), etc.)
  • Format Conversion - JPEG, PNG, WebP, GIF, and optional AVIF
  • Smart Resizing - resize, thumbnail, cover, contain with upscale control
  • Responsive Sets - Generate multiple widths for srcset in one call
  • Placeholders - LQIP data URIs, dominant colors, palettes, SVG, and BlurHash
  • Overlays - Watermarks and text with position, opacity, and scale control
  • Batch Processing - Glob-based batch with optional thread parallelism
  • Framework Integrations - One-line response helpers for Django, Flask, and FastAPI
  • Presets - Opinionated one-call helpers for thumbnails, avatars, OG images, and banners
  • Optimization - Target-size encoding with automatic quality tuning

Installation

pip install nitro-image

Optional extras:

pip install nitro-image[url]   # Load images from URLs (httpx)
pip install nitro-image[avif]  # AVIF format support
pip install nitro-image[blur]  # BlurHash generation
pip install nitro-image[all]   # Everything above

AI Assistant Integration

Add Nitro Image knowledge to your AI coding assistant:

npx skills add nitrosh/nitro-image

This enables AI assistants like Claude Code to understand Nitro Image and generate correct nitro_img code.

Why Nitro Image?

With Pillow alone:

from PIL import Image

img = Image.open("photo.jpg")
img = img.convert("RGB")
width, height = img.size
new_height = int(height * (800 / width))
img = img.resize((800, new_height), Image.LANCZOS)
img.save("photo.webp", "WEBP", quality=80)

With Nitro Image:

from nitro_img import Image

Image("photo.jpg").resize(800).webp(quality=80).save("photo.webp")

Operations queue up and only run when you call an output method like .save() or .to_bytes(), so a long chain still touches the pixels once.

Resize and crop

Image("photo.jpg").resize(800).save("resized.jpg")
Image("photo.jpg").thumbnail(200, 200).save("thumb.jpg")
Image("photo.jpg").cover(400, 400).save("square.jpg")
Image("photo.jpg").contain(400, 400).save("contained.jpg")
Image("photo.jpg").crop(500, 400, anchor="center").save("cropped.jpg")

Format conversion

Image("photo.jpg").webp(quality=80).save("photo.webp")
Image("photo.jpg").png().save("photo.png")
Image("photo.jpg").jpeg(quality=90).save("photo.jpg")
Image("photo.jpg").auto_format().save("photo.webp")  # picks best format

Adjustments and effects

Image("photo.jpg").brightness(1.2).contrast(1.1).save("enhanced.jpg")
Image("photo.jpg").sharpen(1.5).save("sharp.jpg")
Image("photo.jpg").blur(2.0).save("blurred.jpg")
Image("photo.jpg").grayscale().save("gray.jpg")
Image("photo.jpg").sepia().save("sepia.jpg")
Image("photo.jpg").rounded_corners(20).png().save("rounded.png")

Watermarks and text overlays

Image("photo.jpg").watermark("logo.png", position="bottom-right", opacity=0.5).save("watermarked.jpg")
Image("photo.jpg").text_overlay("Sample", font_size=48).save("labeled.jpg")

Responsive images

widths = Image("photo.jpg").responsive([400, 800, 1200, 1600])
# Returns {400: bytes, 800: bytes, 1200: bytes, 1600: bytes}

Image("photo.jpg").webp().save_responsive("output/", [400, 800, 1200], name="hero")
# Saves output/hero_400.webp, output/hero_800.webp, output/hero_1200.webp

Placeholders

Image("photo.jpg").lqip()            # Low-quality base64 data URI
Image("photo.jpg").dominant_color()  # "#3a6b8c"
Image("photo.jpg").color_palette(5)  # ["#3a6b8c", "#d4a574", ...]
Image("photo.jpg").svg_placeholder() # SVG with dominant color
Image("photo.jpg").blurhash()        # "LKO2:N%2Tw=w]~RBVZRi..."

Optimization

Image("photo.jpg").optimize(target_kb=200)
# Returns the encoded bytes, auto-tuning quality to hit the target size

Presets

Presets are opinionated one-call helpers. They take a source (path, bytes, or file-like) and return encoded bytes.

from nitro_img import presets, Image

presets.thumbnail("photo.jpg")                 # 200x200 WebP
presets.avatar("photo.jpg", size=128)          # 128px circle-cropped PNG
presets.og_image("photo.jpg")                  # 1200x630 JPEG social card
presets.banner("photo.jpg")                    # 1920x400 JPEG banner
presets.avatar_placeholder("SN")               # Initials avatar

# Presets are also available via Image.preset for convenience:
Image.preset.thumbnail("photo.jpg")

Batch processing

from nitro_img import BatchImage

BatchImage("photos/*.jpg").resize(800).webp().save("output/{name}.webp")
BatchImage("photos/*.jpg").resize(800).jpeg().save("output/{name}.jpg", parallel=True)

{name} in the save pattern is replaced with each source file's stem.

Web framework responses

# Django
return Image("photo.jpg").resize(400).webp().to_django_response()

# Flask
return Image("photo.jpg").resize(400).webp().to_flask_response()

# FastAPI / Starlette
return Image("photo.jpg").resize(400).webp().to_fastapi_response()

Loading from anywhere

Image("photo.jpg")                          # File path
Image.from_bytes(raw_bytes)                 # Bytes
Image.from_base64(b64_string)               # Base64 string
Image.from_url("https://example.com/img")   # URL (requires httpx)
Image.from_file(file_object)                # File-like object

Output options

img = Image("photo.jpg").resize(400).webp()

img.save("output.webp")       # Save to file
img.to_bytes()                # Raw bytes
img.to_base64()               # Base64 encoded string
img.to_data_uri()             # data:image/webp;base64,...
img.to_response()             # {"body": bytes, "content_type": str, "content_length": int}

Chain everything

All operations are chainable and lazily evaluated:

(
    Image("photo.jpg")
    .resize(800)
    .brightness(1.1)
    .contrast(1.05)
    .sharpen(1.2)
    .sepia()
    .rounded_corners(10)
    .png()
    .save("final.png")
)

Configuration

from nitro_img import config

config.update(
    default_jpeg_quality=85,
    default_webp_quality=80,
    default_png_compression=6,
    allow_upscale=False,
    auto_orient=True,
    strip_metadata=False,
    max_output_dimensions=10_000,
)

Requirements

  • Python 3.10+
  • Pillow 10.0+

Ecosystem

License

This project is licensed under the BSD 3-Clause License. See the LICENSE file for details.

API Reference

Auto-generated from the installed package's public API. Signatures and docstrings come directly from the source.

Image

Image#

Image(source: 'str | Path') -> 'None'

Chainable image processing interface with lazy execution.

50 methods
auto_format(self) -> 'Image'

Defer format choice until output time, picking the best fit.

blur(self, radius: 'float' = 2.0) -> 'Image'

Queue a Gaussian blur.

blurhash(self, components_x: 'int' = 4, components_y: 'int' = 3) -> 'str'

Return a BlurHash string representing the source image.

brightness(self, factor: 'float') -> 'Image'

Queue a brightness adjustment.

color_palette(self, count: 'int' = 5) -> 'list[str]'

Return the top ``count`` colors as a list of hex strings.

contain(self, width: 'int', height: 'int', bg: 'str' = 'white', *, allow_upscale: 'bool | None' = None) -> 'Image'

Queue a contain-fit resize, padding empty space.

contrast(self, factor: 'float') -> 'Image'

Queue a contrast adjustment.

cover(self, width: 'int', height: 'int', *, allow_upscale: 'bool | None' = None) -> 'Image'

Queue a cover-fit resize, centre-cropping overflow.

crop(self, width: 'int', height: 'int', anchor: 'Anchor' = 'center') -> 'Image'

Queue a crop to the given dimensions at an anchor point.

dominant_color(self) -> 'str'

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

flip(self) -> 'Image'

Queue a vertical flip (top-to-bottom).

format(self, fmt: 'Format | str', quality: 'int | None' = None) -> 'Image'

Select an output format by enum or case-insensitive string.

from_base64(b64_string: 'str') -> 'Image'

Create an ``Image`` from a base64-encoded string.

from_bytes(data: 'bytes') -> 'Image'

Create an ``Image`` from raw encoded image bytes.

from_file(file_obj: 'IO[bytes]') -> 'Image'

Create an ``Image`` from a binary file-like object.

from_url(url: 'str') -> 'Image'

Fetch and load an image from a URL.

get_metadata(self) -> 'dict'

Return metadata from the original source image.

gif(self) -> 'Image'

Select GIF as the output format.

grayscale(self) -> 'Image'

Queue a grayscale conversion.

height()

Height of the original source image in pixels.

jpeg(self, quality: 'int | None' = None) -> 'Image'

Select JPEG as the output format.

lqip(self, width: 'int' = 20) -> 'str'

Return a Low Quality Image Placeholder as a base64 data URI.

mirror(self) -> 'Image'

Queue a horizontal flip (left-to-right).

optimize(self, target_kb: 'int', *, min_quality: 'int' = 10, max_quality: 'int' = 95) -> 'bytes'

Encode while binary-searching quality to hit a target file size.

png(self) -> 'Image'

Select PNG as the output format.

resize(self, width: 'int | None' = None, height: 'int | None' = None, *, allow_upscale: 'bool | None' = None) -> 'Image'

Queue a proportional resize, preserving aspect ratio.

responsive(self, widths: 'list[int] | None' = None, *, fmt: 'Format | None' = None, quality: 'int | None' = None, allow_upscale: 'bool' = False) -> 'dict[int, bytes]'

Render the pipeline at multiple widths and return the encoded bytes.

rotate(self, degrees: 'float', *, expand: 'bool' = True, fill: 'str' = 'white') -> 'Image'

Queue a rotation by the given number of degrees counter-clockwise.

rounded_corners(self, radius: 'int') -> 'Image'

Queue rounded-corner masking, producing an alpha channel.

saturation(self, factor: 'float') -> 'Image'

Queue a saturation adjustment.

save(self, path: 'str | Path') -> 'Path'

Run the pipeline and write the result to disk.

save_responsive(self, output_dir: 'str | Path', widths: 'list[int] | None' = None, name: 'str | None' = None, *, fmt: 'Format | None' = None, quality: 'int | None' = None, allow_upscale: 'bool' = False) -> 'dict[int, Path]'

Render the pipeline at multiple widths and save each to disk.

sepia(self) -> 'Image'

Queue a sepia tone effect.

sharpen(self, factor: 'float' = 2.0) -> 'Image'

Queue a sharpness adjustment.

size()

Size of the original source image as ``(width, height)``.

source_format()

Detected format of the original source image, if known.

strip_metadata(self) -> 'Image'

Queue removal of EXIF, IPTC, and XMP metadata from the output.

svg_placeholder(self, width: 'int | None' = None, height: 'int | None' = None) -> 'str'

Return a tiny SVG placeholder filled with the dominant color.

text_overlay(self, text: 'str', position: 'str' = 'bottom-right', font_path: 'str | None' = None, font_size: 'int' = 24, color: 'str | tuple' = 'white', opacity: 'float' = 1.0, margin: 'int' = 10) -> 'Image'

Queue a text overlay drawn on top of the image.

thumbnail(self, width: 'int', height: 'int', *, allow_upscale: 'bool | None' = None) -> 'Image'

Queue an in-place thumbnail fit inside a box.

to_base64(self) -> 'str'

Run the pipeline and return a base64-encoded string.

to_bytes(self) -> 'bytes'

Run the pipeline and return the encoded image bytes.

to_data_uri(self) -> 'str'

Run the pipeline and return an inline data URI.

to_django_response(self, *, filename: 'str | None' = None) -> 'object'

Run the pipeline and return a Django ``HttpResponse``.

to_fastapi_response(self, *, filename: 'str | None' = None) -> 'object'

Run the pipeline and return a FastAPI/Starlette ``Response``.

to_flask_response(self, *, filename: 'str | None' = None) -> 'object'

Run the pipeline and return a Flask ``Response``.

to_response(self) -> 'dict'

Run the pipeline and return a framework-agnostic response dict.

watermark(self, source: 'str | Path | PILImage.Image', position: 'str' = 'bottom-right', opacity: 'float' = 0.3, scale: 'float | None' = None, margin: 'int' = 10) -> 'Image'

Queue an image watermark overlay.

webp(self, quality: 'int | None' = None) -> 'Image'

Select WebP as the output format.

width()

Width of the original source image in pixels.

BatchImage

BatchImage#

BatchImage(pattern: 'str') -> 'None'

Apply the same pipeline to every file matching a glob pattern.

26 methods
blur(self, radius: 'float' = 2.0) -> 'BatchImage'

Record a Gaussian blur applied to every file.

brightness(self, factor: 'float') -> 'BatchImage'

Record a brightness adjustment applied to every file.

contain(self, width: 'int', height: 'int', bg: 'str' = 'white', **kw: 'object') -> 'BatchImage'

Record a contain-fit resize applied to every file.

contrast(self, factor: 'float') -> 'BatchImage'

Record a contrast adjustment applied to every file.

count()

Number of files matched by the glob pattern.

cover(self, width: 'int', height: 'int', **kw: 'object') -> 'BatchImage'

Record a cover-fit resize applied to every file.

crop(self, width: 'int', height: 'int', anchor: 'Anchor' = 'center') -> 'BatchImage'

Record a crop applied to every file.

flip(self) -> 'BatchImage'

Record a vertical flip applied to every file.

format(self, fmt: 'Format | str', quality: 'int | None' = None) -> 'BatchImage'

Select the output format by enum or case-insensitive string.

grayscale(self) -> 'BatchImage'

Record a grayscale conversion applied to every file.

jpeg(self, quality: 'int | None' = None) -> 'BatchImage'

Select JPEG for every image in the batch.

mirror(self) -> 'BatchImage'

Record a horizontal flip applied to every file.

paths()

List of matched source file paths, sorted alphabetically.

png(self) -> 'BatchImage'

Select PNG for every image in the batch.

resize(self, width: 'int | None' = None, height: 'int | None' = None, **kw: 'object') -> 'BatchImage'

Record a proportional resize applied to every file.

rotate(self, degrees: 'float', **kw: 'object') -> 'BatchImage'

Record a rotation applied to every file.

rounded_corners(self, radius: 'int') -> 'BatchImage'

Record rounded-corner masking applied to every file.

saturation(self, factor: 'float') -> 'BatchImage'

Record a saturation adjustment applied to every file.

save(self, pattern: 'str', *, parallel: 'bool' = False, max_workers: 'int | None' = None) -> 'list[Path]'

Save every matched image through the recorded pipeline.

sepia(self) -> 'BatchImage'

Record a sepia tone effect applied to every file.

sharpen(self, factor: 'float' = 2.0) -> 'BatchImage'

Record a sharpness adjustment applied to every file.

strip_metadata(self) -> 'BatchImage'

Record metadata stripping (EXIF/IPTC/XMP) for every file.

text_overlay(self, text: 'str', **kw: 'object') -> 'BatchImage'

Record a text overlay applied to every file.

thumbnail(self, width: 'int', height: 'int', **kw: 'object') -> 'BatchImage'

Record a thumbnail fit inside ``width`` x ``height`` for each file.

watermark(self, source: 'object', position: 'str' = 'bottom-right', opacity: 'float' = 0.3, scale: 'float | None' = None, margin: 'int' = 10) -> 'BatchImage'

Record an image watermark applied to every file.

webp(self, quality: 'int | None' = None) -> 'BatchImage'

Select WebP for every image in the batch.

Presets

presets#

One-call helpers for the most common image tasks.

Config

config#

Process-wide defaults for load, processing, and output behaviour.

Types

Format#

Format(*values)

Image format identifier used throughout the public API.

2 methods
name()

The name of the Enum member.

value()

The value of the Enum member.

Position#

Position(*values)

Anchor position for crops, watermarks, and text overlays.

2 methods
name()

The name of the Enum member.

value()

The value of the Enum member.

ResizeStrategy#

ResizeStrategy(*values)

How a resize operation fits the source into target dimensions.

2 methods
name()

The name of the Enum member.

value()

The value of the Enum member.

Exceptions

NitroImgError#

Base class for every exception raised by nitro-img.

ImageFormatError#

Raised when the output format is missing, invalid, or unsupported.

ImageLoadError#

Raised when the source image cannot be read or decoded.

ImageOutputError#

Raised when the processed image cannot be written or encoded.

ImageProcessingError#

Raised when a queued pipeline operation fails during execution.

ImageSizeError#

Raised when an image exceeds a configured size limit.