DEV Community

Cover image for Getting Started with ImageEngine and Angular
Micael Nussbaumer
Micael Nussbaumer

Posted on

Getting Started with ImageEngine and Angular

Intro

In this article we'll go through using @imageengine/angular package in a sample project to easily take advantage of ImageEngine's CDN and optimisation engine.

ImageEngine is a super fast CDN that exposes an accessible engine to optimise your app image assets. You can optimise your distributions (which we'll call Engines) in different ways - through ImageEngine's dashboard, defining settings for your Engine or specific paths in your Engine, or through simple query parameters (called directives in ImageEngine), that allow you to dynamically, on-the-fly, execute optimizations to your assets.

The part we'll be leveraging here, through this component, is the query parameters - although this is mostly hidden from you and you don't have to worry about it to use the component.

To deliver actual real optimizations to your images you need an ImageEngine Engine , nonetheless this package can still be used by itself for enabling lazy loading and preventing Cumulative Layout Shift without one.

You can check the small app we'll be building, and the GitHub repo. It's a very basic sample app to show you how to use the component and visualise the different fit types in relation to image size and container size. After we write our app you can also try out all other properties of the component by changing the input parameters used.

The slashed border around the image represents the size you pick from the size select, the image inside it is the actual image, displayed according to the type of fit you chose.

Summary:

Intro
Setting up the Angular Project
Defining our Layout and Content
Deploy to Github Pages
Create ImageEngine Engine
Conclusion

Setting up the Angular Project

To follow along you'll need to have Node.js, NPM (that comes with node.js), and Angular CLI installed.

The versions used for this tutorial are npm 7.15.1, node.js v16.3.0 and Angular CLI 12.1.1.
To deploy on GitHub pages you'll also need a Github account and git installed, the version used here was 2.25.1.

With those installed, from the command line run, (answering N to the prompt for Angular Router and choosing CSS for the styles):

ng new ie-angular-sample
cd ie-angular-sample
npm install @imageengine/angular
Enter fullscreen mode Exit fullscreen mode

Now we need to add the @imageengine/angular module and the Forms module to our app imports.

Open src/app/app.module.ts and make sure it looks like this:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { NgxImageengineModule } from "@imageengine/angular";

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
      BrowserModule,
      NgxImageengineModule,
      FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

FormsModule is included in angular - we need it for form bindings - and NgxImageengineModule is the module exported from the @imageengine/angular package.

Now we're ready to use the image component in our app.
To save some space we're not going to show here the CSS we'll be using, because it has also a style reset sheet included, but make sure to copy it over from styles.css (replace the contents of src/styles.css with those of that file).

Defining our Layout and Content

Now let's edit src/app/app.component.ts to:

import { Component, ChangeDetectorRef, AfterViewInit } from '@angular/core';
import { IEDirectives, IEFit } from "@imageengine/imageengine-helpers";

const OPTIONS = [
    [1920, 920],
    [960, 460],
    [480, 230],
    [240, 115]
];

const FITS: IEFit[] = [
    "stretch",
    "box",
    "letterbox",
    "cropbox"
];

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent implements AfterViewInit {
    image_path: string = "assets/h-lightbox-3.jpeg";
    sizes: number[][] = [...OPTIONS];
    size: number = 3;
    fits: IEFit[] = [...FITS];
    fit: IEFit = "cropbox";
    width: string = `${OPTIONS[3][0]}px`;
    height: string = `${OPTIONS[3][1]}px`;
    src_url: string | null = "";

    constructor(private change_detector: ChangeDetectorRef) { }

    ngAfterViewInit(): void {
       this.set_src_url();
    }

    change_size(new_val: number): void {
        this.size = new_val;
        this.width = `${this.sizes[new_val][0]}px`;
        this.height = `${this.sizes[new_val][1]}px`;
        this.change_detector.detectChanges();
        this.set_src_url();
    }

    change_fit(new_val: IEFit): void {
        this.fit = new_val;
        this.change_detector.detectChanges();
        this.set_src_url();
    }

    set_src_url() {
        let img = document.querySelector(".ngx-ie-image");
        if (img) {
            this.src_url = img.getAttribute("src");
            this.change_detector.detectChanges();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We import some type helpers from @imageengine/imageengine-helpers, this is a package that is a dependency of @imageengine/angular and contains the type specifications that the component uses plus some helpers that that package utilises (although we're not going to need them here).

Our component is very simple, we have a few properties we'll use to drive our UI, some functions to be used as callbacks (change_size and change_fit) and a helper (set_src_url) to get the url that was computed and in actual use by the component to fetch the asset.

Let's also edit src/app/app.component.html to:

<div id="main-container" >
  <div class="image-holder" [style.minWidth]="width">
    <div style="display: flex; flex-flow: row wrap; width: 100%; justify-content: center; align-items: center;">
      <select [ngModel]="size" name="select-size" (ngModelChange)="change_size($event)">
          <option [value]="index" *ngFor="let opt of sizes, index as index">{{opt[0]}}px x {{opt[1]}}px</option>
      </select>
      <select [ngModel]="fit" name="select-fit" (ngModelChange)="change_fit($event)">
          <option [value]="opt" *ngFor="let opt of fits">{{opt}}</option>
      </select>
    </div>
    <p style="margin: 20px auto; color: white;">Generated URL: {{src_url}}</p>
    <ngx-imageengine [wrapper_styles]="{width: width, height: height}" [derive_size]="true" [path]="image_path" [directives]="{format: 'jpg', fit: fit, compression: 10, sharpness: 15}"></ngx-imageengine> 
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Besides the normal html and the select's bound to the component properties, we have our ngx-imageengine component and our focus will be on that.

We won't cover everything about the component here, but in case you're curious just read the package readme as it provides a much more in depth overview of both the properties and details to make the best use of it. Here we'll explain some of the most common and useful properties of the component.

Our web-app displays an image inside a rectangle element and allows us to set the size of this container rectangle and the fit we wish for our image (the way the image is placed and sized relative to the rectangle/sizes we define). It also shows us the resulting url generated to fetch that image taking into account the directives we set from ImageEngine's CDN. It also allows you to visualise how the type of fit affects the display of the image.

For the component properties we set:

  • wrapper_styles The component itself renders a wrapper div, with an img tag inside of it. We can pass specific styles to be inlined both on the wrapper and img tag (and classes as well). Here we're only defining the width and height according to the properties of our component (that reflect the chosen option). This will make the wrapper itself assume these dimensions.
  • derive_size We set this property to true to indicate that we want our component to automatically derive the desired sizes for the image from the size of the wrapper. Notice that this would also work if the wrapper was styled through normal CSS - in which case we wouldn't need to pass the wrapper_styles object.
  • path The relative path to the image asset.
  • directives This is an object specifying what directives we want to apply to our image when requesting it from ImageEngine's CDN. In this case we're specifically setting the image format to jpg, the fit to whatever is selected from the fit select input, a compression of 10 (interpreted as percentage, goes from 0 to 100, defaults to 0 when not specified) and sharpness of 15, to add a bit of sharpness to the image. All of these could be dynamically set as is the case with fit.

To see all options you can check the @imageengine/angular readme

Right now because we didn't specify a host from where to fetch the image the component will fetch it from its relative location on your app. If you visit the sample app or the Github Repository you might have noticed that the path includes "/ie-loader-images/" instead of /assets/ but if you're following this tutorial that is irrelevant (it's related to the Engine and CDN I'm using for this example).

To recap - we set a size for the wrapper, we tell our component to derive the image size from the wrapper, and we give it a few directives to optimise our image. Notice we didn't have to pass width and height directives since we're using derive_size.

Before seeing our app in action, let's add the remaining missing things.
Edit src/app/app.component.css to:

#main-container {
    width: 100vw;
    min-height: 100vh;
    background-color: transparent;
    padding-top: 50px;
}

.image-holder {
    background-color: transparent;
    min-width: 100vw;
    text-align: center;
}

.image-holder select {
    font-size: 18px;
    height: 40px;
    padding: 20px;
    font-weight: lighter;
    background-color: black;
    color: white;
}
Enter fullscreen mode Exit fullscreen mode

Which is just some basic styling for our elements.

Let's also add our image, you can copy it from the GitHub repo.
Put it in the src/assets folder.

Once that is done we should be able to start our app with, from the command line on the root of our project:
npm run start

And then visiting localhost:4200.

If you now change the settings from the selects you should see how that affects the image and the url. If you change on the component itself the directives being passed you will also see them reflected on the url and on the image.

One thing to take into consideration is the fact that we're using device_pixel_ratio conversions, so if you're visualising the app in a monitor with higher resolution than 1:1 (like a retina display) the sizes you see being encoded in the url will respect that and get the correct dimensions for the intrinsic size you want to display.

You can turn that off and there's other properties as well to help you make the most out of ImageEngine easily.

This also means that some images might be requested at higher dimensions than your original images have. When this is the case ImageEngine won't apply crop directives, nor upscale the image and instead return the image in its original size (with other non-size related directives applied).

The component will take care of displaying them according to the chosen fit without you needing to do anything. You can see this by choosing 1920x920 as the size and cropbox fit. The original image is smaller in size than that, so ImageEngine will return it in its original dimensions without the cropbox factor we specified, but our component still displays it as the cropbox would look - because the original size is smaller than the final intrinsic size you might notice the image loose some quality.

You'll also see, if you change the size to a bigger one and then back to a smaller one, that the component changes from the smaller to the bigger one, but then keeps the bigger one as long as it's bigger than the new selection. This can be turned off by using the @Input force_size_recalculation with true.

It defaults to false because in practice , it means that if you have responsive and/or derive_size on, if the sizes change to a smaller one, instead of asking for a new image in those smaller sizes (that would need to be transferred) it keeps the sizing that's bigger - meaning it uses the already fetched image. Nonetheless if you're changing ratios between breakpoints for instance, and using cropbox you might actually want to force a refetch, in those cases you can turn that flag on, but in almost every case it's better, more performant to just keep force_size_recalculation as false.

One last note regarding the directives the component supports. Setting the letterbox colouring isn't possible directly through the component, but is easily achievable by just giving a background-color to the wrapper element and using the box fit method.

Deploy to Github Pages

Angular has support for deploying on GitHub pages with the help of a package named angular-cli-ghpages.

We won't cover how to create a GitHub repo, but once you have that, add your repo as a remote:

git remote add origin git@github.com:<YOUR_USERNAME>/<THE_NAME_OF_YOUR_REPO>.git

And substitute the values between <...> by your username and the name you gave the repo.
Now let's add the angular-cli-ghpages with:

ng add angular-cli-ghpages

Followed by:

ng deploy --base-href=/<THE_NAME_OF_YOUR_REPO>/

(if you have a custom domain associated with your GitHub pages, you need to add the flag cname to the command, like --cname=your_domain.com)

Now you should be able to check the website online from Github Pages, the address should be https://YOUR_GITHUB_USERNAME.github.io/THE_NAME_OF_YOUR_REPO.

You can see that it works even without an host Engine - obviously it won't do any real optimization to your assets until we add one.

Create ImageEngine Engine

Since now we have our resources online, we are able to create a CDN distribution for it with ImageEngine. You can sign up for a free trial of ImageEngine by following the instructions on the following video and using the address of your newly created website.


imageengine.io

Once that is done you should have an ImageEngine delivery address. We'll now add that to our component and re-deploy with the delivery address set. To do that change your component to have the property host set to whatever is the address you got previously.

<ngx-imageengine [wrapper_styles]="{width: width, height: height}"
                 [derive_size]="true"
                 [path]="image_path"
                 [directives]="{format: 'jpg', fit: fit, compression: 10, sharpness: 15}"
                 host="http://YOUR-ADDRESS-IN-IE.cdn.imgeng.in/">
</ngx-imageengine>
Enter fullscreen mode Exit fullscreen mode

(if you have a domain and are running Github Pages with HTTPS then you need to enable it on your Image Engine settings as well)

Notice we put a forward slash at the end of the host address we just added. This is because the paths to work in Github Pages (at least with cname and base-href) need to be absent of leading slashes to be relative but we need it when prepending the domain. In this case it's simpler to add it to the host address.

And run again:
ng deploy --base-href=/<THE_NAME_OF_YOUR_REPO>/

Conclusion

And that's it, now you can see that the generated URL includes the domain. Because it works without an Engine as well (without optimising the images obviously) you can run and test locally your app, assert on the generated urls, and confirm it's working as expected, besides actually seeing how the images will look like. You can do the same in a staging environment without setting up multiple Engine's to deal with different origins.

To make it more production ready you would probably add an environment variable to control the host value, so that when building for production that is set and the ImageEngine CDN used, and when running locally it's not.

As you can see it's quite easy to add advanced image optimisations to your website. Exploring the potential of ImageEngine's CDN and Optimization Engine allow you to drastically reduce your image's payloads, leading to a much leaner and snappier experience for your end-users.

With the aid of this component you can easily create fully maintainable layouts and designs, that only require editing of your CSS/styles to be updated when they change, provide automatically the exact best fit and size according to that styling, prevent Cumulative Layout Shift, have responsiveness to screen orientation/size changes, lazy-loading and are aware of your end-user screen pixel ratio.

Hope you find it useful!

Discussion (0)