Closed yuanxiang1990 closed 4 years ago
Thanks for posting this issue. However I don't have a device with Safari mobile so I'm not able to reproduce.
When you say the "cropped image", are you talking about the image in the pop-up after clicking the Crop button ?
Thanks for posting this issue. However I don't have a device with Safari mobile so I'm not able to reproduce.
When you say the "cropped image", are you talking about the image in the pop-up after clicking the Crop button ?
Yes, maybe Safari on the mobile terminal will fail to draw a large picture on canvas.
Actually, on codesandbox, we are using a less efficient approach to create the image. If you look at the createImage.js
file, there is a commented block (As a blob): try to use this one instead of the block above.
Actually, on codesandbox, we are using a less efficient approach to create the image. If you look at the
createImage.js
file, there is a commented block (As a blob): try to use this one instead of the block above.
Thank you for your advice. I tried,The problem still exists.I think it's caused by the following code in the createimage.js
file
const safeArea = Math.max(image.width, image.height) * 2
canvas.width = safeArea
canvas.height = safeArea
// translate canvas context to a central location on image to allow rotating around the center.
ctx.translate(safeArea / 2, safeArea / 2)
ctx.rotate(getRadianAngle(rotation))
ctx.translate(-safeArea / 2, -safeArea / 2)
// draw rotated image and store data.
ctx.drawImage(
image,
safeArea / 2 - image.width * 0.5,
safeArea / 2 - image.height * 0.5
)
The width and height of the canvas can be large, which will lead to canvas rotation and painting failure under the mobile Safari browser with low performance.
Do you get any error message in the console when the black screen appears?
I am having the same issue on iOS 13 Safari. Images from camera are failing to be able to be cropped and appearing as a black image.
Are you also talking about the image that we crop on the client-side and display in the pop-up? Or about the image in the cropper component?
The image that is appearing black is after the cropping has finished and when we are trying to display the output.
We resolved it by modifying our getCroppedImage code to divide the width and height by 2.
Before:
const image: HTMLImageElement = await createImage(imageSrc);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const safeArea = Math.max(image.width, image.height) * 2;
// set each dimensions to double largest dimension to allow for a safe area for the
// image to rotate in without being clipped by canvas context
canvas.width = safeArea;
canvas.height = safeArea;
if (!ctx) {
setIsLoading(false);
return null;
}
const getRadianAngle = (degreeValue: number) => {
return (degreeValue * Math.PI) / 180;
};
// translate canvas context to a central location on image to allow rotating around the center.
ctx.translate(safeArea / 2, safeArea / 2);
ctx.rotate(getRadianAngle(rotation));
ctx.translate(-safeArea / 2, -safeArea / 2);
// draw rotated image and store data.
ctx.drawImage(
image,
safeArea / 2 - image.width * 0.5,
safeArea / 2 - image.height * 0.5
);
const data = ctx.getImageData(0, 0, safeArea, safeArea);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image with correct offsets for x,y crop values.
ctx.putImageData(
data,
0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x,
0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y
);
return canvas.toDataURL('image/jpeg');
After:
const image: HTMLImageElement = await createImage(imageSrc);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
image.width = image.width / 2;
image.height = image.height / 2;
const safeArea = Math.max(image.width, image.height) * 2;
// set each dimensions to double largest dimension to allow for a safe area for the
// image to rotate in without being clipped by canvas context
canvas.width = safeArea;
canvas.height = safeArea;
if (!ctx) {
setIsLoading(false);
return null;
}
const getRadianAngle = (degreeValue: number) => {
return (degreeValue * Math.PI) / 180;
};
// translate canvas context to a central location on image to allow rotating around the center.
ctx.translate(safeArea / 2, safeArea / 2);
ctx.rotate(getRadianAngle(rotation));
ctx.translate(-safeArea / 2, -safeArea / 2);
// draw rotated image and store data.
ctx.drawImage(
image,
safeArea / 2 - image.width,
safeArea / 2 - image.height
);
const data = ctx.getImageData(0, 0, safeArea, safeArea);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image with correct offsets for x,y crop values.
ctx.putImageData(
data,
0 - safeArea / 2 + image.width - pixelCrop.x,
0 - safeArea / 2 + image.height - pixelCrop.y
);
return canvas.toDataURL('image/jpeg');
Thanks for sharing this @brennick. Indeed, I found another issue thread on another React crop component sharing this. Making the canvas smaller was also the solution there.
I'm closing this issue now as this is actually not an issue with the library itself. Creating the cropped image is not done by the lib. Also a solution was shared above.
Just a small update: I've changed the logic in the demo to use a smaller safeArea
: it used to be twice the max of width and height. Now it uses this formula which only uses the minimal size:
const maxSize = Math.max(image.width, image.height)
const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2))
problem still exists for iPhone , iPhone just warns us that its been exceeds the maximum memory allocated
OK then it means you need to make the safeArea even smaller to reduce the memory footprint.
@ValentinH Thank you for the work. I had the same issue and took me some time to find this. and seems to be same for others https://github.com/ricardo-ch/react-easy-crop/issues/98, https://github.com/ricardo-ch/react-easy-crop/issues/147
I thought it would be great to be documented as tips
or known issue
The thing is that the cropping logic using canvas are not part of this library. They are just provided as an example. I don't want to have this in the library because there are a lot of edge cases as we can see in the issues.
@ValentinH What worked for me is to utilize canvas-size library to detect max canvas size supported, and then do something like below to make sure safeArea is within max canvas sizes. On mobile Safari the limit is 4096 * 4096 for example.
const canvasLimitation = await canvasSize.maxArea({
usePromise: true,
useWorker: true,
});
if (safeArea > canvasLimitation.height) {
safeArea *= canvasLimitation.height / safeArea;
}
Thank you really much for sharing this! I think that we could use this to limit that image output size instead of only limiting the safe area. Otherwise, some parts might be cut when using rotation.
It means that we first need to resize the image to the maximum supported size (if smaller than image size) before applying the crop operations.
@ValentinH No problem : ) Right, it makes sense, I was using this without rotation.
Another solution to the problem is to normalise all images first, e.g. the size of the input via compressor-js:
const onFileChange = async e => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0]
const imageDataUrl = await readFile(file)
setImage(imageDataUrl) // <Cropper image={image} />
}
}
const readFile = useCallback(file => {
return new Promise((resolve, reject) => {
try {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
getNormalizedFile(file)
.then(normalizedFile => reader.readAsDataURL(normalizedFile))
.catch(error => reject(error))
} catch (error) {
reject(error)
}
})
}, [])
export const getNormalizedFile = file => {
return new Promise((resolve, reject) => {
new Compressor(file, {
maxWidth: 1000,
maxHeight: 1000,
success(normalizedFile) {
resolve(normalizedFile)
},
error(error) {
reject(error)
},
})
})
}
The image that is appearing black is after the cropping has finished and when we are trying to display the output.
We resolved it by modifying our getCroppedImage code to divide the width and height by 2.
Before:
const image: HTMLImageElement = await createImage(imageSrc); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const safeArea = Math.max(image.width, image.height) * 2; // set each dimensions to double largest dimension to allow for a safe area for the // image to rotate in without being clipped by canvas context canvas.width = safeArea; canvas.height = safeArea; if (!ctx) { setIsLoading(false); return null; } const getRadianAngle = (degreeValue: number) => { return (degreeValue * Math.PI) / 180; }; // translate canvas context to a central location on image to allow rotating around the center. ctx.translate(safeArea / 2, safeArea / 2); ctx.rotate(getRadianAngle(rotation)); ctx.translate(-safeArea / 2, -safeArea / 2); // draw rotated image and store data. ctx.drawImage( image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5 ); const data = ctx.getImageData(0, 0, safeArea, safeArea); // set canvas width to final desired crop size - this will clear existing context canvas.width = pixelCrop.width; canvas.height = pixelCrop.height; // paste generated rotate image with correct offsets for x,y crop values. ctx.putImageData( data, 0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x, 0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y ); return canvas.toDataURL('image/jpeg');
After:
const image: HTMLImageElement = await createImage(imageSrc); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); image.width = image.width / 2; image.height = image.height / 2; const safeArea = Math.max(image.width, image.height) * 2; // set each dimensions to double largest dimension to allow for a safe area for the // image to rotate in without being clipped by canvas context canvas.width = safeArea; canvas.height = safeArea; if (!ctx) { setIsLoading(false); return null; } const getRadianAngle = (degreeValue: number) => { return (degreeValue * Math.PI) / 180; }; // translate canvas context to a central location on image to allow rotating around the center. ctx.translate(safeArea / 2, safeArea / 2); ctx.rotate(getRadianAngle(rotation)); ctx.translate(-safeArea / 2, -safeArea / 2); // draw rotated image and store data. ctx.drawImage( image, safeArea / 2 - image.width, safeArea / 2 - image.height ); const data = ctx.getImageData(0, 0, safeArea, safeArea); // set canvas width to final desired crop size - this will clear existing context canvas.width = pixelCrop.width; canvas.height = pixelCrop.height; // paste generated rotate image with correct offsets for x,y crop values. ctx.putImageData( data, 0 - safeArea / 2 + image.width - pixelCrop.x, 0 - safeArea / 2 + image.height - pixelCrop.y ); return canvas.toDataURL('image/jpeg');
thanks, mate. I converted your code into responsive size only
const createImage = (url) =>
new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox
image.src = url;
});
function getRadianAngle(degreeValue) {
return (degreeValue * Math.PI) / 180;
}
/**
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
* @param {File} image - Image File url
* @param {Object} pixelCrop - pixelCrop Object provided by react-easy-crop
* @param {number} rotation - optional rotation parameter
*/
export default async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) {
let image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// console.log("before", image.width);
if (window.innerWidth < 1300 && (image.width > 1000 || image.width > 1000)) {
image.width = image.width / 2;
image.height = image.height / 2;
}
let maxSize = Math.max(image.width, image.height);
// console.log("after", image.width);
let safeArea = 2 * ((maxSize / 2) * Math.sqrt(2));
if (window.innerWidth < 1300 && (image.width > 1000 || image.width > 1000)) {
safeArea = Math.max(image.width, image.height) * 2;
}
// console.log("safearea", safeArea);
// set each dimensions to double largest dimension to allow for a safe area for the
// image to rotate in without being clipped by canvas context
canvas.width = safeArea;
canvas.height = safeArea;
// translate canvas context to a central location on image to allow rotating around the center.
ctx.translate(safeArea / 2, safeArea / 2);
ctx.rotate(getRadianAngle(rotation));
ctx.translate(-safeArea / 2, -safeArea / 2);
// draw rotated image and store data.
if (window.innerWidth < 1300 && (image.width > 1000 || image.width > 1000)) {
ctx.drawImage(
image,
safeArea / 2 - image.width,
safeArea / 2 - image.height
);
} else {
ctx.drawImage(
image,
safeArea / 2 - image.width * 0.5,
safeArea / 2 - image.height * 0.5
);
}
const data = ctx.getImageData(0, 0, safeArea, safeArea);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image with correct offsets for x,y crop values.
if (window.innerWidth < 1300 && (image.width < 1000 || image.width > 1000)) {
ctx.putImageData(
data,
0 - safeArea / 2 + image.width - pixelCrop.x,
0 - safeArea / 2 + image.height - pixelCrop.y
);
} else {
ctx.putImageData(
data,
Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x),
Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y)
);
}
// As Base64 string
// return canvas.toDataURL('image/jpeg');
// As a blob
return new Promise((resolve) => {
canvas.toBlob((file) => {
resolve(file);
}, "image/jpeg");
});
}
here's a snippet of how me and @hejdesgit solved scaling the image before we send it to crop.
if needed, we scale the image to a size that is safe within the browsers maximum canvas size.
import canvasSize from 'canvas-size';
import Compressor from 'compressorjs';
function getImageDimensions(file) {
const imgObjectUrl = URL.createObjectURL(file);
const img = new Image();
return new Promise((resolve, reject) => {
img.onload = () => {
const width = img.naturalWidth;
const height = img.naturalHeight;
URL.revokeObjectURL(imgObjectUrl);
resolve({ width, height });
};
img.onerror = reject;
img.src = imgObjectUrl;
});
}
function compressImage(imgObjectUrl, maxImageDimension) {
return new Promise((resolve, reject) => {
new Compressor(imgObjectUrl, {
maxWidth: maxImageDimension,
maxHeight: maxImageDimension,
success(normalizedFile) {
resolve(normalizedFile);
},
error(error) {
reject(error);
}
});
});
}
export default async function getScaledImg(file) {
let fileToUse = file;
const imageDimensions = await getImageDimensions(file);
const maxCanvasSize = await canvasSize.maxArea({
usePromise: true,
useWorker: true
});
// Reverse of safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)) in getCroppedImg.js
const maxImageDimension = Math.floor(
Math.max(maxCanvasSize.width, maxCanvasSize.height) / Math.sqrt(2)
);
if (
imageDimensions.width > maxImageDimension ||
imageDimensions.height > maxImageDimension
) {
fileToUse = await compressImage(file, maxImageDimension);
}
return fileToUse;
}
https://github.com/ValentinH/react-easy-crop/issues/91#issuecomment-766167263. This solution worked for me. My issue was safari throwing error like this
Unable to get image data from canvas. Requested size was
The cropped image is black on mobile Safari when the image is very large.(https://codesandbox.io/embed/react-easy-crop-custom-image-demo-hgqst?codemirror=1).