Camera
Capacitor’s Camera plugin gives you native camera + photo-picker access from one API across iOS, Android, and the web. Configure permissions, handle the resulting blob, and you have a working photo upload in under a hundred lines.
plugin, permissions, gallery, upload
EXAMPLE
// 1) Install
// npm install @capacitor/camera @capacitor/filesystem
// npx cap sync
import { Component, signal } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
// 2) Basic capture
@Component({
selector: 'app-photo',
template: `
<ion-button (click)="takePhoto()">Take photo</ion-button>
<img *ngIf="image()" [src]="image()" alt="photo" style="max-width:100%">
`,
})
export class PhotoPage {
image = signal<string | null>(null);
async takePhoto() {
const photo: Photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri, // file:// URI
source: CameraSource.Camera, // CAMERA, PHOTOS, or PROMPT
saveToGallery: false,
correctOrientation: true,
width: 1080,
height: 1080,
promptLabelHeader: 'Photo',
promptLabelPhoto: 'From gallery',
promptLabelPicture: 'Take new',
});
this.image.set(photo.webPath ?? null); // safe to use in <img>
}
}
// 3) Permissions (iOS info.plist)
// <key>NSCameraUsageDescription</key>
// <string>Take photos to attach to your posts.</string>
// <key>NSPhotoLibraryUsageDescription</key>
// <string>Choose photos from your library.</string>
// <key>NSPhotoLibraryAddUsageDescription</key>
// <string>Save shots back to your photo library.</string>
// Android — AndroidManifest.xml
// <uses-permission android:name="android.permission.CAMERA"/>
// <uses-feature android:name="android.hardware.camera" android:required="false"/>
// (Storage permissions handled by Capacitor + scoped storage on Android 11+)
// 4) Request permissions explicitly when you need to
const status = await Camera.checkPermissions();
if (status.camera !== 'granted' || status.photos !== 'granted') {
const after = await Camera.requestPermissions({ permissions: ['camera', 'photos'] });
if (after.camera !== 'granted') {
// Direct user to settings
}
}
// 5) Result types — pick the right one for your flow
// • CameraResultType.Uri — returns webPath (file://) — fastest, no memory copy
// • CameraResultType.Base64 — returns base64String — easy to embed, large memory hit
// • CameraResultType.DataUrl — returns a data: URI ready for <img src> — also memory-heavy
//
// Use Uri for any photo bigger than a thumbnail; convert as needed.
// 6) Read the photo bytes from Filesystem
import { Filesystem, Directory } from '@capacitor/filesystem';
async function readAsBlob(webPath: string): Promise<Blob> {
const response = await fetch(webPath); // webPath is a fetch-safe URL
return await response.blob();
}
async function readAsBase64(uri: string): Promise<string> {
const r = await Filesystem.readFile({ path: uri });
return r.data as string;
}
// 7) Save to app sandbox (persistent across launches)
async function persist(uri: string, filename: string) {
const blob = await readAsBlob(uri);
const reader = new FileReader();
return new Promise<void>((resolve, reject) => {
reader.onload = async () => {
const base64 = (reader.result as string).split(',')[1];
await Filesystem.writeFile({
path: `photos/${filename}`,
data: base64,
directory: Directory.Data,
recursive: true,
});
resolve();
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
// 8) Upload to your API
async function upload(uri: string) {
const blob = await readAsBlob(uri);
const form = new FormData();
form.append('file', blob, 'photo.jpg');
form.append('purpose', 'avatar');
const res = await fetch('https://api.example.com/upload', {
method: 'POST',
body: form,
});
return await res.json();
}
// On native, use the Capacitor HTTP plugin to bypass CORS and get reliable
// background uploads — see the @capacitor/http docs.
// 9) Choose the source — camera vs gallery vs prompt
await Camera.getPhoto({ source: CameraSource.Camera }); // straight to camera (with permission)
await Camera.getPhoto({ source: CameraSource.Photos }); // skip camera, use gallery picker
await Camera.getPhoto({ source: CameraSource.Prompt }); // user picks ('Take new' / 'Choose from library')
// 10) Multiple photos — Capacitor 5+ has Camera.pickImages
const { photos } = await Camera.pickImages({ quality: 80, limit: 10 });
photos.forEach(p => console.log(p.webPath));
// 11) Web fallback
// On the web, getPhoto opens the system file picker (input type="file" capture="environment")
// Photos return as webPath (blob: URL). FileSystem APIs are limited; use IndexedDB or remote storage.
// 12) Quality + size considerations
// • quality: 1-100 (jpeg)
// • width / height — Capacitor downscales server-side; cheaper than uploading the full image
// • Ask for the smallest dimensions your UI actually needs
// • For profile photos, 512x512 is usually enough; for documents, 1500-2000px on the long side
// 13) Handling errors
try {
const p = await Camera.getPhoto({ /* … */ });
} catch (e: any) {
if (e?.message?.includes('User cancelled')) {
return; // user just backed out — quiet failure
}
showError('Could not open camera. Check permissions.');
}
// 14) UX patterns
// • Show a placeholder + 'Add photo' button BEFORE permission is requested
// • Explain why you need the camera/library access (a sentence above the trigger)
// • Offer 'Take new' AND 'Choose from library' (use CameraSource.Prompt)
// • Show progress for uploads of large photos (network can be slow on mobile)
// • Provide a 'Retake' option after preview
// 15) Background uploads (Android) — Capacitor doesn't ship a background uploader.
// Use a community plugin (@capacitor-community/background-task) or build a foreground service.
// Alternative: resilient chunked uploads from the foreground with progress + retry.
// 16) iOS Live Photo / RAW / HEIC
// • getPhoto returns JPEG by default; HEIC available on supported devices via the format option
// • Live photos: only the still frame is returned; the motion video requires the iOS Photos framework
// 17) Privacy — handling the resulting image responsibly
// • Don't ship EXIF GPS to the server unless you need it; strip with an image library
// • Don't write photos to a publicly readable Directory.External path on Android
// • Delete files when the user removes them; use Filesystem.deleteFile
// 18) Common bugs
// • Missing usage description → app crashes on first prompt (iOS) or silent denial (Android)
// • Using src="file://…" on the web — only webPath / DataUrl are valid for <img> there
// • Forgetting npx cap sync after install — native side doesn't know about the new plugin
// • Capturing huge images (quality 100, no width) → memory pressure, OOM on mid-range phones
// • Granting 'photos' permission but not 'camera' — getPhoto with CameraSource.Camera silently fails
// • Uploading the base64 string instead of the blob → 33% bigger payload
// • Not handling 'user cancelled' → confusing error toast
Why it matters
Reach for CameraResultType.Uri over Base64 for performance, set width/quality server-side to avoid memory pressure on cheap devices, and write the required usage strings up front — NSCameraUsageDescription and friends turn silent crashes into a friendly permission prompt. Use CameraSource.Prompt when you want users to choose between “take new” and “pick from library”.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { Camera, CameraResultType } from '@capacitor/camera';
const photo = await Camera.getPhoto({
quality: 80,
resultType: CameraResultType.Uri,
});
console.log(photo.webPath);
Try it Yourself »
Discussion
Loading…