diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..dcd8db3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,67 @@
+# Contributing
+
+This guide provides instructions for contributing to this Capacitor plugin.
+
+## Native code
+
+This repository contains minimal code for native Android and iOS. The implementation for native mobile exists in separate repositories:
+
+- [Contributing for Android](https://github.com/ionic-team/ion-android-camera?tab=readme-ov-file#contributing)
+- [Contributing for iOS](https://github.com/ionic-team/ion-ios-camera?tab=readme-ov-file#contributing)
+
+## Developing
+
+### Local Setup
+
+1. Fork and clone the repo.
+2. Install the dependencies to use the plugin.
+
+ ```shell
+ npm install
+ ```
+
+3. Install SwiftLint if you're on macOS.
+
+ ```shell
+ brew install swiftlint
+ ```
+
+### Scripts
+
+#### `npm run build`
+
+Build the plugin web assets and generate plugin API documentation using [`@capacitor/docgen`](https://github.com/ionic-team/capacitor-docgen).
+
+It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/esm/`. These files are used in apps with bundlers when your plugin is imported.
+
+Then, Rollup will bundle the code into a single file at `dist/plugin.js`. This file is used in apps without bundlers by including it as a script in `index.html`.
+
+#### `npm run verify`
+
+Build and validate the web and native projects.
+
+This is run in CI to verify that the plugin builds for all platforms.
+
+#### `npm run lint` / `npm run fmt`
+
+Check formatting and code quality, autoformat/autofix if possible.
+
+The `lint` command is run in CI. We recommend running `lint` and/or `fmt` before opening a PR,
+
+## Commits/PR's
+
+Commits and PR's should use the [conventional-commits](https://www.conventionalcommits.org/) format so the release process can version and create changelog correctly.
+
+## Publishing
+
+Publishing is automated based on the branch committed to. When a commit or merge is made to a branch a release that corresponds with the branch will be created (main requires manual trigger):
+
+| Branch Name | Build Type | NPM Tag | Example NPM Version |
+| ----------- | ----------------------------- | ------- | ---------------------------------- |
+| development | dev | dev | @capacitor/camera@1.0.0-dev.1 |
+| next | next (these are betas/alphas) | next | @capacitor/camera@1.0.0-next.1 |
+| main | latest | latest | @capacitor/camera@1.0.0 |
+
+- In general new developments will go straight to `main`. If we want to have non-stable versions (e.g. for new Capacitor versions or when there are many changes), we may use `next` or `development` branch, and then once they are ready for stable version, open a PR to main (should be merged to keep commit hashes for `semantic-release`).
+
+> **Note**: The [`files`](https://docs.npmjs.com/cli/configuring-npm/package-json) array in `package.json` specifies which files get published. If you rename files/directories or add files elsewhere, you may need to update it.
diff --git a/README.md b/README.md
index 4f5423b..fb988f7 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ Older devices and Android Go devices running Android 11 or 12 that support Googl
```
-If that entry is not added, the devices that don't support the Photo Picker, the Photo Picker component fallbacks to `Intent.ACTION_OPEN_DOCUMENT`.
+If that entry is not added, on devices that don't support the Photo Picker, the Photo Picker component falls back to `Intent.ACTION_OPEN_DOCUMENT`.
The Camera plugin requires no permissions, unless using `saveToGallery: true`, in that case the following permissions should be added to your `AndroidManifest.xml`:
@@ -73,31 +73,351 @@ This plugin will use the following project variables (defined in your app's `var
## PWA Notes
-[PWA Elements](https://capacitorjs.com/docs/web/pwa-elements) are required for Camera plugin to work.
+On Web, `takePhoto` can use the [PWA Elements](https://capacitorjs.com/docs/web/pwa-elements) `pwa-camera-modal` custom element to provide a native-like camera UI. If the element is not registered, the plugin falls back to an `` picker. `chooseFromGallery` always uses `` on Web, regardless of whether PWA Elements are installed.
-## Example
+### Installing PWA Elements programmatically
+
+See the [PWA Elements installation guide](https://capacitorjs.com/docs/web/pwa-elements#installation) for full instructions.
+
+### Providing a custom camera element
+
+Instead of using `@ionic/pwa-elements`, you can register your own `pwa-camera-modal` custom element. The plugin interacts with it using the following interface:
+
+| Member | Type | Description |
+|---|---|---|
+| `facingMode` | `string` property | Set to `'user'` (front camera) or `'environment'` (rear camera) before presenting |
+| `componentOnReady()` | method → `Promise` | Called by the plugin after creating the element; resolve when the element is ready |
+| `present()` | method | Called by the plugin to display the camera UI |
+| `dismiss()` | method | Called by the plugin to close the camera UI after a photo is taken or cancelled |
+| `onPhoto` | event | Dispatched when the user takes a photo or cancels. `event.detail` must be a `Blob` (photo taken), `null` (user cancelled), or an `Error` (something went wrong) |
+
+```typescript
+class MyCameraModal extends HTMLElement {
+ facingMode = 'environment';
+
+ componentOnReady() {
+ return Promise.resolve();
+ }
+
+ present() {
+ // Show your custom camera UI, then dispatch exactly one 'onPhoto' event when done:
+ // - Blob: user took a photo
+ // - null: user cancelled
+ // - Error: something went wrong
+ // Example:
+ this.dispatchEvent(new CustomEvent('onPhoto', { detail: photoBlob }));
+ }
+
+ dismiss() {
+ // Hide your custom camera UI (called by the plugin after receiving 'onPhoto')
+ }
+}
+
+customElements.define('pwa-camera-modal', MyCameraModal);
+```
+
+## Examples
+
+### Taking a photo
```typescript
-import { Camera, CameraResultType } from '@capacitor/camera';
+import { Camera } from '@capacitor/camera';
const takePicture = async () => {
- const image = await Camera.getPhoto({
- quality: 90,
- allowEditing: true,
- resultType: CameraResultType.Uri
- });
-
- // image.webPath will contain a path that can be set as an image src.
- // You can access the original file using image.path, which can be
- // passed to the Filesystem API to read the raw data of the image,
- // if desired (or pass resultType: CameraResultType.Base64 to getPhoto)
- var imageUrl = image.webPath;
-
- // Can be set to the src of an image now
- imageElement.src = imageUrl;
+ try {
+ const result = await Camera.takePhoto({
+ quality: 90,
+ includeMetadata: true,
+ });
+
+ // result.webPath can be set directly as the src of an image element
+ imageElement.src = result.webPath;
+
+ // On native: pass result.uri to the Filesystem API to get the full-resolution base64,
+ // or use result.thumbnail for a lower-resolution base64 preview.
+ // On Web: result.thumbnail contains the full image base64 encoded.
+
+ console.log('Format:', result.metadata?.format);
+ console.log('Resolution:', result.metadata?.resolution);
+ } catch (e) {
+ const error = e as any;
+ // error.code contains the structured error code (e.g. 'OS-PLUG-CAMR-0003')
+ // when thrown by the native layer. See the Errors section for all codes.
+ const message = error.code ? `[${error.code}] ${error.message}` : error.message;
+ console.error('takePhoto failed:', message);
+ }
+};
+```
+
+### Choosing from the gallery
+
+```typescript
+import { Camera, MediaTypeSelection } from '@capacitor/camera';
+
+const pickMedia = async () => {
+ try {
+ const { results } = await Camera.chooseFromGallery({
+ mediaType: MediaTypeSelection.All, // photos, videos, or both
+ allowMultipleSelection: true,
+ limit: 5,
+ includeMetadata: true,
+ });
+
+ for (const item of results) {
+ console.log('Type:', item.type); // MediaType.Photo or MediaType.Video
+ console.log('webPath:', item.webPath);
+ console.log('Format:', item.metadata?.format);
+ console.log('Size:', item.metadata?.size);
+ }
+ } catch (e) {
+ const error = e as any;
+ const message = error.code ? `[${error.code}] ${error.message}` : error.message;
+ console.error('chooseFromGallery failed:', message);
+ }
+};
+```
+
+### Recording and playing a video
+
+```typescript
+import { Camera } from '@capacitor/camera';
+
+const recordAndPlay = async () => {
+ let videoUri: string | undefined;
+
+ try {
+ const result = await Camera.recordVideo({
+ saveToGallery: false,
+ isPersistent: true, // keep the file available across app launches
+ includeMetadata: true,
+ });
+
+ videoUri = result.uri;
+ console.log('Duration:', result.metadata?.duration);
+ console.log('Saved to gallery:', result.saved);
+ } catch (e) {
+ const error = e as any;
+ const message = error.code ? `[${error.code}] ${error.message}` : error.message;
+ console.error('recordVideo failed:', message);
+ return;
+ }
+
+ if (videoUri) {
+ try {
+ await Camera.playVideo({ uri: videoUri });
+ } catch (e) {
+ const error = e as any;
+ const message = error.code ? `[${error.code}] ${error.message}` : error.message;
+ console.error('playVideo failed:', message);
+ }
+ }
};
```
+### Editing a photo from a base64 string
+
+`editPhoto` opens an in-app editor from a base64-encoded image and returns the edited image as a base64 string in `outputImage`.
+
+```typescript
+import { Camera } from '@capacitor/camera';
+
+const editFromBase64 = async (base64Image: string) => {
+ try {
+ const { outputImage } = await Camera.editPhoto({
+ inputImage: base64Image, // raw base64, no data URL prefix
+ });
+
+ // outputImage is the edited image, base64 encoded
+ imageElement.src = `data:image/jpeg;base64,${outputImage}`;
+ } catch (e) {
+ const error = e as any;
+ const message = error.code ? `[${error.code}] ${error.message}` : error.message;
+ console.error('editPhoto failed:', message);
+ }
+};
+```
+
+### Editing a photo from a URI
+
+`editURIPhoto` opens an in-app editor from a file URI (e.g. from `takePhoto` or the Filesystem API) and returns a `MediaResult`.
+
+```typescript
+import { Camera } from '@capacitor/camera';
+
+const editFromURI = async (uri: string) => {
+ try {
+ const result = await Camera.editURIPhoto({
+ uri,
+ saveToGallery: false,
+ includeMetadata: true,
+ });
+
+ // result.webPath can be used directly as an image src
+ imageElement.src = result.webPath;
+
+ console.log('Format:', result.metadata?.format);
+ console.log('Size:', result.metadata?.size);
+ console.log('Saved to gallery:', result.saved);
+ } catch (e) {
+ const error = e as any;
+ const message = error.code ? `[${error.code}] ${error.message}` : error.message;
+ console.error('editURIPhoto failed:', message);
+ }
+};
+```
+
+## Migrating to the New API
+
+Version 8.1.0 introduces a new improved API and deprecates `getPhoto` and `pickImages`.
+
+### Replacing `getPhoto`
+
+`getPhoto` handled three sources via `CameraSource`: `Camera`, `Photos`, and `Prompt`. `Camera` and `Photos` now map to different methods, while `Prompt` was removed.
+
+#### `CameraSource.Camera` to `takePhoto`
+
+`CameraResultType.Base64` and `CameraResultType.DataUrl` are not supported in the new API. See [Result type changes](#result-type-changes) for alternatives.
+
+```typescript
+// Before
+const photo = await Camera.getPhoto({
+ source: CameraSource.Camera,
+ quality: 90,
+ allowEditing: true,
+ resultType: CameraResultType.Uri,
+ direction: CameraDirection.Rear,
+ width: 1280,
+ height: 720,
+});
+const imageUrl = photo.webPath;
+
+// After
+const result = await Camera.takePhoto({
+ quality: 90,
+ editable: 'in-app', // replaces allowEditing: true
+ cameraDirection: CameraDirection.Rear, // replaces direction
+ targetWidth: 1280, // replaces width (1)
+ targetHeight: 720, // replaces height (1)
+});
+const imageUrl = result.webPath;
+```
+
+**(1)** `width`/`height` each worked independently and set a maximum dimension while preserving aspect ratio. `targetWidth`/`targetHeight` must be used together — setting only one has no effect.
+
+#### `CameraSource.Photos` to `chooseFromGallery`
+
+```typescript
+// Before
+const photo = await Camera.getPhoto({
+ source: CameraSource.Photos,
+ quality: 90,
+ resultType: CameraResultType.Uri,
+});
+const imageUrl = photo.webPath;
+
+// After
+const { results } = await Camera.chooseFromGallery({
+ quality: 90,
+});
+const imageUrl = results[0].webPath;
+```
+
+#### `CameraSource.Prompt` (or default)
+
+`getPhoto` previously displayed a native prompt letting the user choose between the camera and the gallery. This prompt is no longer part of the plugin. You should build the prompt using your own UI (for example, with `@capacitor/action-sheet`) and then call `takePhoto` or `chooseFromGallery` based on the user's selection.
+
+```typescript
+// Before
+const photo = await Camera.getPhoto({
+ // source defaults to CameraSource.Prompt
+ quality: 90,
+ resultType: CameraResultType.Uri,
+});
+
+// After: show your own UI to determine the source, then call the appropriate method
+const result = await Camera.takePhoto({ quality: 90 });
+// or
+const { results } = await Camera.chooseFromGallery({ quality: 90 });
+```
+
+#### Result type changes
+
+`getPhoto` returned a `Photo` object where the fields available depended on `resultType`. The new API removes `resultType` entirely — `MediaResult` has a fixed set of fields regardless of how the photo was taken.
+
+| `Photo` field | `MediaResult` equivalent |
+|---|---|
+| `path` | `uri` |
+| `webPath` | `webPath` |
+| `base64String` | `thumbnail` (on Web, contains the full image base64 encoded; on native, contains a thumbnail) |
+| `dataUrl` | No direct equivalent — see note below |
+| `saved` | `saved` |
+| `format` | `metadata.format` (requires `includeMetadata: true`) |
+| `exif` | `metadata.exif` (requires `includeMetadata: true`) |
+
+**Constructing a data URL** — two options are available depending on your needs:
+
+On all platforms, you can combine `thumbnail` and `metadata.format` (requires `includeMetadata: true`). On native, `thumbnail` is lower-resolution:
+
+```typescript
+const dataUrl = `data:image/${result.metadata.format};base64,${result.thumbnail}`;
+```
+
+On native, if you need the full-resolution base64, read `uri` via the Filesystem API and construct the data URL from there:
+
+```typescript
+import { Filesystem } from '@capacitor/filesystem';
+
+const { data } = await Filesystem.readFile({ path: result.uri });
+const dataUrl = `data:image/${result.metadata.format};base64,${data}`;
+```
+
+### Replacing `pickImages` → `chooseFromGallery`
+
+`pickImages` allowed selecting multiple photos from the gallery. Pass `allowMultipleSelection: true` to `chooseFromGallery` to get the same behaviour.
+
+```typescript
+// Before
+const { photos } = await Camera.pickImages({
+ quality: 90,
+ limit: 5,
+ width: 1280,
+ height: 720,
+});
+for (const photo of photos) {
+ console.log(photo.webPath);
+}
+
+// After
+const { results } = await Camera.chooseFromGallery({
+ allowMultipleSelection: true,
+ quality: 90,
+ limit: 5,
+ targetWidth: 1280, // replaces width (1)
+ targetHeight: 720, // replaces height (1)
+});
+for (const result of results) {
+ console.log(result.webPath);
+}
+```
+
+**(1)** `width`/`height` each worked independently and set a maximum dimension while preserving aspect ratio. `targetWidth`/`targetHeight` must be used together — setting only one has no effect.
+
+`chooseFromGallery` can also select videos or mixed media by setting `mediaType` to `MediaTypeSelection.Video` or `MediaTypeSelection.All`.
+
+### Option rename summary
+
+| Old option | New option | Applies to |
+|---|---|---|
+| `width` | `targetWidth` (1) | `takePhoto`, `chooseFromGallery` |
+| `height` | `targetHeight` (1) | `takePhoto`, `chooseFromGallery` |
+| `direction` | `cameraDirection` | `takePhoto` |
+| `allowEditing` | `editable: 'in-app'` | `takePhoto`, `chooseFromGallery` |
+| `resultType` | — (removed, see [Result type changes](#result-type-changes)) | — |
+| `source` | — (removed, use separate methods) | — |
+| `promptLabel*` | — (removed, build your own UI) | — |
+
+**(1)** `width`/`height` each worked independently and set a maximum dimension while preserving aspect ratio. `targetWidth`/`targetHeight` must be used together — setting only one has no effect.
+
## API
@@ -120,6 +440,8 @@ const takePicture = async () => {
+For a list of existing error codes, see [Errors](#errors).
+
@@ -601,3 +923,35 @@ Allows the user to pick multiple pictures from the photo gallery.
| **`Photos`** | 'PHOTOS' | Pick an existing photo from the gallery or photo album. |
+
+### Errors
+
+The plugin returns structured errors on Android and iOS. Each error has a `code` (e.g. `OS-PLUG-CAMR-0003`) and a `message` with a human-readable description. Note that these are only available for native platforms starting on the new APIs introduced in version `8.1.0`: `takePhoto`, `chooseFromGallery`, `editPhoto`, `editURIPhoto`, `recordVideo`, and `playVideo`.
+
+| Error code | Platform(s) | Description |
+|---|---|---|
+| OS-PLUG-CAMR-0003 | Android, iOS | Couldn't access camera. Check your camera permissions and try again. |
+| OS-PLUG-CAMR-0005 | Android, iOS | Couldn't access your photo gallery because access wasn't provided. |
+| OS-PLUG-CAMR-0006 | Android, iOS | Couldn't take photo because the process was canceled. |
+| OS-PLUG-CAMR-0007 | Android, iOS | No camera available. |
+| OS-PLUG-CAMR-0008 | iOS | The selected file contains data that isn't valid. |
+| OS-PLUG-CAMR-0009 | Android, iOS | Couldn't edit image. |
+| OS-PLUG-CAMR-0010 | Android, iOS | Couldn't take photo. |
+| OS-PLUG-CAMR-0011 | iOS | Couldn't get image from the gallery. |
+| OS-PLUG-CAMR-0012 | Android, iOS | Couldn't process image. |
+| OS-PLUG-CAMR-0013 | Android, iOS | Couldn't edit photo because the process was canceled. |
+| OS-PLUG-CAMR-0014 | iOS | Couldn't decode the 'Take Photo' action parameters. |
+| OS-PLUG-CAMR-0016 | Android, iOS | Couldn't record video. |
+| OS-PLUG-CAMR-0017 | Android, iOS | Couldn't record video because the process was canceled. |
+| OS-PLUG-CAMR-0018 | Android, iOS | Couldn't choose media from the gallery. |
+| OS-PLUG-CAMR-0019 | iOS | Couldn't encode the media result. |
+| OS-PLUG-CAMR-0020 | Android, iOS | Couldn't choose media from the gallery because the process was canceled. |
+| OS-PLUG-CAMR-0021 | Android | Couldn't get media file path. |
+| OS-PLUG-CAMR-0023 | Android, iOS | Couldn't play video. |
+| OS-PLUG-CAMR-0024 | Android | URI parameter cannot be empty. |
+| OS-PLUG-CAMR-0025 | iOS | Couldn't get video from the gallery. |
+| OS-PLUG-CAMR-0026 | iOS | There's an issue with the plugin. |
+| OS-PLUG-CAMR-0027 | Android, iOS | The selected file doesn't exist. |
+| OS-PLUG-CAMR-0028 | Android, iOS | Couldn't retrieve image from the URI. |
+| OS-PLUG-CAMR-0031 | Android | Invalid argument provided to plugin method. |
+| OS-PLUG-CAMR-0033 | Android | Unable to get the context. |