HTML5 Canvas drawImage ratio bug iOS - javascript

I want to resize the image taken from the iOS camera on the client side with HTML5 Canvas but I keep running in this weird bug where the image has a wrong ratio if bigger than ~1.5mb
It works on the desktop but not in the latest iOS version with the media upload API.
You can see an example here: http://jsbin.com/ekuros/1
Any idea how to fix this please? Is this a memory issue?
$('#file').on('change', function (e) {
var file = e.currentTarget.files[0];
var reader = new FileReader();
reader.onload = function (e) {
var image = $('<img/>');
image.on('load', function () {
var square = 320;
var canvas = document.createElement('canvas');
canvas.width = square;
canvas.height = square;
var context = canvas.getContext('2d');
context.clearRect(0, 0, square, square);
var imageWidth;
var imageHeight;
var offsetX = 0;
var offsetY = 0;
if (this.width > this.height) {
imageWidth = Math.round(square * this.width / this.height);
imageHeight = square;
offsetX = - Math.round((imageWidth - square) / 2);
} else {
imageHeight = Math.round(square * this.height / this.width);
imageWidth = square;
offsetY = - Math.round((imageHeight - square) / 2);
}
context.drawImage(this, offsetX, offsetY, imageWidth, imageHeight);
var data = canvas.toDataURL('image/jpeg');
var thumb = $('<img/>');
thumb.attr('src', data);
$('body').append(thumb);
});
image.attr('src', e.target.result);
};
reader.readAsDataURL(file);
});

If you still need to use the long version of the drawImage function you can change this:
context.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
to this:
drawImageIOSFix(context, img, sx, sy, sw, sh, dx, dy, dw, dh);
You just need to include these two functions somewhere:
/**
* Detecting vertical squash in loaded image.
* Fixes a bug which squash image vertically while drawing into canvas for some images.
* This is a bug in iOS6 devices. This function from https://github.com/stomita/ios-imagefile-megapixel
*
*/
function detectVerticalSquash(img) {
var iw = img.naturalWidth, ih = img.naturalHeight;
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = ih;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
var data = ctx.getImageData(0, 0, 1, ih).data;
// search image edge pixel position in case it is squashed vertically.
var sy = 0;
var ey = ih;
var py = ih;
while (py > sy) {
var alpha = data[(py - 1) * 4 + 3];
if (alpha === 0) {
ey = py;
} else {
sy = py;
}
py = (ey + sy) >> 1;
}
var ratio = (py / ih);
return (ratio===0)?1:ratio;
}
/**
* A replacement for context.drawImage
* (args are for source and destination).
*/
function drawImageIOSFix(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) {
var vertSquashRatio = detectVerticalSquash(img);
// Works only if whole image is displayed:
// ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio);
// The following works correct also when only a part of the image is displayed:
ctx.drawImage(img, sx * vertSquashRatio, sy * vertSquashRatio,
sw * vertSquashRatio, sh * vertSquashRatio,
dx, dy, dw, dh );
}
This will work fine whether it is run on iOS or other platforms.
This is based on the great work by stomita and you should credit him in your work.

There is a JavaScript canvas resize library which works around the subsampling and vertical squash issues encountered when drawing scaled images on canvas on iOS devices:
http://github.com/stomita/ios-imagefile-megapixel
There are side issues when scaling images with alpha channel (as it uses the alpha channel for the issues detection) and when trying to resize existing canvas elements, however it's the first solution I've found that actually works with the issue at hand.
stomita is also a StackOverflow user and posted his solution here:
https://stackoverflow.com/a/12615436/644048

It looks like this is an iOS 6 bug. There is no reason for the aspect to get out of whack from your code. I have the same problem which was only introduced in iOS 6. It seems that their sub-sampling routine gives the wrong height. I submitted a bug report to Apple, and you should do the same. The more bug reports they get for this the better.

I've experienced the same problem. It seems that this is an iOS limitation, jpg over 2 megapixel are subsampled.
See Creating Compatible Web Content for Safari on IPhone

A modified version of the above code.
Edit: saw L0LN1NJ4's code at http://jsfiddle.net/gWY2a/24/ .. guess that one's a bit better...
function drawImageIOSFix (ctx, img) {
var vertSquashRatio = detectVerticalSquash (img)
var arg_count = arguments.length
switch (arg_count) {
case 4 : ctx.drawImage (img, arguments[2], arguments[3] / vertSquashRatio); break
case 6 : ctx.drawImage (img, arguments[2], arguments[3], arguments[4], arguments[5] / vertSquashRatio); break
case 8 : ctx.drawImage (img, arguments[2], arguments[3], arguments[4], arguments[5], arguments[6], arguments[7] / vertSquashRatio); break
case 10 : ctx.drawImage (img, arguments[2], arguments[3], arguments[4], arguments[5], arguments[6], arguments[7], arguments[8], arguments[9] / vertSquashRatio); break
}
// Detects vertical squash in loaded image.
// Fixes a bug which squash image vertically while drawing into canvas for some images.
// This is a bug in iOS6 (and IOS7) devices. This function from https://github.com/stomita/ios-imagefile-megapixel
function detectVerticalSquash (img) {
var iw = img.naturalWidth, ih = img.naturalHeight
var canvas = document.createElement ("canvas")
canvas.width = 1
canvas.height = ih
var ctx = canvas.getContext('2d')
ctx.drawImage (img, 0, 0)
var data = ctx.getImageData(0, 0, 1, ih).data
// search image edge pixel position in case it is squashed vertically.
var sy = 0, ey = ih, py = ih
while (py > sy) {
var alpha = data[(py - 1) * 4 + 3]
if (alpha === 0) {ey = py} else {sy = py}
py = (ey + sy) >> 1
}
var ratio = (py / ih)
return (ratio === 0) ? 1 : ratio
}
}

Related

Canvas drawImage: Proportion Issue when linearly interpolating from "fit"- to "fill"-style settings

I have a canvas and I want to be able to draw an image in different sizes from "fit" (like CSS "contain") to "fill" (like CSS "cover"). I use drawImage() with different source and destination properties for fit and fill. Both extremes work perfectly as expected, but in between the image proportions are way off, and the image looks flat. I use linear interpolation to calculate the between source and destination properties.
"fit/contain" properties:
ctx.drawImage(
img, // image
0, // source x
0, // source y
img.width, // source width
img.height, // source height
(canvas.width - canvas.height * imageAspect) / 2, // destination x
0, // destination y
canvas.height * imageAspect, // destination width
canvas.height // destination height
)
"fill/cover" Properties:
ctx.drawImage(
img, // image
0, // source x
(image.height - img.width / canvasAspect) / 2, // source y
img.width, // source width
img.width / canvasAspect, // source height
0, // destination x
0, // destination y
canvas.width, // destination width
canvas.height // destination height
)
These are both fine, but linear interpolation of all the values get the wrong proportions of the image. Here's a quick demo that is not working as expected, I animated the interpolation so that you can see the squished effect more clearly:
Code Pen
The desired result would be keeping the image's proportions right in every step between 0 (fit) and 1 (fill). What am I missing here?
EDIT: The easiest solution would be to always take the full source image (not crop it with sX, sY, sWidth, and sHeight) and then draw the destination with negative coordinate values on the canvas when the image is bigger than the canvas. This is working but it is not the desired behavior. Because further on I need to be able to draw only to a certain sub-rectangle in the canvas, where the overlapping ("negative values") would be seen. I don't want to draw outside the rectangle. I am quite sure it is just a small mathematical issue here that needs to be solved.
For me, the solution in your "Edit" is the way to go.
If later on you want to clip the image in a smaller rectangle than the canvas, use the clip() method:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 800;
canvas.height = 150;
let step = 1;
let direction = 1;
// control the clipping rect position with the mouse
const mouse = {x: 400, y: 75};
onmousemove = (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
};
function getBetweenValue(from, to, stop) {
return from + (to - from) * stop;
}
const image = new Image();
image.src =
"https://w7.pngwing.com/pngs/660/154/png-transparent-perspective-grid-geometry-grid-perspective-grid-geometric-grid-grid.png";
let imageAspect = 0;
let canvasAspect = canvas.width / canvas.height;
let source;
let containDestination;
let coverDestination;
function draw(image) {
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Clip the context in a sub-rectangle
ctx.beginPath();
ctx.rect(mouse.x - 150, mouse.y - 50, 300, 100);
ctx.stroke();
ctx.clip();
// Since our image scales from the middle of the canvas,
// set the context's origin there, that makes our BBox values simpler
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.drawImage(
image,
source.x,
source.y,
source.width,
source.height,
getBetweenValue(containDestination.x, coverDestination.x, step),
getBetweenValue(containDestination.y, coverDestination.y, step),
getBetweenValue(containDestination.width, coverDestination.width, step),
getBetweenValue(containDestination.height, coverDestination.height, step)
);
ctx.restore(); // remove clip & transform
}
image.addEventListener("load", () => {
imageAspect = image.width / image.height;
source = {
x: 0,
y: 0,
width: image.width,
height: image.height
};
containDestination = {
x: -(canvas.height * imageAspect) / 2,
y: -(canvas.height / 2),
width: canvas.height * imageAspect,
height: canvas.height
};
coverDestination = {
x: -image.width / 2,
y: -image.height / 2,
width: image.width,
height: image.height
};
raf();
});
function raf() {
draw(image);
step += .005 * direction;
if (step > 1 || step < 0) {
direction *= -1;
}
window.requestAnimationFrame(raf);
}
canvas {
border:1px solid red;
}
img {
max-width:30em;
height:auto;
}
Use your mouse to move the clipping rectangle<br>
<canvas></canvas><br><br>
Original image proportions:<br>
<img src="https://w7.pngwing.com/pngs/660/154/png-transparent-perspective-grid-geometry-grid-perspective-grid-geometric-grid-grid.png" alt="">

Why canvas image size break in Safari?

First, the website width and height is based on the user screen.
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
c.width = window.innerWidth;
c.height = window.innerHeight;
window.addEventListener("resize", function () {
c.width = window.innerWidth;
c.height = window.innerHeight;
})
Then, I am using drawImage
class castle {
constructor() {
const castle = new Image()
castle.src = './Img/castle.png'
castle.onload = () => {
this.scale = 0.5
this.image = castle
this.width = this.image.width * this.scale
this.height = this.image.height * this.scale
this.position = {
x: c.width / 2 - this.width / 2,
y: c.height / 2 - this.height / 2
}
this.center = {
x: c.width / 2,
y: c.height / 2
}
}
}
draw() {
ctx.drawImage(
this.image,
this.position.x,
this.position.y,
this.width,
this.height
)
}
}
Same monitor:
Why the image shown different size between Google Chrome and Safari?
The performance of my project with Google Chrome is fine.
However, the image size with Safari is too too big
I googled:
image to canvas on chrome but not safari
Em...
Should I upload all image to imgur? It's better than open a folder Img in my project?
Safari has issues handling html canvas size. Probably this is why your images are rendering differently.
Canvas size must be: width * height < 16777216.
This happened to me a while ago while debugging one of my projects on Safari Mobile.

Getting ugly image while simulating cover in canvas

I'm trying to display the image using cover simulation in canvas. I've found some cool answer on how to do it.
The thing is when I do it with a large picture, it's being displayed ugly. How to fix that?
Here's my Codepen
HTML
<canvas id="canvas"></canvas>
CSS
canvas {
width: 100%;
height: 100vh;
}
JS
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = 'https://upload.wikimedia.org/wikipedia/commons/0/0f/2010-02-19_3000x2000_chicago_skyline.jpg';
function draw() {
drawImageProp(ctx, this, 0, 0, canvas.width, canvas.height);
//drawImageProp(ctx, this, 0, 0, canvas.width, canvas.height, 0.5, 0.5);
}
/**
* By Ken Fyrstenberg
*
* drawImageProp(context, image [, x, y, width, height [,offsetX, offsetY]])
*
* If image and context are only arguments rectangle will equal canvas
*/
function drawImageProp(ctx, img, x, y, w, h, offsetX, offsetY) {
if (arguments.length === 2) {
x = y = 0;
w = ctx.canvas.width;
h = ctx.canvas.height;
}
/// default offset is center
offsetX = offsetX ? offsetX : 0.5;
offsetY = offsetY ? offsetY : 0.5;
/// keep bounds [0.0, 1.0]
if (offsetX < 0) offsetX = 0;
if (offsetY < 0) offsetY = 0;
if (offsetX > 1) offsetX = 1;
if (offsetY > 1) offsetY = 1;
var iw = img.width,
ih = img.height,
r = Math.min(w / iw, h / ih),
nw = iw * r, /// new prop. width
nh = ih * r, /// new prop. height
cx, cy, cw, ch, ar = 1;
/// decide which gap to fill
if (nw < w) ar = w / nw;
if (nh < h) ar = h / nh;
nw *= ar;
nh *= ar;
/// calc source rectangle
cw = iw / (nw / w);
ch = ih / (nh / h);
cx = (iw - cw) * offsetX;
cy = (ih - ch) * offsetY;
/// make sure source rectangle is valid
if (cx < 0) cx = 0;
if (cy < 0) cy = 0;
if (cw > iw) cw = iw;
if (ch > ih) ch = ih;
/// fill image in dest. rectangle
ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
}
To accomplish this you could use several techniques like HTML/CSS, CSS Only, Jquery or JS/Canvas. For more on this look here.
You do not have to set the width and height of your canvas in HTML like David Skx mentioned. You do have to erase your CSS, remove it completely.
In your JS you should set your canvas size (just define it in 1 place, don't let different languages interfere):
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Window means the whole browser, Use pixels here if you want to limit it to just a canvas and not the whole background
That's all.
You have to specify the width and height in pixels directly on the <canvas>-Element, else it will distort it:
<canvas id="canvas" width="500" height="500"></canvas>
Use JavaScript to measure the window width and height and set it dynamically. Something like:
var canvas = document.getElementById('canvas');
canvas.setAttribute('width', window.innerWidth);
canvas.setAttribute('height', window.innerHeight);
UPDATE:
As Matthijs van Hest pointed out, the width and height attributes on the <canvas>-element are just optional.

How to rotate image in Canvas

I have built a canvas project with https://github.com/petalvlad/angular-canvas-ext
<canvas width="640" height="480" ng-show="activeateCanvas" ap-canvas src="src" image="image" zoomable="true" frame="frame" scale="scale" offset="offset"></canvas>
I am successfully able to zoom and pan the image using following code
scope.zoomIn = function() {
scope.scale *= 1.2;
}
scope.zoomOut = function() {
scope.scale /= 1.2;
}
Additionally I want to rotate the image. any help i can get with which library i can use and how can i do it inside angularjs.
You can rotate an image using context.rotate function in JavaScript.
Here is an example of how to do this:
var canvas = null;
var ctx = null;
var angleInDegrees = 0;
var image;
var timerid;
function imageLoaded() {
image = document.createElement("img");
canvas = document.getElementById("canvas");
ctx = canvas.getContext('2d');
image.onload = function() {
ctx.drawImage(image, canvas.width / 2 - image.width / 2, canvas.height / 2 - image.height / 2);
};
image.src = "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcT7vR66BWT_HdVJpwxGJoGBJl5HYfiSKDrsYrzw7kqf2yP6sNyJtHdaAQ";
}
function drawRotated(degrees) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(degrees * Math.PI / 180);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
ctx.restore();
}
<button onclick="imageLoaded();">Load Image</button>
<div>
<canvas id="canvas" width=360 height=360></canvas><br>
<button onclick="javascript:clearInterval(timerid);
timerid = setInterval(function() {
angleInDegrees += 2;
drawRotated(angleInDegrees);
}, 100);">
Rotate Left
</button>
<button onclick="javascript:clearInterval(timerid);
timerid = setInterval(function() {
angleInDegrees -= 2;
drawRotated(angleInDegrees);
}, 100);">
Rotate Right
</button>
</div>
With curtesy to this page!
Once you can get your hands on the canvas context:
// save the context's co-ordinate system before
// we screw with it
context.save();
// move the origin to 50, 35 (for example)
context.translate(50, 35);
// now move across and down half the
// width and height of the image (which is 128 x 128)
context.translate(64, 64);
// rotate around this point
context.rotate(0.5);
// then draw the image back and up
context.drawImage(logoImage, -64, -64);
// and restore the co-ordinate system to its default
// top left origin with no rotation
context.restore();
To do it in a single state change. The ctx transformation matrix has 6 parts. ctx.setTransform(a,b,c,d,e,f); (a,b) represent the x,y direction and scale the top of the image will be drawn along. (c,d) represent the x,y direction and scale the side of the image will be drawn along. (e,f) represent the x,y location the image will be draw.
The default matrix (identity matrix) is ctx.setTransform(1,0,0,1,0,0) draw the top in the direction (1,0) draw the side in the direction (0,1) and draw everything at x = 0, y = 0.
Reducing state changes improves the rendering speed. When its just a few images that are draw then it does not matter that much, but if you want to draw 1000+ images at 60 frames a second for a game you need to minimise state changes. You should also avoid using save and restore if you can.
The function draws an image rotated and scaled around its center point that will be at x,y. Scale less than 1 makes the images smaller, greater than one makes it bigger. ang is in radians with 0 having no rotation, Math.PI is 180deg and Math.PI*0.5 Math.PI*1.5 are 90 and 270deg respectively.
function drawImage(ctx, img, x, y, scale, ang){
var vx = Math.cos(ang) * scale; // create the vector along the image top
var vy = Math.sin(ang) * scale; //
// this provides us with a,b,c,d parts of the transform
// a = vx, b = vy, c = -vy, and d = vx.
// The vector (c,d) is perpendicular (90deg) to (a,b)
// now work out e and f
var imH = -(img.Height / 2); // get half the image height and width
var imW = -(img.Width / 2);
x += imW * vx + imH * -vy; // add the rotated offset by mutliplying
y += imW * vy + imH * vx; // width by the top vector (vx,vy) and height by
// the side vector (-vy,vx)
// set the transform
ctx.setTransform(vx, vy, -vy, vx, x, y);
// draw the image.
ctx.drawImage(img, 0, 0);
// if needed to restore the ctx state to default but should only
// do this if you don't repeatably call this function.
ctx.setTransform(1, 0, 0, 1, 0, 0); // restores the ctx state back to default
}

Javascript canvas drawImage with greather bounds that original image

I am doing an image zooming/croping in canvas and I ran in a problem. If the image I try to generate from doesn't fit perfectly, then:
in chrome (2nd image) all is Ok
Opera generates the image, but incorrect (3rd)
Firefox and IE give error. IndexSizeError: Index or size is negative or greater than the allowed amount
I need to generate the image correctly, keep ratio, if the image doesn't fit perfectly then gray area should be transparent.
Image dimensions are 259x194. In the example variables have values: sx = 0, sy = 0, sw = 259, sh = 259, x = 0, y = 0, width = 119, height = 119. Problem is that I try to get pixels from image that have vertical position more than 194px (<= 259). If I change sy to image height then the image generates, but proportions are wrong. How to keep them?
ctx.drawImage(resizableBgImage, sx, sy, sw, sh, x, y, width, height);
You can use context transforms (scaling) to scale your original image with transparent background.
save the unscaled context state.
translate to the center of the canvas
draw the scaled image offset by half the image width and height
restore the unscaled context state.
Here's example code and a Demo: http://jsfiddle.net/m1erickson/765Jt/
function drawScaled(img,scaleFactor,canvas,ctx){
var cw=canvas.width;
var ch=canvas.height;
var scaledIW=img.width*scaleFactor;
var scaledIH=img.height*scaleFactor;
// save the unscaled context state
ctx.save();
// translate to the center of the canvas
// Note: the 0,0 coordinate is now center-canvas
ctx.translate(cw/2,ch/2);
// draw the image using the extended drawImage scaling ability
// be sure to offset your canvas drawing by half the image size
// since the 0,0 coordinate is now center-canvas
ctx.drawImage(img,
0,0,img.width,img.height,
-scaledIW/2,-scaledIH/2,scaledIW,scaledIH
);
ctx.restore();
}
I had the same problem, here is what I use:
function patchedDrawImage(source, destination, sx, sy, sw, sh, dx, dy, dw, dh) {
var scaled = dw !== undefined;
if (scaled) {
var wRatio = dw / sw;
var hRatio = dh / sh;
}
if (sx < 0) {
dx -= sx;
sx = 0;
if (scaled) {
dw += wRatio * sx;
sw += sx; // or sw -= (-sx), same for dw, sh, dh
}
}
if (sy < 0) {
dy -= sy;
sy = 0;
if (scaled) {
dh += hRatio * sy;
sh += sy;
}
}
if (scaled) {
if (sx + sw > source.width) {
dw -= wRatio * (sx + sw - source.width);
sw -= (sx + sw - source.width);
}
if (sy + sh > source.height) {
dh -= hRatio * (sy + sh - source.height);
sh -= (sy + sh - source.height);
}
}
destination.getContext("2d").drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh); }
Note that this function changes the arguments, I (and you :)) should change that.

Categories

Resources