Image cropping is a common feature in many mobile apps—from profile picture editing to document uploads. In this updated tutorial, you'll learn how to capture an image, crop it, and upload it to a server using Ionic 8, Angular 20, and Capacitor 5, replacing the old Cordova and Ionic Native approach.
We’ll use:
-
@capacitor/camera for taking or picking a photo
-
CropperJS for cropping inside a modern Ionic interface
-
@capacitor/filesystem and Angular's HttpClient to handle uploads
Step 1: Create a New Ionic Angular App
Make sure you have the latest Ionic CLI installed:
npm install -g @ionic/cli
Create the project:
ionic start image-crop-upload blank --type=angular
cd image-crop-upload
Add Capacitor:
ionic integrations enable capacitor
As usual, run the Ionic App for the first time.
ionic serve
The browser will open automatically.
Step 2: Install Required Plugins
Install Capacitor Camera and Filesystem:
npm install @capacitor/camera @capacitor/filesystem
npx cap sync
Install CropperJS:
npm install cropperjs
Step 3: Add CropperJS CSS
In angular.json
, include CropperJS's stylesheet:
"styles": [
"src/global.scss",
"src/theme/variables.scss",
"node_modules/cropperjs/dist/cropper.css"
],
Or, simply import it inside src/global.scss
:
@import "~cropperjs/dist/cropper.css";
Step 4: Create the Image Crop Component
Run the generator:
ionic generate page cropper
In cropper.page.ts
, set up the logic to select, crop, and upload the image.
cropper.page.html
<ion-header>
<ion-toolbar>
<ion-title>Crop & Upload</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button expand="block" (click)="selectImage()">Select Image</ion-button>
<div *ngIf="imageUrl" class="cropper-container">
<img #image [src]="imageUrl" alt="Selected" />
</div>
<ion-button expand="block" color="success" (click)="cropAndUpload()" *ngIf="imageUrl">Crop & Upload</ion-button>
</ion-content>
cropper.page.ts
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonContent, IonHeader, IonTitle, IonToolbar, IonButton } from '@ionic/angular/standalone';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import Cropper from 'cropperjs';
@Component({
selector: 'app-cropper',
templateUrl: './cropper.page.html',
styleUrls: ['./cropper.page.scss'],
standalone: true,
imports: [IonButton, IonContent, IonHeader, IonTitle, IonToolbar, CommonModule, FormsModule]
})
export class CropperPage implements OnInit {
@ViewChild('image', { static: false }) imageElement!: ElementRef;
imageUrl: string | null = null;
private cropper!: Cropper;
constructor(private http: HttpClient) { }
ngOnInit() {
}
async selectImage() {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.DataUrl,
source: CameraSource.Prompt,
});
this.imageUrl = image.dataUrl || null;
// Wait for view to update
setTimeout(() => {
if (this.imageElement && this.imageUrl) {
this.cropper = new Cropper(this.imageElement.nativeElement, {
container: '.cropper-container'
});
}
}, 100);
}
async cropAndUpload() {
if (!this.cropper) return;
// Correct method in CropperJS v2
const canvas = this.cropper.getCropperCanvas() as unknown as HTMLCanvasElement;
if (!canvas) {
console.error('Canvas not returned by cropper');
return;
}
// New in v2: convertToBlob() is async and returns a Promise<Blob>
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((b) => {
if (b) resolve(b);
else reject(new Error('Blob conversion failed'));
}, 'image/jpeg', 0.9);
});
// Convert Blob to File
const file = new File([blob], 'cropped-image.jpg', { type: blob.type });
// Create FormData and upload
const formData = new FormData();
formData.append('file', file);
this.http.post('https://your-server.com/upload', formData).subscribe({
next: (res) => console.log('Upload success:', res),
error: (err) => console.error('Upload failed:', err),
});
}
private blobToBase64(blob: Blob): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject('Failed to convert blob to base64');
reader.onload = () => resolve((reader.result as string).split(',')[1]); // Remove data:image/jpeg;base64,
reader.readAsDataURL(blob);
});
}
}
cropper.page.scss
.cropper-container {
margin-top: 20px;
max-width: 100%;
overflow: hidden;
}
.cropper-container img {
width: 100%;
height: auto;
display: block;
}
Sample Upload Endpoint (Node.js Express)
app.post('/upload', express.json({ limit: '10mb' }), (req, res) => {
const { filename, data } = req.body;
const buffer = Buffer.from(data, 'base64');
fs.writeFileSync(path.join(__dirname, 'uploads', filename), buffer);
res.json({ success: true });
});
Or, we already have a full guide to create an image uploader here.
Step 5: Capacitor Android & iOS Permissions Setup
1. Add Native Platforms (if not done yet)
npx cap add android
npx cap add ios
2. Android Setup
🔹 a. Add Permissions to AndroidManifest.xml
Open android/app/src/main/AndroidManifest.xml
, and inside the <manifest>
tag (above <application>
), add:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
READ_MEDIA_IMAGES
is required for Android 13+ (API 33+) when accessing images.
🔹 b. Configure File URI Handling (optional, for Filesystem plugin)
Add this inside <application>
if you plan to access internal storage:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
And create android/app/src/main/res/xml/file_paths.xml
:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="." />
</paths>
Add them if there are no exists
🔹 c. Request Permissions at Runtime (if needed)
For Android 13+ (API 33), Capacitor automatically handles permission prompts via the Camera plugin, but you can explicitly request it like this:
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
const status = await Camera.requestPermissions();
3. iOS Setup
🔹 a. Update Info.plist
Open ios/App/App/Info.plist
, and add the following keys inside <dict>
:
<key>NSCameraUsageDescription</key>
<string>We need access to your camera to take photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to select images.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need access to save cropped images.</string>
🔹 b. Run and Sync Platforms
Once everything is updated:
npx cap sync
npx cap open android
npx cap open ios
Run the apps using Android Studio and Xcode to test permissions and device features.
4. Permissions Summary
Plugin | Android Permissions | iOS Info.plist Keys |
---|---|---|
@capacitor/camera |
CAMERA , READ_MEDIA_IMAGES /READ_EXTERNAL_STORAGE |
NSCameraUsageDescription , NSPhotoLibraryUsageDescription |
@capacitor/filesystem |
Depends on file location | NSPhotoLibraryAddUsageDescription (optional) |
Step 6: Final Testing & Conclusion
🔍 Final Testing Checklist
Before publishing or shipping your Ionic image cropper app, test the following:
-
✅ Image capture from camera and selection from photo library
-
✅ CropperJS UI loads and works smoothly across screen sizes
-
✅ Cropped image is correctly uploaded as a file (
multipart/form-data
) -
✅ Upload works on both Android and iOS real devices
-
✅ Permissions prompts appear and function correctly
-
✅ Works offline or handles permission denials gracefully
🎉 Conclusion
In this updated tutorial, you built a modern mobile image upload flow using:
-
Ionic 8 and Angular 20 for the UI
-
Capacitor Camera Plugin to capture or select images
-
CropperJS v2 to crop images inside the app
-
FormData + HttpClient to upload a cropped
File
to your backend -
Android and iOS permission handling to ensure compatibility
This approach is Cordova-free, lightweight, and production-ready, aligning with the latest standards in Ionic and Angular development.
🚀 What’s Next?
To take this project further:
-
🧪 Add upload progress indicators using
HttpClient
withreportProgress
-
📤 Save uploaded images to the local device gallery using Capacitor Filesystem
-
📦 Upload to cloud storage (Firebase Storage, AWS S3, Supabase, etc.)
-
🔐 Add authentication before uploading (JWT, Firebase Auth, etc.)
-
🌍 Translate and internationalize with
@ngx-translate
You can get the full source code from our GitHub.
We know that building beautifully designed Ionic apps from scratch can be frustrating and very time-consuming. Check Ionic 6 - Full Starter App and save development and design time. Android, iOS, and PWA, 100+ Screens and Components, the most complete and advance Ionic Template.
That's just the basics. If you need more deep learning about Ionic, Angular, and TypeScript, you can take the following cheap course:
- Ionic Apps with Firebase
- Ionic Apps for WooCommerce: Build an eCommerce Mobile App
- Ionic 8+: Build Food Delivery App from Beginner to Advanced
- IONIC - Build Android & Web Apps with Ionic
- Full Stack Development with Go, Vuejs, Ionic & MongoDB
- Create a Full Ionic App with Material Design - Full Stack
Thanks!