I'm making a very basic image editor for mobile using the canvas and cordova. Unfortunately, javascript is a pig for memory when the canvas is involved, this causes everything to crash outright on mobile.
I am using cropperjs to handle the cropping. (If you have a better one, please let me know). Cropper only allows images to be retrieved by using base 64 data URLs, which appear to be huge memory wasters. Once the image is cropped, I need to redisplay it for cropping again. A bonus is the image is being turned to black and white based on how it was cropped. That part is working very well, but again doubles the memory size probably since it's ultimately using dataURLs extracted from a canvas.
A button on the page calls this function. This is where the trouble seems to get started.
var originalImage = document.createElement('img');
var cropper;
function finish() {
var data=cropper.getCroppedCanvas().toDataURL();
originalImage.onload = function () {
cropper.replace(originalImage, false);
cropper.clear();
originalImage.onload=undefined;
};
originalImage.src=data;
}
I am guessing the ultimate issue is that the dataURL is so massive, even when not in the DOM it burns memory. This little bit of code causes chrome and firefox to add around 700mb of RAM usage for 600kb photo. Is there a better way to store modified images in memory? Something smaller? Or alternatively, is there a way to make a new temp file and load that? Or am I on the wrong track entirely?
-Edits for Blindman67's answer
const originalImage = document.createElement('canvas');
originalImage.ctx = originalImage.getContext("2d");
var cropper; //cropper is created and destroyed on image load, so I can't use const?
function finish() {
const cropped = cropper.getCroppedCanvas();
originalImage.width = cropped.width;
originalImage.height = cropped.height;
originalImage.ctx.drawImage(cropped,0,0);
cropper.clear();
cropper.replace(originalImage , false); //errors, TypeError: t.match is not a function, cropper.min.js (line 11, col 4244)
}
Code that loads the image from a file upload
$('#file').on('change', function (ev) {
var f = ev.target.files[0];
var fr = new FileReader();
fr.onload = function (ev2) {
console.dir(ev2);
if (cropper !== undefined) {
cropper.destroy();
}
//Probably something wrong with this part
$('#img').on("load",function(){
originalImage.width = this.width;
originalImage.height = this.height;
originalImage.ctx.drawImage(document.getElementById("img"),0,0);
}).attr('src', ev2.target.result);
//^^^^^
//obvious I've been at this awhile, efficiency went down the tubes \/
var image = document.getElementById("img");
var options = {
viewMode: 0,
dragMode: 'crop',
responsive: true,
autoCrop: false,
movable: false,
scalable: false,
zoomable: false,
zoomOnTouch: false,
zoomOnWheel: false,
ready: cropReady
};
cropper = new Cropper(image, options);
};
fr.readAsDataURL(f);
});
The page itself is basically
<div >
<img id="img" style="max-width: 100%; max-height: 100%"/>
</div>
-Edit It looks like using canvases instead of imgs works well enough.
html:
<div class="span-filler">
<img id="img" style="max-width: 100%; max-height: 650px"/>
<canvas id="originalImg" style="display:none;max-width: 100%;max-height: 100%;">Please use Chrome or Firefox
</canvas>
</div>
Scripts
var cropper;
var gBrightness = 0;
var orgImg = document.getElementById("originalImg");
function finish() {
cropper.replace(orgImg, true); //doesn't need to data URL oddly
var data = cropper.getCroppedCanvas();
orgImg.width = data.width;
orgImg.height = data.height;
orgImg.getContext("2d").drawImage(data, 0, 0);
cropper.replace(orgImg.toDataURL("Image/jpeg"), false); //does need it
cropper.clear();
}
$('#file').on('change', function (ev) {
var f = ev.target.files[0];
var fr = new FileReader();
fr.onload = function (ev2) {
console.dir(ev2);
if (cropper !== undefined) {
cropper.destroy();
}
$('#img').attr('src', ev2.target.result);
var image = document.getElementById("img");
var options = {
viewMode: 0,
dragMode: 'crop',
responsive: true,
autoCrop: false,
movable: false,
scalable: false,
zoomable: false,
zoomOnTouch: false,
zoomOnWheel: false,
ready: cropReady
};
cropper = new Cropper(image, options);
};
fr.readAsDataURL(f);
});
function cropReady() {
var data = cropper.getCroppedCanvas();
orgImg.width = data.width;
orgImg.height = data.height;
orgImg.getContext("2d").drawImage(data, 0, 0);
processImage(); //converts to black and white using web workers
}
//still looking for efficiencies here
function processImage(brightness) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = orgImg.width;
canvas.height = orgImg.height;
var imgPixels;
var imgPixelsSrc = orgImg.getContext("2d").getImageData(0, 0, orgImg.width, orgImg.height);
var myWorker = new Worker('js/image_editor/imageWorker.js');
myWorker.onmessage = function (e) {
imgPixels = e.data[0];
gBrightness = e.data[2];
ctx.putImageData(imgPixels, 0, 0);
cropper.replace(canvas.toDataURL("image/jpeg"), true);
};
if (brightness === undefined) {
myWorker.postMessage([imgPixelsSrc, true]);
} else {
myWorker.postMessage([imgPixelsSrc, false, brightness]);
}
}
The canvas can be used as an image and is a HTML image element. There is no need to convert from a canvas to an image just to display the results.
const originalImage = document.createElement('canvas');
originalImage.ctx = originalImage.getContext("2d");
const cropper;
function finish() {
// check documentation to ensure cropper
// is not creating a copy but rather
// is returning just a reference
const cropped = cropper.getCroppedCanvas();
originalImage.width = cropped.width;
originalImage.height = cropped.height;
originalImage.ctx.drawImage(cropped,0,0);
cropper.clear();
}