Crop, resize, rotate, and encode local images
One.ImageManipulator.transform reads a local image and writes a new
JPEG, PNG, or WebP file in the app cache. It never changes the source, runs off
the JavaScript thread, and needs no photo permission. Reach for it to
prepare uploads, thumbnails, and avatars, or to straighten a photo before
replacing its library content.
On iOS it renders through Core Image on a dedicated serial queue. On
Android it decodes with BitmapFactory, applies EXIF orientation, and
encodes on a worker thread. On web it draws to a canvas on the browser
thread and returns a blob URL.
import { One } from 'one'
async function editPhoto(photoUri: string) { const edited = await One.ImageManipulator.transform(photoUri, { crop: { x: 20, y: 20, width: 400, height: 300 }, resize: { width: 200 }, rotate: 90, flip: 'horizontal', format: 'jpeg', quality: 0.8, }) console.log(edited.uri, edited.width, edited.height, edited.size)}Operations run in one fixed order: crop, resize, rotate, then flip. The result carries the new file URI, its pixel size, and its encoded byte length.
Crop coordinates are whole pixels from the top left of the upright
source: orientation is applied first on every platform, including
mirrored EXIF orientations on web. Resize takes a width, a height, or
both; one dimension preserves the aspect ratio, and both pin the exact
size. Rotation is clockwise and accepts only 0, 90, 180, or 270 degrees.
Flip mirrors the rotated output horizontally or vertically. The default
format is JPEG at quality 0.9; quality runs 0 to 1 and applies to JPEG
and WebP. PNG preserves transparency and ignores quality, while JPEG
drops it. WebP keeps small file sizes with quality-controlled lossy
compression; on iOS it encodes through SDWebImageWebPCoder (libwebp),
the same coder Expo uses, since ImageIO has no WebP writer. Output is capped at 25 megapixels and 10,000 pixels
per side; anything larger rejects with E_IMAGE_INPUT.
On native the source must be a local file:// URI. Download network
images to a file first. The result is a cache file yours to keep, move,
or delete: pass it to One.PhotoLibrary.saveImage to add it to Photos,
or delete it with One.FileSystem.delete when finished. On web the
source can also be a data or blob URL, or a remote image that permits
cross-origin canvas access, and the result is a blob URL that
One.UI.Image, HTML images, and fetch all accept; release it with
URL.revokeObjectURL when finished. Failures reject with coded errors:
a non-file URI, a missing file, an undecodable image, invalid input, an
encoder failure, or a write failure, each with its own E_IMAGE_* code.
Server rendering rejects since there is no decoder without a browser.
| Methods | Type | Description |
|---|---|---|
transform(uri: string, options: ImageManipulatorOptions) => Promise<ImageTransformResult>Crop, resize, rotate, flip, and encode. Format defaults to jpeg. | (uri: string, options: ImageManipulatorOptions) => Promise<ImageTransformResult> | Crop, resize, rotate, flip, and encode. Format defaults to jpeg. |
| Options | Type | Description |
|---|---|---|
cropImageCropWhole pixels from the top left, applied first. | ImageCrop | Whole pixels from the top left, applied first. |
resizeImageResizeOne side keeps ratio; both pin the size. | ImageResize | One side keeps ratio; both pin the size. |
rotatenumberClockwise degrees: 0, 90, 180, or 270. | number | Clockwise degrees: 0, 90, 180, or 270. |
flipOneImageFlipMirror axis applied after rotation. | OneImageFlip | Mirror axis applied after rotation. |
formatOneImageFormatJpeg, png, or webp. | OneImageFormat | Jpeg, png, or webp. |
qualitynumberJPEG or WebP quality 0 to 1. Default 0.9. | number | JPEG or WebP quality 0 to 1. Default 0.9. |
| Crop | Type | Description |
|---|---|---|
xnumberLeft edge in whole pixels. | number | Left edge in whole pixels. |
ynumberTop edge in whole pixels. | number | Top edge in whole pixels. |
widthnumberCrop width in whole pixels. | number | Crop width in whole pixels. |
heightnumberCrop height in whole pixels. | number | Crop height in whole pixels. |
| Resize | Type | Description |
|---|---|---|
widthnumberTarget width. Optional. | number | Target width. Optional. |
heightnumberTarget height. Optional. | number | Target height. Optional. |
| Result | Type | Description |
|---|---|---|
uristringThe new file, or blob URL on web. | string | The new file, or blob URL on web. |
widthnumberOutput width in pixels. | number | Output width in pixels. |
heightnumberOutput height in pixels. | number | Output height in pixels. |
sizenumberEncoded length in bytes. | number | Encoded length in bytes. |
| iOS | Android | Web | |
|---|---|---|---|
| Engine | Core Image | Bitmap tools | Canvas 2D |
| Accepts | Local files | Local files | URLs and blobs too |
| Writes | Cache file | Cache file | Blob URL |
| Orientation | Applied first | Applied first | Applied first |
E_IMAGE_INPUT.horizontal mirrors left to right,
vertical mirrors top to bottom; anything else rejects with
E_IMAGE_INPUT.URL.revokeObjectURL when finished with one.import { One } from 'one'
async function thumbnail(photoUri: string) { const small = await One.ImageManipulator.transform(photoUri, { resize: { width: 400 }, format: 'jpeg', quality: 0.7, }) const bytes = await (await fetch(small.uri)).arrayBuffer() await One.FileSystem.delete(small.uri) return bytes}One resize dimension keeps the aspect ratio while bounding the width. The cached file is read once for the upload bytes, then deleted.
import { One } from 'one'
async function replaceWithUpright(identifier: string, photoUri: string) { const edited = await One.ImageManipulator.transform(photoUri, { rotate: 90, format: 'jpeg', }) await One.PhotoLibrary.replaceImageContent(identifier, edited.uri) await One.FileSystem.delete(edited.uri)}Library replacement needs upright JPEG pixels rather than orientation metadata, which is exactly what a transform produces. The temp file is deleted once Photos has the new content.
import { One } from 'one'
async function avatar(photoUri: string, side: number) { const output = await One.ImageManipulator.transform(photoUri, { crop: { x: 0, y: 0, width: side, height: side }, resize: { width: 256, height: 256 }, format: 'png', }) return output.uri}The square crop runs before the resize, so the resize pins an exact 256-pixel square without distortion. PNG keeps transparency for avatars with soft or shaped edges.
Edit this page on GitHub.