Skip to content

Commit 4ccefcb

Browse files
paodbjavier-godoy
authored andcommitted
fix: size the cropped image at its natural resolution
The cropped output was derived from the displayed image and scaled by devicePixelRatio, so crops were too small when the image was scaled down to fit and too large on high-density displays. Close #26 Close #21
1 parent fc6a0af commit 4ccefcb

1 file changed

Lines changed: 36 additions & 10 deletions

File tree

src/main/resources/META-INF/resources/frontend/src/image-crop.tsx

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -161,34 +161,60 @@ class ImageCropElement extends ReactAdapterElement {
161161
);
162162
}
163163

164+
/**
165+
* Draws the selected crop region onto an off-screen canvas and dispatches the
166+
* resulting data URI through a {@code cropped-image} event.
167+
*
168+
* <p>The crop rectangle reported by react-image-crop is expressed in the
169+
* image's <em>displayed</em> (rendered) pixels, which can be smaller or larger
170+
* than the image's intrinsic resolution when the browser scales it to fit the
171+
* layout. The selected region is mapped back to the source's <em>natural</em>
172+
* pixels using {@code scaleX}/{@code scaleY} for both the source rectangle and
173+
* the output canvas, so the cropped image keeps the original resolution of the
174+
* selected area rather than the (smaller or larger) on-screen size (see issue
175+
* #26).</p>
176+
*
177+
* <p>Note: a {@code px} crop is measured in rendered pixels, so the exported
178+
* size is the rendered crop scaled to natural resolution, not necessarily the
179+
* configured pixel value. The output is not multiplied by
180+
* {@code window.devicePixelRatio}, so the original pixels are used verbatim
181+
* instead of being upsampled on high-density displays (see issue #21).</p>
182+
*/
164183
public _updateCroppedImage(crop: PixelCrop|PercentCrop) {
165184
const image = this.querySelector("img");
166185
if (crop && image) {
167186

168187
crop = convertToPixelCrop(crop, image.width, image.height);
169-
188+
170189
// create a canvas element to draw the cropped image
171190
const canvas = document.createElement("canvas");
172191

173192
// draw the image on the canvas
174193
const ccrop = crop;
194+
195+
// Ratio between the image's natural resolution and its displayed size.
196+
// Greater than 1 when the image is scaled down to fit the screen.
175197
const scaleX = image.naturalWidth / image.width;
176198
const scaleY = image.naturalHeight / image.height;
177199
const ctx = canvas.getContext("2d");
178-
const pixelRatio = window.devicePixelRatio;
179-
canvas.width = ccrop.width * pixelRatio;
180-
canvas.height = ccrop.height * pixelRatio;
200+
201+
// Size the output in the crop region's natural pixels so the cropped
202+
// image keeps the source's resolution rather than the on-screen size.
203+
const outWidth = Math.round(ccrop.width * scaleX);
204+
const outHeight = Math.round(ccrop.height * scaleY);
205+
206+
// Setting canvas dimensions resets the 2D context, so it must happen
207+
// before any drawing/clipping state is configured below.
208+
canvas.width = outWidth;
209+
canvas.height = outHeight;
181210

182211
if (ctx) {
183-
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
184212
ctx.imageSmoothingQuality = "high";
185213
ctx.save();
186214

187215
if (this.circularCrop) {
188-
canvas.width = ccrop.width;
189-
canvas.height = ccrop.height;
190216
ctx.beginPath();
191-
ctx.arc(ccrop.width / 2, ccrop.height / 2, ccrop.height / 2, 0, Math.PI * 2, true);
217+
ctx.arc(outWidth / 2, outHeight / 2, outHeight / 2, 0, Math.PI * 2, true);
192218
ctx.closePath();
193219
ctx.clip();
194220
}
@@ -201,8 +227,8 @@ class ImageCropElement extends ReactAdapterElement {
201227
ccrop.height * scaleY,
202228
0,
203229
0,
204-
ccrop.width,
205-
ccrop.height
230+
outWidth,
231+
outHeight
206232
);
207233

208234
ctx.restore();

0 commit comments

Comments
 (0)