Javascript canvas memory issue in Firefox - javascript

I'm trying to resize an image in multiple steps to increase quality of the final image. This works fine in Opera browser, but i think there is a memory issue in Firefox. After resizing a few images, the following error occours: Error: Image corrupt or truncated: blob:[XY]. So my question: do you see a memory issue in the following code?
loadImage.halfSize = function (i) {
var c = document.createElement("canvas");
c.width = i.width / 2;
c.height = i.height / 2;
var ctx = c.getContext("2d");
ctx.drawImage(i, 0, 0, c.width, c.height);
return c;
};
loadImage.renderImageToCanvas = function (
canvas,
img,
sourceX,
sourceY,
sourceWidth,
sourceHeight,
destX,
destY,
destWidth,
destHeight
) {
canvas.width = sourceWidth;
canvas.height = sourceHeight;
canvas.getContext('2d').drawImage(
img,
sourceX,
sourceY,
sourceWidth,
sourceHeight);
if(sourceWidth <= destWidth && sourceHeight <= destHeight) {
return canvas;
}
while (canvas.width > destWidth * 2) {
canvas = loadImage.halfSize (canvas);
}
var canvasRes = document.createElement("canvas");
canvasRes.width = destWidth;
canvasRes.height = destHeight;
var canvasResCtx = canvasRes.getContext("2d");
canvasResCtx.drawImage(canvas, 0, 0, canvasRes.width, canvasRes.height);
return canvasRes;
};
I can't see a leak, because i'm using local variables everywhere, so memory of the created canvas-elements should be freed after each call to halfSize()?
Thanks for your help!
Ben

Related

return canvas as webp image

How can I return the canvas on this function as webp image?
function capture(video) {
if(scaleFactor == null){
scaleFactor = 1;
}
//var w = video.videoWidth * scaleFactor;
//var h = video.videoHeight * scaleFactor;
var w = 700;
var h = 400;
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, w, h);
var uniq = 'img_' + (new Date()).getTime();
canvas.setAttribute('id', uniq);
return canvas ;
}
Canvas has a method known as .toDataURL(type, encoderOptions).
I this case the following snippet should suffice
canvas.toDataURL('image/webp');
This will give you a data url which is a base64 encoding of the image and will look something like
data:image/webp;base64,UklGRqgCAABXRUJQVlA4WAoAAAAwAAAAxwAAYwAASUNDUBgCAAAAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANkFMUEgPAAAAAQcQEREAUKT//yWi/6lwAFZQOCBSAAAA8AcAnQEqyABkAD5tNplJpCMioSBoAIANiWlu4XXwADuh1VJsmIdVSbJiHVUmyYh1VJsmIdVSbJiHVUmyYh1VJsmIdVSbJhYAAP7/wbAAAAAAAA==
More information for this can be found on https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL

How can I fix this pixelate bugs with canvas NodeJS problems?

I have developed a game in NodeJS where you have to guess an image's name meanwhile the image depixelates.
The problem is that the server uses canvas to pixelate the image but the render don't fit entirely in the frame as you can see :
The pixelate function :
function pixelate(image, ctx, canvas, value) {
var size = value / 100,
w = canvas.width * size,
h = canvas.height * size;
ctx.drawImage(image, 0, 0, w, h);
ctx.msImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height)
}
And the loop were i pixelate the image :
function image_pixel(bool = 1) {
if (bool) {
if (pixel_state > 24) {
restartGame("", false);
} else {
loadImage('image.jpg').then((image) => {
pixel_state += 0.1;
var canvas = createCanvas(image.width, image.height);
var ctx = canvas.getContext('2d');
pixelate(image, ctx, canvas, pixel_state);
io.emit('image', canvas.toDataURL());
})
}
} else { // Image without pixelisation
loadImage('image.jpg').then((image) => {
var canvas = createCanvas(image.width, image.height);
var ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
io.emit('image', canvas.toDataURL());
})
}
};
I tried to round the "w" and "h", the image will fill entirely in frame but some of the images sent will be the same so it'll feel laggy for the user.
Finally found something, I resized all of my pictures to square aspect ratio and then for the "pixel_state" if it's like 100/(2^x) i won't have any ghost pixels almost anymore.

Canvas Rotate, toDataUrl, and then Crop is ruining image quality

I have an image that I'm allowing users to rotate 90 degrees in any direction. Every time they rotate, I use canvas to perform the image manipulations and then save the data returned by the canvas.toDataURL("image/png", 1).
The problem is that the image quality decreases every time I rotate the image.
My end goal is to rotate a rectangular image without losing image quality and also saving the new data url.
function rotateAndSave(image: HTMLImageElement, degrees: number): string {
const imageWidth = image.naturalWidth;
const imageHeight = image.naturalHeight;
const startedHorizontalEndedVertical = imageWidth > imageHeight;
const canvasSize = startedHorizontalEndedVertical ? imageWidth : imageHeight;
const canvas = document.createElement("canvas");
canvas.width = canvasSize;
canvas.height = canvasSize;
const ctx = canvas.getContext("2d");
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// center and rotate canvas
const translateCanvas = canvasSize / 2;
ctx.translate(translateCanvas, translateCanvas);
ctx.rotate(degrees * Math.PI / 180);
// draw from center
const translateImageX = startedHorizontalEndedVertical ? -translateCanvas : (-imageWidth / 2);
const translateImageY = startedHorizontalEndedVertical ? (-imageHeight / 2) : -translateCanvas;
ctx.drawImage(image, translateImageX, translateImageY);
// I got 'cropPlusExport' from another stackoverflow question.
function cropPlusExport(img, cropX, cropY, cropWidth, cropHeight) {
// create a temporary canvas sized to the cropped size
const canvas1 = document.createElement('canvas');
canvas1.width = cropWidth;
canvas1.height = cropHeight;
const ctx1 = canvas1.getContext('2d');
ctx1.setTransform(1, 0, 0, 1, 0, 0);
ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
// use the extended from of drawImage to draw the
// cropped area to the temp canvas
ctx1.drawImage(img, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
return canvas1.toDataURL("image/png", 1);
}
// Start Cropping
let squareImage = new Image();
squareImage.src = canvas.toDataURL("image/png", 1);
squareImage.onload = () => {
const sx = startedHorizontalEndedVertical ? ((canvasSize - imageHeight) / 2) : 0;
const sy = startedHorizontalEndedVertical ? 0 : ((canvasSize - imageWidth) / 2);
const sw = imageHeight;
const sh = imageWidth;
const data = cropPlusExport(squareImage, sx, sy, sw, sh);
// Update DOM via angular binding...
const dataUrl = data.split(",")[1];
this.imageSource = dataUrl;
squareImage = null;
}
example html
<div class="view">
<img [src]="imageSource" />
</div>
Keep in mind that I am cropping to the natural width and height of the image. So, what's weird is that if I don't crop, then the image quality doesn't change but when I do crop, the image quality changes.
Canvas drawing is lossy, and rotating an image induce hard modifications of the pixels. So indeed, if you start always from the last state, you'll end up adding more and more artifacts to your image.
Simply store the original image somewhere and always start from there instead of using the modified version.
// will fire in a loop
img.onload = e => elem.rotateAndSave(1);
const elem = {
// store a copy of the original image
originalimage: img.cloneNode(),
angle: 0,
rotateAndSave(degrees) {
// always use the stored original image
const image = this.originalimage;
// keep track of current transform
this.angle += degrees;
const imageWidth = image.naturalWidth;
const imageHeight = image.naturalHeight;
const startedHorizontalEndedVertical = imageWidth > imageHeight;
const canvasSize = startedHorizontalEndedVertical ? imageWidth : imageHeight;
const canvas = document.createElement("canvas");
canvas.width = canvasSize;
canvas.height = canvasSize;
const ctx = canvas.getContext("2d");
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// center and rotate canvas
const translateCanvas = canvasSize / 2;
ctx.translate(translateCanvas, translateCanvas);
ctx.rotate(this.angle * Math.PI / 180);
// draw from center
const translateImageX = startedHorizontalEndedVertical ? -translateCanvas : (-imageWidth / 2);
const translateImageY = startedHorizontalEndedVertical ? (-imageHeight / 2) : -translateCanvas;
ctx.drawImage(image, translateImageX, translateImageY);
// I got 'cropPlusExport' from another stackoverflow question.
function cropPlusExport(img, cropX, cropY, cropWidth, cropHeight) {
// create a temporary canvas sized to the cropped size
const canvas1 = document.createElement('canvas');
canvas1.width = cropWidth;
canvas1.height = cropHeight;
const ctx1 = canvas1.getContext('2d');
ctx1.setTransform(1, 0, 0, 1, 0, 0);
ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
// use the extended from of drawImage to draw the
// cropped area to the temp canvas
ctx1.drawImage(img, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
return canvas1.toDataURL("image/png", 1);
}
// Start Cropping
let squareImage = new Image();
squareImage.src = canvas.toDataURL("image/png", 1);
squareImage.onload = () => {
const sx = startedHorizontalEndedVertical ? ((canvasSize - imageHeight) / 2) : 0;
const sy = startedHorizontalEndedVertical ? 0 : ((canvasSize - imageWidth) / 2);
const sw = imageHeight;
const sh = imageWidth;
const data = cropPlusExport(squareImage, sx, sy, sw, sh);
// Update DOM via angular binding...
img.src = data;
}
}
};
<img crossorigin src="https://upload.wikimedia.org/wikipedia/commons/5/55/John_William_Waterhouse_A_Mermaid.jpg" id="img">

Fabric Canvas - Image resolution is very poor

I'm implementing Fabricjs in my application for an editing tool. I need to set a high resolution image into the canvas. If I use setBackgroundImage method, it is only working for small sized (Size very less than canvas) images. So I need to reduce the size of the image (but need to keep the ratio) to have something good looking.
My first idea is down sampling which works fine for some pictures but not for all. Here is the code :
Method 1)
var steps = 2;
var imgAspect = img.width / img.height;
var oc = document.createElement('canvas');
octx = oc.getContext('2d');
for(var i = 0; i < steps; i++){
if(i == 0){
oc.width = img.width * 0.5;
oc.height = img.height * 0.5;
octx.drawImage(img, 0, 0, oc.width, oc.height);
}
else{
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);
}
}
_w = canvas.width;
_h = canvas.height;
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5, 0, 0, _w, _h);
var data = ctx.getImageData(0, 0, canvas.width, canvas.height);
var c = document.createElement('canvas');
c.setAttribute('id', '_temp_canvas');
c.width = canvas.width;
c.height = canvas.height;
c.getContext('2d').putImageData(data, 0, 0);
var img = fabric.Image.fromURL(c.toDataURL(), function(img) {
img.left = 00;
img.top = 00;
img.isFixed = true;
img.selectable = false;
canvas.add(img);
c = null;
$('#_temp_canvas').remove();
canvas.renderAll();
});
My second idea, I'm following from here which looks very simple
var canvasCopy = document.createElement("canvas")
var copyContext = canvasCopy.getContext("2d")
var ratio = 1;
if(img.width > canvas.width){
ratio = canvas.width / img.width;
}
else if(img.height > canvas.height){
ratio = canvas.height / img.height;
}
canvasCopy.width = img.width;
canvasCopy.height = img.height;
copyContext.drawImage(img, 0, 0);
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
The issue is none of them producing good resolution. Method 1 works well for some pictures but it is failed for few pictures. If I try the method 2 it is working for the failed pictures from method 1. Can somebody help what is missing regarding the resolution ?
See the fiddle1 and fiddle2 with 2 different images. You can see the difference by calling the method1 and method2 in the _img.onload function to see the difference.
I don't know if this is your case, but for me worked imageSmoothingEnabled=false like
var canvasCopy = document.createElement("canvas")
var copyContext = canvasCopy.getContext("2d")
copyContext.imageSmoothingEnabled = false;
reference http://fabricjs.com/lanczos-webgl

Pixelated resize using canvas with transparent PNG

I want to accomplish a pixelated effect using the canvas option imageSmoothingEnabled=false; so the image "unblurs" on scroll.
Everything works fine until using transparent images namely PNGs. The scaled image is projected, which stays in the background.
Also the image does not get loaded until the user has scrolled a few pixels.
I've found out that the canvas.drawImage() function owns parameters to set the offset. However I haven't found a solution to this.
Demo https://jsfiddle.net/aLjfemru/
var ctx = canvas.getContext('2d'),
img = new Image(),
play = false;
/// turn off image smoothing - this will give the pixelated effect
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
/// wait until image is actually available
img.onload = function(){
image1.src="nf.png";
context.drawImage(image1, 50, 50, 10, 10);
};
img.src = 'https://upload.wikimedia.org/wikipedia/commons/b/bb/Gorgosaurus_BW_transparent.png';
/// MAIN function
function pixelate(v) {
document.getElementById("v").innerHTML = "(v): " + v;
/// if in play mode use that value, else use slider value
var size = v * 0.01;
var w = canvas.width * size;
var h = canvas.height * size;
/// draw original image to the scaled size
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, w, h);
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height);
}
function onScroll() {
$(window).on('scroll', function() {
var y = window.pageYOffset;
if (y > 10) {
y = Math.pow(y, 0.8);
if (y >= 60) {
y = 100;
}
pixelate(y);
}
});
}
onScroll();
Some quick changes to get it happening
Use a second canvas to do the pixelation
Wait for the images to load before doing the rendering.
The onscroll will not fire until you scroll, so when image has loaded call the rendering function to display the image.
canvas.width = innerWidth-20;
ctx = canvas.getContext("2d");
var ctxImage;
const img = new Image;
img.src = 'https://upload.wikimedia.org/wikipedia/commons/b/bb/Gorgosaurus_BW_transparent.png';
/// wait until image is actually available
img.onload = function(){
// I dont knwo what this is for so removed the following two lines
//image1.src="nf.png";
//context.drawImage(image1, 50, 50, 10, 10);
// Create a canvas to match the image
var c = document.createElement("canvas");
canvas.width = Math.min(canvas.width,(c.width = this.naturalWidth));
canvas.height = c.height = this.naturalHeight;
ctxImage = c.getContext("2d");
// changing canvas size resets the state so need to set this again.
ctx.imageSmoothingEnabled = false;
onScroll();
pixelate(100); // call first time
};
ctx.font = "32px arial";
ctx.textAlign = "center";
ctx.fillText("Loading please wait.",ctx.canvas.width /2, ctx.canvas.height / 4);
/// MAIN function
function pixelate(v) {
document.getElementById("v").innerHTML = "(v): " + v;
/// if in play mode use that value, else use slider value
var size = Number(v) * 0.01;
var w = img.width * size;
var h = img.height * size;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctxImage.clearRect(0, 0, ctxImage.canvas.width, ctxImage.canvas.height);
ctxImage.drawImage(img, 0, 0, w, h);
ctx.drawImage(ctxImage.canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height);
}
function onScroll() {
addEventListener("scroll", function() {
var y = window.pageYOffset;
if (y > 10) {
y = Math.pow(y, 0.65);
if (y >= 100) {
y = 100;
}
pixelate(y);
}
});
}
#fix {
position: fixed;
}
html {
height: 2000px;
}
<div id="fix">
<p id="v" value="Animate">1</p><br />
<canvas id="canvas"></canvas>
</div>
This has since been made into an extremely minimalist library, and my PR for PNG support can be found here.
Once it has been merged I will come back and update this answer.
The full code, generalized and simplified from #Blindman67's answer:
/**
* 8bit
*
* A module that converts an image into a pixelated version (just like
* 8bit artwork).
*
* #author rogeriopvl <https://github.com/rogeriopvl>
* #license MIT
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define([], factory);
} else if (typeof exports === "object") {
module.exports = factory();
} else {
root.eightBit = factory();
}
} (this, function () {
// Necessary to hide the original image with PNG transparency
const invisibleCanvas = document.createElement("canvas");
const invisibleCtx = invisibleCanvas.getContext("2d");
/**
* Draws a pixelated version of an image in a given canvas.
* #param {object} canvas - a canvas object
* #param {object} image - an image HTMLElement object
* #param {number} quality - the new quality: between 0 and 100
*/
const eightBit = function (canvas, image, quality) {
quality /= 100;
canvas.width = invisibleCanvas.width = image.width;
canvas.height = invisibleCanvas.height = image.height;
const scaledW = canvas.width * quality;
const scaledH = canvas.height * quality;
const ctx = canvas.getContext("2d");
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
// Draws image scaled to desired quality on the invisible canvas, then
// draws that scaled image on the visible canvas.
ctx.clearRect(0, 0, canvas.width, canvas.height);
invisibleCtx.clearRect(0, 0, invisibleCtx.canvas.width, invisibleCtx.canvas.height);
invisibleCtx.drawImage(image, 0, 0, scaledW, scaledH);
ctx.drawImage(invisibleCtx.canvas, 0, 0, scaledW, scaledH, 0, 0, canvas.width, canvas.height);
};
return eightBit;
}));

Categories

Resources