From e4942e53efee865c1f06b25237e1ee4517542622 Mon Sep 17 00:00:00 2001 From: Prakash Meena Date: Sat, 12 Sep 2026 02:30:30 +0530 Subject: [PATCH] fix(image): fix pixelDensity idempotence and high-DPI canvas resize (#9152) - Derive logical dimensions from physical canvas dimensions and pixel density in pixelDensity() so repeated calls are idempotent and resetting to 1 restores original size - Scale backing canvas by pixel density in resize() to preserve high-DPI resolution - Fix Color reference in Image.prototype.set() - Add unit tests for pixelDensity and high-DPI resize --- src/image/p5.Image.js | 47 +++++++++++++++----------- test/unit/image/p5.Image.js | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 4823ad8bf4..1b92f15ef2 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -8,6 +8,7 @@ * drawing images to the main display canvas. */ import Filters from './filters'; +import { Color } from '../color/p5.Color'; import { Renderer } from '../core/p5.Renderer'; import { downloadFile, _checkFileExtension } from '../io/utilities'; @@ -56,9 +57,9 @@ class Image { this._pixelDensity = density; - // Adjust canvas dimensions based on pixel density - this.width /= density; - this.height /= density; + // Adjust logical dimensions based on physical canvas dimensions and pixel density + this.width = this.canvas.width / density; + this.height = this.canvas.height / density; return this; // Return the image instance for chaining if needed } else { @@ -611,7 +612,7 @@ class Image { a = imgOrCol[3]; //this.updatePixels.call(this); } - } else if (imgOrCol instanceof p5.Color) { + } else if (imgOrCol instanceof Color) { if (idx < pixelsState.pixels.length) { [r, g, b, a] = imgOrCol._getRGBA([255, 255, 255, 255]); //this.updatePixels.call(this); @@ -721,20 +722,24 @@ class Image { // auto-resize if (width === 0 && height === 0) { - width = this.canvas.width; - height = this.canvas.height; + width = this.width; + height = this.height; } else if (width === 0) { - width = (this.canvas.width * height) / this.canvas.height; + width = (this.width * height) / this.height; } else if (height === 0) { - height = (this.canvas.height * width) / this.canvas.width; + height = (this.height * width) / this.width; } width = Math.floor(width); height = Math.floor(height); + const pd = this._pixelDensity; + const canvasWidth = Math.floor(width * pd); + const canvasHeight = Math.floor(height * pd); + const tempCanvas = document.createElement('canvas'); - tempCanvas.width = width; - tempCanvas.height = height; + tempCanvas.width = canvasWidth; + tempCanvas.height = canvasHeight; if (this.gifProperties) { const props = this.gifProperties; @@ -755,8 +760,8 @@ class Image { }; for (let i = 0; i < props.numFrames; i++) { const resizedImageData = this.drawingContext.createImageData( - width, - height + canvasWidth, + canvasHeight ); nearestNeighbor(props.frames[i].image, resizedImageData); props.frames[i].image = resizedImageData; @@ -773,25 +778,27 @@ class Image { this.canvas.height, 0, 0, - tempCanvas.width, - tempCanvas.height + canvasWidth, + canvasHeight ); // Resize the original canvas, which will clear its contents - this.canvas.width = this.width = width; - this.canvas.height = this.height = height; + this.width = width; + this.height = height; + this.canvas.width = canvasWidth; + this.canvas.height = canvasHeight; //Copy the image back this.drawingContext.drawImage( tempCanvas, 0, 0, - width, - height, + canvasWidth, + canvasHeight, 0, 0, - width, - height + canvasWidth, + canvasHeight ); if (this.pixels.length > 0) { diff --git a/test/unit/image/p5.Image.js b/test/unit/image/p5.Image.js index 9962a2dfe6..56ffc20099 100644 --- a/test/unit/image/p5.Image.js +++ b/test/unit/image/p5.Image.js @@ -41,6 +41,44 @@ suite('p5.Image', function () { }); }); + suite('p5.Image.prototype.pixelDensity', function () { + test('it sets and gets pixel density', function () { + const img = myp5.createImage(100, 100); + assert.strictEqual(img.pixelDensity(), 1); + img.pixelDensity(2); + assert.strictEqual(img.pixelDensity(), 2); + assert.strictEqual(img.width, 50); + assert.strictEqual(img.height, 50); + assert.strictEqual(img.canvas.width, 100); + assert.strictEqual(img.canvas.height, 100); + }); + + test('repeated calls are idempotent and can be restored', function () { + const img = myp5.createImage(100, 100); + img.pixelDensity(2); + assert.strictEqual(img.width, 50); + assert.strictEqual(img.height, 50); + + // Calling again should not divide dimensions further + img.pixelDensity(2); + assert.strictEqual(img.width, 50); + assert.strictEqual(img.height, 50); + + // Resetting to 1 restores original logical dimensions + img.pixelDensity(1); + assert.strictEqual(img.width, 100); + assert.strictEqual(img.height, 100); + }); + + test('setting non-positive density defaults to 1', function () { + const img = myp5.createImage(100, 100); + img.pixelDensity(0); + assert.strictEqual(img.pixelDensity(), 1); + assert.strictEqual(img.width, 100); + assert.strictEqual(img.height, 100); + }); + }); + suite('p5.Image.prototype.resize', function () { test('it should resize the image', function () { let img = myp5.createImage(10, 17); @@ -49,6 +87,35 @@ suite('p5.Image', function () { assert.strictEqual(img.width, 10); assert.strictEqual(img.height, 30); }); + + test('it should resize backing canvas with pixel density > 1', function () { + const img = myp5.createImage(100, 100); + img.pixelDensity(2); + assert.strictEqual(img.width, 50); + assert.strictEqual(img.height, 50); + + img.resize(40, 60); + assert.strictEqual(img.width, 40); + assert.strictEqual(img.height, 60); + assert.strictEqual(img.canvas.width, 80); + assert.strictEqual(img.canvas.height, 120); + }); + + test('it allows get() and set() across full logical dimensions after resize with high pixel density', function () { + const img = myp5.createImage(100, 100); + img.pixelDensity(2); + img.resize(50, 50); + + const red = myp5.color(255, 0, 0, 255); + img.set(30, 30, red); + img.updatePixels(); + + const pixel = img.get(30, 30); + assert.strictEqual(pixel[0], 255); + assert.strictEqual(pixel[1], 0); + assert.strictEqual(pixel[2], 0); + assert.strictEqual(pixel[3], 255); + }); }); suite.todo('p5.Image.prototype.mask', function () {