Skip to content

Commit de3195e

Browse files
committed
Huge performance optimization, fixed mirror, rotate and sharpen
1 parent be9d22f commit de3195e

15 files changed

+163
-167
lines changed

README.md

-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ Available processing functions:
99
- rotate
1010
- mirror
1111
- noop
12-
- output
1312

1413
_For best results I recommend to always use `sharpen` after `resize` - that's what Photoshop does, too ;)_
1514

@@ -44,7 +43,6 @@ getBlobForFile(file).then(base64 => {
4443
.pipe(
4544
resize({maxWidth: 800, maxHeight: 800}),
4645
sharpen(),
47-
output(),
4846
)
4947
.then(processedBase64 => {
5048
// Do whatever with your happy result :)

demo/src/app/app.component.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ <h3>Rotate & mirror</h3>
7373
<div class="form-group">
7474
<div class="form-row">
7575
<div class="col-6">
76-
<div class="btn btn-secondary btn-sm btn-block" (click)="rotateImgResult = srcBase64">
76+
<div class="btn btn-secondary btn-sm btn-block" (click)="rotateImgResult = srcBase64; rotateProcessingTime = null;">
7777
Use source image
7878
</div>
7979
</div>
8080
<div class="col-6">
81-
<button class="btn btn-secondary btn-sm btn-block" (click)="rotateImgResult = resizeImgResult" [disabled]="!resizeImgResult">
81+
<button class="btn btn-secondary btn-sm btn-block" (click)="rotateImgResult = resizeImgResult; rotateProcessingTime = null;" [disabled]="!resizeImgResult">
8282
Use resized image
8383
</button>
8484
</div>

demo/src/app/app.component.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
sharpen,
66
rotate,
77
mirror,
8-
output,
98
imageProcessor,
109
applyExifOrientation,
1110
noop,
@@ -82,7 +81,6 @@ export class AppComponent implements OnInit {
8281
sharpen({
8382
sharpness: +sharpness,
8483
}),
85-
output(),
8684
).then(resultBase64 => {
8785
const t1 = performance.now();
8886
this.resizeProcessingTime = Math.round((t1 - t0) * 100) / 100;
@@ -100,12 +98,11 @@ export class AppComponent implements OnInit {
10098
.src(this.rotateImgResult)
10199
.pipe(
102100
rotate(),
103-
output(),
104101
)
105102
.then(base64 => {
106-
const t1 = performance.now();
103+
const t1 = performance.now();
107104
this.rotateProcessingTime = Math.round((t1 - t0) * 100) / 100;
108-
this.rotateImgResult = base64;
105+
this.rotateImgResult = base64;
109106
})
110107
;
111108
}
@@ -117,12 +114,11 @@ export class AppComponent implements OnInit {
117114
.src(this.rotateImgResult)
118115
.pipe(
119116
mirror(),
120-
output(),
121117
)
122118
.then(base64 => {
123-
const t1 = performance.now();
119+
const t1 = performance.now();
124120
this.rotateProcessingTime = Math.round((t1 - t0) * 100) / 100;
125-
this.rotateImgResult = base64;
121+
this.rotateImgResult = base64;
126122
})
127123
;
128124
}

src/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
// Base
22
export * from './lib/utils';
3-
export * from './lib/canvas.service';
3+
export * from './lib/canvasService';
44
export * from './lib/image-processor.service';
55
export * from './lib/models';
66

77
// Operators
88
export * from './lib/operators/applyExifOrientation';
99
export * from './lib/operators/mirror';
1010
export * from './lib/operators/noop';
11-
export * from './lib/operators/output';
1211
export * from './lib/operators/resize';
1312
export * from './lib/operators/rotate';
1413
export * from './lib/operators/sharpen';

src/lib/canvas.service.ts src/lib/canvasService.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,8 @@ import { base64ToImgElement } from './utils';
33
class CanvasServiceSrc {
44
readonly canvas = document.createElement('canvas');
55
readonly canvasCtx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
6-
private helperCanvas = document.createElement('canvas');
7-
private helperCanvasCtx = this.helperCanvas.getContext('2d') as CanvasRenderingContext2D;
8-
private defaultOptions = {
9-
jpgQuality: 0.9,
10-
type: 'image/jpeg',
11-
};
6+
readonly helperCanvas = document.createElement('canvas');
7+
readonly helperCanvasCtx = this.helperCanvas.getContext('2d') as CanvasRenderingContext2D;
128

139
constructor() {
1410
}
@@ -48,9 +44,18 @@ class CanvasServiceSrc {
4844
this.canvasCtx.drawImage(this.helperCanvas, 0, 0);
4945
}
5046

47+
resize(width: number, height: number) {
48+
this.helperCanvas.width = width;
49+
this.helperCanvas.height = height;
50+
this.helperCanvasCtx.drawImage(this.canvas, 0, 0, width, height);
51+
this.canvas.width = width;
52+
this.canvas.height = height;
53+
this.canvasCtx.drawImage(this.helperCanvas, 0, 0);
54+
}
55+
5156
getDataUrl(type: string, jpgQuality: number): string {
5257
return this.canvas.toDataURL(type, jpgQuality);
5358
}
5459
}
5560

56-
export const CanvasService = new CanvasServiceSrc();
61+
export const canvasService = new CanvasServiceSrc();

src/lib/image-process.model.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { OperatorFunction } from './models';
1+
import { canvasService } from './canvasService';
2+
import { OperatorFunction, SrcOptions } from './models';
23

34
export class ImageProcess {
4-
constructor(private base64: string) {
5+
constructor(private base64: string, private options: SrcOptions) {
56
}
67

78
/**
@@ -28,9 +29,16 @@ export class ImageProcess {
2829
return Promise.resolve(this.base64);
2930
}
3031

31-
return operations.reduce((promiseChain, currentOperation) => {
32-
return promiseChain.then(base64 =>
33-
currentOperation(base64).then(currentBase64 => currentBase64));
34-
}, Promise.resolve(this.base64));
32+
return new Promise(resolve => {
33+
canvasService.drawBase64(this.base64).then(() => {
34+
operations.reduce((promiseChain, currentOperation) => {
35+
return promiseChain.then(() =>
36+
currentOperation());
37+
}, Promise.resolve())
38+
.then(() => {
39+
resolve(canvasService.getDataUrl(this.options.type, this.options.jpgQuality));
40+
});
41+
});
42+
});
3543
}
3644
}

src/lib/image-processor.service.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import { SrcOptions } from './models';
1010
*/
1111
class ImageProcessorService {
1212
src(base64: string, options: SrcOptions = {}): ImageProcess {
13-
return new ImageProcess(base64);
13+
// Set default values
14+
if (!options.type) {
15+
options.type = 'image/jpeg';
16+
}
17+
if (!options.jpgQuality) {
18+
options.jpgQuality = 0.9;
19+
}
20+
21+
return new ImageProcess(base64, options);
1422
}
1523
}
1624

src/lib/models.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export interface SrcOptions {
2+
jpgQuality?: number;
3+
type?: string;
4+
}
5+
16
export interface ResizeOptions {
27
maxWidth: number;
38
maxHeight: number;
@@ -7,21 +12,17 @@ export interface SharpenOptions {
712
sharpness?: number;
813
}
914

10-
export interface OutputOptions {
11-
jpgQuality?: number;
12-
type?: string;
13-
}
14-
1515
export interface Base64ImageData {
1616
width: number;
1717
height: number;
1818
imgElement: HTMLImageElement;
1919
}
2020

2121
export interface OperatorFunction {
22-
(base64: string): Promise<string>;
22+
(): Promise<void>;
2323
}
2424

25-
export interface SrcOptions {
26-
applyExifOrientation?: boolean;
25+
export interface RotateOptions {
26+
degrees?: 90 | 180 | 270;
27+
clockwise?: boolean;
2728
}
+3-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { OperatorFunction } from '../models';
22

33
export function applyExifOrientation(): OperatorFunction {
4-
return (base64: string) => {
5-
return new Promise<string>(resolve => {
6-
// TODO
7-
resolve(base64);
4+
return () => {
5+
return new Promise(resolve => {
6+
resolve();
87
});
98
};
109
}

src/lib/operators/mirror.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
import { CanvasService } from '../canvas.service';
1+
import { canvasService } from '../canvasService';
22
import { OperatorFunction } from '../models';
3-
import { base64ToImgElement } from '../utils';
43

5-
// Need img/canvas source
6-
// Need width/height
74
export function mirror(): OperatorFunction {
8-
return (base64: string) => {
5+
return () => {
96
return new Promise(resolve => {
10-
base64ToImgElement(base64).then(image => {
11-
CanvasService.setSize(image.width, image.height);
12-
CanvasService.canvasCtx.scale(-1, 1);
13-
CanvasService.canvasCtx.drawImage(image.imgElement, -image.width, 0);
14-
resolve(CanvasService.canvas.toDataURL());
15-
});
7+
canvasService.canvasCtx.scale(-1, 1);
8+
canvasService.canvasCtx.drawImage(canvasService.canvas, -canvasService.canvas.width, 0);
9+
resolve();
1610
});
1711
};
1812
}

src/lib/operators/noop.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OperatorFunction } from '../models';
22

33
export function noop(): OperatorFunction {
4-
return (base64: string) => {
5-
return Promise.resolve(base64);
4+
return () => {
5+
return Promise.resolve();
66
};
77
}

src/lib/operators/output.ts

-21
This file was deleted.

src/lib/operators/resize.ts

+43-48
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { CanvasService } from '../canvas.service';
1+
import { canvasService } from '../canvasService';
22
import { OperatorFunction, ResizeOptions } from '../models';
3-
import { base64ToImgElement } from '../utils';
43

5-
// Need img/canvas source
6-
// Need width/height
74
/**
85
* @see https://hacks.mozilla.org/2011/01/how-to-develop-a-html5-image-uploader/
96
* @see http://stackoverflow.com/a/19262385/5688490
@@ -14,60 +11,58 @@ import { base64ToImgElement } from '../utils';
1411
* TODO: Accept File, too
1512
*/
1613
export function resize(options: ResizeOptions): OperatorFunction {
17-
return (base64: string) => {
14+
return () => {
1815
return new Promise(resolve => {
19-
base64ToImgElement(base64).then(img => {
20-
const resizeNeeded = img.width > options.maxWidth || img.height > options.maxHeight;
16+
const oldWidth = canvasService.canvas.width;
17+
const oldHeight = canvasService.canvas.height;
2118

22-
if (!resizeNeeded) {
23-
console.log(`No resizing needed, image of ${img.width}x${img.height}px is smaller than ${options.maxWidth}x${options.maxHeight}px`);
24-
CanvasService.setSize(img.width, img.height);
25-
CanvasService.drawImage(img.imgElement, img.width, img.height);
26-
} else {
27-
let newWidth: number;
28-
let newHeight: number;
29-
let downscalingSteps: number;
19+
const resizeNeeded = oldWidth > options.maxWidth || oldHeight > options.maxHeight;
3020

31-
// TODO: Is this really working for all formats? What if maxWidth = 2000, maxHeight = 20 ?
32-
if (img.width > img.height) {
33-
newWidth = options.maxWidth;
34-
newHeight = Math.round(img.height * newWidth / img.width);
35-
downscalingSteps = Math.ceil(Math.log(img.width / newWidth) / Math.log(2));
36-
} else {
37-
newHeight = options.maxHeight;
38-
newWidth = Math.round(img.width * newHeight / img.height);
39-
downscalingSteps = Math.ceil(Math.log(img.height / newHeight) / Math.log(2));
40-
}
21+
if (!resizeNeeded) {
22+
console.log(`No resizing needed, image of ${oldWidth}x${oldHeight}px is smaller than ${options.maxWidth}x${options.maxHeight}px`);
23+
resolve();
24+
} else {
25+
let newWidth: number;
26+
let newHeight: number;
27+
let downscalingSteps: number;
4128

42-
console.log(`Resizing from ${img.width}x${img.height}px to ${newWidth}x${newHeight} in ${downscalingSteps} steps`);
29+
// TODO: Is this really working for all formats? What if maxWidth = 2000, maxHeight = 20 ?
30+
if (oldWidth > oldHeight) {
31+
newWidth = options.maxWidth;
32+
newHeight = Math.round(oldHeight * newWidth / oldWidth);
33+
downscalingSteps = Math.ceil(Math.log(oldWidth / newWidth) / Math.log(2));
34+
} else {
35+
newHeight = options.maxHeight;
36+
newWidth = Math.round(oldWidth * newHeight / oldHeight);
37+
downscalingSteps = Math.ceil(Math.log(oldHeight / newHeight) / Math.log(2));
38+
}
4339

44-
if (downscalingSteps > 1) {
45-
// To get the best result, we need to resize the image by 50% again and again until we reach the final dimensions
46-
// Step 1
47-
let oldScale = 1;
48-
let currentStepScale = .5;
49-
CanvasService.setSize(img.width * currentStepScale, img.height * currentStepScale);
50-
CanvasService.drawImage(img.imgElement, img.width * currentStepScale, img.height * currentStepScale);
40+
console.log(`Resizing from ${oldWidth}x${oldHeight}px to ${newWidth}x${newHeight} in ${downscalingSteps} steps`);
5141

52-
// Step i
53-
for (let i = 2; i < downscalingSteps; i++) {
54-
oldScale = currentStepScale;
55-
currentStepScale = currentStepScale * .5;
56-
CanvasService.drawImage(CanvasService.canvas, img.width * currentStepScale, img.height * currentStepScale, img.width * oldScale, img.height * oldScale);
57-
}
42+
if (downscalingSteps === 1) {
43+
// The image can directly be resized to the final dimensions
44+
canvasService.resize(newWidth, newHeight);
45+
} else {
46+
// To get the best result, we need to resize the image by 50% again and again until we reach the final dimensions
47+
// Step 1
48+
let oldScale = 1;
49+
let currentStepScale = 1;
50+
// canvasService.resize(oldWidth * currentStepScale, oldHeight * currentStepScale);
5851

59-
// Down-scaling step i+1 (draw final result)
60-
CanvasService.drawImage(CanvasService.canvas, newWidth, newHeight, img.width * currentStepScale, img.height * currentStepScale);
61-
CanvasService.crop(newWidth, newHeight);
62-
} else {
63-
// The image can directly be resized to the final dimensions
64-
CanvasService.setSize(newWidth, newHeight);
65-
CanvasService.drawImage(img.imgElement, newWidth, newHeight);
52+
// Step i
53+
for (let i = 1; i < downscalingSteps; i++) {
54+
oldScale = currentStepScale;
55+
currentStepScale = currentStepScale * .5;
56+
canvasService.drawImage(canvasService.canvas, oldWidth * currentStepScale, oldHeight * currentStepScale, oldWidth * oldScale, oldHeight * oldScale);
6657
}
58+
59+
// Down-scaling step i+1 (draw final result)
60+
canvasService.drawImage(canvasService.canvas, newWidth, newHeight, oldWidth * currentStepScale, oldHeight * currentStepScale);
61+
canvasService.crop(newWidth, newHeight);
6762
}
63+
}
6864

69-
resolve(CanvasService.canvas.toDataURL());
70-
});
65+
resolve();
7166
});
7267
};
7368
}

0 commit comments

Comments
 (0)