Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading images using workers #298

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"bowser": "2.7.0",
"code": "^5.2.4",
"gl-matrix": "3.1.0",
"hammerjs": "2.0.4",
"minimal-event-emitter": "1.0.0"
Expand Down
160 changes: 113 additions & 47 deletions src/loaders/HtmlImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,38 @@ function HtmlImageLoader(stage) {
throw new Error('Stage type incompatible with loader');
}
this._stage = stage;

const self = this;

this._useWorkers = false;

// This variable will have the response callbacks where the keys will be
// the image URL and the value will be a function
this._imageFetchersCallbacks = {};

function imageFetcherWorkerOnMessage(event) {
self._imageFetchersCallbacks[event.data.imageURL](event);
delete self._imageFetchersCallbacks[event.data.imageURL];
}

// Check what method can use for loading the images
// Check if the browser supports `OffscreenCanvas` and `createImageBitmap`
// else using only fetch
if (
typeof window.OffscreenCanvas === "function" &&
typeof window.createImageBitmap === "function"
) {
this._imageFetcherNoResizeWorker =
new Worker("../workers/fetchImageUsingImageBitmap.js");

this._imageFetcherResizeWorker =
new Worker("../workers/fetchImageUsingOffscreenCanvas.js");

this._imageFetcherNoResizeWorker.onmessage = imageFetcherWorkerOnMessage;
this._imageFetcherResizeWorker.onmessage = imageFetcherWorkerOnMessage;

this._useWorkers = true;
}
}

/**
Expand All @@ -53,64 +85,98 @@ function HtmlImageLoader(stage) {
* @return {function()} A function to cancel loading.
*/
HtmlImageLoader.prototype.loadImage = function(url, rect, done) {
var img = new Image();

// Allow cross-domain image loading.
// This is required to be able to create WebGL textures from images fetched
// from a different domain. Note that setting the crossorigin attribute to
// 'anonymous' will trigger a CORS preflight for cross-domain requests, but no
// credentials (cookies or HTTP auth) will be sent; to do so, the attribute
// would have to be set to 'use-credentials' instead. Unfortunately, this is
// not a safe choice, as it causes requests to fail when the response contains
// an Access-Control-Allow-Origin header with a wildcard. See the section
// "Credentialed requests and wildcards" on:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
img.crossOrigin = 'anonymous';

var x = rect && rect.x || 0;
var y = rect && rect.y || 0;
var width = rect && rect.width || 1;
var height = rect && rect.height || 1;

done = once(done);

img.onload = function() {
if (x === 0 && y === 0 && width === 1 && height === 1) {
done(null, new StaticAsset(img));
var cancelFunction;
var shouldCancel = false;

if (!this._useWorkers) {
var img = new Image();

// Allow cross-domain image loading.
// This is required to be able to create WebGL textures from images fetched
// from a different domain. Note that setting the crossorigin attribute to
// 'anonymous' will trigger a CORS preflight for cross-domain requests, but no
// credentials (cookies or HTTP auth) will be sent; to do so, the attribute
// would have to be set to 'use-credentials' instead. Unfortunately, this is
// not a safe choice, as it causes requests to fail when the response contains
// an Access-Control-Allow-Origin header with a wildcard. See the section
// "Credentialed requests and wildcards" on:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
img.crossOrigin = 'anonymous';

img.onload = function() {
if (x === 0 && y === 0 && width === 1 && height === 1) {
done(null, new StaticAsset(img));
}
else {
x *= img.naturalWidth;
y *= img.naturalHeight;
width *= img.naturalWidth;
height *= img.naturalHeight;

var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var context = canvas.getContext('2d');

context.drawImage(img, x, y, width, height, 0, 0, width, height);

done(null, new StaticAsset(canvas));
}
};

img.onerror = function() {
// TODO: is there any way to distinguish a network error from other
// kinds of errors? For now we always return NetworkError since this
// prevents images to be retried continuously while we are offline.
done(new NetworkError('Network error: ' + url));
};

img.src = url;

cancelFunction = function() {
img.onload = img.onerror = null;
img.src = '';
done.apply(null, arguments);
}
else {
x *= img.naturalWidth;
y *= img.naturalHeight;
width *= img.naturalWidth;
height *= img.naturalHeight;

var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var context = canvas.getContext('2d');

context.drawImage(img, x, y, width, height, 0, 0, width, height);

done(null, new StaticAsset(canvas));
} else {
this._imageFetchersCallbacks[url] = function(event) {
if (shouldCancel) return;
done(null, new StaticAsset(event.data.imageBitmap));
};

cancelFunction = function() {
shouldCancel = true;
done.apply(null, arguments);
}
};

img.onerror = function() {
// TODO: is there any way to distinguish a network error from other
// kinds of errors? For now we always return NetworkError since this
// prevents images to be retried continuously while we are offline.
done(new NetworkError('Network error: ' + url));
};

img.src = url;

function cancel() {
img.onload = img.onerror = null;
img.src = '';
done.apply(null, arguments);
if (x === 0 && y === 0 && width === 1 && height === 1) {
this._imageFetcherNoResizeWorker.postMessage({ imageURL: url });
} else {
const mainCanvas = document.createElement("canvas");
const mainCanvasOffscreen = mainCanvas.transferControlToOffscreen();

this._imageFetcherResizeWorker.postMessage(
{
imageURL: url,
canvas: mainCanvasOffscreen,
x: x,
y: y,
width: width,
height: height
},
[mainCanvasOffscreen]
);
}
}

return cancel;
return cancelFunction;
};

module.exports = HtmlImageLoader;
module.exports = HtmlImageLoader;
42 changes: 42 additions & 0 deletions src/workers/fetchImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @class FetchImage
* @classdesc
*
* Worker that load an image using fetch and return the blob of it
*/

/**
* @typedef {Object} WorkerResult
* @property {String} imageURL the URL of the image for identification
* @property {Blob} imageBlob the image data as a blob
*/

/**
* The listener of the worker
*
* @param {String} imageURL the URL of the image
*
* @returns {WorkerResult}
*/
onmessage = async event => {
postMessage({
imageURL: event.data.imageURL,
imageBlob: await (await fetch(event.data.imageURL)).blob()
});
};
47 changes: 47 additions & 0 deletions src/workers/fetchImageUsingImageBitmap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @class FetchImageUsingImageBitmap
* @classdesc
*
* Worker that load an image using fetch and createImageBitmap the return it
*/

/**
* @typedef {Object} WorkerResult
* @property {String} imageURL the URL of the image for identification
* @property {ImageBitmap} imageBitmap the image as an image bitmap
*/

/**
* The listener of the worker
*
* @param {String} imageURL the URL of the image
*
* @returns {WorkerResult}
*/
onmessage = async event => {
const blob = await (await fetch(event.data.imageURL)).blob();

postMessage({
imageURL: event.data.imageURL,
imageBitmap: await createImageBitmap(
blob,
{ imageOrientation: "flipY" }
)
});
};
64 changes: 64 additions & 0 deletions src/workers/fetchImageUsingOffscreenCanvas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @class FetchImageUsingOffscreenCanvasWorker
* @classdesc
*
* Worker that load an image using fetch and createImageBitmap then resize it
* using an offscreenCanvas and return an imageBitmap of the image
*/

/**
* @typedef {Object} WorkerResult
* @property {String} imageURL the URL of the image for identification
* @property {ImageBitmap} imageBitmap the image as an image bitmap
*/

/**
* The listener of the worker
*
* @param {String} imageURL the URL of the image
* @param {OffscreenCanvas} canvas the canvas to draw the loaded image
* @param {Number} x The x coordinate
* @param {Number} y The y coordinate
* @param {Number} width The width
* @param {Number} height The width
*
* @returns {WorkerResult}
*/
onmessage = async event => {
const blob = await (await fetch(event.data.imageURL)).blob();
let imageBitmap = await createImageBitmap(
blob,
{ imageOrientation: "flipY" }
);

const x = event.data.x;
const y = event.data.y;
const width = event.data.width;
const height = event.data.height;

const canvas = event.data.canvas;
const ctx = canvas.getContext("2d");
ctx.drawImage(b, x, y, width, height, 0, 0, width, height);
imageBitmap = await createImageBitmap(await canvas.convertToBlob());

postMessage({
imageURL: event.data.imageURL,
imageBitmap: imageBitmap
});
};