Save, browse, edit, and export photos and videos on iOS and Android
One.PhotoLibrary is the full read and write API for the system photo
library: save captures, browse asset metadata page by page, organize albums,
mark favorites, replace content in place, and export originals to files.
Reach for it when your app manages the library itself; for a one-shot
attachment, the system pickers on the image picker page are simpler.
On iOS it drives PHPhotoLibrary from the Photos framework. On Android it
queries MediaStore through the content resolver. On web there is no
library, so permissions read denied, lists come back empty, and calls that
return a value reject.
import { One } from 'one'
async function saveCapture(imageFileUri: string) { const permission = await One.PhotoLibrary.requestAddPermission() if (permission === 'authorized') { const assetId = await One.PhotoLibrary.saveImage(imageFileUri) console.log('Saved Photos asset', assetId) }}Saving needs only add permission, which never prompts on current Android. The save resolves with the new asset identifier once the library commits the change.
Two permissions split the API. Add-only covers saveImage and saveVideo.
Read/write covers everything else: listing, albums, favorites, deletion,
replacement, and export. Declare the messages your app uses in
native.app.photoLibrary before prebuild: addOnly becomes
NSPhotoLibraryAddUsageDescription and readWrite becomes
NSPhotoLibraryUsageDescription on iOS, plus a flag that stops the system
from showing the limited-access alert on its own, so your app chooses when
to call presentLimitedLibraryPicker. On Android the same config stamps
the READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, and legacy storage permissions
into the manifest. Calls without their message reject with
E_PHOTO_LIBRARY_MANIFEST before touching the library.
Reads fetch newest first in bounded pages: integer offset from 0 to
1,000,000 and integer limit from 1 to 100, defaulting to the first 50.
Each asset carries its identifier, media type, pixel size, duration,
optional creation date, and favorite and Live Photo flags. Metadata only;
no call returns image or video bytes. On iOS fetches run on a background
queue and writes commit through performChanges, resolving only after
Photos accepts the change. On Android queries run on a worker thread
against MediaStore, and albums are media buckets, listed but never
edited: renaming, membership changes, deletion, and content
replace and revert resolve without doing anything, and creating an album
rejects. Deleting an asset removes it from the whole library and may show
a system confirmation; Photos can retain the item in Recently Deleted.
Saves accept local file:// URIs produced by the file system, image
picker, or document picker, never network URLs, and resolve with the new
identifier. An app with read/write access can save without requesting
add-only access. Exports write to a unique temporary file and return its
file:// URI: the untouched original, the visible edited image, or the
visible edited video. Each takes an allowNetwork flag, default false,
that permits an iCloud download; without it, a cloud-only asset fails
rather than using data. Content replacement keeps the asset identifier
and the original, so revertAssetContent restores it; Photos may ask the
user to confirm either edit. On web, permissions read denied, lists and
the picker return empty results, void writes resolve silently, and every
call that returns an asset, album, file, or identifier rejects.
| Methods: permissions | Type | Description |
|---|---|---|
getAddPermissionStatus() => PhotoLibraryPermissionStatusThe add-only status, without prompting. | () => PhotoLibraryPermissionStatus | The add-only status, without prompting. |
requestAddPermission() => Promise<PhotoLibraryPermissionStatus>Ask for add-only access for saving. | () => Promise<PhotoLibraryPermissionStatus> | Ask for add-only access for saving. |
getReadPermissionStatus() => PhotoLibraryPermissionStatusThe read and write status, without prompting. | () => PhotoLibraryPermissionStatus | The read and write status, without prompting. |
requestReadPermission() => Promise<PhotoLibraryPermissionStatus>Ask for read and write access for browsing. | () => Promise<PhotoLibraryPermissionStatus> | Ask for read and write access for browsing. |
presentLimitedLibraryPicker() => Promise<string[]>Let a limited user pick more assets. Resolves with the new ids. | () => Promise<string[]> | Let a limited user pick more assets. Resolves with the new ids. |
| Methods: reading | Type | Description |
|---|---|---|
listAssets(offset: number, limit: number) => Promise<PhotoLibraryAssetPage>One newest-first page. Defaults to offset 0, limit 50. | (offset: number, limit: number) => Promise<PhotoLibraryAssetPage> | One newest-first page. Defaults to offset 0, limit 50. |
getAsset(identifier: string) => Promise<PhotoLibraryAsset>Current metadata for one asset id. | (identifier: string) => Promise<PhotoLibraryAsset> | Current metadata for one asset id. |
| Methods: albums | Type | Description |
|---|---|---|
listAlbums(offset: number, limit: number) => Promise<PhotoLibraryAlbumPage>User albums by title. Defaults to offset 0, limit 50. | (offset: number, limit: number) => Promise<PhotoLibraryAlbumPage> | User albums by title. Defaults to offset 0, limit 50. |
getAlbum(identifier: string) => Promise<PhotoLibraryAlbum>Current title for one album id. | (identifier: string) => Promise<PhotoLibraryAlbum> | Current title for one album id. |
createAlbum(title: string) => Promise<string>Create an album. Resolves with its id. iOS only. | (title: string) => Promise<string> | Create an album. Resolves with its id. iOS only. |
renameAlbum(identifier: string, title: string) => Promise<void>Rename an album. Silent on Android. | (identifier: string, title: string) => Promise<void> | Rename an album. Silent on Android. |
listAlbumAssets(identifier: string, offset: number, limit: number) => Promise<PhotoLibraryAssetPage>One newest-first page of an album. Defaults to offset 0, limit 50. | (identifier: string, offset: number, limit: number) => Promise<PhotoLibraryAssetPage> | One newest-first page of an album. Defaults to offset 0, limit 50. |
addAssetToAlbum(albumIdentifier: string, assetIdentifier: string) => Promise<void>Add an asset to an album. Silent on Android. | (albumIdentifier: string, assetIdentifier: string) => Promise<void> | Add an asset to an album. Silent on Android. |
removeAssetFromAlbum(albumIdentifier: string, assetIdentifier: string) => Promise<void>Remove an asset from an album. Silent on Android. | (albumIdentifier: string, assetIdentifier: string) => Promise<void> | Remove an asset from an album. Silent on Android. |
deleteAlbum(identifier: string) => Promise<void>Delete the album, keeping its assets. Silent on Android. | (identifier: string) => Promise<void> | Delete the album, keeping its assets. Silent on Android. |
| Methods: writing | Type | Description |
|---|---|---|
setFavorite(identifier: string, favorite: boolean) => Promise<void>Mark or unmark an asset as a favorite. | (identifier: string, favorite: boolean) => Promise<void> | Mark or unmark an asset as a favorite. |
deleteAsset(identifier: string) => Promise<void>Delete an asset from the whole library. | (identifier: string) => Promise<void> | Delete an asset from the whole library. |
replaceImageContent(identifier: string, uri: string) => Promise<void>Replace a still with an upright JPEG file. iOS only. | (identifier: string, uri: string) => Promise<void> | Replace a still with an upright JPEG file. iOS only. |
replaceVideoContent(identifier: string, uri: string) => Promise<void>Replace a video with an upright movie file. iOS only. | (identifier: string, uri: string) => Promise<void> | Replace a video with an upright movie file. iOS only. |
revertAssetContent(identifier: string) => Promise<void>Restore the original after a replace. iOS only. | (identifier: string) => Promise<void> | Restore the original after a replace. iOS only. |
saveImage(uri: string) => Promise<string>Save a local image file. Resolves with the new id. | (uri: string) => Promise<string> | Save a local image file. Resolves with the new id. |
saveVideo(uri: string) => Promise<string>Save a local video file. Resolves with the new id. | (uri: string) => Promise<string> | Save a local video file. Resolves with the new id. |
| Methods: export | Type | Description |
|---|---|---|
exportOriginalAsset(identifier: string, allowNetwork: boolean) => Promise<string>Write the untouched original to a temp file. Network off by default. | (identifier: string, allowNetwork: boolean) => Promise<string> | Write the untouched original to a temp file. Network off by default. |
exportCurrentImage(identifier: string, allowNetwork: boolean) => Promise<string>Write the visible edited image to a temp file. | (identifier: string, allowNetwork: boolean) => Promise<string> | Write the visible edited image to a temp file. |
exportCurrentVideo(identifier: string, allowNetwork: boolean) => Promise<string>Write the visible edited video to a temp file. | (identifier: string, allowNetwork: boolean) => Promise<string> | Write the visible edited video to a temp file. |
| Asset | Type | Description |
|---|---|---|
identifierstringThe stable id to pass to the other methods. | string | The stable id to pass to the other methods. |
mediaTypePhotoLibraryMediaTypeImage, video, audio, or unknown. | PhotoLibraryMediaType | Image, video, audio, or unknown. |
widthnumberPixel width of the asset. | number | Pixel width of the asset. |
heightnumberPixel height of the asset. | number | Pixel height of the asset. |
durationMsnumberPlayback length in milliseconds. | number | Playback length in milliseconds. |
creationDateMsnumberCreation time, when the library knows it. | number | Creation time, when the library knows it. |
isFavoritebooleanWhether the asset is a favorite. | boolean | Whether the asset is a favorite. |
isLivePhotobooleanWhether the asset is a Live Photo. | boolean | Whether the asset is a Live Photo. |
| Asset page | Type | Description |
|---|---|---|
assetsPhotoLibraryAsset[]The assets on this page, newest first. | PhotoLibraryAsset[] | The assets on this page, newest first. |
totalCountnumberThe assets visible to the app in total. | number | The assets visible to the app in total. |
| Album | Type | Description |
|---|---|---|
identifierstringThe stable id to pass to the album methods. | string | The stable id to pass to the album methods. |
titlestringThe current album title. | string | The current album title. |
| Album page | Type | Description |
|---|---|---|
albumsPhotoLibraryAlbum[]The albums on this page, by title. | PhotoLibraryAlbum[] | The albums on this page, by title. |
totalCountnumberThe user albums in total. | number | The user albums in total. |
| iOS | Android | Web | |
|---|---|---|---|
| Store | Photo library | Media store | None, stubbed |
| Saving | Needs add rights | Allowed past 28 | Rejects |
| Browsing | Needs read rights | Needs media rights | Empty pages |
| Albums | Full editing | Listing only | Empty, silent |
| Favorites | Supported | Needs Android 11 | Silent |
| Replacing | Edits in place | Silent, no change | Silent, no change |
| Picker | New picks only | New picks only | Empty list |
file://
URIs only. Anything else rejects with E_PHOTO_LIBRARY_URI.import { useState } from 'react'import { Button, Text } from 'react-native'import { One } from 'one'
function SavePhoto({ fileUri }: { fileUri: string }) { const [saved, setSaved] = useState(false)
async function save() { const permission = await One.PhotoLibrary.requestAddPermission() if (permission !== 'authorized') return await One.PhotoLibrary.saveImage(fileUri) setSaved(true) }
return ( <> <Button title="Save to library" onPress={save} /> {saved ? <Text>Saved.</Text> : null} </> )}Add-only permission is the cheapest grant that can save, and on current Android it resolves authorized without a prompt. The button stays usable after a deny, so the user can retry once they grant access in Settings.
import { useEffect, useState } from 'react'import { Text } from 'react-native'import { One } from 'one'
type LibraryAsset = Awaited<ReturnType<typeof One.PhotoLibrary.listAssets>>['assets'][number]
function NewestPhotos() { const [assets, setAssets] = useState<LibraryAsset[]>([])
useEffect(() => { async function load() { const permission = await One.PhotoLibrary.requestReadPermission() if (permission !== 'authorized' && permission !== 'limited') return const page = await One.PhotoLibrary.listAssets(0, 50) setAssets(page.assets) } load() }, [])
return ( <> {assets.map((asset) => ( <Text key={asset.identifier}> {asset.mediaType} {asset.width}x{asset.height} {asset.isLivePhoto ? ' live' : ''} </Text> ))} </> )}Limited access still returns a page, restricted to the user’s selection,
so both grants share the same listing path. An asset flagged live can be
shown with One.iOS.LivePhotoView.
import { One } from 'one'
async function copyOriginal(identifier: string, share: (uri: string) => Promise<void>) { const uri = await One.PhotoLibrary.exportOriginalAsset(identifier) try { const info = await One.FileSystem.getInfo(uri) console.log('Exported bytes:', info.size) await share(uri) } finally { await One.FileSystem.delete(uri) }}The export lands in a temporary file outside the library, so reading it
needs no further permission. The finally deletes it once the share
finishes, keeping the temp directory from filling with exports.
Edit this page on GitHub.