Skip to content

Commit

Permalink
fix: update crop modal to not inflate image sizes (#1514)
Browse files Browse the repository at this point in the history
  • Loading branch information
spaenleh authored Oct 11, 2024
1 parent cb64f4c commit f6cc03b
Showing 1 changed file with 58 additions and 34 deletions.
92 changes: 58 additions & 34 deletions src/components/common/CropModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ const CropModal = ({ onConfirm, onClose, src }: CropProps): JSX.Element => {
const { t } = useBuilderTranslation();

const handleOnConfirm = async () => {
// get the image html element
const image = imageRef.current;

if (!image || !completedCrop) {
// this should never happen but we better check
setIsError(true);
throw new Error('Crop canvas does not exist');
throw new Error('Crop canvas does not exist, this should never happen');
}

// declare the canvas that will be used to render the cropped image
// we use an off-screen canvas to not overload the main thread
const offscreen = new OffscreenCanvas(
completedCrop.width,
completedCrop.height,
Expand All @@ -76,51 +80,71 @@ const CropModal = ({ onConfirm, onClose, src }: CropProps): JSX.Element => {
throw new Error('No 2d context');
}

const scaleX = image.naturalWidth / image.width;
const scaleY = image.naturalHeight / image.height;
// devicePixelRatio slightly increases sharpness on retina devices
// at the expense of slightly slower render times and needing to
// size the image back down if you want to download/upload and be
// true to the images natural size.
const pixelRatio = window.devicePixelRatio;
// const pixelRatio = 1
/**
* Compute the relative width and height
* They express the size of the crop in terms of a relative portion of the original image.
* For example if only the first quarter of the image was used in the crop,
* the relativeCrop would be 0.5, 0.5, meaning half of the height, and half of the width
*/
const relativeCrop = {
width: completedCrop.width / image.width,
height: completedCrop.height / image.height,
};

/**
* The scaling factor between the "real" media size and the size shown in the preview window
* We need to compute this as the crop is expressed in terms of the preview size but we want to apply the crop to the "real" image
* `image.width` is the size of the preview
* `image.naturalWidth` is the size of the uploaded media
*/
const uiScalingFactor = image.naturalWidth / image.width;

// compute the final canvas size given the relative size of the crop and the initial size of the image
const finalCanvasWidth = Math.floor(
relativeCrop.width * image.naturalWidth,
);
const finalCanvasHeight = Math.floor(
relativeCrop.height * image.naturalHeight,
);

offscreen.width = Math.floor(completedCrop.width * scaleX * pixelRatio);
offscreen.height = Math.floor(completedCrop.height * scaleY * pixelRatio);
// assign the final size to the canvas
offscreen.width = finalCanvasWidth;
offscreen.height = finalCanvasHeight;

ctx.scale(pixelRatio, pixelRatio);
// smoothing factor to use, this ensures the image keep a high quality when drawn on the canvas
ctx.imageSmoothingQuality = 'high';

const cropX = completedCrop.x * scaleX;
const cropY = completedCrop.y * scaleY;

const centerX = image.naturalWidth / 2;
const centerY = image.naturalHeight / 2;
// compute the source image offsets and size so it can be drawn on to the canvas
const sourceOffsetX = completedCrop.x * uiScalingFactor;
const sourceOffsetY = completedCrop.y * uiScalingFactor;
const sourceWidth = image.naturalWidth * relativeCrop.width;
const sourceHeight = image.naturalHeight * relativeCrop.height;

ctx.save();

// 5) Move the crop origin to the canvas origin (0,0)
ctx.translate(-cropX, -cropY);
// 4) Move the origin to the center of the original position
ctx.translate(centerX, centerY);
// 1) Move the center of the image to the origin (0,0)
ctx.translate(-centerX, -centerY);
// finally draw the image on to the canvas
ctx.drawImage(
// the source HTML element from which to draw
image,
// the sx and sy offsets on the source image
sourceOffsetX,
sourceOffsetY,
// source image width and height
sourceWidth,
sourceHeight,
// dx and dy values denote the origin where to draw on the canvas
0,
0,
image.naturalWidth,
image.naturalHeight,
0,
0,
image.naturalWidth,
image.naturalHeight,
// we want to fill the whole canvas
finalCanvasWidth,
finalCanvasHeight,
);

// You might want { type: "image/jpeg", quality: <0 to 1> } to
// reduce image size
// the blob is converted to webP as this is the format that
// the backend uses anyway to store the images. This allows transparency in opposition to jpeg
const blob = await offscreen.convertToBlob({
type: 'image/png',
type: 'image/webp',
// Use a quality factor less than 1 to allow lossy compression.
// This reduces the size of the uploaded image
quality: 0.8,
});
onConfirm(blob);
};
Expand Down

0 comments on commit f6cc03b

Please sign in to comment.