Resize, convert, and optimize images with a chainable API. Generate responsive sizes in one call.
Project description
Nitro Image
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.
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
Quickstart
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
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nitro_image-1.1.1.tar.gz.
File metadata
- Download URL: nitro_image-1.1.1.tar.gz
- Upload date:
- Size: 65.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4d25914d9349a95c65c229336f8d8b1c0b4bf23c49b2dd70879d370d09f7a53
|
|
| MD5 |
d498e310c87418a089f7f2428a84859e
|
|
| BLAKE2b-256 |
d842e06c869de1b952049a0d820c60649d3ba02aa423f51b6928748af16bc99a
|
File details
Details for the file nitro_image-1.1.1-py3-none-any.whl.
File metadata
- Download URL: nitro_image-1.1.1-py3-none-any.whl
- Upload date:
- Size: 41.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
54fe4a2d17a130d02df9d166c13228fd8ad646d6b7e84f2231072d06cb4f3458
|
|
| MD5 |
01dcfe81c366138bc78942fd3987468d
|
|
| BLAKE2b-256 |
04f51a3972d1075a642bdb828675b512478bd9bcabfe9cec6c34b62cf3a4b2a6
|