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,containwith upscale control - Responsive Sets - Generate multiple widths for
srcsetin 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
- nitro-ui - Build HTML with Python, not strings
- nitro-cli - Python-powered static site generator
- nitro-datastore - Schema-free JSON data store with dot notation access
- nitro-dispatch - Framework-agnostic plugin system
- nitro-validate - Dependency-free data validation
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. Defer format choice until output time, picking the best fit. Queue a Gaussian blur. Return a BlurHash string representing the source image. Queue a brightness adjustment. Return the top ``count`` colors as a list of hex strings. Queue a contain-fit resize, padding empty space. Queue a contrast adjustment. Queue a cover-fit resize, centre-cropping overflow. Queue a crop to the given dimensions at an anchor point. Return the dominant color of the image as a hex string. Queue a vertical flip (top-to-bottom). Select an output format by enum or case-insensitive string. Create an ``Image`` from a base64-encoded string. Create an ``Image`` from raw encoded image bytes. Create an ``Image`` from a binary file-like object. Fetch and load an image from a URL. Return metadata from the original source image. Select GIF as the output format. Queue a grayscale conversion. Height of the original source image in pixels. Select JPEG as the output format. Return a Low Quality Image Placeholder as a base64 data URI. Queue a horizontal flip (left-to-right). Encode while binary-searching quality to hit a target file size. Select PNG as the output format. Queue a proportional resize, preserving aspect ratio. Render the pipeline at multiple widths and return the encoded bytes. Queue a rotation by the given number of degrees counter-clockwise. Queue rounded-corner masking, producing an alpha channel. Queue a saturation adjustment. Run the pipeline and write the result to disk. Render the pipeline at multiple widths and save each to disk. Queue a sepia tone effect. Queue a sharpness adjustment. Size of the original source image as ``(width, height)``. Detected format of the original source image, if known. Queue removal of EXIF, IPTC, and XMP metadata from the output. Return a tiny SVG placeholder filled with the dominant color. Queue a text overlay drawn on top of the image. Queue an in-place thumbnail fit inside a box. Run the pipeline and return a base64-encoded string. Run the pipeline and return the encoded image bytes. Run the pipeline and return an inline data URI. Run the pipeline and return a Django ``HttpResponse``. Run the pipeline and return a FastAPI/Starlette ``Response``. Run the pipeline and return a Flask ``Response``. Run the pipeline and return a framework-agnostic response dict. Queue an image watermark overlay. Select WebP as the output format. Width of the original source image in pixels.50 methods
auto_format(self) -> 'Image'blur(self, radius: 'float' = 2.0) -> 'Image'blurhash(self, components_x: 'int' = 4, components_y: 'int' = 3) -> 'str'brightness(self, factor: 'float') -> 'Image'color_palette(self, count: 'int' = 5) -> 'list[str]'contain(self, width: 'int', height: 'int', bg: 'str' = 'white', *, allow_upscale: 'bool | None' = None) -> 'Image'contrast(self, factor: 'float') -> 'Image'cover(self, width: 'int', height: 'int', *, allow_upscale: 'bool | None' = None) -> 'Image'crop(self, width: 'int', height: 'int', anchor: 'Anchor' = 'center') -> 'Image'dominant_color(self) -> 'str'flip(self) -> 'Image'format(self, fmt: 'Format | str', quality: 'int | None' = None) -> 'Image'from_base64(b64_string: 'str') -> 'Image'from_bytes(data: 'bytes') -> 'Image'from_file(file_obj: 'IO[bytes]') -> 'Image'from_url(url: 'str') -> 'Image'get_metadata(self) -> 'dict'gif(self) -> 'Image'grayscale(self) -> 'Image'height()jpeg(self, quality: 'int | None' = None) -> 'Image'lqip(self, width: 'int' = 20) -> 'str'mirror(self) -> 'Image'optimize(self, target_kb: 'int', *, min_quality: 'int' = 10, max_quality: 'int' = 95) -> 'bytes'png(self) -> 'Image'resize(self, width: 'int | None' = None, height: 'int | None' = None, *, allow_upscale: 'bool | None' = None) -> 'Image'responsive(self, widths: 'list[int] | None' = None, *, fmt: 'Format | None' = None, quality: 'int | None' = None, allow_upscale: 'bool' = False) -> 'dict[int, bytes]'rotate(self, degrees: 'float', *, expand: 'bool' = True, fill: 'str' = 'white') -> 'Image'rounded_corners(self, radius: 'int') -> 'Image'saturation(self, factor: 'float') -> 'Image'save(self, path: 'str | Path') -> 'Path'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]'sepia(self) -> 'Image'sharpen(self, factor: 'float' = 2.0) -> 'Image'size()source_format()strip_metadata(self) -> 'Image'svg_placeholder(self, width: 'int | None' = None, height: 'int | None' = None) -> 'str'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'thumbnail(self, width: 'int', height: 'int', *, allow_upscale: 'bool | None' = None) -> 'Image'to_base64(self) -> 'str'to_bytes(self) -> 'bytes'to_data_uri(self) -> 'str'to_django_response(self, *, filename: 'str | None' = None) -> 'object'to_fastapi_response(self, *, filename: 'str | None' = None) -> 'object'to_flask_response(self, *, filename: 'str | None' = None) -> 'object'to_response(self) -> 'dict'watermark(self, source: 'str | Path | PILImage.Image', position: 'str' = 'bottom-right', opacity: 'float' = 0.3, scale: 'float | None' = None, margin: 'int' = 10) -> 'Image'webp(self, quality: 'int | None' = None) -> 'Image'width()
BatchImage
BatchImage#
BatchImage(pattern: 'str') -> 'None'Apply the same pipeline to every file matching a glob pattern. Record a Gaussian blur applied to every file. Record a brightness adjustment applied to every file. Record a contain-fit resize applied to every file. Record a contrast adjustment applied to every file. Number of files matched by the glob pattern. Record a cover-fit resize applied to every file. Record a crop applied to every file. Record a vertical flip applied to every file. Select the output format by enum or case-insensitive string. Record a grayscale conversion applied to every file. Select JPEG for every image in the batch. Record a horizontal flip applied to every file. List of matched source file paths, sorted alphabetically. Select PNG for every image in the batch. Record a proportional resize applied to every file. Record a rotation applied to every file. Record rounded-corner masking applied to every file. Record a saturation adjustment applied to every file. Save every matched image through the recorded pipeline. Record a sepia tone effect applied to every file. Record a sharpness adjustment applied to every file. Record metadata stripping (EXIF/IPTC/XMP) for every file. Record a text overlay applied to every file. Record a thumbnail fit inside ``width`` x ``height`` for each file. Record an image watermark applied to every file. Select WebP for every image in the batch.26 methods
blur(self, radius: 'float' = 2.0) -> 'BatchImage'brightness(self, factor: 'float') -> 'BatchImage'contain(self, width: 'int', height: 'int', bg: 'str' = 'white', **kw: 'object') -> 'BatchImage'contrast(self, factor: 'float') -> 'BatchImage'count()cover(self, width: 'int', height: 'int', **kw: 'object') -> 'BatchImage'crop(self, width: 'int', height: 'int', anchor: 'Anchor' = 'center') -> 'BatchImage'flip(self) -> 'BatchImage'format(self, fmt: 'Format | str', quality: 'int | None' = None) -> 'BatchImage'grayscale(self) -> 'BatchImage'jpeg(self, quality: 'int | None' = None) -> 'BatchImage'mirror(self) -> 'BatchImage'paths()png(self) -> 'BatchImage'resize(self, width: 'int | None' = None, height: 'int | None' = None, **kw: 'object') -> 'BatchImage'rotate(self, degrees: 'float', **kw: 'object') -> 'BatchImage'rounded_corners(self, radius: 'int') -> 'BatchImage'saturation(self, factor: 'float') -> 'BatchImage'save(self, pattern: 'str', *, parallel: 'bool' = False, max_workers: 'int | None' = None) -> 'list[Path]'sepia(self) -> 'BatchImage'sharpen(self, factor: 'float' = 2.0) -> 'BatchImage'strip_metadata(self) -> 'BatchImage'text_overlay(self, text: 'str', **kw: 'object') -> 'BatchImage'thumbnail(self, width: 'int', height: 'int', **kw: 'object') -> 'BatchImage'watermark(self, source: 'object', position: 'str' = 'bottom-right', opacity: 'float' = 0.3, scale: 'float | None' = None, margin: 'int' = 10) -> 'BatchImage'webp(self, quality: 'int | None' = None) -> 'BatchImage'
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. The name of the Enum member. The value of the Enum member.2 methods
name()value()
Position#
Position(*values)Anchor position for crops, watermarks, and text overlays. The name of the Enum member. The value of the Enum member.2 methods
name()value()
ResizeStrategy#
ResizeStrategy(*values)How a resize operation fits the source into target dimensions. The name of the Enum member. The value of the Enum member.2 methods
name()value()
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.