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.0.tar.gz (47.7 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.0-py3-none-any.whl (35.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: arvel_image-0.6.0.tar.gz
  • Upload date:
  • Size: 47.7 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.0.tar.gz
Algorithm Hash digest
SHA256 56df68be4899d7377538cfdb227188f38fafdbe1a1784c6c8cc6604762d150df
MD5 7afa5c99a17aa70cc37965a9bc6cdef8
BLAKE2b-256 51705dfeae7e405a539ba9ce6e33d84f3e8169afcc03243a196e318c62a1ed77

See more details on using hashes here.

Provenance

The following attestation bundles were made for arvel_image-0.6.0.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.0-py3-none-any.whl.

File metadata

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

Hashes for arvel_image-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dee1a9523966fa5b9b2dd62c0c324182291ec59c1fc0220b22c45605985c464b
MD5 f3067af37068a03e5712da0432153e37
BLAKE2b-256 b18f5cb4290170ecd8b51bafe9a088f9927ff3a3a2ffea7d8984b2b2f88aee14

See more details on using hashes here.

Provenance

The following attestation bundles were made for arvel_image-0.6.0-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