How to Apply Instagram like Photo Filters with Ionic & Capacitor Last update: 2020-05-05

How to Apply Instagram like Photo Filters with Ionic & Capacitor

When you want to manipulate the images like Instagram with a Photo Filter within your Ionic app there are multiple ways to achieve a filtered photo effect, but the approach we are using today will make it unbelievable easy for you!

We will use the web photo filter package which has a great set of predefined filters that we can apply to our images, and we’ll do all of this in combination with Capacitor.

ionic-instagram-photo-filter

Because the photo filter package is also based on WebGL and is a standard web component, everything we do will work perfectly inside our browser preview - no Cordova plugins today!

Setting up the Photo Filter Project

Before we dive into the code we need a new app with Capacitor enabled (which is the recommendation as of now as well) and install the before mentioned package.

Since we are capturing images within the browser we also need to install the PWA Elements package from Capacitor which gives a nice web camera UI when capturing photos.

You can then as well add the native platforms, but you can also stick to the browser for now:

ionic start imageFilter blank --type=angular --capacitor
cd ./imageFilter
npm install web-photo-filter
npx cap init


# PWA Elements for camera preview inside the browser
npm install @ionic/pwa-elements

# Add native platforms if want
ionic build
npx cap add ios
npx cap add android

To use the package we need to perform a few steps. First, we need to add it to our app/index.html inside the head area:

<script async src="webphotofilter.js"></script>

Then we need to tell Angular to please load some assets from the package folder and move them to our final output directory. You can do this by changing the angular.json and adding the lines below inside the array of assets:

{
  "glob": "webphotofilter.js",
  "input": "node_modules/web-photo-filter/dist",
  "output": "./"
},
{
  "glob": "webphotofilter/*",
  "input": "node_modules/web-photo-filter/dist",
  "output": "./"
}

The last step is to enable the PWA elements from Capacitor, so it’s not related to the web photo filter package itself but Capacitor. For this, simply add the following lines to your app/main.ts:

import { defineCustomElements } from '@ionic/pwa-elements/loader';

// Call the element loader after the platform has been bootstrapped
defineCustomElements(window);

Now we are ready to create some nice filters!

Creating the Photo Filter Logic

To use the component in a page, we need to add the CUSTOM_ELEMENTS_SCHEMA to the according module of the page, which means in our case simply change your home/home.module.ts to:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ])
  ],
  declarations: [HomePage],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HomePageModule {}

Now the fun begins, and I created an array of all the possible filters which you can also find in the web photo filter showcase.

The idea is to show a preview of all filters attached to an image inside a scroll view (slides) and a big preview of the currently selected filter (just like on IG).

So before we can attach filters, we need to capture an image using the standard Capacitor plugins. This will give back a base64 string, which might also be a problem on a device due to memory constraints.

There’s not really any filtering happening from code - we are simply setting a selected filter, which will be used by the actual component in the view in the next step.

Go ahead and change your home/home.page.ts to:

import { Component } from '@angular/core';
import { Plugins, CameraResultType, CameraSource } from '@capacitor/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  selectedFilter = '';
  selectedIndex = 0;
  result: HTMLElement;

  image: any = '';

  slideOpts = {
    slidesPerView: 3.5,
    spaceBetween: 5,
    slidesOffsetBefore: 20,
    freeMode: true
  };

  filterOptions = [
    { name: 'Normal', value: '' },
    { name: 'Sepia', value: 'sepia' },
    { name: 'Blue Monotone', value: 'blue_monotone' },
    { name: 'Violent Tomato', value: 'violent_tomato' },
    { name: 'Grey', value: 'greyscale' },
    { name: 'Brightness', value: 'brightness' },
    { name: 'Saturation', value: 'saturation' },
    { name: 'Contrast', value: 'contrast' },
    { name: 'Hue', value: 'hue' },
    { name: 'Cookie', value: 'cookie' },
    { name: 'Vintage', value: 'vintage' },
    { name: 'Koda', value: 'koda' },
    { name: 'Technicolor', value: 'technicolor' },
    { name: 'Polaroid', value: 'polaroid' },
    { name: 'Bgr', value: 'bgr' }
  ];

  constructor() {}

  async selectImage() {
    const image = await Plugins.Camera.getPhoto({
      quality: 100,
      allowEditing: false,
      resultType: CameraResultType.DataUrl,
      source: CameraSource.Camera
    });
    this.image = image.dataUrl;
  }

  filter(index) {
    this.selectedFilter = this.filterOptions[index].value;
    this.selectedIndex = index;
  }

  imageLoaded(e) {
    // Grab a reference to the canvas/image
    this.result = e.detail.result;
  }

  saveImage() {
    let base64 = '';
    if (!this.selectedFilter) {
      // Use the original image!
      base64 = this.image;
    } else {
      let canvas = this.result as HTMLCanvasElement;
      // export as dataUrl or Blob!
      base64 = canvas.toDataURL('image/jpeg', 1.0);
    }

    // Do whatever you want with the result, e.g. download on desktop
    this.downloadBase64File(base64);
  }

  // https://stackoverflow.com/questions/16996319/javascript-save-base64-string-as-file
  downloadBase64File(base64) {
    const linkSource = `${base64}`;
    const downloadLink = document.createElement('a');
    document.body.appendChild(downloadLink);

    downloadLink.href = linkSource;
    downloadLink.target = '_self';
    downloadLink.download = 'test.webp';
    downloadLink.click();
  }
}

I’ve also added a function to download the image with a dummy function inside the browser (won’t work on a device like this!).

We can do this because we grab a reference to the filtered image inside the imageLoaded function, that will be called from the component once the filter is applied.

Let’s see how we can finally apply the filters based on this code.

Building the Instagram Photo Filter View

We already prepared everything we need in terms of logic, now we just need to create the UI: We can use the web photo filter in multiple places, and so we will have one bigger preview using the selectedFilter, and an iteration of slides based on the array to show all the possible filters already attached to our image.

On the component you can specify to keep the original image, which helps to also show the non filtered version - but we need some additional CSS to hide the part within the web component that we don’t need. We’ll get to that later, just note that this is where the keep property becomes relevant!

We now also see how our imageLoaded is called. Whenever the filter for the main image is applied, our function will be triggered with the details of the component.

Now go ahead and change your home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Photo Filter
    </ion-title>
    <ion-buttons slot="end" *ngIf="image">
      <ion-button (click)="saveImage()">Save</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button expand="full" (click)="selectImage()">Select Image</ion-button>

  <!-- Big preview of selected filter -->
  <div id="preview" *ngIf="image">
    <web-photo-filter
      [class.no-original]="selectedFilter != ''"
      [class.only-original]="selectedFilter == ''"
      [src]="image"
      keep="true"
      [filter]="selectedFilter"
      (filterLoad)="imageLoaded($event)">
    </web-photo-filter>
  </div>

  <!-- Slides with thumbnail preview of all filters -->
  <ion-slides [options]="slideOpts" *ngIf="image">

    <ion-slide *ngFor="let opts of filterOptions; let i = index;" tappable (click)="filter(i)">
      <ion-text [class.selected]="i == selectedIndex">{{ opts.name }}</ion-text>
      <div id="preview">
        <web-photo-filter
          [class.no-original]="i > 0"
          [src]="image"
          keep="false"
          [filter]="opts.value">
        </web-photo-filter>
      </div>
    </ion-slide>

  </ion-slides>

</ion-content>

Right now this will look very odd in your view, since the element take up too much space. Therefore, let’s add some CSS to change the general UI of our page within the home/home.page.scss:

ion-slides {
  margin-top: 50px;
}

ion-slide {
  flex-direction: column;
  font-size: small;
  ion-text {
    padding: 5px;
  }
  .selected {
    font-weight: 600;
  }
}

This won’t change much by now, since we need to define the real CSS rules for the component inside the :root pseudo class, which we can also do within our app/theme/variables.scss:

:root {
  #preview {
    web-photo-filter {
      canvas, img {
        max-width: 100%;
      }
    }

    web-photo-filter.no-original {
        img {
          display: none;
        }
    }

    web-photo-filter.only-original {
      canvas {
        display: none;
      }
  }
}

// Already existing variables
--ion-color-primary...

These rules now target the component, and the reason for the different cases is simple: The web photo filter renders the original image inside an img tag, while the filtered image is displayed inside a canvas element!

So if you want to see only the original, hide the canvas. If you want to see only the filtered, hide the canvas.

We need these rules because we have set the keep property to true in our preview, otherwise we wouldn’t be able to easily switch between filtered and normal photo anymore!

Conclusion

Now you can test drive your application on the browser, capture an image and immediately apply all the filters. You can even download the selected filtered photo directly in your browser!

On a real device I had problems with this approach, perhaps because we are rendering a lot of preview images based on base64 data which might cause memory issues. In general it worked, but only if the array only contained like 3 filters.

You might have to use the real file URL after capturing an image or use a different non.Instagram like representation if you experience crashes on older devices.

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

https://youtu.be/CpeshhBtaQI