@@ -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