skia2/modules/canvaskit/tests/util.js
Elliot Evans 2879619a29 Added CanvasKit.MakeImageFromCanvasImageSource which is useful as an alternative to
CanvasKit.MakeImageFromEncoded, when used with Browser APIs for loading/decoding images.

 - `CanvasKit.MakeImageFromCanvasImageSource` takes either an HTMLImageElement,
   SVGImageElement, HTMLVideoElement, HTMLCanvasElement, ImageBitmap, or OffscreenCanvas and returns
   an SkImage. This function is an alternative to `CanvasKit.MakeImageFromEncoded` for creating
   SkImages when loading and decoding images. In the future, codesize of CanvasKit may be able to be
   reduced by removing image codecs in wasm, if browser APIs for decoding images are used along with
   `CanvasKit.MakeImageFromCanvasImageSource` instead of `CanvasKit.MakeImageFromEncoded`.
-  Three usage examples of `CanvasKit.MakeImageFromCanvasImageSource` in core.spec.ts. These 
   examples use browser APIs to decode images including 2d canvas, bitmaprenderer canvas, 
   HTMLImageElement and Blob.
-  Added support for asynchronous callbacks in perfs and tests.

Here are notes on the image decoding approaches we tested and perfed in the process of finding ways 
to use Browser APIs to decode images:

1. pipeline:
  ArrayBuffer → ImageData → ctx.putImageData →
  context.getImageData → Uint8Array → CanvasKit.MakeImage
 Problem: ImageData constructor expects decoded bytes already.

2. interface.js - CanvasKit.ExperimentalCanvas2DMakeImageFromEncoded (async function)
pipeline:
  ArrayBuffer → Blob -> HTMLImageElement ->
  draw on Canvas2d -> context.getImageData → Uint8Array →
  CanvasKit.MakeImage
 Works
⏱ Performance: 3rd place (in my testing locally)

3. interface.js - CanvasKit.ExperimentalCanvas2DMakeImageFromEncoded2 (async function)
  ArrayBuffer → Blob → ImageBitmap → draw on Canvas2d →
  context.getImageData → Uint8Array → CanvasKit.MakeImage
 Works
⏱ Performance: 2nd place (in my testing locally)

4. interface.js - CanvasKit.ExperimentalCanvas2DMakeImageFromEncoded3 (async function)
  ArrayBuffer → Blob → ImageBitmap →
  draw on canvas 1 using bitmaprenderer context →
  draw canvas 1 on canvas 2 using drawImage → context2d.getImageData →
  Uint8Array → CanvasKit.MakeImage
 Works
⏱ Performance: 1st place (in my testing locally) - quite surprising, this in some ways seems to be a more roundabout way of CanvasKit.ExperimentalCanvas2DMakeImageFromEncoded2, but it seems bitmaprenderer context is fairly fast.

Bug: skia:10360
Change-Id: I6fe94b8196dfd1ad0d8929f04bb1697da537ca18
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/295390
Reviewed-by: Kevin Lubick <kjlubick@google.com>
2020-06-15 19:35:09 +00:00

212 lines
7.7 KiB
JavaScript

// The size of the golden images (DMs)
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 600;
const _commonGM = (it, pause, name, callback, assetsToFetchOrPromisesToWaitOn) => {
const fetchPromises = [];
for (const assetOrPromise of assetsToFetchOrPromisesToWaitOn) {
// https://stackoverflow.com/a/9436948
if (typeof assetOrPromise === 'string' || assetOrPromise instanceof String) {
const newPromise = fetch(assetOrPromise)
.then((response) => response.arrayBuffer());
fetchPromises.push(newPromise);
} else if (typeof assetOrPromise.then === 'function') {
fetchPromises.push(assetOrPromise);
} else {
throw 'Neither a string nor a promise ' + assetOrPromise;
}
}
it('draws gm '+name, (done) => {
const surface = CanvasKit.MakeCanvasSurface('test');
expect(surface).toBeTruthy('Could not make surface')
if (!surface) {
done();
return;
}
// if fetchPromises is empty, the returned promise will
// resolve right away and just call the callback.
Promise.all(fetchPromises).then((values) => {
try {
// If callback returns a promise, the chained .then
// will wait for it.
return callback(surface.getCanvas(), values);
} catch (e) {
console.log(`gm ${name} failed with error`, e);
expect(e).toBeFalsy();
debugger;
done();
}
}).then(() => {
surface.flush();
if (pause) {
reportSurface(surface, name, null);
} else {
reportSurface(surface, name, done);
}
}).catch((e) => {
console.log(`could not load assets for gm ${name}`, e);
debugger;
done();
});
})
}
/**
* Takes a name, a callback, and any number of assets or promises. It executes the
* callback (presumably, the test) and reports the resulting surface to Gold.
* @param name {string}
* @param callback {Function}, has two params, the first is a CanvasKit.SkCanvas
* and the second is an array of results from the passed in assets or promises.
* If a given assetOrPromise was a string, the result will be an ArrayBuffer.
* @param assetsToFetchOrPromisesToWaitOn {string|Promise}. If a string, it will
* be treated as a url to fetch and return an ArrayBuffer with the contents as
* a result in the callback. Otherwise, the promise will be waited on and its
* result will be whatever the promise resolves to.
*/
const gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
_commonGM(it, false, name, callback, assetsToFetchOrPromisesToWaitOn);
}
/**
* fgm is like gm, except only tests declared with fgm, force_gm, or fit will be
* executed. This mimics the behavior of Jasmine.js.
*/
const fgm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
_commonGM(fit, false, name, callback, assetsToFetchOrPromisesToWaitOn);
}
/**
* force_gm is like gm, except only tests declared with fgm, force_gm, or fit will be
* executed. This mimics the behavior of Jasmine.js.
*/
const force_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
fgm(name, callback, assetsToFetchOrPromisesToWaitOn);
}
/**
* skip_gm does nothing. It is a convenient way to skip a test temporarily.
*/
const skip_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
console.log(`Skipping gm ${name}`);
// do nothing, skip the test for now
}
/**
* pause_gm is like fgm, except the test will not finish right away and clear,
* making it ideal for a human to manually inspect the results.
*/
const pause_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
_commonGM(fit, true, name, callback, assetsToFetchOrPromisesToWaitOn);
}
const _commonMultipleCanvasGM = (it, pause, name, callback) => {
it(`draws gm ${name} on both CanvasKit and using Canvas2D`, (done) => {
const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
skcanvas._config = 'software_canvas';
const realCanvas = document.getElementById('test');
realCanvas._config = 'html_canvas';
realCanvas.width = CANVAS_WIDTH;
realCanvas.height = CANVAS_HEIGHT;
if (pause) {
console.log('debugging canvaskit version');
callback(realCanvas);
callback(skcanvas);
const png = skcanvas.toDataURL();
const img = document.createElement('img');
document.body.appendChild(img);
img.src = png;
debugger;
return;
}
const promises = [];
for (const canvas of [skcanvas, realCanvas]) {
callback(canvas);
// canvas has .toDataURL (even though skcanvas is not a real Canvas)
// so this will work.
promises.push(reportCanvas(canvas, name, canvas._config));
}
Promise.all(promises).then(() => {
skcanvas.dispose();
done();
}).catch(reportError(done));
});
}
/**
* Takes a name and a callback. It executes the callback (presumably, the test)
* for both a CanvasKit.SkCanvas and a native Canvas2D. The result of both will be
* uploaded to Gold.
* @param name {string}
* @param callback {Function}, has one param, either a CanvasKit.SkCanvas or a native
* Canvas2D object.
*/
const multipleCanvasGM = (name, callback) => {
_commonMultipleCanvasGM(it, false, name, callback);
}
/**
* fmultipleCanvasGM is like multipleCanvasGM, except only tests declared with
* fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This
* mimics the behavior of Jasmine.js.
*/
const fmultipleCanvasGM = (name, callback) => {
_commonMultipleCanvasGM(fit, false, name, callback);
}
/**
* force_multipleCanvasGM is like multipleCanvasGM, except only tests declared
* with fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This
* mimics the behavior of Jasmine.js.
*/
const force_multipleCanvasGM = (name, callback) => {
fmultipleCanvasGM(name, callback);
}
/**
* pause_multipleCanvasGM is like fmultipleCanvasGM, except the test will not
* finish right away and clear, making it ideal for a human to manually inspect the results.
*/
const pause_multipleCanvasGM = (name, callback) => {
_commonMultipleCanvasGM(fit, true, name, callback);
}
/**
* skip_multipleCanvasGM does nothing. It is a convenient way to skip a test temporarily.
*/
const skip_multipleCanvasGM = (name, callback) => {
console.log(`Skipping multiple canvas gm ${name}`);
}
function reportSurface(surface, testname, done) {
// In docker, the webgl canvas is blank, but the surface has the pixel
// data. So, we copy it out and draw it to a normal canvas to take a picture.
// To be consistent across CPU and GPU, we just do it for all configurations
// (even though the CPU canvas shows up after flush just fine).
let pixels = surface.getCanvas().readPixels(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
pixels = new Uint8ClampedArray(pixels.buffer);
const imageData = new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT);
const reportingCanvas = document.getElementById('report');
reportingCanvas.getContext('2d').putImageData(imageData, 0, 0);
reportCanvas(reportingCanvas, testname).then(() => {
// TODO(kjlubick): should we call surface.delete() here?
done();
}).catch(reportError(done));
}
function starPath(CanvasKit, X=128, Y=128, R=116) {
const p = new CanvasKit.SkPath();
p.moveTo(X + R, Y);
for (let i = 1; i < 8; i++) {
let a = 2.6927937 * i;
p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a));
}
p.close();
return p;
}