sekoyo / react-image-crop

A responsive image cropping tool for React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Preview Canvas resolution is huge.

bensquire opened this issue · comments

I'm not sure if this is the intended behaviour or not, but I've seen a few issue in the repo complaining about huge files when extracting the final image from the preview canvas.

I think the issue comes down to people expecting the final image to have the width/height of the canvas, but instead it's the same has a higher resolution than the input image.

You can see this in the demo app by looking at the inspector:

Screenshot 2023-10-06 at 16 53 13

While we have set a width/height of 355px x 200px the actual canvas is 5828px x 3276px. When I export that image and compare it to the original, it's filesize has ballooned.

Screenshot 2023-10-06 at 16 55 43

For our own project, I've worked round this by building out a new version of canvasPreview.tsx, but I've removed the stuff we don't need; this allows me to set the desired output size. This probably isn't fit 4 purpose, because it's so heavily tailored towards our needs, but maybe it can help rectify an ongoing issue?

interface CropObject {
	x: number;
	y: number;
	width: number;
	height: number;
}

export function canvasPreview(
	image: HTMLImageElement,
	canvas: HTMLCanvasElement,
	crop: CropObject,
	canvasWidth: number,
	canvasHeight: number
) {
	// Get the canvas context
	const ctx = canvas.getContext('2d');

	// If there's no context, throw an error
	if (!ctx) {
		throw new Error('No 2d context available');
	}

	// Given that the canvas is always 250x250
	canvas.width = canvasWidth;
	canvas.height = canvasHeight;

	// Calculate scaling factors based on natural dimensions of the image
	const scaleX = image.naturalWidth / image.width;
	const scaleY = image.naturalHeight / image.height;

	// Calculate the real crop dimensions
	const cropX = crop.x * scaleX;
	const cropY = crop.y * scaleY;
	const cropWidth = crop.width * scaleX;
	const cropHeight = crop.height * scaleY;

	// Clear the canvas (Useful if this function is called multiple times)
	ctx.clearRect(0, 0, canvas.width, canvas.height);

	// Perform the drawing operation
	ctx.drawImage(
		image,
		cropX, cropY, cropWidth, cropHeight, // Source coordinates
		0, 0, canvas.width, canvas.height // Destination coordinates
	);
}

The issue is that for correct quality on devices with a larger pixel ratio than 1 (window.devicePixelRatio) like retina screens, you have to have a larger canvas than is actually rendered.

I was lazy and didn't resize it down to the correct value on click of the Download button.

What I should do is if (window.devicePixelRatio !== 1) create an Offscreen Canvas of the actual crop size and copy the preview canvas to it (ctx.drawImage can resize as its copying).

But yes your way is the simple solution of always rendering it at the actual size which works too (at the expense of some visual quality on retina devices) ;)

I am keen to improve quality if possible... If I were to use my method and the offscreen canvas, would that give me better quality output, or is it not as simple as that?

I've updated the example here: https://codesandbox.io/s/react-image-crop-demo-with-react-hooks-y831o?file=/src/App.tsx

async function onDownloadCropClick() {
  const image = imgRef.current
  const previewCanvas = previewCanvasRef.current
  if (!image || !previewCanvas || !completedCrop) {
    throw new Error('Crop canvas does not exist')
  }

  // This will size relative to the uploaded image
  // size. If you want to size according to what they
  // are looking at on screen, remove scaleX + scaleY
  const scaleX = image.naturalWidth / image.width
  const scaleY = image.naturalHeight / image.height

  const offscreen = new OffscreenCanvas(
    completedCrop.width * scaleX,
    completedCrop.height * scaleY,
  )
  const ctx = offscreen.getContext('2d')
  if (!ctx) {
    throw new Error('No 2d context')
  }

  ctx.drawImage(
    previewCanvas,
    0,
    0,
    previewCanvas.width,
    previewCanvas.height,
    0,
    0,
    offscreen.width,
    offscreen.height,
  )
  // You might want { type: "image/jpeg", quality: <0 to 1> } to
  // reduce image size
  const blob = await offscreen.convertToBlob({
    type: 'image/png',
  })

  if (blobUrlRef.current) {
    URL.revokeObjectURL(blobUrlRef.current)
  }
  blobUrlRef.current = URL.createObjectURL(blob)
  hiddenAnchorRef.current!.href = blobUrlRef.current
  hiddenAnchorRef.current!.click()
}

Note the comment about scaleX + scaleY, remove those to get a crop exactly as the user sees it. Otherwise it will be based on the uploaded image which could be a lot bigger than that.

Sorry @dominictobias I forgot to reply, this worked a treat thank-you.