Skip to content

Commit 97c657e

Browse files
authored
use matrix rotate, if rotate angle is multiple of 90 degrees (#1209)
1 parent 1ff8a39 commit 97c657e

File tree

2 files changed

+167
-62
lines changed

2 files changed

+167
-62
lines changed

packages/plugin-rotate/src/index.js

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,94 @@
1-
import { throwError, isNodePattern } from "@jimp/utils";
1+
import { isNodePattern, throwError } from "@jimp/utils";
2+
3+
/**
4+
* Rotates an image counter-clockwise by multiple of 90 degrees. NB: 'this' must be a Jimp object.
5+
*
6+
* This function is based on matrix rotation. Check this to get an initial idea how it works: https://stackoverflow.com/a/8664879/10561909
7+
*
8+
* @param {number} deg the number of degrees to rotate the image by, it should be a multiple of 90
9+
*/
10+
function matrixRotate(deg) {
11+
if (Math.abs(deg) % 90 !== 0) {
12+
throw new Error("Unsupported matrix rotation degree");
13+
}
14+
15+
deg %= 360;
16+
if (Math.abs(deg) === 0) {
17+
// no rotation for 0, 360, -360, 720, -720, ...
18+
return;
19+
}
20+
21+
const w = this.bitmap.width;
22+
const h = this.bitmap.height;
23+
24+
// decide which rotation angle to use
25+
let angle;
26+
switch (deg) {
27+
// 90 degree & -270 degree are same
28+
case 90:
29+
case -270:
30+
angle = 90;
31+
break;
32+
33+
case 180:
34+
case -180:
35+
angle = 180;
36+
break;
37+
38+
case 270:
39+
case -90:
40+
angle = -90;
41+
break;
42+
43+
default:
44+
throw new Error("Unsupported matrix rotation degree");
45+
}
46+
// After this switch block, angle will be 90, 180 or -90
47+
48+
// calculate the new width and height
49+
const nW = angle === 180 ? w : h;
50+
const nH = angle === 180 ? h : w;
51+
52+
const dstBuffer = Buffer.alloc(this.bitmap.data.length);
53+
54+
// function to translate the x, y coordinate to the index of the pixel in the buffer
55+
function createIdxTranslationFunction(w, h) {
56+
return function (x, y) {
57+
return (y * w + x) << 2;
58+
};
59+
}
60+
61+
const srcIdxFunction = createIdxTranslationFunction(w, h);
62+
const dstIdxFunction = createIdxTranslationFunction(nW, nH);
63+
64+
for (let x = 0; x < w; x++) {
65+
for (let y = 0; y < h; y++) {
66+
const srcIdx = srcIdxFunction(x, y);
67+
const pixelRGBA = this.bitmap.data.readUInt32BE(srcIdx);
68+
69+
let dstIdx;
70+
switch (angle) {
71+
case 90:
72+
dstIdx = dstIdxFunction(y, w - x - 1);
73+
break;
74+
case -90:
75+
dstIdx = dstIdxFunction(h - y - 1, x);
76+
break;
77+
case 180:
78+
dstIdx = dstIdxFunction(w - x - 1, h - y - 1);
79+
break;
80+
default:
81+
throw new Error("Unsupported matrix rotation angle");
82+
}
83+
84+
dstBuffer.writeUInt32BE(pixelRGBA, dstIdx);
85+
}
86+
}
87+
88+
this.bitmap.data = dstBuffer;
89+
this.bitmap.width = nW;
90+
this.bitmap.height = nH;
91+
}
292

393
/**
494
* Rotates an image counter-clockwise by an arbitrary number of degrees. NB: 'this' must be a Jimp object.
@@ -141,7 +231,12 @@ export default () => ({
141231
return throwError.call(this, "mode must be a boolean or a string", cb);
142232
}
143233

144-
advancedRotate.call(this, deg, mode, cb);
234+
if (Math.abs(deg % 90) === 0) {
235+
// apply matrixRotate if the angle is a multiple of 90 degrees (eg: 180 or -90)
236+
matrixRotate.call(this, deg);
237+
} else {
238+
advancedRotate.call(this, deg, mode, cb);
239+
}
145240

146241
if (isNodePattern(cb)) {
147242
cb.call(this, null, this);

packages/plugin-rotate/test/rotation.test.js

Lines changed: 70 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,30 @@ describe("Rotate a image with even size", () => {
135135
expectToBeJGD(
136136
imgSrc.clone().rotate(90, true).getJGDSync(),
137137
mkJGD(
138-
" ",
139-
"▰▪▪▪▴▴▴▦ ",
140-
"▪▪▪▪▴▴▴▴ ",
141-
"▪▪▪▪▴▴▴▴ ",
142-
"▪▪▪▪▴▴▴▴ ",
143-
"▴▴▴▴▪▪▪▪ ",
144-
"▴▴▴▴▪▪▪▪ ",
145-
"▴▴▴▴▪▪▪▪ ",
146-
"▰▴▴▴▪▪▪▦ ",
147-
" "
138+
"▰▪▪▪▴▴▴▦",
139+
"▪▪▪▪▴▴▴▴",
140+
"▪▪▪▪▴▴▴▴",
141+
"▪▪▪▪▴▴▴▴",
142+
"▴▴▴▴▪▪▪▪",
143+
"▴▴▴▴▪▪▪▪",
144+
"▴▴▴▴▪▪▪▪",
145+
"▰▴▴▴▪▪▪▦"
146+
)
147+
);
148+
});
149+
150+
it("-90 degrees", () => {
151+
expectToBeJGD(
152+
imgSrc.clone().rotate(-90, true).getJGDSync(),
153+
mkJGD(
154+
"▦▪▪▪▴▴▴▰",
155+
"▪▪▪▪▴▴▴▴",
156+
"▪▪▪▪▴▴▴▴",
157+
"▪▪▪▪▴▴▴▴",
158+
"▴▴▴▴▪▪▪▪",
159+
"▴▴▴▴▪▪▪▪",
160+
"▴▴▴▴▪▪▪▪",
161+
"▦▴▴▴▪▪▪▰"
148162
)
149163
);
150164
});
@@ -195,16 +209,14 @@ describe("Rotate a image with even size", () => {
195209
expectToBeJGD(
196210
imgSrc.clone().rotate(180, true).getJGDSync(),
197211
mkJGD(
198-
" ",
199-
" ▦▴▴▴▪▪▪▦ ",
200-
" ▴▴▴▴▪▪▪▪ ",
201-
" ▴▴▴▴▪▪▪▪ ",
202-
" ▴▴▴▴▪▪▪▪ ",
203-
" ▪▪▪▪▴▴▴▴ ",
204-
" ▪▪▪▪▴▴▴▴ ",
205-
" ▪▪▪▪▴▴▴▴ ",
206-
" ▰▪▪▪▴▴▴▰ ",
207-
" "
212+
"▦▴▴▴▪▪▪▦",
213+
"▴▴▴▴▪▪▪▪",
214+
"▴▴▴▴▪▪▪▪",
215+
"▴▴▴▴▪▪▪▪",
216+
"▪▪▪▪▴▴▴▴",
217+
"▪▪▪▪▴▴▴▴",
218+
"▪▪▪▪▴▴▴▴",
219+
"▰▪▪▪▴▴▴▰"
208220
)
209221
);
210222
});
@@ -235,16 +247,14 @@ describe("Rotate a image with even size", () => {
235247
expectToBeJGD(
236248
imgSrc.clone().rotate(270, true).getJGDSync(),
237249
mkJGD(
238-
" ▦▪▪▪▴▴▴▰ ",
239-
" ▪▪▪▪▴▴▴▴ ",
240-
" ▪▪▪▪▴▴▴▴ ",
241-
" ▪▪▪▪▴▴▴▴ ",
242-
" ▴▴▴▴▪▪▪▪ ",
243-
" ▴▴▴▴▪▪▪▪ ",
244-
" ▴▴▴▴▪▪▪▪ ",
245-
" ▦▴▴▴▪▪▪▰ ",
246-
" ",
247-
" "
250+
"▦▪▪▪▴▴▴▰",
251+
"▪▪▪▪▴▴▴▴",
252+
"▪▪▪▪▴▴▴▴",
253+
"▪▪▪▪▴▴▴▴",
254+
"▴▴▴▴▪▪▪▪",
255+
"▴▴▴▴▪▪▪▪",
256+
"▴▴▴▴▪▪▪▪",
257+
"▦▴▴▴▪▪▪▰"
248258
)
249259
);
250260
});
@@ -275,16 +285,14 @@ describe("Rotate a image with even size", () => {
275285
expectToBeJGD(
276286
imgSrc.clone().rotate(360, true).getJGDSync(),
277287
mkJGD(
278-
"▰▴▴▴▪▪▪▰ ",
279-
"▴▴▴▴▪▪▪▪ ",
280-
"▴▴▴▴▪▪▪▪ ",
281-
"▴▴▴▴▪▪▪▪ ",
282-
"▪▪▪▪▴▴▴▴ ",
283-
"▪▪▪▪▴▴▴▴ ",
284-
"▪▪▪▪▴▴▴▴ ",
285-
"▦▪▪▪▴▴▴▦ ",
286-
" ",
287-
" "
288+
"▰▴▴▴▪▪▪▰",
289+
"▴▴▴▴▪▪▪▪",
290+
"▴▴▴▴▪▪▪▪",
291+
"▴▴▴▴▪▪▪▪",
292+
"▪▪▪▪▴▴▴▴",
293+
"▪▪▪▪▴▴▴▴",
294+
"▪▪▪▪▴▴▴▴",
295+
"▦▪▪▪▴▴▴▦"
288296
)
289297
);
290298
});
@@ -474,18 +482,14 @@ describe("Rotate a non-square image", () => {
474482
it("90 degrees", () => {
475483
expectToBeJGD(
476484
imgSrc.clone().rotate(90, true).getJGDSync(),
477-
mkJGD(
478-
" ",
479-
"▪▪▴▴ ",
480-
"▪▪▴▴ ",
481-
"▪▪▴▴ ",
482-
"▪▪▴▴ ",
483-
"▴▴▦▦ ",
484-
"▴▴▦▦ ",
485-
"▴▴▦▦ ",
486-
"▴▴▦▦ ",
487-
" "
488-
)
485+
mkJGD("▪▪▴▴", "▪▪▴▴", "▪▪▴▴", "▪▪▴▴", "▴▴▦▦", "▴▴▦▦", "▴▴▦▦", "▴▴▦▦")
486+
);
487+
});
488+
489+
it("-90 degrees", () => {
490+
expectToBeJGD(
491+
imgSrc.clone().rotate(-90, true).getJGDSync(),
492+
mkJGD("▦▦▴▴", "▦▦▴▴", "▦▦▴▴", "▦▦▴▴", "▴▴▪▪", "▴▴▪▪", "▴▴▪▪", "▴▴▪▪")
489493
);
490494
});
491495

@@ -510,14 +514,7 @@ describe("Rotate a non-square image", () => {
510514
it("180 degrees", () => {
511515
expectToBeJGD(
512516
imgSrc.clone().rotate(180, true).getJGDSync(),
513-
mkJGD(
514-
" ",
515-
" ▴▴▴▴▦▦▦▦ ",
516-
" ▴▴▴▴▦▦▦▦ ",
517-
" ▪▪▪▪▴▴▴▴ ",
518-
" ▪▪▪▪▴▴▴▴ ",
519-
" "
520-
)
517+
mkJGD("▴▴▴▴▦▦▦▦", "▴▴▴▴▦▦▦▦", "▪▪▪▪▴▴▴▴", "▪▪▪▪▴▴▴▴")
521518
);
522519
});
523520

@@ -556,4 +553,17 @@ describe("Rotate a non-square image", () => {
556553
)
557554
);
558555
});
556+
it("-180 degrees", () => {
557+
expectToBeJGD(
558+
imgSrc.clone().rotate(-180, true).getJGDSync(),
559+
mkJGD("▴▴▴▴▦▦▦▦", "▴▴▴▴▦▦▦▦", "▪▪▪▪▴▴▴▴", "▪▪▪▪▴▴▴▴")
560+
);
561+
});
562+
563+
it("-270 degrees", () => {
564+
expectToBeJGD(
565+
imgSrc.clone().rotate(-270, true).getJGDSync(),
566+
mkJGD("▪▪▴▴", "▪▪▴▴", "▪▪▴▴", "▪▪▴▴", "▴▴▦▦", "▴▴▦▦", "▴▴▦▦", "▴▴▦▦")
567+
);
568+
});
559569
});

0 commit comments

Comments
 (0)