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.
Add image-manipulator to modules in your app config, then import it in your code.
[tool.pyxpo]
modules = ["image-manipulator"]from pyxpo_image_manipulator import ImageManipulator, SaveFormat, FlipTypeThis 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
Quick start
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.
| iOS | None. |
|---|---|
| Android | None. |
API reference
ImageManipulator
All methods are coroutines. snake_case names also work: manipulate_async and so on.
await manipulateAsync(uri, actions=None, saveOptions=None)→ ImageResultApplies the actions in order and saves a new file.
await compressToSizeAsync(uri, maxBytes, format=SaveFormat.JPEG, minEdge=320)→ ImageResult + quality, scaleFinds 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)→ ImageInfoReads size and format without decoding the whole image.
Types
Results and options are plain Python dicts with these keys.
Actions
| Key | Type | Description |
|---|---|---|
{"resize": {"width", "height"}} | dict | Give one side to keep the aspect ratio. |
{"rotate": degrees} | int | Multiples of 90 only. Other values raise NotSupportedError. |
{"flip": FlipType.Vertical} | FlipType | Vertical or Horizontal. |
{"crop": {"originX", "originY", "width", "height"}} | dict | Must be inside the image, otherwise ValueError. |
saveOptions
| Key | Type | Description |
|---|---|---|
compress | float | 0 to 1. Default 1. |
format | SaveFormat | JPEG (default), PNG, WEBP (Android only), HEIC (iOS only). |
base64 | bool | Also return the image as a base64 string. |
ImageResult
| Key | Type | Description |
|---|---|---|
uri | str | The new file. |
width / height / fileSize | int | |
format / mimeType | str | |
base64 | str | Only when asked. |
ImageInfo
| Key | Type | Description |
|---|---|---|
uri / format / mimeType | str | |
width / height / fileSize | int | |
hasExif / hasGps | bool | Whether the source carries metadata. |
Examples
Crop to a square and rotate
Actions run in the order you give them.
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.
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.
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.