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

Uploaded Python 3

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

Hashes for arvel_image-0.7.0.tar.gz
Algorithm Hash digest
SHA256 0a57f1c8abfb8941dddeee0511e24921e8a2109637af89e375d751cf495f1378
MD5 25b400c2973d39c54a587eea1ff814c8
BLAKE2b-256 325ee0d0828aae810dc846b9b3467c3838d07e9e167dcf8afafcc08174a12711

See more details on using hashes here.

Provenance

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

Hashes for arvel_image-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e29e332566b576296a80729a56508ff7a1b6803d84a75e5bd692d62f4cc671ec
MD5 2a526fc10e25c8ba7481479a67de58a3
BLAKE2b-256 af1378965ae54bd2a63fd271adb058cb2ac2048e51391e53e9b4b538d93d2e37

See more details on using hashes here.

Provenance

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