Using the HTML5 Filereader API and PHP to upload images from Ionic Framework apps

January 23, 2018, 9:00 am Categories:

Categories

From Canvas and Web Audio to Video manipulation and file handling the HTML5 APIs have opened up a lot of previously unreachable development possibilities.

In the following tutorial I want to take you through using one of these API's: the HTML5 FileReader API to create a web based Ionic application that allows the user to select images which can then be uploaded and saved to a remote server - courtesy of PHP.

Notice that I said web based?

I'm deliberately avoiding Native App development for this tutorial as I want to explore how we could implement image capture/upload functionality that could be used in the context of a Progressive Web App (we're only going to concentrate on developing the aforementioned functionality - we WON'T be developing a full blown Progressive Web App.....just sayin').

Before we continue onwards though we do need to be aware of the level of available browser support for the HTML5 FileReader API (always important when introducing functionality into projects that might be intended for a wider audience):

Browser support
Browser Supported version
Internet Explorer 11 (partial support)
Edge 15+
Firefox 57+
Chrome 49+
Safari 10.1+
iOS Safari 9.3+
Chome for Android 62

As far as support goes using the HTML5 FileReader API is a little restrictive if you need to target older platforms (in which case you might opt for a different approach or use graceful degradation techniques to ensure that legacy browsers have a reduced but usable experience).

What we'll be developing

We won't be winning any design awards for such minimal aesthetics but our application starts with a simple HTML input field:

HTML input field for selecting image files

Once the selected image has been retrieved and parsed by the FileReader API object the previous file upload button is replaced within the template by the displayed image and an Upload button:

The locally selected image displayed in the template

Upon the data being uploaded to the PHP script on the server (which will subsequently parse that data and write it to the correct file format) the user receives notification that the task has been completed successfully:

The locally selected image has been successfully exported to the remote server and created using PHP

Remember that I stated this is purely for web based apps only?

Okay, let's crack on then...

Getting started

Create an ionic application named ionic-uploada and an images service courtesy of the following commands issued from your system CLI software:

ionic start ionic-uploada blank
cd ./ionic-uploada
ionic g provider images

Once completed open up the application root module - ionic-uploada/src/app/app.module.ts - and add the HttpClientModule import like so:

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { ImagesProvider } from '../providers/images/images';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    ImagesProvider
  ]
})
export class AppModule {}

WHY the Ionic CLI doesn't automatically import/add this to the application root module when a service is generated I have no idea (if anyone out there knows please give me a heads up as I find this quite irritating).

With the bare bones of the application in place we can now turn our attention to adding the necessary logic for the ImagesProvider service before we begin developing the HomePage component.

Handling file selection

Our ImagesProvider service will manage the following tasks:

  • Use the FileReader API to retrieve the selected image
  • Check that the selected file matches a specified image format
  • Handles uploading the selected image to the remote PHP script

We'll handle each of these with their own dedicated methods but before we do we start by declaring the following properties within the ImagesProvider service which will store the FileReader API object and the remote URI for the PHP script:

/**
 * @name _READER 
 * @type object
 * @private
 * @description              Creates a FileReader API object
 */
private _READER : any  			=	new FileReader();



/**
 * @name _REMOTE_URI 
 * @type String
 * @private
 * @description              The URI for the remote PHP script that will handle the image upload/parsing
 */
private _REMOTE_URI : string 	=	"http://YOUR-REMOTE-URI-HERE/parse-upload.php";

With these properties now declared (and we'll reference these throughout the service) we can start crafting the first of our methods - handleImageSelection - which will use the _READER object to leverage specific FileReader API functions to handle image selection in our application.

This will retrieve the selected image from the HomePage component view, convert the file into a base 64 data URL string and, on completion, allow the HomePage component class to subscribe to that in the form of an observable:

/**
 * @public
 * @method handleImageSelection
 * @param event  {any}     	The DOM event that we are capturing from the File input field
 * @description    			Uses the FileReader API to capture the input field event, retrieve 
 *                 			the selected image and return that as a base64 data URL courtesy of 
 *							an Observable
 * @return {Observable}
 */
handleImageSelection(event : any) : Observable<any>
{
   let file 		: any 		= event.target.files[0];

   this._READER.readAsDataURL(file);
   return Observable.create((observer) => 
   {
      this._READER.onloadend = () =>
      {
         observer.next(this._READER.result);
         observer.complete();
      }
   });
}

Our second method - isCorrectFileType - is used to determine whether the selected file is identified as a specific image type or not courtesy of a regular expression which checks for the following mime types:

  • gif
  • jpeg
  • jpg
  • png
/**
 * @public
 * @method isCorrectFile
 * @param file  {String}    The file type we want to check
 * @description    			Uses a regular expression to check that the supplied file format 
 *                 			matches those specified within the method
 * @return {any}
 */
isCorrectFileType(file)
{
   return (/^(gif|jpg|jpeg|png)$/i).test(file);
}

Our final method - uploadImageSelection - handles posting the selected image data to the remote PHP script using Angular's HttpClient and HttpHeaders modules (supplying the image file name in the form of a timestamp):

/**
 * @public
 * @method uploadImageSelection
 * @param file  		{String}    The file data to be uploaded
 * @param mimeType  	{String}    The file's MimeType (I.e. jpg, gif, png etc)
 * @description    		Uses the Angular HttpClient to post the data to a remote
 *                 		PHP script, returning the observable to the parent script 
 * 						allowing that to be able to be subscribed to
 * @return {any}
 */
uploadImageSelection(file 		: string, 
                     mimeType 	: string) : Observable<any>
{
   let headers 	: any		= new HttpHeaders({'Content-Type' : 'application/octet-stream'}),
       fileName : any       = Date.now() + '.' + mimeType,
       options 	: any		= { "name" : fileName, "file" : file };

   return this.http.post(this._REMOTE_URI, JSON.stringify(options), headers);
}

In full then the ionic-uploada/src/providers/images.images.ts service should resemble the following (obviously you will need to supply your own URL for the PHP script according to your website address/directory location):

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';


@Injectable()
export class ImagesProvider {



   /**
    * @name _READER 
    * @type object
    * @private
    * @description              Creates a FileReader API object
    */
   private _READER : any  			=	new FileReader();



   /**
    * @name _REMOTE_URI 
    * @type String
    * @private
    * @description              The URI for the remote PHP script that will handle the image upload/parsing
    */
   private _REMOTE_URI : string 	=	"http://YOUR-REMOTE-URI-HERE/parse-upload.php";



   constructor(public http: HttpClient) 
   {}



   /**
    * @public
    * @method handleImageSelection
    * @param event  {any}     	The DOM event that we are capturing from the File input field
    * @description    			Uses the FileReader API to capture the input field event, retrieve 
    *                 			the selected image and return that as a base64 data URL courtesy of 
    *							an Observable
    * @return {Observable}
    */
   handleImageSelection(event : any) : Observable<any>
   {
      let file 		: any 		= event.target.files[0];

      this._READER.readAsDataURL(file);
      return Observable.create((observer) => 
      {
         this._READER.onloadend = () =>
         {
            observer.next(this._READER.result);
            observer.complete();
         }
      });
   }



   /**
    * @public
    * @method isCorrectFile
    * @param file  {String}     The file type we want to check
    * @description    			Uses a regular expression to check that the supplied file format 
    *                 			matches those specified within the method
    * @return {any}
    */
   isCorrectFileType(file)
   {
      return (/^(gif|jpg|jpeg|png)$/i).test(file);
   }



   /**
    * @public
    * @method uploadImageSelection
    * @param file  		{String}    The file data to be uploaded
    * @param mimeType  	{String}    The file's MimeType (I.e. jpg, gif, png etc)
    * @description    				Uses the Angular HttpClient to post the data to a remote
    *                 				PHP script, returning the observable to the parent script 
    * 								allowing that to be able to be subscribed to
    * @return {any}
    */
   uploadImageSelection(file 		: string, 
                        mimeType 	: string) : Observable<any>
   {
      let headers 	: any		= new HttpHeaders({'Content-Type' : 'application/octet-stream'}),
          fileName  : any       = Date.now() + '.' + mimeType,
          options 	: any		= { "name" : fileName, "file" : file };

      return this.http.post(this._REMOTE_URI, JSON.stringify(options), headers);
   }

}

Crafting the HomePage component logic

Now that our ImagesProvider service contains the necessary methods for handling file selection, checking and uploading we can turn our attention to the logic for the HomePage component.

This will effectively manage the following scenarios (with the necessary help provided courtesy of the ImagesProvider service):

  • Select image files from the user's computer
  • Allow selected images to be uploaded
  • Inform the user of the outcome of these operations

Open the ionic-uploada/src/pages/home/home.ts component class and start by importing the required components and services:

import { Component } from '@angular/core';
import { AlertController, NavController } from 'ionic-angular';
import { ImagesProvider } from '../../providers/images/images';

We then follow this by declaring a handful of properties to manage storing the image as a base64 data URI string, enabling/disabling DOM elements within the component view and storing the image mime type:

/**
 * @name image 
 * @type String
 * @public
 * @description              Will store the selected image file data (in the form of a base64 data URI)
 */
public image : string;



/**
 * @name isSelected 
 * @type Boolean
 * @public
 * @description              Used to switch DOM elements on/off depending on whether an image has been selected
 */
public isSelected : boolean 		=	false;



/**
 * @name _SUFFIX 
 * @type String
 * @private
 * @description              Will store the selected image's MimeType
 */
private _SUFFIX : string;

With these in place we then initialise the required components and services within the class constructor like so:

constructor(public navCtrl 		: NavController,
            private _ALERT      : AlertController,
            private _IMAGES 	: ImagesProvider) 
{ }

Next we declare our first method - selectFileToUpload - which, as the name implies, handles the selection of the image file from the user's computer:

/**
 * @public
 * @method selectFileToUpload
 * @param event  {any}     	The DOM event that we are capturing from the File input field
 * @description    			Handles the selection of image files from the user's computer,  
 *                 			validates they are of the correct file type and displays the  
 *							selected image in the component template along with an upload
 * 							button
 * @return {none}
 */
selectFileToUpload(event) : void
{
   this._IMAGES
   .handleImageSelection(event)
   .subscribe((res) =>
   {
         
      // Retrieve the file type from the base64 data URI string
      this._SUFFIX 			= res.split(':')[1].split('/')[1].split(";")[0];


      // Do we have correct file type?
      if(this._IMAGES.isCorrectFileType(this._SUFFIX))
      {

         // Hide the file input field, display the image in the component template
         // and display an upload button
         this.isSelected 	= true
         this.image 			= res;
      }

      // If we don't alert the user
      else
      {
         this.displayAlert('You need to select an image file with one of the following types: jpg, gif or png');
      }
   },
   (error) =>
   {
      console.dir(error);
      this.displayAlert(error.message);
   });
}

With this in place our next method - uploadFile - handles, courtesy of the ImageProvider service, the posting of the selected image to the remote PHP script and subsequent feedback on the success/failure of that operation:

/**
 * @public
 * @method uploadFile
 * @description    			Handles uploading the selected image to the remote PHP script
 * @return {none}
 */
uploadFile() : void
{
   this._IMAGES
   .uploadImageSelection(this.image, 
                         this._SUFFIX)
   .subscribe((res) =>
   {
      this.displayAlert(res.message);     
   },
   (error : any) =>
   {
      console.dir(error);
      this.displayAlert(error.message);
   });
}

Finally, the displayAlert method handles returning feedback to the user courtesy of the Ionic AlertController API:

/**
 * @public
 * @method displayAlert
 * @param message  {string}  The message to be displayed to the user
 * @description    			Use the Ionic AlertController API to provide user feedback
 * @return {none}
 */
displayAlert(message : string) : void
{
   let alert : any   = this._ALERT.create({
       title 		: 'Heads up!',
       subTitle 	: message,
       buttons 	    : ['Got it']
   });
   alert.present();
}

In full then our HomePage component class should appear as follows:

import { Component } from '@angular/core';
import { AlertController, NavController } from 'ionic-angular';
import { ImagesProvider } from '../../providers/images/images';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {



   /**
    * @name image 
    * @type String
    * @public
    * @description              Will store the selected image file data (in the form of a base64 data URI)
    */
   public image : string;



   /**
    * @name isSelected 
    * @type Boolean
    * @public
    * @description              Used to switch DOM elements on/off depending on whether an image has been selected
    */
   public isSelected : boolean 		=	false;



   /**
    * @name _SUFFIX 
    * @type String
    * @private
    * @description              Will store the selected image's MimeType
    */
   private _SUFFIX : string;



   constructor(public navCtrl 		: NavController,
               private _ALERT       : AlertController,
               private _IMAGES 		: ImagesProvider) 
   {
 
   }



   /**
    * @public
    * @method selectFileToUpload
    * @param event  {any}     	The DOM event that we are capturing from the File input field
    * @description    			Handles the selection of image files from the user's computer,  
    *                 			validates they are of the correct file type and displays the  
    *							selected image in the component template along with an upload
    * 							button
    * @return {none}
    */
   selectFileToUpload(event) : void
   {
      this._IMAGES
      .handleImageSelection(event)
      .subscribe((res) =>
      {
         
         // Retrieve the file type from the base64 data URI string
         this._SUFFIX 			= res.split(':')[1].split('/')[1].split(";")[0];


         // Do we have correct file type?
         if(this._IMAGES.isCorrectFileType(this._SUFFIX))
         {

            // Hide the file input field, display the image in the component template
            // and display an upload button
            this.isSelected 	= true
            this.image 			= res;
         }

         // If we don't alert the user
         else
         {
            this.displayAlert('You need to select an image file with one of the following types: jpg, gif or png');
         }
      },
      (error) =>
      {
         console.dir(error);
         this.displayAlert(error.message);
      });
   }




   /**
    * @public
    * @method uploadFile
    * @description    			Handles uploading the selected image to the remote PHP script
    * @return {none}
    */
   uploadFile() : void
   {
      this._IMAGES
      .uploadImageSelection(this.image, 
                            this._SUFFIX)
      .subscribe((res) =>
      {
         this.displayAlert(res.message);     
      },
      (error : any) =>
      {
         console.dir(error);
         this.displayAlert(error.message);
      });
   }




   /**
    * @public
    * @method displayAlert
    * @param message  {string}  The message to be displayed to the user
    * @description    			Use the Ionic AlertController API to provide user feedback
    * @return {none}
    */
   displayAlert(message : string) : void
   {
      let alert : any   = this._ALERT.create({
         title 		: 'Heads up!',
         subTitle 	: message,
         buttons 	: ['Got it']
      });
      alert.present();
   }

}

Templating

The HomePage component view handles the display of the following DOM elements:

  • The File input field
  • The selected image and upload button

We'll implement these using Angular's ngIf directive to conditionally display those elements based on the value of the isSelected boolean property that we declared within the HomePage component class.

This will be crafted within the ionic-uploada/src/pages/home/home.html template as follows:

<ion-header>
  <ion-navbar>
    <ion-title>
      Ionic Uploada
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  
   <div *ngIf="!isSelected">
      <h2>Select image and upload</h2>
      <input 
         type="file" 
         (change)="selectFileToUpload($event)"> 
   </div>



   <div *ngIf="isSelected">
   	 <button 
   	    ion-button 
   	    color="primary" 
   	    (click)="uploadFile()">Upload</button>

         <!-- Display the selected image -->
   	 <img [src]="image">
   </div>


</ion-content>

Finally we'll add a smattering of style rules for the template's File input field within the ionic-uploada/src/pages/home/home.scss stylesheet:

page-home {

   input[type="file"] {
   	  background: rgba(230, 230, 230,1);
      padding: 1em;
   }

}

Server side scripting

As we'll be using PHP to receive and parse the image data (that's supplied as a base64 data URI string) we can take advantage of the following file functions provided by this server-side language:

These are ideal for our purposes as the image data is supplied to the PHP script in the form of a string.

Our PHP script, although relatively simple in scope, will perform a number of tasks to successfully parse this supplied data and publish that to an image file.

Firstly, the image data will be decoded and formatted using the following PHP built-in methods:

  • json_decode - Takes a JSON encoded string and converts that to a PHP variable
  • substr - Returns part of a string
  • strpos - Finds the first occurrence of the position of a substring within a string
  • str_replace - Replaces all occurrences of the provided search string with the provided replacement string
  • base64_decode - Decodes data encoded with base64

These will ensure that the string containing the image data is properly formatted with no invalid characters that might corrupt the file writing process when it is supplied to the file_put_contents function.

Secondly, the script will attempt to write the formatted image string data to a file using the file_put_contents method.

Finally, when attempting to write the string data to a file we check for errors and keep the user informed of the success/failure of the operation.

I've named this script parse-upload.php and placed that in a suitable directory on my remote server. Within this directory is a sub-directory named uploads which is where the generated image file will be written to and stored.

One last thing before we cover the PHP code - IF your remote server ISN'T configured to accept certain headers then you might receive error warnings like the following:

Failed to load resource: Preflight response is not successful

Content-Type is not allowed by Access-Control-Allow-Headers

As frustrating as such messages are these can thankfully be overcome by adding the following headers to your PHP script:

header('Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Content-Range, Content-Disposition, Content-Description');

You might also need to add a CORS header to your script too (mine is located within the .htaccess file for the website) which you can find more information on here.

Always good to know there's a solution available when you need one!

With that stated here's the parse-upload.php script in full:

<?php
header('Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Content-Range, Content-Disposition, Content-Description');


   // Retrieve and decode the posted JSON data
   $posted        = file_get_contents("php://input");
   $obj           =  json_decode($posted);


   // Separate out the supplied keys/values
   $fileName      =  strip_tags($obj->name);
   $fileData      =  strip_tags($obj->file);


   // Format the supplied base64 data URI so that we remove the initial base64 identifier
   $uri           =  substr($fileData,strpos($fileData,",")+1);


   // Replace any spaces in the formatted base64 URI string with pluses to avoid corrupted file data
   $encodedData   = str_replace(' ','+',$uri);


   // Decode the formatted base64 URI string
   $decodedData   = base64_decode($encodedData);


   try {
      
      // Write the base64 URI data to a file in a sub-directory named uploads
      if(!file_put_contents('uploads/' . $fileName, $decodedData))
      {
         // Uh-oh! Something went wrong - inform the user
         echo json_encode(array('message' => 'Error! The supplied data was NOT written '));
      }

      // Everything went well - inform the user :)
      echo json_encode(array('message' => 'The file was successfully uploaded'));

   } 
   catch(Exception $e)
   {
      // Uh-oh! Something went wrong - inform the user
      echo json_encode(array('message' => 'Fail!'));
   }


?>

With this in place on your remote server and the address to that script added to the _REMOTE_URI property of the ImagesProvider service you can now test that the application works in your web browser by running the following command within the Ionic CLI:

ionic serve

All things being well you should see the application being loaded and find yourself able to select images and upload these to the remote server - as demonstrated in the following screen capture (the Batman choice of imagery is entirely optional!):

The locally selected image has been successfully exported to the remote server and created using PHP

In summary

I purposely wanted to explore using Ionic in a purely web based context for this tutorial so we could look at how to achieve file capture and upload without the use of Ionic Native plugins (thanks to the HTML5 FileReader API and Angular's HttpClient module).

If however we want to target native mobile platforms instead we do need to look at using Ionic Native plugins to capture/select images.

There doesn't appear to be anyway around this unfortunately but, in the context of this tutorial, you've hopefully learnt some useful approaches/techniques that you can apply within your own projects when deploying Ionic applications as Progressive Web Apps.

What did you think of this article? Was there anything that I missed that should have been covered? Share your thoughts, reactions and suggestions in the comments section below.

If you liked what was written here then please consider signing up to my FREE mailing list to stay updated on further articles and e-books that I have in the pipeline.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top