
Angular Image Resizing
Description
This is a simple image resizing service that can be used to resize images.
Installation
npm i pica
npm i -D @types/pica
Implementation
// image.service.ts
import { Injectable, NgZone, inject } from '@angular/core';
import * as PicaClient from 'pica';
import { SnackBarService } from './snack-bar.service';
const fitInside = ([x, y]: [number, number], [w, h]: [number, number]) => {
if (x <= w && y <= h) return;
x > w && ((y /= x / w), (x = w));
y > h && ((x /= y / h), (y = h));
return [Math.floor(x), Math.floor(y)];
};
export interface UploadThumbOption {
maxSize?: number;
type?: RegExp;
exportTo?: string;
resize?: [number, number];
}
const defaultThumbOption = {
maxSize: 4 * 1024 * 1024,
type: /^image\/(png|jpeg)$/,
resize: [480, 480] as [number, number],
};
@Injectable({ providedIn: 'root' })
export class ImageService {
readonly #ngZone = inject(NgZone);
readonly #snackBarService = inject(SnackBarService);
readonly #picaClient = PicaClient();
public async resize(image: HTMLImageElement, to: [number, number] = [600, 600]) {
const res = fitInside([image.naturalWidth, image.naturalHeight], to);
if (!res) return;
const picaClient = this.#picaClient;
const canvas = document.createElement('canvas');
canvas.width = res[0];
canvas.height = res[1];
const resized = await picaClient.resize(image, canvas, {
unsharpAmount: 80,
unsharpRadius: 0.6,
unsharpThreshold: 2,
});
canvas.remove();
return await picaClient.toBlob(resized, 'image/jpeg', 0.9);
}
#prepareImage(file: File): Promise<HTMLImageElement> {
const image = document.createElement('img');
image.width = 200;
image.height = 200;
const process: Promise<HTMLImageElement> = new Promise((s, f) => {
image.onload = () => s(image);
image.onerror = f;
});
const url = URL.createObjectURL(file);
image.src = url;
return process;
}
#removeImage(image: HTMLImageElement) {
URL.revokeObjectURL(image.src);
image.remove();
}
public async resizeFromFile(file: File, opt: { exportTo?: string; resize?: [number, number] } = {}): Promise<Blob> {
return this.#ngZone.runOutsideAngular(() => this.#resizeFromFile(file, opt));
}
async #resizeFromFile(file: File, opt: { exportTo?: string; resize?: [number, number] } = {}): Promise<Blob> {
opt.resize ||= defaultThumbOption.resize;
const image = await this.#prepareImage(file);
const res = fitInside([image.naturalWidth, image.naturalHeight], opt.resize);
if (!res) {
this.#removeImage(image);
return file;
}
const picaClient = this.#picaClient;
const canvas = document.createElement('canvas');
canvas.width = res[0];
canvas.height = res[1];
const resized = await picaClient.resize(image, canvas, {
unsharpAmount: 80,
unsharpRadius: 0.6,
unsharpThreshold: 2,
});
const blob = await picaClient.toBlob(resized, opt.exportTo || 'image/jpeg', 0.9);
canvas.remove();
this.#removeImage(image);
return blob;
}
public async resizeForUpload(file: File, options: UploadThumbOption = {}) {
if (file.size > (options.maxSize || defaultThumbOption.maxSize)) {
this.#snackBarService.warn('Image should be less then 4MB');
return;
}
if (!(options.type || defaultThumbOption.type).test(file.type)) {
this.#snackBarService.warn('Image formats invalids should be png/jpeg');
return;
}
options.resize ||= defaultThumbOption.resize;
return this.#ngZone.runOutsideAngular(() => this.#resizeFromFile(file, options));
}
}
Usage
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, inject } from '@angular/core';
import { FilePickerService } from '../../services/file-picker.service';
import { ImageService } from '../../services/image.service';
@Component({
selector: 'app-test',
standalone: true,
imports: [CommonModule],
template: `
<div class="grow flex flex-col">
<button (click)="previewImage()">Preview</button>
<img *ngIf="preview" [src]="preview" />
</div>
`,
})
export class TestComponent implements OnDestroy {
readonly #cdRef = inject(ChangeDetectorRef);
readonly #imageService = inject(ImageService);
readonly #filePickerService = inject(FilePickerService);
public preview: string;
async previewImage() {
const [file] = await this.#filePickerService.pick({
accept: 'image/jpeg,image/png',
});
if (!file) return; // user canceled
const imageBlob = await this.#imageService.resizeForUpload(file);
/*
// or with custom resize
const imageBlob = await this.#imageService.resizeForUpload(file, {resize: [200, 200]});
// or with custom resize and export type
const imageBlob = await this.#imageService.resizeForUpload(file, {resize: [200, 200], exportTo: 'image/png'});
*/
if (this.preview?.startsWith('blob:')) URL.revokeObjectURL(this.preview);
this.preview = URL.createObjectURL(imageBlob);
this.#cdRef.detectChanges();
}
ngOnDestroy(): void {
if (this.preview?.startsWith('blob:')) URL.revokeObjectURL(this.preview);
}
}
Explanation
ImageService
is a service that can be used to resize images.FilePickerService
is a service that can be used to pick files from the user.previewImage
is a method that is called when the user clicks on the preview button.this.#filePickerService.pick
is used to pick a file from the user.this.#imageService.resizeForUpload
is used to resize the image.this.#cdRef.detectChanges()
is used to detect changes in the component.