Fluent, type-safe image manipulation for Arvel — Spatie Image v3 parity, Pillow-only.
Project description
arvel-image
Image transforms and a polymorphic media library for Arvel — Python ports of two Spatie packages in one wheel:
- spatie/image v3 — fluent, Pillow-backed transforms.
- spatie/laravel-medialibrary v11 — a
polymorphic
mediatable that attaches files to any model.
Status: Pre-alpha.
Documentation: https://arvel.dev/packages/image
Install
uv add arvel-image
# or: pip install arvel-image
For AVIF / HEIC support, add Pillow-HEIF:
uv add 'arvel-image[heif]'
Image transforms
Image is a fluent, synchronous wrapper around Pillow — no external binaries, no shell calls.
from arvel_image import Image
(
Image.load("photo.jpg")
.orient() # honour EXIF rotation
.fit("cover", 800, 600)
.format("webp")
.quality(85)
.save("photo.webp")
)
# Get bytes (useful inside a request handler)
thumbnail: bytes = (
Image.load("avatar.jpg").fit("cover", 256, 256).format("png").to_bytes()
)
The chain is lazy — load and the pixel operations just record what to do, and nothing decodes
or transforms until a terminal runs. So in an async handler, reach for the *_async terminals;
they offload the whole pipeline (decode + transforms + encode) to a worker thread, keeping the
event loop free:
from arvel_image import Image
data: bytes = await (
Image.load(source).fit("cover", 256, 256).format("webp").to_bytes_async()
)
await Image.load(source).fit("cover", 256, 256).save_async("avatar.webp")
Operations
| Method | Description |
|---|---|
Image.load(source) |
Load from a path, file-like object, or bytes |
.orient() |
Auto-rotate based on EXIF orientation |
.fit(mode, width, height) |
"cover" or "contain" |
.resize(width=…, height=…) |
Stretch to exact dimensions |
.crop(left=…, top=…, width=…, height=…) |
Crop to a fixed window |
.width(px) / .height(px) |
Single-axis resize, preserves aspect ratio |
.format(fmt) |
"jpeg", "png", "webp", "gif" |
.quality(q) |
1–100, applies to JPEG and WebP |
.background(color) |
Fill transparent areas (e.g. "white", "#fff") |
.optimize() |
Enable Pillow's optimizer pass |
.save(path) |
Write to disk |
.save_async(path) |
awaitable .save() — offloads to a thread |
.to_bytes() |
Return raw bytes |
.to_bytes_async() |
awaitable .to_bytes() — offloads to a thread |
Media library
HasMedia gives any model a polymorphic media collection — upload, store, retrieve, and auto-convert
files attached to any row.
Register the provider in bootstrap/providers.py and run the migration:
from arvel_image import ImageServiceProvider
providers = [
# ...other providers...
ImageServiceProvider,
]
arvel migrate
Add HasMedia to a model
from arvel.database import Model, Timestamps, id_, string
from arvel_image import HasMedia, MediaCollection, Conversion
class Post(Model, Timestamps, HasMedia):
__tablename__ = "posts"
id: int = id_()
title: str = string(200)
def register_media_collections(self) -> None:
(
MediaCollection("cover")
.single_file(True) # one cover image per post
.with_conversions(
Conversion("thumb").fit("cover", 400, 300).format("webp"),
Conversion("og").fit("contain", 1200, 630).format("jpeg").quality(90),
)
.register_on(self)
)
Attach media
post = await Post.find_or_fail(post_id)
# From a file path
await post.add_media("uploads/photo.jpg").to_media_collection("cover")
# From uploaded bytes
await post.add_media(file_bytes, file_name="cover.jpg").to_media_collection("cover")
# From a URL (SSRF-guarded)
await post.add_media_from_url("https://example.com/image.jpg").to_media_collection("cover")
# From a base64 data URI
await post.add_media_from_base64(data_uri, file_name="cover.jpg").to_media_collection("cover")
Retrieve media
media_list = await post.get_media("cover") # all, ordered
url = await post.get_media_url("cover") # original
thumb_url = await post.get_media_url("cover", conversion="thumb") # derived
first = await post.get_first_media("cover")
await post.clear_media_collection("cover")
Conversions
Conversions are declarative chains. They run in a background job
(GenerateImageConversionsJob) dispatched automatically after each add_media call — no manual
wiring:
from arvel_image.media.conversion import Conversion
Conversion("thumb").fit("cover", 400, 300).format("webp").quality(80)
Conversion("og").fit("contain", 1200, 630).format("jpeg").quality(90)
Conversion("avatar").resize(width=128, height=128).format("png")
Collection options
MediaCollection is a fluent builder:
(
MediaCollection("gallery")
.single_file(False) # keep all files (default)
.only_keep_latest(10) # prune oldest beyond 10
.accept_mime_types(["image/jpeg", "image/png"])
.max_file_size(5 * 1024 * 1024) # 5 MB limit
.use_disk("s3") # separate disk for originals
.use_conversions_disk("s3-public") # separate disk for derivatives
.use_fallback_url("/images/placeholder.jpg")
.register_on(self)
)
Why one package?
Laravel apps that use spatie/image almost always use spatie/laravel-medialibrary too. Shipping
both in one wheel means one extras flag (arvel[image]), one arvel migrate, and one provider to
register. The transform API (Image) is standalone — you can use it without the media library.
License
MIT — see LICENSE.
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 arvel_image-0.7.0.tar.gz.
File metadata
- Download URL: arvel_image-0.7.0.tar.gz
- Upload date:
- Size: 46.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a57f1c8abfb8941dddeee0511e24921e8a2109637af89e375d751cf495f1378
|
|
| MD5 |
25b400c2973d39c54a587eea1ff814c8
|
|
| BLAKE2b-256 |
325ee0d0828aae810dc846b9b3467c3838d07e9e167dcf8afafcc08174a12711
|
Provenance
The following attestation bundles were made for arvel_image-0.7.0.tar.gz:
Publisher:
publish.yml on mohamed-rekiba/arvel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arvel_image-0.7.0.tar.gz -
Subject digest:
0a57f1c8abfb8941dddeee0511e24921e8a2109637af89e375d751cf495f1378 - Sigstore transparency entry: 1703455338
- Sigstore integration time:
-
Permalink:
mohamed-rekiba/arvel@d52e3f0ef0aa4db5aecab57250bb2d3bd13404b0 -
Branch / Tag:
refs/tags/arvel-image-v0.7.0 - Owner: https://github.com/mohamed-rekiba
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d52e3f0ef0aa4db5aecab57250bb2d3bd13404b0 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file arvel_image-0.7.0-py3-none-any.whl.
File metadata
- Download URL: arvel_image-0.7.0-py3-none-any.whl
- Upload date:
- Size: 35.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e29e332566b576296a80729a56508ff7a1b6803d84a75e5bd692d62f4cc671ec
|
|
| MD5 |
2a526fc10e25c8ba7481479a67de58a3
|
|
| BLAKE2b-256 |
af1378965ae54bd2a63fd271adb058cb2ac2048e51391e53e9b4b538d93d2e37
|
Provenance
The following attestation bundles were made for arvel_image-0.7.0-py3-none-any.whl:
Publisher:
publish.yml on mohamed-rekiba/arvel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arvel_image-0.7.0-py3-none-any.whl -
Subject digest:
e29e332566b576296a80729a56508ff7a1b6803d84a75e5bd692d62f4cc671ec - Sigstore transparency entry: 1703455404
- Sigstore integration time:
-
Permalink:
mohamed-rekiba/arvel@d52e3f0ef0aa4db5aecab57250bb2d3bd13404b0 -
Branch / Tag:
refs/tags/arvel-image-v0.7.0 - Owner: https://github.com/mohamed-rekiba
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d52e3f0ef0aa4db5aecab57250bb2d3bd13404b0 -
Trigger Event:
workflow_dispatch
-
Statement type: