The Ionic 4 Images Guide (Capture, Store & Upload with POST) Last update: 2018-10-02

The Ionic 4 Images Guide (Capture, Store & Upload with POST)

If your app is working with images and you need to handle both local files and upload files things can get a bit tricky with Ionic. Especially as debugging the filesystem and paths is cumbersome, it’s not automatically clear how everything needs to work.

This Ionic 4 images guide aims to clarify how you can:

  • Capture images using camera or photo library
  • Store image files inside your Ionic 4 app
  • Upload files from their local path using a POST request

Want a more up to date version of this tutorial? Check out the Ionic Image guide with Capacitor!

The result will be a simple app like you can see below.

ionic-4-image-upload

For this tutorial I use Ionic 4 so perhaps some details might slightly change over the next time!

Starting our Ionic 4 Image Upload App

To get started we create a blank new Ionic 4 app and install all the plugins we need.

Of course we need the camera and file plugin, but we also need the new Webview package for a reason we will see later. For now go ahead and run:

ionic start devdacticImages blank --type=angular
cd devdacticImages

# Ionic Native Packages
npm i @ionic-native/camera
npm i @ionic-native/file
npm i @ionic-native/ionic-webview
npm i @ionic-native/file-path

# Cordova Packages
ionic cordova plugin add cordova-plugin-camera
ionic cordova plugin add cordova-plugin-file
ionic cordova plugin add cordova-plugin-ionic-webview
ionic cordova plugin add cordova-sqlite-storage
ionic cordova plugin add cordova-plugin-filepath

Now we need to hook up everything inside our module so we can use the plugins later. We also make use of the Ionic Storage not to store the files but the path to a file later on.

Go ahead and change your src/app/app.module.ts to:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule, RouteReuseStrategy, Routes } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { HttpClientModule } from '@angular/common/http';

import { Camera } from '@ionic-native/Camera/ngx';
import { File } from '@ionic-native/File/ngx';
import { WebView } from '@ionic-native/ionic-webview/ngx';
import { FilePath } from '@ionic-native/file-path/ngx';

import { IonicStorageModule } from '@ionic/storage';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    HttpClientModule,
    IonicStorageModule.forRoot()
    ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    Camera,
    File,
    WebView,
    FilePath
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

We don’t need any special routing so our image upload app is prepared for some action!

Fix the WkWebView Plugin

At the time writing this tutorial there was also a bug within the webview plugin for iOS which leads to an app crash. You can see the details in this issue which might already be fixed later.

UPDATE: The issue is closed, so you shouldn’t get into any trouble.

If you still encounter problems, you can open your platforms/ios/devdacticImages/Plugins/cordova-plugin-ionic-webview/CDVWKWebViewEngine.m and replace it with the contents of the fixed file.

The View for Our Image Upload & Management App

Let’s start with the easiest part which is the view in our Ionic 4 image upload app. We need to display a button so users can select am image to upload. This will trigger an action sheet, and once the user has finished the image dialog a new image will be displayed inside the list.

The list itself shows all of our locally stored files (more on the logic later) with a button to upload the image and to delete the file and all references.

There isn’t really much about this so first of all now change your src/app/home/home.page.html to this:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Image Upload
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <h3 *ngIf="images.length == 0" text-center>Please Select Image!</h3>

  <ion-list>
    <ion-item *ngFor="let img of images; index as pos" text-wrap>
      <ion-thumbnail slot="start">
        <ion-img [src]="img.path"></ion-img>
      </ion-thumbnail>
      <ion-label>
        {{ img.name }}
      </ion-label>
      <ion-button slot="end" fill="clear" (click)="startUpload(img)">
        <ion-icon slot="icon-only" name="cloud-upload"></ion-icon>
      </ion-button>
      <ion-button slot="end" fill="clear" (click)="deleteImage(img, pos)">
        <ion-icon slot="icon-only" name="trash"></ion-icon>
      </ion-button>
    </ion-item>
  </ion-list>
</ion-content>

<ion-footer>
  <ion-toolbar color="primary">
    <ion-button fill="clear" expand="full" color="light" (click)="selectImage()">
      <ion-icon slot="start" name="camera"></ion-icon>
      Select Image</ion-button>
  </ion-toolbar>
</ion-footer>

The Basics for Our Image Upload

Let’s dive into the real action. I’ll split the code for the class in multiple sections so we can go over them one by one. To start, let’s add all the dependencies our class needs (which are quite a few) and the first function that will be called once the app starts.

In the beginning we will look inside our storage to see if we have any stored information about images already captured. This array will only contain the name of a file like “1234.webp”, so for each entry we need to resolve the name to the local path of our app which we add to the object as filePath.

To display the image we need another path, and here we can make use of the webview.convertFileSr() which resolves a file:// path to a path that the WebView understands. All of this information goes to the local array which our previous view can then iterate.

Now make the first changes inside your src/app/home/home.page.ts to get started:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { Camera, CameraOptions, PictureSourceType } from '@ionic-native/Camera/ngx';
import { ActionSheetController, ToastController, Platform, LoadingController } from '@ionic/angular';
import { File, FileEntry } from '@ionic-native/File/ngx';
import { HttpClient } from '@angular/common/http';
import { WebView } from '@ionic-native/ionic-webview/ngx';
import { Storage } from '@ionic/storage';
import { FilePath } from '@ionic-native/file-path/ngx';

import { finalize } from 'rxjs/operators';

const STORAGE_KEY = 'my_images';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {

  images = [];

  constructor(private camera: Camera, private file: File, private http: HttpClient, private webview: WebView,
    private actionSheetController: ActionSheetController, private toastController: ToastController,
    private storage: Storage, private plt: Platform, private loadingController: LoadingController,
    private ref: ChangeDetectorRef, private filePath: FilePath) { }

  ngOnInit() {
    this.plt.ready().then(() => {
      this.loadStoredImages();
    });
  }

  loadStoredImages() {
    this.storage.get(STORAGE_KEY).then(images => {
      if (images) {
        let arr = JSON.parse(images);
        this.images = [];
        for (let img of arr) {
          let filePath = this.file.dataDirectory + img;
          let resPath = this.pathForImage(filePath);
          this.images.push({ name: img, path: resPath, filePath: filePath });
        }
      }
    });
  }

  pathForImage(img) {
    if (img === null) {
      return '';
    } else {
      let converted = this.webview.convertFileSrc(img);
      return converted;
    }
  }

  async presentToast(text) {
    const toast = await this.toastController.create({
        message: text,
        position: 'bottom',
        duration: 3000
    });
    toast.present();
  }

  // Next functions follow here...

}

Adding New Images

The next step is to add new images. We start this process by displaying an action sheet from which the user can either select the camera or photo library as a source.

Once the source is defined we use the camera like always and we are not using a base64 as a result but the real FILE_URI of the image. Otherwise we would have to store those super big strings inside the storage which is not really considered best practice.

After the image was selected we want to copy the file over to our apps data directory with a new name so we are not more dependent on where the file really exists as we have our own copy.

Go ahead and add the 2 functions below what you already got:

async selectImage() {
    const actionSheet = await this.actionSheetController.create({
        header: "Select Image source",
        buttons: [{
                text: 'Load from Library',
                handler: () => {
                    this.takePicture(this.camera.PictureSourceType.PHOTOLIBRARY);
                }
            },
            {
                text: 'Use Camera',
                handler: () => {
                    this.takePicture(this.camera.PictureSourceType.CAMERA);
                }
            },
            {
                text: 'Cancel',
                role: 'cancel'
            }
        ]
    });
    await actionSheet.present();
}

takePicture(sourceType: PictureSourceType) {
    var options: CameraOptions = {
        quality: 100,
        sourceType: sourceType,
        saveToPhotoAlbum: false,
        correctOrientation: true
    };

    this.camera.getPicture(options).then(imagePath => {
        if (this.platform.is('android') && sourceType === this.camera.PictureSourceType.PHOTOLIBRARY) {
            this.filePath.resolveNativePath(imagePath)
                .then(filePath => {
                    let correctPath = filePath.substr(0, filePath.lastIndexOf('/') + 1);
                    let currentName = imagePath.substring(imagePath.lastIndexOf('/') + 1, imagePath.lastIndexOf('?'));
                    this.copyFileToLocalDir(correctPath, currentName, this.createFileName());
                });
        } else {
            var currentName = imagePath.substr(imagePath.lastIndexOf('/') + 1);
            var correctPath = imagePath.substr(0, imagePath.lastIndexOf('/') + 1);
            this.copyFileToLocalDir(correctPath, currentName, this.createFileName());
        }
    });

}

Copy Files & Store Local Reference

So we got the image file and want to copy it to our app. For the copy function we need the path to the original file, the name, the new path and the new name.

This function will then copy over the original file under the new name to our data directory.

Now it’s not really a good idea to list only the files inside our app to keep track of them, as you might perhaps also need to store additional information.

Therefore, we update our Storage and store the information with the JSON.stringify() as an array back. Also, we create one additional object that we add to our local array with the according information and resolved paths like we also do when our app starts.

Here’s the logic to copy the file and store the information:

createFileName() {
    var d = new Date(),
        n = d.getTime(),
        newFileName = n + ".jpg";
    return newFileName;
}

copyFileToLocalDir(namePath, currentName, newFileName) {
    this.file.copyFile(namePath, currentName, this.file.dataDirectory, newFileName).then(success => {
        this.updateStoredImages(newFileName);
    }, error => {
        this.presentToast('Error while storing file.');
    });
}

updateStoredImages(name) {
    this.storage.get(STORAGE_KEY).then(images => {
        let arr = JSON.parse(images);
        if (!arr) {
            let newImages = [name];
            this.storage.set(STORAGE_KEY, JSON.stringify(newImages));
        } else {
            arr.push(name);
            this.storage.set(STORAGE_KEY, JSON.stringify(arr));
        }

        let filePath = this.file.dataDirectory + name;
        let resPath = this.pathForImage(filePath);

        let newEntry = {
            name: name,
            path: resPath,
            filePath: filePath
        };

        this.images = [newEntry, ...this.images];
        this.ref.detectChanges(); // trigger change detection cycle
    });
}

Delete Local Files

We got almost all important parts in place, but if we want to finish this example we also need the delete function. At this point you need to take care of 3 elements:

  • Remove the object from our local images array
  • Remove the item from the Ionic Storage
  • Remove the actual file from our app folder

But don’t worry, all of those actions can be performed with just one simple function:

deleteImage(imgEntry, position) {
    this.images.splice(position, 1);

    this.storage.get(STORAGE_KEY).then(images => {
        let arr = JSON.parse(images);
        let filtered = arr.filter(name => name != imgEntry.name);
        this.storage.set(STORAGE_KEY, JSON.stringify(filtered));

        var correctPath = imgEntry.filePath.substr(0, imgEntry.filePath.lastIndexOf('/') + 1);

        this.file.removeFile(correctPath, imgEntry.name).then(res => {
            this.presentToast('File removed.');
        });
    });
}

Upload Files with POST and Form Data

We got files, we got the storage, we got all information we need but still the upload process for files isn’t that easy. In our example we will have a PHP backend that accepts our file (we’ll build it in the last step) and we need to post it somehow. Also, we don’t need to use the old file transfer plugin anymore, all of this also works with the general POST of Angular HTTP.

Some of the following logic for reading the local file comes from this great tutorial.

The idea is that we first need to resolve the path of our local file which results in a FileEntry object. On this object we can than call the file()function and use a FileReader to read in the data of a file object.

The result is a blob the we can add as FormData to our request which is in the end a standard HTTP POST call with the form data.

Now go ahead and add the last functions to your class:

// Inspired by https://golb.hplar.ch/2017/02/Uploading-pictures-from-Ionic-2-to-Spring-Boot.html

startUpload(imgEntry) {
    this.file.resolveLocalFilesystemUrl(imgEntry.filePath)
        .then(entry => {
            ( < FileEntry > entry).file(file => this.readFile(file))
        })
        .catch(err => {
            this.presentToast('Error while reading file.');
        });
}

readFile(file: any) {
    const reader = new FileReader();
    reader.onload = () => {
        const formData = new FormData();
        const imgBlob = new Blob([reader.result], {
            type: file.type
        });
        formData.append('file', imgBlob, file.name);
        this.uploadImageData(formData);
    };
    reader.readAsArrayBuffer(file);
}

async uploadImageData(formData: FormData) {
    const loading = await this.loadingController.create({
        message: 'Uploading image...',
    });
    await loading.present();

    this.http.post("http://localhost:8888/upload.php", formData)
        .pipe(
            finalize(() => {
                loading.dismiss();
            })
        )
        .subscribe(res => {
            if (res['success']) {
                this.presentToast('File upload complete.')
            } else {
                this.presentToast('File upload failed.')
            }
        });
}

That’s all for the Ionic image upload app, the app will work now besides the upload part but you can run it already now.

Make sure that you are running the app on the simulator/device as we use the camera and filesystem so it will only work where Cordova is available!

The PHP Upload Logic

Now I’m not a PHP expert so I’ll make this as quick as possible.

If you have a server you can use that one, otherwise I simply recommend to download XAMPP and install it local.

I’m not going to cover that process since this is about Ionic image upload and not how to configure PHP. If you have set it up, you can first of all create a upload.php to accept uploads:

<?php
header('Access-Control-Allow-Origin: *');
$target_path = "uploads/";

$target_path = $target_path . basename( $_FILES['file']['name']);

if(move_uploaded_file($_FILES['file']['tmp_name'], $target_path)) {
    header('Content-type: application/json');
    $data = ['success' => true, 'message' => 'Upload and move success'];
    echo json_encode( $data );
} else{
    header('Content-type: application/json');
    $data = ['success' => false, 'message' => 'There was an error uploading the file, please try again!'];
    echo json_encode( $data );
}

?>

Also, make sure to create a uploads folder next to this file, as it will copy the images into that folder.

Additionally, to see the results of our hard work, I created a little HTML file that will scan the uploads folder and show them so we can directly see if our upload worked, create this as index.php next to the previous file and insert:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
  <title>Devdactic Image Upload</title>
</head>
<body>
<h1>Ionic Image Upload</h1>
  <?php
  $scan = scandir('uploads');
  foreach($scan as $file)
  {
    if (!is_dir($file))
    {
        echo '<h3>'.$file.'</h3>';
      echo '<img src="uploads/'.$file.'" style="width: 400px;"/><br />';
    }
  }
  ?>
</body>
</html>

You can now start your local MAMP server and navigate to http://localhost:8888 which will display your Ionic Images overview.

In our example we used this URL for the upload, but this will only work if you run the app on the simulator. If you deploy the app to your iOS device you need to change the URL to the IP of your computer!

Conclusion

The process of capturing image with Ionic 4 is still the same, but storing local files and uploading works actually a bit easier then with previous versions.

If you don’t see your files it’s most likely an issue with the WKWebView and how local files can be displayed, so if you encounter any problems please provide some further logs!

You can also find a video version of this article below.

https://youtu.be/4XiBXFDXesg