Skip to main content

Fluent, type-safe image manipulation for Arvel — Spatie Image v3 parity, Pillow-only.

Project description

arvel-image

PyPI MIT License

Image transforms and a polymorphic media library for Arvel — Python ports of two Spatie packages in one wheel:

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

arvel_image-0.6.1.tar.gz (46.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

arvel_image-0.6.1-py3-none-any.whl (35.2 kB view details)

Uploaded Python 3

File details

Details for the file arvel_image-0.6.1.tar.gz.

File metadata

  • Download URL: arvel_image-0.6.1.tar.gz
  • Upload date:
  • Size: 46.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for arvel_image-0.6.1.tar.gz
Algorithm Hash digest
SHA256 ae4e1726457dfe2ea6d7251999efec8fa4beb038aed7dc6473053a67e79f5eb0
MD5 9080cf92f5e0f214f488d8b38f5cb2d8
BLAKE2b-256 3d642f9ad850485e8aa2115b07ae717b5c96cce2dc31f5e33bc3e2429873595a

See more details on using hashes here.

Provenance

The following attestation bundles were made for arvel_image-0.6.1.tar.gz:

Publisher: publish.yml on mohamed-rekiba/arvel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file arvel_image-0.6.1-py3-none-any.whl.

File metadata

  • Download URL: arvel_image-0.6.1-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

Hashes for arvel_image-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9dd0541857d4714828cc1f345bc53dfc33186bbe354605d7d0c75abad27133e9
MD5 06ceb3f572dce7d167dc27b0744aeea2
BLAKE2b-256 6713ac5b29cc6de9f685ebfe2fd3642a5c9c6d26b5d301081b2bd3c629e8bc07

See more details on using hashes here.

Provenance

The following attestation bundles were made for arvel_image-0.6.1-py3-none-any.whl:

Publisher: publish.yml on mohamed-rekiba/arvel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page