Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update crop modal to not inflate image sizes #1514

Merged
merged 1 commit into from
Oct 11, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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