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 — v0.3.0.


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

Uploaded Python 3

File details

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

File metadata

  • Download URL: arvel_image-0.5.0.tar.gz
  • Upload date:
  • Size: 47.5 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.5.0.tar.gz
Algorithm Hash digest
SHA256 7771470e08de2256336c72855cb8545ba30b148b3e99536fb82cec72a820f9bb
MD5 289e35389a5c3a5b3c4ac32cbf29d27b
BLAKE2b-256 bec738a2d4986eb4c31f4f5ab890bca6a560d4ba1610bb093a4e88cf53ed68ae

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: arvel_image-0.5.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.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 33cde96095c8d5f800977bfd7e5d8523d70c58d7439ba0910efe3fdfecef2af3
MD5 2de9026ee124c24271f8c8bc1f492aec
BLAKE2b-256 daa6fd2815158e413de9261905f5be95849138095c56ba3ffa2f08d7c859b1ec

See more details on using hashes here.

Provenance

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