From 56cea434dffb3c60ff8748d1ece3a1467ccda4ce Mon Sep 17 00:00:00 2001 From: Sayed Kaif Date: Mon, 19 Oct 2026 04:41:50 +0530 Subject: [PATCH] normalize rotate and tile angle modulo 360 to avoid 32-bit truncation --- lib/operation.mjs | 2 +- lib/output.mjs | 2 +- test/unit/rotate.js | 11 +++++++++++ test/unit/tile.js | 19 +++++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/operation.mjs b/lib/operation.mjs index d10aa8d00..4dc07fbc8 100644 --- a/lib/operation.mjs +++ b/lib/operation.mjs @@ -62,7 +62,7 @@ function rotate (angle, options) { this.options.rotationAngle = 0; } if (is.integer(angle) && !(angle % 90)) { - this.options.angle = angle; + this.options.angle = angle % 360; } else if (is.number(angle)) { this.options.rotationAngle = angle; if (is.object(options) && options.background) { diff --git a/lib/output.mjs b/lib/output.mjs index a5ec90cdb..739a54261 100644 --- a/lib/output.mjs +++ b/lib/output.mjs @@ -1518,7 +1518,7 @@ function tile (options) { // Angle of rotation, if (is.defined(options.angle)) { if (is.integer(options.angle) && !(options.angle % 90)) { - this.options.tileAngle = options.angle; + this.options.tileAngle = options.angle % 360; } else { throw is.invalidParameterError('angle', 'positive/negative multiple of 90', options.angle); } diff --git a/test/unit/rotate.js b/test/unit/rotate.js index efb4c2deb..3d18cf83a 100644 --- a/test/unit/rotate.js +++ b/test/unit/rotate.js @@ -183,6 +183,17 @@ suite('Rotation', () => { }); }); + // A 90-multiple angle larger than the 32-bit integer range is normalised + // modulo 360 rather than silently truncated when read natively + [90 * 47721859, 90 * 47721861, -90 * 47721859].forEach((angle) => { + test(`Rotate by a 90-multiple angle beyond 32-bit range (${angle}deg)`, async (t) => { + t.plan(2); + const { info } = await bufferWithInfo(sharp(fixtures.inputJpg320x240).rotate(angle)); + t.assert.strictEqual(240, info.width); + t.assert.strictEqual(320, info.height); + }); + }); + [-3750, -510, -150, 30, 390, 3630].forEach((angle) => { test(`Rotate by any 30-multiple angle (${angle}deg)`, async (t) => { t.plan(2); diff --git a/test/unit/tile.js b/test/unit/tile.js index 3e60bfda6..c7b744559 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -411,6 +411,25 @@ suite('Tile', () => { t.assert.strictEqual(true, metadata.height === 170); }); + test('Deep Zoom layout with angle beyond 32-bit integer range', async (t) => { + const directory = fixtures.path('output.512_largeangle.dzi_files'); + await fs.rm(directory, { recursive: true, force: true }); + // 2147483970 is a multiple of 90 equal to 90 modulo 360 but larger than + // the signed 32-bit range, so it must rotate as 90 rather than truncate + const info = await sharp(fixtures.inputJpg) + .tile({ + size: 512, + angle: 2147483970 + }) + .toFile(fixtures.path('output.512_largeangle.dzi')); + t.plan(3); + t.assert.strictEqual('dz', info.format); + const tile = path.join(directory, '10', '0_1.jpeg'); + const metadata = await sharp(tile).metadata(); + t.assert.strictEqual(true, metadata.width === 512); + t.assert.strictEqual(true, metadata.height === 170); + }); + test('Deep Zoom layout with depth of one', async (t) => { const directory = fixtures.path('output.512_depth_one.dzi_files'); await fs.rm(directory, { recursive: true, force: true });