packages

Image Manipulator

Runs a list of edits on an image with the platform's own image code and saves the result as a new file. It can also find the best quality and size to fit a byte limit, which is useful before an upload. Output files never carry EXIF or GPS data.

  • pyxpo-image-manipulator
  • v0.1.0a1
  • Native: Swift + Kotlin
  • Like expo-image-manipulator

Install

Install the package into the same environment as pyxpo.

$pip install pyxpo-image-manipulator

Add image-manipulator to modules in your app config, then import it in your code.

pyproject.toml
[tool.pyxpo]
modules = ["image-manipulator"]
python
from pyxpo_image_manipulator import ImageManipulator, SaveFormat, FlipType

This module has native code. After you add it, run pyxpo run or pyxpo build again so it is compiled into the app. Hot reload alone is not enough.

Platforms

iOSUIKit and ImageIO
AndroidBitmap
Desktop previewNeeds a simulator or device

Quick start

app/index.py
from pyxpo_image_manipulator import ImageManipulator, SaveFormat

async def thumbnail(uri):
    out = await ImageManipulator.manipulateAsync(
        uri,
        [{"resize": {"width": 400}}],
        {"compress": 0.7, "format": SaveFormat.JPEG},
    )
    return out["uri"]

Permissions

pyxpo adds these to Info.plist and AndroidManifest.xml for you when the module is listed.

iOSNone.
AndroidNone.

API reference

ImageManipulator

All methods are coroutines. snake_case names also work: manipulate_async and so on.

await manipulateAsync(uri, actions=None, saveOptions=None)→ ImageResult

Applies the actions in order and saves a new file.

await compressToSizeAsync(uri, maxBytes, format=SaveFormat.JPEG, minEdge=320)→ ImageResult + quality, scale

Finds the best quality, and a smaller size if needed, to fit maxBytes. Raises ImageTooLargeError when the long side would have to go below minEdge.

await getInfoAsync(uri)→ ImageInfo

Reads size and format without decoding the whole image.

Types

Results and options are plain Python dicts with these keys.

Actions

KeyTypeDescription
{"resize": {"width", "height"}}dictGive one side to keep the aspect ratio.
{"rotate": degrees}intMultiples of 90 only. Other values raise NotSupportedError.
{"flip": FlipType.Vertical}FlipTypeVertical or Horizontal.
{"crop": {"originX", "originY", "width", "height"}}dictMust be inside the image, otherwise ValueError.

saveOptions

KeyTypeDescription
compressfloat0 to 1. Default 1.
formatSaveFormatJPEG (default), PNG, WEBP (Android only), HEIC (iOS only).
base64boolAlso return the image as a base64 string.

ImageResult

KeyTypeDescription
uristrThe new file.
width / height / fileSizeint
format / mimeTypestr
base64strOnly when asked.

ImageInfo

KeyTypeDescription
uri / format / mimeTypestr
width / height / fileSizeint
hasExif / hasGpsboolWhether the source carries metadata.

Examples

Crop to a square and rotate

Actions run in the order you give them.

python
from pyxpo_image_manipulator import FlipType, ImageManipulator, SaveFormat

async def square(uri):
    info = await ImageManipulator.getInfoAsync(uri)
    side = min(info["width"], info["height"])
    out = await ImageManipulator.manipulateAsync(
        uri,
        [
            {"crop": {"originX": 0, "originY": 0, "width": side, "height": side}},
            {"rotate": 90},
            {"flip": FlipType.Horizontal},
        ],
        {"format": SaveFormat.PNG},
    )
    return out["uri"]

Fit an upload limit

Shrink a photo to 500 KB before you send it.

python
from pyxpo_image_manipulator import ImageManipulator, ImageTooLargeError

async def for_upload(uri):
    try:
        out = await ImageManipulator.compressToSizeAsync(uri, 500_000)
    except ImageTooLargeError:
        return None
    print(out["fileSize"], out["quality"], out["scale"])
    return out["uri"]

Remove location data

Every saved image is written without EXIF and GPS data.

python
from pyxpo_image_manipulator import ImageManipulator

async def strip_location(uri):
    info = await ImageManipulator.getInfoAsync(uri)
    if not info["hasGps"]:
        return uri
    out = await ImageManipulator.manipulateAsync(uri, [], {"compress": 0.95})
    return out["uri"]

Good to know

  • The source file is never changed. Each call writes a new file in the cache folder.
  • compressToSizeAsync decodes at most 4096 pixels on the long side while it searches.
  • Use the result with pyxpo-media-library to save it to the gallery, or pyxpo-sharing to send it.