Scaling an image to fit on canvas - javascript

I have a form that allows a user to upload an image.
Once the image is loaded, we perform some scaling on it in order to reduce its filesize before we pass it back to the server.
To do this, we place it on the canvas and manipulate it there.
This code will render the scaled image on the canvas, with the canvas of size 320 x 240px:
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
... where canvas.width and canvas.height is the image height and width x a scaling factor based on the size of the original image.
But when I go to use the code:
ctx.drawImage(img, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height
... I only get part of the image on the canvas, in this case the top left corner. I need the whole image 'scaled' to fit on the canvas, despite the actual image size being larger than the 320x240 canvas size.
So for the code above, the width and heights are 1142x856, as that is the final image size. I need to maintain that size to pass beck to the server when the form is submitted, but only want a smaller view of it to appear in the canvas for the user.
What am I missing here? Can anyone point me in the right direction please?

You made the error, for the second call, to set the size of source to the size of the target.
Anyway i bet that you want the same aspect ratio for the scaled image, so you need to compute it :
var hRatio = canvas.width / img.width ;
var vRatio = canvas.height / img.height ;
var ratio = Math.min ( hRatio, vRatio );
ctx.drawImage(img, 0,0, img.width, img.height, 0,0,img.width*ratio, img.height*ratio);
i also suppose you want to center the image, so the code would be :
function drawImageScaled(img, ctx) {
var canvas = ctx.canvas ;
var hRatio = canvas.width / img.width ;
var vRatio = canvas.height / img.height ;
var ratio = Math.min ( hRatio, vRatio );
var centerShift_x = ( canvas.width - img.width*ratio ) / 2;
var centerShift_y = ( canvas.height - img.height*ratio ) / 2;
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.drawImage(img, 0,0, img.width, img.height,
centerShift_x,centerShift_y,img.width*ratio, img.height*ratio);
}
you can see it in a jsbin here :
http://jsbin.com/funewofu/1/edit?js,output

Provide the source image (img) size as the first rectangle:
ctx.drawImage(img, 0, 0, img.width, img.height, // source rectangle
0, 0, canvas.width, canvas.height); // destination rectangle
The second rectangle will be the destination size (what source rectangle will be scaled to).
Update 2016/6: For aspect ratio and positioning (ala CSS' "cover" method), check out:
Simulation background-size: cover in canvas

I guess that you want the image to be scaled to a smaller size, without losing the ratio of the dimensions. I have a solution.
var ratio = image.naturalWidth / image.naturalHeight;
var width = canvas.width;
var height = width / ratio;
ctx.drawImage(image, 0, 0, width, height);
the ratio will be maintained. And the image drawn on the canvas will be of the same ratio. you can use the if loop if the height of the image is long, you can replace the canvas.width to some other width

You can call ctx.scale() before calling ctx.drawImage:
var factor = Math.min ( canvas.width / img.width, canvas.height / img.height );
ctx.scale(factor, factor);
ctx.drawImage(img, 0, 0);
ctx.scale(1 / factor, 1 / factor);
This should preserve the aspect ratio.

HTML:
<div id="root"></div>
JavaScript:
const images = [
'https://cdn.pixabay.com/photo/2022/07/25/15/18/cat-7344042_960_720.jpg',
'https://cdn.pixabay.com/photo/2022/06/27/08/37/monk-7287041_960_720.jpg',
'https://cdn.pixabay.com/photo/2022/07/18/19/57/dog-7330712_960_720.jpg',
'https://cdn.pixabay.com/photo/2022/05/22/18/25/spain-7214284_960_720.jpg',
];
const root = document.getElementById('root');
const image = new Image();
image.crossOrigin = 'anonumys';
image.src = images[3];
const canvas = document.createElement('canvas');
canvas.classList.add('track');
canvas.width = 600;
canvas.height = 400;
const ctx = canvas.getContext('2d');
const meta = {
ratio: image.width / image.height,
width: 0,
height: 0,
offsetX: 0,
offsetY: 0,
};
if (meta.ratio >= 1) {
meta.width = canvas.width > image.width ? image.width : canvas.width;
meta.height = meta.width / meta.ratio;
} else {
meta.height = canvas.height > image.height ? image.height : canvas.height;
meta.width = meta.height * meta.ratio;
}
meta.offsetX = canvas.width > meta.width ? (canvas.width - meta.width) / 2 : 0;
meta.offsetY = canvas.height > meta.height ? (canvas.height - meta.height) / 2 : 0;
image.addEventListener('load', () => {
ctx.drawImage(image, meta.offsetX, meta.offsetY, meta.width, meta.height);
root.append(canvas);
});
console.log(meta);

Related

How to draw behind an image in canvas?

I am using canvas for a picture crop feature and I cannot figure out how to fill the empty spaces.
First, this is what I'm doing:
Canvas element:
<canvas
id="previewCanvas"
ref={previewCanvasRef}
style={{
border: '1px solid black',
objectFit: 'contain',
width: '300px',
height: '300px',
}}
/>
This is the function I use to draw the image:
const TO_RADIANS = Math.PI / 180;
export async function canvasPreview(image, canvas, crop, scale = 1, rotate = 0) {
const ctx = canvas.getContext('2d');
if (!ctx) {
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
canvas.width = Math.floor(crop.width * scaleX * pixelRatio);
canvas.height = Math.floor(crop.height * scaleY * pixelRatio);
ctx.scale(pixelRatio, pixelRatio);
ctx.imageSmoothingQuality = 'high';
const cropX = crop.x * scaleX;
const cropY = crop.y * scaleY;
const rotateRads = rotate * TO_RADIANS;
const centerX = image.naturalWidth / 2;
const centerY = image.naturalHeight / 2;
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);
// 3) Rotate around the origin
ctx.rotate(rotateRads);
// 2) Scale the image
ctx.scale(scale, scale);
// 1) Move the center of the image to the origin (0,0)
ctx.translate(-centerX, -centerY);
ctx.drawImage(
image,
0,
0,
image.naturalWidth,
image.naturalHeight,
0,
0,
image.naturalWidth,
image.naturalHeight
);
ctx.restore();
}
This is how I see the canvas on the browser:
Now, when I convert the image to a blob, then to a file object, I get an image like this :
What I expect to happen is to get an image like this: (exactly like in the preview)
What am I doing wrong ?
you can follow the StackOverflow link: https://stackoverflow.com/questions/15457619/html-canvas-drawing-grid-below-a-plot

How to scale small ImageData to large HTML canvas

I am trying to put image data 100x100 to canvas 1000x1000 , but cant able to do it ,
let width=1000; //canvas width
let height=1000; //canvas height
let img_w=100; //image width
let img_h=100; //image height
let img=new Image();
img.width=img_w
img.height=img_h
img.src="./flower.jpg"
var canvas = document.getElementById('mycanvas');
var context = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
let pixels,scannedimg;
img.onload=()=>{
context.drawImage(img, 0, 0,width,height );
scannedimg = context.getImageData(0, 0, img.width, img.height);
pixels=scannedimg.data
console.log(pixels)
redraw();
}
let row=4*img_w;
let col=img_h;
function redraw(){
for(let i=0;i<row;i+=4){
for(let j=0;j<col;j++){
pixels[i+j*row]=0;
pixels[i+j*row+1]=0;
pixels[i+j*row+2]=0;
//pixels[i+j*400+3]=0;
}
}
scannedimg.data=pixels;
console.log(scannedimg);
context.putImageData(scannedimg,0,0,0,0,width,height);
}
i have converted the original array into a black image array (array of zeros) , but while putting on canvas , it is still 100x100
How to scale it to 1000x1000?
i don't want to iterate through 1000x1000 and set it to zero ,
i need a computationally efficient answer
Unless you outsource the pixel calculations to a WebAssembly module a JavaScript-only approach would indeed be rather slow for a large image.
Honestly I'm not sure what you are actually doing in your code.
First your drawing an unknown-sized .jpg to a 1000x1000 canvas which - unless the .jpg is also 1000x1000 - will scale and eventually distort the source image.
let width=1000;
let height=1000;
context.drawImage(img, 0, 0, width, height);
Secondly you're obtaining the pixel data of a 100x100 region from the top-left of your 1000x1000 canvas.
let img_w=100;
let img_h=100;
img.width=img_w;
img.height=img_h;
scannedimg = context.getImageData(0, 0, img.width, img.height);
Finally in your redraw() function you're rather randomly setting some of the pixels to black and draw it back to the canvas at 1000x1000 (which doesn't work that way but I will get into it later).
Let's do it a little different. Say we have a 300x200 image. First we need to draw it to a 100x100 canvas while maintaining it's aspect ratio to get the 100x100 imagedata.
This can be done using a dynamically created off-screen <canvas> element as we don't need to see it.
Now the tricky part is the CanvasRenderingContext2D putImageData() method. I assume you were thinking that the last pair of parameters for the width & height would stretch existing pixel data to fill the region specifid by (x, y, width, height). Well that's not the case. Instead we need to - again - paint the 100x100 pixel data to a same-sized off-screen canvas (or for simlicity re-use the existing) and draw it to the final canvas using the drawImage() method.
Here's everything put together:
let pixelsWidth = 100;
let pixelsHeight = 100;
let finalWidth = 500;
let finalHeight = 500;
let tempCanvas = document.createElement('canvas');
let tempContext = tempCanvas.getContext('2d');
tempCanvas.width = pixelsWidth;
tempCanvas.height = pixelsHeight;
let pixelData;
let img = new Image();
img.crossOrigin = 'anonymous';
img.onload = (e) => {
let scale = e.target.naturalWidth >= e.target.naturalHeight ? pixelsWidth / e.target.naturalWidth : pixelsHeight / e.target.naturalHeight;
let tempWidth = e.target.naturalWidth * scale;
let tempHeight = e.target.naturalHeight * scale;
tempContext.drawImage(e.target, pixelsWidth / 2 - tempWidth / 2, pixelsHeight / 2 - tempHeight / 2, tempWidth, tempHeight);
pixelData = tempContext.getImageData(0, 0, pixelsWidth, pixelsHeight);
redraw();
}
img.src = 'https://picsum.photos/id/237/300/200';
function redraw() {
let canvas = document.getElementById('canvas');
let context = canvas.getContext('2d');
canvas.width = finalWidth;
canvas.height = finalHeight;
tempContext.putImageData(pixelData, 0, 0);
context.drawImage(tempCanvas, 0, 0, finalWidth, finalHeight);
}
canvas {
background: #cccccc;
}
<canvas id="canvas"></canvas>

Reduce the dimension of base64 image maintaining aspect ratio in javascript

I have a base64 string for an image, which i have to display as an icon. If the dimension of an image are bigger i have to display icon maintaining aspect ratio.
I have written a logic to identify if the image is landscape or portrait based on which height and width will be settled for canvas. But seems height and width of icons are not proper as i have hard coded it.
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var height, width;
if (this.height>this.width) {
height = 50;
width = 30;
} else if (this.height<this.width) {
height = 30;
width = 50;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(image,
0, 0, this.width, this.height,
0, 0, canvas.width, canvas.height
);
var scaledImage = new Image();
scaledImage.src = canvas.toDataURL();
Is there any way we can calculate it dynamically or any other way to preserve aspect ratio for icons.
Working Example can be found on https://codepen.io/imjaydeep/pen/ewBZRK
It will be fine if some space will be left on x-y axis.
You just need to calculate the scale or ratio and multiply both dimensions by that. Here's an example function, and here's your edited codepen.
Creates trimmed, scaled image:
function scaleDataURL(dataURL, maxWidth, maxHeight){
return new Promise(done=>{
var img = new Image;
img.onload = ()=>{
var scale, newWidth, newHeight, canvas, ctx;
if(img.width < maxWidth){
scale = maxWidth / img.width;
}else{
scale = maxHeight / img.height;
}
newWidth = img.width * scale;
newHeight = img.height * scale;
canvas = document.createElement('canvas');
canvas.height = newHeight;
canvas.width = newWidth;
ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, newWidth, newHeight);
done(canvas.toDataURL());
};
img.src = dataURL;
});
}
Or, if you want the image to always be the provided dimensions with empty surrounding area, you can use this: (codepen)
Creates scaled image of exact provided dimensions:
function scaleDataURL(dataURL, maxWidth, maxHeight){
return new Promise(done=>{
var img = new Image;
img.onload = ()=>{
var scale, newWidth, newHeight, canvas, ctx, dx, dy;
if(img.width < maxWidth){
scale = maxWidth / img.width;
}else{
scale = maxHeight / img.height;
}
newWidth = img.width * scale;
newHeight = img.height * scale;
canvas = document.createElement('canvas');
canvas.height = maxWidth;
canvas.width = maxHeight;
ctx = canvas.getContext('2d');
dx = (maxWidth - newWidth) / 2;
dy = (maxHeight - newHeight) / 2;
console.log(dx, dy);
ctx.drawImage(img, 0, 0, img.width, img.height, dx, dy, newWidth, newHeight);
done(canvas.toDataURL());
};
img.src = dataURL;
});
}
For someone who search for a solution when the image is bigger that you need.
function scaleDataURL(dataURL: string, maxWidth: number, maxHeight: number) {
return new Promise((done) => {
var img = new Image()
img.onload = () => {
var scale, newWidth, newHeight, canvas, ctx
if (img.width > maxWidth) {
scale = maxWidth / img.width
} else if (img.height > maxHeight) {
scale = maxHeight / img.height
} else {
scale = 1
}
newWidth = img.width * scale
newHeight = img.height * scale
canvas = document.createElement('canvas')
canvas.height = newHeight
canvas.width = newWidth
ctx = canvas.getContext('2d')
console.log('img', 'scale', scale, 0, 0, img.width, img.height, 0, 0, newWidth, newHeight)
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, newWidth, newHeight)
done(canvas.toDataURL())
}
img.src = dataURL
})
}
Usage:
scaleDataURL(base64data, 600, 200)
.then((newBase64data) => {
console.log(newBase64data)
})
.catch((err) => console.log(err))

crop an image using canvas - result is small image

I'm trying to crop an image using canvas.
Original image is 2217 x 790.
Loading on page it is scaled to 1515 x 540
Canvas is 960 x 540.
Both image and canvas are on center of screen, so aligned horizontally.
I need to crop central area - 960 x 540.
var img = document.getElementById("imgt");
var canvas = document.getElementById("canvasa");
var ctx = canvas.getContext("2d");
var a = $('#imgt').width() - 960;
var a = a/2; // this is 277.7...
ctx.drawImage(img, a, 0, 960, 540, 0, 0, 960, 540);
//also tried:
ctx.drawImage(img, 0, 0, 960, 540, 0, 0, 960, 540);
var newimg = new Image();
newimg.src = canvas.toDataURL('image/jpeg');
var dl = document.createElement("a");
dl.href = canvas.toDataURL("image/jpeg");
dl.download = true;
document.body.appendChild(dl);
dl.click();
Downloading newimg what I see - it is 300 x 150 !
See my comment for differences between width attribute and width as style. They are not exactly the same. Besides, I just made a fiddle since you didn't and I don't get the same behavior! The downloaded image is 960 * 540
HERE - fires with delay
ONLOAD
setTimeout(function(){
var canvas = document.createElement("canvas");
canvas.width = "960";
canvas.height="540";
var ctx = canvas.getContext("2d");
ctx.drawImage(document.images[0], 0, 0, 960, 540, 0, 0, 960, 540);
var a = document.createElement("a");
a.download = "image.jpeg";
a.href = canvas.toDataURL("image/jpeg");
a.click();
},5000);
In general, if you want to crop an area from a source image, and draw it into a canvas without breaking the aspect ratio and not hardcoding the dimensions into the routine, you can do this:
const canvasAspectRatio = canvas.width / canvas.height;
const cropWidth = canvas.width;
const cropHeight = cropWidth / canvasAspectRatio;
const sx = img.width / 2 - cropWidth / 2;
const sy = img.height / 2 - cropHeight / 2;
ctx.drawImage(img, sx, sy, cropWidth, cropHeight, 0, 0, canvas.width, canvas.height);
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const img = document.getElementById('img');
const canvasAspectRatio = canvas.width / canvas.height;
// Crop central canvas sized rectangle area into canvas
const cropWidth = canvas.width;
const cropHeight = cropWidth / canvasAspectRatio; // Here you should calculate the height based on aspect ratio instead of assuming it matches that of the canvas
const sx = img.width / 2 - cropWidth / 2;
const sy = img.height / 2 - cropHeight / 2;
ctx.drawImage(img, sx, sy, cropWidth, cropHeight, 0, 0, canvas.width, canvas.height);
body {
background-color: black;
}
#img {
visibility: hidden;
}
#canvas {
border: 1px solid #f00;
}
<canvas id="canvas" width="960" height="540"></canvas>
<img id="img" src="http://via.placeholder.com/2217x790">

How can i resize my canvas image

page 1:
//Calls the function from page 2 and the callback image is set as the source of the control.
previewImage(current, function(img) {
jQuery(".mapItem").attr("src",img.src);
});
page 2:
//The functions callback returns an image which we use in page 1 (above)
var canvas = document.createElement('canvas');
context = canvas.getContext('2d');
context.drawImage(this.m_Images[i],0,0,canvas.width,canvas.height);
var t = new Image();
t.src = canvas.toDataURL();
callback(t);
The issue:
I have 2 JavaScript pages, the first one has an image control and the second one has a function that returns a callback as an image.
My control in page 1 (.mapItem) has a height and width of 75.2px (fixed). The image that is coming from the callback however will have a different size each time e.g one day it can be 200px * 300px and one day it can be 150px * 200px etc
How can I clip or CUT the image of the callback? I want the image (t) to zero (0) as x and y starting points and then clip the image where ever the .mapItem control height and width is.
I need this to be proportional ratio. So I can't just add the following code:
context.drawImage(this.m_Images[i],0,0,72.5,72.5); because this will ruin the image as we dont even know if it is square shaped.
Thanks in advance :)
You can determine the proportions of callback image and then apply them to the page 1 image thus:
Let's assume that the callback image is 300x200px. The ratio of the image's height-to-width can be expressed as...
var ratio = callbackImage.height / callbackImage.width;
...or, in this case...
var ratio = 200 / 300; // 0.666...
We know the width of the page 1 canvas is 72.5 so we can apply the ratio to that value to determine the proportional height of the callback Image like so...
var canvasWidthHeight = 72.5;
var imageHeight = canvasWidthHeight * ratio; // 48.333...
To center the callback Image on the page 1 canvas calculate the y offset like so..
var y = (canvasWidthHeight - imageHeight) / 2;
...and now you can use the canvas drawImage method with these values...
context.drawImage(
this.m_Images[i],
0, y,
canvasWidthHeight, imageHeight
);
If the callback image was higher than it was wide you'd apply the ratio the page 1 canvas dimensions to work out the x offset rather than the y offest. If the callback image was square then its ratio would be 1.0 and you could simply paint it into the square page 1 canvas at
context.drawImage(
this.m_Images[i],
0, 0,
canvasWidthHeight, canvasWidthHeight
);
All together the code might look something like this...
var image = this.m_Images[i];
var canvas = {
width: 72.5,
height: 72.5
};
var wh = 0;
var ratio = image.height / image.width;
if (image.width > image.height) { // landscape
wh = canvas.width * ratio;
context.drawImage(
image,
0, (canvas.height - wh) / 2,
canvas.width, wh
);
} else if (image.width < image.height) { // portrait
ratio = image.width / image.height;
wh = canvas.height * ratio;
context.drawImage(
image,
(canvas.width - wh) / 2, 0,
wh, canvas.height
);
} else { // square
context.drawImage(
image,
0, 0,
canvas.width, canvas.height
);
}
Hope that helps. ;)
EDIT: You may need to ensure that the new Image() has fully loaded before initiating the callback. You can do this with the following snippet...
// callback preparation code as before...
var t = new Image();
t.addEventListener('load', function() {
callback(this);
});
t.src = canvas.toDataURL();

Categories