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.
Metadata
Release files for nitro-image 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nitro_image-1.1.1.tar.gz | 65.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nitro_image-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 106.6 kB
Release files / nitro_image-1.1.1.tar.gz
| Download URL | nitro_image-1.1.1.tar.gz |
|---|---|
| Size | 65.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f4d25914d9349a95c65c229336f8d8b1c0b4bf23c49b2dd70879d370d09f7a53
|
|
BLAKE2b-256 checksum How to use checksums |
d842e06c869de1b952049a0d820c60649d3ba02aa423f51b6928748af16bc99a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / nitro_image-1.1.1-py3-none-any.whl
| Download URL | nitro_image-1.1.1-py3-none-any.whl |
|---|---|
| Size | 41.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
54fe4a2d17a130d02df9d166c13228fd8ad646d6b7e84f2231072d06cb4f3458
|
|
BLAKE2b-256 checksum How to use checksums |
04f51a3972d1075a642bdb828675b512478bd9bcabfe9cec6c34b62cf3a4b2a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|