meltingice / CamanJS

Javascript HTML5 (Ca)nvas (Man)ipulation

Home Page:http://camanjs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support WebWorker Environment

jocooler opened this issue · comments

It would be great if Caman could run in a webworker. I know that's where Caman came from, but it seems challenging enough to implement on the current codebase that is warrants creating an issue. Some of this was discussed in #173, but this is an official issue/request for the feature.

Couple of thoughts off the top of my head:

  1. Caman would have to accept and operate directly on UInt8ClampedArrays in addition to images, canvases, etc.
  2. Caman would have to postMessage back a UInt8ClampedArray instead of rendering to canvas.

Adding typeof document !== "undefined" in a few places made the code not have errors in a Worker, but without UInt8ClampedArray support, it won't process any images.

Since there's no access to the DOM/creating elements, all the Caman.prototype.initXXXXX functions run into errors.

Things that will work in a worker:

  1. Math operations on an array of pixel data

Things that won't work in the worker:

  1. Any of the canvas native functions (toDataURL, drawImage, etc)
  2. Loading additional images (for layering, etc)
  3. Any image decoding/encoding, or we'd have to include our own PNG/JPG/WebP codecs

I made some progress on this (initializes in the worker from a UInt8ClampedArray) but realized I've been working from the 4.1.1 zip I got from the website, not the latest github pull.

So it's back to the ol' drawing board for a while.

Ok, so I have a branch that is mostly working. Layering and resizing/cropping don't work at present but I'm working on that.

https://github.com/jocooler/CamanJS/tree/webworker-environment

The big thing that worker implementations must do is define an exposeImageData(data, width, height) function that will handle the processed data.

@jocooler Any progress on this?

@EmanH - I'm working on other projects for the time being. Figuring out how to decode/encode the image in the webworker is hard, potentially more computationally expensive than the savings from forking the webworker. And beyond my skill at this point. And a lot of this would be better handled by the GPU. So no further progress and I'm not actively working on it now.

@jocooler I ended up getting it working. Crop and Vignette is done in the browser, the rest in the webworker.

commented

@EmanH is there a chance to add a demo or some explanation on how u managed to run inside the worker ?

@EmanH: Would you be so kind to share your solution? Thanks!

@ctf0 and @av01d

Something like this:

editor = Caman(selector);

$scope.settings = {
	brightness: 0,
	contrast: 0,
	saturation: 0,
	vibrance: 0,
	exposure: 0,
	hue: 0,
	sepia: 0,
	gamma: 0,
	blur: 0,
	size: 0,
	strength: 40,
	blacks: 50,
	shadows: 80,
	midtones: 180,
	highlights: 255,
	red: 0,
	green: 0,
	blue: 0
};

var original = angular.copy($scope.settings);

if (window.Worker) {
	var workerProcess = new Worker(ROOT + 'js/admin/imageProcessor.js');

	var keys = ['image', 'tabs', 'settings', 'cropped', 'rotate'],
		output = {};
	_.each(keys, function(key) {
		output[key] = $scope[key];
	});

	output.width = ($scope.cropped.is && $scope.cropped.curCrop.w) || dimensions.width
	output.height = ($scope.cropped.is && $scope.cropped.curCrop.h) || dimensions.height;

	workerProcess.postMessage({ scope: angular.toJson(output), image: editor.imageData, original: original });

	workerProcess.onmessage = function(e) {

		$scope.$apply(function() {

			sv.message = e.data.message;
			if (e.data.percent) {
				sv.percent = percent + ((e.data.percent / 100) * (65 - percent));
			}

			if (e.data.type == 'finished') {

				sv.percent = 65;

				var newImgData = new ImageData(e.data.image, editor.imageData.width, editor.imageData.height);

				editor.imageData.data = e.data.image;
				editor.pixelData = e.data.image;

				editor.render(function() {

					sv.percent = 70;

					db.q('saveImageEdit', {
						base64: this.toBase64(),
						original: $scope.image.kp_imageID,
						overwrite: overwrite
					}, undefined, undefined, function(e) {
						if (e.lengthComputable) {
							sv.percent = 70 + ((e.loaded / e.total) * 30);
						}
					}).then(function(r) {
						sv.percent = 100;
						sv.message = "Upload Complete";
						workerProcess.terminate();
						if (r.image) {
							ngModel.$setViewValue(r.image.kp_imageID);
							$scope.ngModel = r.image.kp_imageID;
							ngModel.$render();
						}
						def.resolve(r);
					});


				}, newImgData);
			}

		});
	}

}

imageProcessor.js

var window = {Uint8Array: 1};
importScripts('caman.zaak.full.min.js');
importScripts('underscore-min.js');

this.exposeImageData = function (imgData){
	onmessage = undefined;
	postMessage({ type:"finished", message: "Processing Complete. Now saving...", image: imgData });
}

onmessage = function(e) {

    var data = e.data,
        $scope = JSON.parse(data.scope),
		original = data.original,
		editor;

    function apply() {

        var numFilters = 0;

        var st = $scope.settings;
        _.each($scope.tabs, function(itm) {
            if (itm.label == 'Vignette') {

                if (st.size > 0) {
                    editor.vignette(st.size + '%', st.strength);
                    numFilters++;
                }

            } else if (itm.link == 'bal') {

                if (st.red !== 0 || st.green !== 0 || st.blue !== 0) {
                    editor.channels({
                        red: st.red,
                        green: st.green,
                        blue: st.blue
                    });
                    numFilters++;
                }

            } else if (itm.link == 'levels') {

                var changed = 0;
                _.each(itm.adjustments, function(adj) {
                    if (st[adj[0]] !== original[adj[0]]) changed = 1;
                });

                if (changed) {
                    editor.curves('rgb', [50, st.blacks], [80, st.shadows], [180, st.midtones], [255, st.highlights]);
                    numFilters++;
                }

            } else if (itm.label != 'Crop' && itm.label != 'Rotate') {
                _.each(itm.adjustments, function(key) {
                    var fn = key[0] == 'blur' ? 'stackBlur' : key[0];
                    if (st[key[0]] !== 0) {
                        editor[fn](st[key[0]]);
                        numFilters++;
                    }
                });
            }
        });

		editor.render();

        return numFilters;

    }

    alreadySaving = 0;

    function saveFullRes() {

        if (!alreadySaving) {

            alreadySaving = 1;

            editor = Caman(data.image.data);

			setTimeout(function(){

                var numFilters = 0,
                    numFinished = 0,
                    nowSaving = 0;

                Caman.Event.listen("processStart", function(job) {
                    postMessage({
                        type: 'status',
                        message: "Applying " + job.name,
                        percent: numFilters && (numFinished / numFilters) * 100 || 0
                    });
                });

                numFilters = apply();

                Caman.Event.listen(editor, "processComplete", function(job) {

					numFinished++;

					var percent = numFilters && (numFinished / numFilters) * 100 || 0;

					postMessage({
                        type: 'status',
                        message: "Finished " + job.name,
                        percent: percent
                    });

					if(percent == 100) {
						setTimeout(function(){
							editor = undefined;
							data = undefined;
						}, 500);
					}

                });

			}, 500);
        }
    }

    saveFullRes();

}

Not sure if I would recommend this though. Uploading uncompressed image data is slow. Probably could get a JavaScript jpg library to compress it before save, but I haven't looked into that.

@EmanH - maybe you could (optionally) post back to the main thread and draw on an offscreen canvas - then you can at least use PNG.