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 Engine
s) 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
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 { }
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();
}
}
}
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>
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 wrapperdiv
, with animg
tag inside of it. We can pass specific styles to be inlined both on the wrapper andimg
tag (and classes as well). Here we're only defining thewidth
andheight
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 totrue
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 thewrapper_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 fromImageEngine
's CDN. In this case we're specifically setting the image format tojpg
, thefit
to whatever is selected from the fit select input, acompression
of 10 (interpreted as percentage, goes from 0 to 100, defaults to 0 when not specified) andsharpness
of 15, to add a bit of sharpness to the image. All of these could be dynamically set as is the case withfit
.
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;
}
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.
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>
(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!
Top comments (0)