I'm a server-side dev learning the ropes of client side manipulation, starting with pure JS.
Currently I'm using pure JS to resize the dimensions of images uploaded via the browser.
I'm running into a situation where downsizing a 1018 x 1529 .jpg file to a 400 x 601 .jpeg is producing a file with a bigger size (in bytes). It goes from 70013 bytes to 74823 bytes.
My expectation is that there ought to be a size reduction, not inflation. What is going on, and is there any way to patch this kind of a situation?
Note: one point that especially perplexes me is that each image's compression starts without any prior knowledge of the target's previous compressions. Thus, any quality level below 100 should further degrade the image. This should accordingly always decrease the file size. But that strangely doesn't happen?
If required, my relevant JS code is:
var max_img_width = 400;
var wranges = [max_img_width, Math.round(0.8*max_img_width), Math.round(0.6*max_img_width),Math.round(0.4*max_img_width),Math.round(0.2*max_img_width)];
function prep_image(img_src, text, img_name, target_action, callback) {
var img = document.createElement('img');
var fr = new FileReader();
fr.onload = function(){
var dataURL = fr.result;
img.onload = function() {
img_width = this.width;
img_height = this.height;
img_to_send = resize_and_compress(this, img_width, img_height, "image/jpeg");
callback(text, img_name, target_action, img_to_send);
}
img.src = dataURL;
};
fr.readAsDataURL(img_src);
}
function resize_and_compress(source_img, img_width, img_height, mime_type){
var new_width;
switch (true) {
case img_width < wranges[4]:
new_width = wranges[4];
break;
case img_width < wranges[3]:
new_width = wranges[4];
break;
case img_width < wranges[2]:
new_width = wranges[3];
break;
case img_width < wranges[1]:
new_width = wranges[2];
break;
case img_width < wranges[0]:
new_width = wranges[1];
break;
default:
new_width = wranges[0];
break;
}
var wpercent = (new_width/img_width);
var new_height = Math.round(img_height*wpercent);
var canvas = document.createElement('canvas');//supported
canvas.width = new_width;
canvas.height = new_height;
var ctx = canvas.getContext("2d");
ctx.drawImage(source_img, 0, 0, new_width, new_height);
return dataURItoBlob(canvas.toDataURL(mime_type),mime_type);
}
// converting image data uri to a blob object
function dataURItoBlob(dataURI,mime_type) {
var byteString = atob(dataURI.split(',')[1]);
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);//supported
for (var i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); }
return new Blob([ab], { type: mime_type });
}
If warranted, here's the test image I've used:
Here's the image's original location.
Note that for several other images I tried, the code did behave as expected. It doesn't always screw up the results, but now I can't be sure that it'll always work. Let's stick to pure JS solutions for the scope of this question.
Why Canvas is not the best option to shrink an image file size.
I won't go into too much details, nor in depth explanations, but I will try to explain to you the basics of what you encountered.
Here are a few concepts you need to understand (at least partially).
What is a lossy image format (like JPEG)
What happens when you draw an image to a canvas
What happens when you export a canvas image to an image format
Lossy Image Format.
Image formats can be divided in three categories:
raw Image formats
lossless image formats (tiff, png, gif, bmp, webp ...)
lossy image formats (jpeg, ...)
Lossless image formats generally simply compress the data in a table mapping pixel colors to the pixel positions where this color is used.
On the other hand, Lossy image formats will discard information and produce approximation of the data (artifacts) from the raw image in order to create a perceptively similar image rendering, using less data.
Approximation (artifacts) works because the decompression algorithm knows that it will have to spread the color information on a given area, and thus it doesn't have to keep every pixels information.
But once the algorithm has treated the raw image, and produced the new one, there is no way to find back the lost data.
Drawing an image to the canvas.
When you draw an image on a canvas, the browser will convert the image information to a raw image format.
It won't store any information about what image format was passed to it, and in the case of a lossy image, every pixels contained in the artifacts will become a first class citizen as every other pixels.
Exporting a canvas image
The canvas 2D API has three methods to export its raw data:
getImageData. Which will return the raw pixels RGBA values
toDataURL. Which will apply a compression algorithm corresponding to the MIME you passed as argument, synchronously.
toBlob. Similar to toDataURL, but asynchronously.
The case we are interested in is the one of toDataURL and toBlob along with the "image/jpeg" MIME.
Remember that when calling this method, the browser only sees the current raw pixel data it has on the canvas. So it will apply once again the jpeg algorithm, removing some data, and producing new approximations (artifacts) from this raw image.
So, yes, there is an 0-1 quality parameter available for lossy compression in these methods, so one could think that we could try to know what was the original loss level used to generate the original image, but even then, since we actually produced new image data in the drawing-to-canvas step, the algorithm might not be able to produce a good spreading scheme for these artifacts.
An other thing to take into consideration, mostly for toDataURL, is that browsers have to be as fast as possible when doing these operations, and thus they will generally prefer speed over compression quality.
Alright, the canvas is not good for it. What then?
Not so easy for jpeg images... jpegtran claims it can do a lossless scaling of your jpeg images, so I guess it should be possible to make a js port too, but I don't know any...
Special note about lossless formats
Note that your resizing algorithm can also produce bigger png files, here is an example case, but I'll let the reader guess why this happens:
var ctx= c.getContext('2d');
c.width = 501;
for(var i = 0; i<500; i+=10) {
ctx.moveTo(i+.5, 0);
ctx.lineTo(i+.5, 150);
}
ctx.stroke();
c.toBlob(b=>console.log('original', b.size));
c2.width = 500;
c2.height = (500 / 501) * c.height;
c2.getContext('2d').drawImage(c, 0, 0, c2.width, c2.height);
c2.toBlob(b=>console.log('resized', b.size));
<canvas id="c"></canvas>
<canvas id="c2"></canvas>
This is a recommendation and not really a fix (or a solution).
If you've run into this problem, make sure you compare the file sizes of the two images once you've completed the resize operation. If the new file is larger, then simply fallback to the source image.
Related
When you load a WebGL texture directly from a DOM image, how do you tell if the image has an alpha channel or not? Is there anyway except to guess based on the filename (e.g. "contains .PNG may be RGBA otherwise RGB"). There is a width and height in the DOM image, but nothing I can see that says what format it is. i.e.:
const img = await loadDOMImage(url);
const format = gl.RGBA; //Does this always need to be RGBA? I'm wasting space in most cases where its only RGB
const internalFormat = gl.RGBA;
const type = gl.UNSIGNED_BYTE; //This is guaranteed to be correct, right? No HDR formats supported by the DOM?
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, format, type, img);
My load function looks like this FWIW:
async loadDOMImage(url) {
return new Promise(
(resolve, reject)=>{
const img = new Image();
img.crossOrigin = 'anonymous';
img.addEventListener('load', function() {
resolve(img);
}, false);
img.addEventListener('error', function(err) {
reject(err);
}, false);
img.src = uri;
}
);
}
how do you tell if the image has an alpha channel or not?
You can't. You can only guess.
you could see the URL ends in .png and assume it has alpha. You might be wrong
you could draw image into a 2D canvas then call getImageData, read all the alpha pixels and see if any of them are not 255
const format = gl.RGBA;
Does this always need to be RGBA? I'm wasting space in most cases where its only RGB
It's unlikely to waste space. Most GPUs work best with RGBA values so even if you choose RGB it's unlikely to save space.
const type = gl.UNSIGNED_BYTE;
This is guaranteed to be correct, right?
texImage2D takes the image you pass in and converts it to type and format. It then passes that converted data to the GPU.
No HDR formats supported by the DOM?
That is undefined and browser specific. I know of no HDR image formats supported by any browsers. What image formats a browser supports is up to the browser. For example Firefox and Chrome support animated webp but Safari does not.
A common question some developers have is they want to know if they should turn on blending / transparency for a particular texture. Some mistakenly believe if the texture has alpha they should, otherwise they should not. This is false. Whether blending / transparency should be used is entirely separate from the format of the image and needs to be stored separately.
The same is true with other things related to images. What format to upload the image into the GPU is application and usage specific and has no relation to the format of the image file itself.
I'd like to dynamically downsize some images on my canvas using createjs, and then store the smaller images to be displayed when zooming out of the canvas for performance reasons. Right now, I'm using the following code:
var bitmap = createjs.Bitmap('somefile.png');
// wait for bitmap to load (using preload.js etc.)
var oc = document.createElement('canvas');
var octx = oc.getContext('2d');
oc.width = bitmap.image.width*0.5;
oc.height = bitmap.image.height*0.5;
octx.drawImage(bitmap.image, 0, 0, oc.width, oc.height);
var dataUrl = oc.toDataURL('image/png'); // very expensive
var smallBitmap = new createjs.Bitmap(dataUrl);
This works, but:
The toDataURL operation is very expensive when converting to image/png and too slow to use in practice (and I can't convert to the faster image/jpeg due to the insufficient quality of the output for all settings I tried)
Surely there must be a way to downsize the image without having to resort to separate canvas code, and then do a conversion manually to draw onto the createjs Bitmap object??
I've also tried:
octx.drawImage(bitmap.image, 0, 0, oc.width, oc.height);
var smallBitmap = new createjs.Bitmap(oc);
But although very fast, this doesn't seem to actually work (and in any case I'm having to create a separate canvas element every time to facilitate this.)
I'm wondering if there is a way that I can use drawImage to draw a downsampled version of the bitmap into a createjs Bitmap instance directly without having to go via a separate canvas object or do a conversion to string?
If I understand correctly, internally this is how the createjs cache property works (i.e. uses drawImage internally to write into the DisplayObject) but I'm unable to figure out how use it myself.
You have tagged this post with createjs and easeljs, but your examples show plain Canvas context usage for scaling.
You can use the scale parameter on Bitmap.cache() to get the result you want, then reuse the cacheCanvas as necessary.
// This will create a half-size cache (50%)
// But scale it back up for you when it displays on the stage
var bmp = new createjs.Bitmap(img);
bmp.cache(0, 0, img.width, img.height, 0.5);
// Pull out the generated cache and use it in a new Bitmap
// This will display at the new scaled size.
var bmp2 = new createjs.Bitmap(bmp.cacheCanvas);
// Un-cache the first one to reset it if you want
bmp.uncache();
Here is a fiddle to see it in action: http://jsfiddle.net/lannymcnie/ofdsyn7g/
Note that caching just uses another canvas with a drawImage to scale it down. I definitely would stay away from toDataURL, as it not performant at all.
So, I have table that has a dynamically drawn background on its cells, which gets updated fairly regularly. This table can have hundreds of rows so efficiency is quite important.
My current solution involves drawing the background onto a canvas, then using this drawing as the background image on the cells via a data URI.
cell.css({backgroundImage:'url(' + canvas.toDataURL('image/png')+ ')' });
This works ok, but the html source gets rather large with all the duplication from the encoded image, and ultimately makes some browsers struggle.
Is there a way to somehow reuse the same data URI without duplicating it?
Other ideas I've considered:
- directly use the canvas element as a background with -webkit-canvas or -moz-element, but this does not seem very compatible with internet explorer.
- absolutely position a canvas in each cell and redraw the contents, but this doesn't feel very efficient when we get to hundreds of rows.
Option 1:
Look into using Canvas2Blob.
Basically you can get a URL string by calling URL.createObjectURL(blob); where blob is the object returned from the polyfill canvas.toBlob(function (blob) {...});
The advantage of this is that a Blob is 3/4 the size of a dataURI string because of the encoding, and should not be as memory / processor intensive when using it 100s of times.
Option 2:
If you really want to use canvas.toDataURL('image/png'); then store it to a string variable like
var str = canvas.toDataURL('image/png');
// ...
cell.css({backgroundImage:'url(' + str + ')' });
Depending on the browser implementation, calling .toDataURL() can be a very expensive function call, and since all of the cells need the same background, you'd be better off just storing it to a string.
Is there a way to somehow reuse the same data URI without duplicating it?
No, a Data-URI is just a textual representation of binary data, here a generated PNG (or JPEG) file. If you need to change content the binary data must be changed first, then the data-URI encoded based on that. Therefor it cannot be reused.
toDataURL() is also a very slow process, and using this technique also involves building, encoding, compressing, parsing, decompressing and decoding the bitmap data each and every time, this on top of encoding/decoding the file to and from Base-64 representation.
How to efficiently update a dynamically drawn background on hundreds of elements
Here is what I would do:
Use a single canvas element placed behind the table itself and at the same size (use CSS for placement, but measure the pixel width/height for canvas size).
Make a single function which iterates the table cells to find out their pixel sizes, use this once initially and every time the browser is resized.
Render the rectangles directly to the canvas element
Example
var table = document.querySelector("table"),
canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d");
getArray(); // and on resize if needed
function getArray() {
// todo: iterate table here to find sizes for each cell.
// For simplicity just the table width and height is measured in this example:
var rect = table.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
}
// render for demo -
(function loop() {
render();
requestAnimationFrame(loop)
})();
function render() {
var cw = canvas.width * 0.5,
ch = canvas.height * 0.5;
for(var y = 0; y < 2; y++) {
for(var x = 0; x < 2; x++) {
ctx.fillStyle = "hsl(" + (360*Math.random()) + ",80%,70%)";
ctx.fillRect(x * cw, y * ch, cw, ch);
}
}
}
div {position:relative}
canvas, table {position:absolute; left:0; top:0}
table {border:1px solid #000}
<div>
<canvas></canvas>
<table>
<tr>
<td>Cell</td>
<td>Cell</td>
</tr>
<tr>
<td>Cell</td>
<td>Cell</td>
</tr>
</table>
</div>
Image caching
Another method is to pre-generate an array with images and setting their source to the data-uri.
Then use the image directly as background for the table.
I remove some pixel with clearRect on mouse move on my 200x200px canvas element.
Now, I would like to check if there are no pixels left to remove (all 40.000px are removed), then reset or load a new image.
canvas.onmousemove = function(e) {
x = e.clientX - e.target.offsetLeft;
y = e.clientY - e.target.offsetTop;
context.clearRect(x, y, 20, 20);
}
There are two ways to detect if the canvas is blank:
Compare data-uris
Initially grab a string from the blank canvas before starting to draw to it. If you change the size of the canvas you also need to update this string.
var blankCanvas = canvas.toDataURL('image/bmp');
This will get a BMP (uncompressed) image in browsers which support this format, or default to PNG if not. Obtaining an uncompressed image will increase memory use but also performance when obtaining the string (but slow down when comparing it, if large).
Then do the same when you want to compare:
var currentCanvas = canvas.toDataURL('image/bmp');
if (currentCanvas === blankCanvas) {
/* it's empty */
}
Compare pixels
The only way is to iterate through the pixels to see if all the values are black transparent.
Here is one way: call this method every time you need to check (typically on mouseup event) -
function hasPixels() {
var idata = context.getImageData(0, 0, canvas.width, canvas.height),
buffer = new Uint32Array(idata.data.buffer), // use 32-bit buffer
len = buffer.length,
i;
for(; i < len; i++) {
if (buffer[i] !== 0) return true;
}
return false;
}
This will return true if there are any pixels remaining on canvas.
The 32-bit buffer allows us to check one complete pixel at the time and will perform better that comparing each component.
Notes
Both approaches require CORS requirements in relation to the images be fulfilled for this to work. This is a security mechanism in browsers preventing extraction of pixels from canvas cross-origin.
Which approach is faster depends on various factors such as the size of the canvas, browser implementation and so forth. You would need to check for your scenario. I would perhaps add a coin to the second solution for general usage together with the benefit of low resource consumption. But for a 200x200 canvas both should work well.
Anyone know how I would convert bytes which are sent via a websocket (from a C# app) to an image? I then want to draw the image on a canvas. I can see two ways of doing this:
Somehow draw the image on the canvas in byte form without converting
it.
Convert the bytes to a base64 string somehow in javascript then
draw.
Here's my function which receives the bytes for drawing:
function draw(imgData) {
var img=new Image();
img.onload = function() {
cxt.drawImage(img, 0, 0, canvas.width, canvas.height);
};
// What I was using before...
img.src = "data:image/jpeg;base64,"+imgData;
}
I was receiving the image already converted as a base64 string before, but after learning that sending the bytes is smaller in size (30% smaller?) I would prefer to get this working. I should also mention that the image is a jpeg.
Anyone know how I would do it? Thanks for the help. :)
I used this in the end:
function draw(imgData, frameCount) {
var r = new FileReader();
r.readAsBinaryString(imgData);
r.onload = function(){
var img=new Image();
img.onload = function() {
cxt.drawImage(img, 0, 0, canvas.width, canvas.height);
}
img.src = "data:image/jpeg;base64,"+window.btoa(r.result);
};
}
I needed to read the bytes into a string before using btoa().
If your image is really a jpeg already, you can just convert the received data to a base64 stream. Firefox and Webkit browsers (as I recall) have a certain function, btoa(). It converts the input string to a base64 encoded string. Its counterpart is atob() that does the opposite.
You could use it like the following:
function draw(imgData){
var b64imgData = btoa(imgData); //Binary to ASCII, where it probably stands for
var img = new Image();
img.src = "data:image/jpeg;base64," + b64imgData;
document.body.appendChild(img); //or append it to something else, just an example
}
If the browser you target (IE, for example) isn't Firefox or a Webkit one, you can use one of the multiple conversion function lying around the internet (good one, it also provides statistics of performances in multiple browsers, if you're interested :)