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:
- Caman would have to accept and operate directly on UInt8ClampedArrays in addition to images, canvases, etc.
- 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:
- Math operations on an array of pixel data
Things that won't work in the worker:
- Any of the canvas native functions (toDataURL, drawImage, etc)
- Loading additional images (for layering, etc)
- 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.
@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!
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();
}
I think I used this version of CamanJS:
https://github.com/zaak/zaak.github.io/tree/master/CamanJS-demo
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.