Skip to content

Commit

Permalink
Add bitmap adapter and remove jimp dependency (#43)
Browse files Browse the repository at this point in the history
Don't depend on this until scratchfoundation/scratch-gui#2575 goes in
  • Loading branch information
fsih authored Jul 12, 2018
1 parent 4cf2422 commit 5f15b96
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 71 deletions.
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ jobs:
include:
- stage: test
script:
- npm run test:lint
- npm run test:unit -- --jobs=4
- npm run test:integration -- --jobs=4
- npm run test
- stage: deploy
node_js: 6
script: npm run build
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
"scripts": {
"build": "npm run clean && webpack --progress --colors --bail",
"clean": "rimraf ./dist",
"test": "npm run test:lint",
"test": "npm run test:lint && npm run test:unit",
"test:lint": "eslint . --ext .js",
"test:unit": "echo 'No unit tests yet'",
"test:integration": "echo 'No integration tests yet'",
"test:unit": "tap ./test/*.js",
"watch": "webpack --progress --colors --watch"
},
"author": "Massachusetts Institute of Technology",
Expand All @@ -20,6 +19,7 @@
"url": "git+ssh://git@github.com/LLK/scratch-svg-renderer.git"
},
"dependencies": {
"base64-js": "1.2.1",
"base64-loader": "1.0.0",
"jimp": "0.2.27",
"minilog": "3.1.0",
Expand All @@ -37,6 +37,7 @@
"lodash.defaultsdeep": "4.6.0",
"mkdirp": "^0.5.1",
"rimraf": "^2.6.1",
"tap": "^11.0.1",
"webpack": "^4.8.0",
"webpack-cli": "^2.0.15"
}
Expand Down
145 changes: 145 additions & 0 deletions src/bitmap-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
const base64js = require('base64-js');

/**
* Adapts Scratch 2.0 bitmaps for use in scratch 3.0
*/
class BitmapAdapter {
/**
* @param {?function} makeImage HTML image constructor. Tests can provide this.
* @param {?function} makeCanvas HTML canvas constructor. Tests can provide this.
*/
constructor (makeImage, makeCanvas) {
this._makeImage = makeImage ? makeImage : () => new Image();
this._makeCanvas = makeCanvas ? makeCanvas : () => document.createElement('canvas');
}

// Returns a canvas with the resized image
resize (image, newWidth, newHeight) {
// Resize in a 2 step process, matching width first, then height, in order
// to preserve nearest-neighbor sampling.
const stretchWidthCanvas = this._makeCanvas();
stretchWidthCanvas.width = newWidth;
stretchWidthCanvas.height = image.height;
let context = stretchWidthCanvas.getContext('2d');
context.imageSmoothingEnabled = false;
context.drawImage(image, 0, 0, stretchWidthCanvas.width, stretchWidthCanvas.height);
const stretchHeightCanvas = this._makeCanvas();
stretchHeightCanvas.width = newWidth;
stretchHeightCanvas.height = newHeight;
context = stretchHeightCanvas.getContext('2d');
context.imageSmoothingEnabled = false;
context.drawImage(stretchWidthCanvas, 0, 0, stretchHeightCanvas.width, stretchHeightCanvas.height);
return stretchHeightCanvas;
}

/**
* Scratch 2.0 had resolution 1 and 2 bitmaps. All bitmaps in Scratch 3.0 are equivalent
* to resolution 2 bitmaps. Therefore, converting a resolution 1 bitmap means doubling
* it in width and height.
* @param {!string} dataURI Base 64 encoded image data of the bitmap
* @param {!function} callback Node-style callback that returns updated dataURI if conversion succeeded
*/
convertResolution1Bitmap (dataURI, callback) {
const image = this._makeImage();
image.src = dataURI;
image.onload = () => {
callback(null, this.resize(image, image.width * 2, image.height * 2).toDataURL());
};
image.onerror = () => {
callback('Image load failed');
};
}

/**
* Given width/height of an uploaded item, return width/height the image will be resized
* to in Scratch 3.0
* @param {!number} oldWidth original width
* @param {!number} oldHeight original height
* @return {object} Array of new width, new height
*/
getResizedWidthHeight (oldWidth, oldHeight) {
const STAGE_WIDTH = 480;
const STAGE_HEIGHT = 360;
const STAGE_RATIO = STAGE_WIDTH / STAGE_HEIGHT;

// If both dimensions are smaller than or equal to corresponding stage dimension,
// double both dimensions
if ((oldWidth <= STAGE_WIDTH) && (oldHeight <= STAGE_HEIGHT)) {
return {width: oldWidth * 2, height: oldHeight * 2};
}

// If neither dimension is larger than 2x corresponding stage dimension,
// this is an in-between image, return it as is
if ((oldWidth <= STAGE_WIDTH * 2) && (oldHeight <= STAGE_HEIGHT * 2)) {
return {width: oldWidth, height: oldHeight};
}

const imageRatio = oldWidth / oldHeight;
// Otherwise, figure out how to resize
if (imageRatio >= STAGE_RATIO) {
// Wide Image
return {width: STAGE_WIDTH * 2, height: STAGE_WIDTH * 2 / imageRatio};
}
// In this case we have either:
// - A wide image, but not with as big a ratio between width and height,
// making it so that fitting the width to double stage size would leave
// the height too big to fit in double the stage height
// - A square image that's still larger than the double at least
// one of the stage dimensions, so pick the smaller of the two dimensions (to fit)
// - A tall image
// In any of these cases, resize the image to fit the height to double the stage height
return {width: STAGE_HEIGHT * 2 * imageRatio, height: STAGE_HEIGHT * 2};
}

/**
* Given bitmap data, resize as necessary.
* @param {ArrayBuffer | string} fileData Base 64 encoded image data of the bitmap
* @param {string} fileType The MIME type of this file
* @returns {Promise} Resolves to resized image data Uint8Array
*/
importBitmap (fileData, fileType) {
let dataURI = fileData;
if (fileData instanceof ArrayBuffer) {
dataURI = this.convertBinaryToDataURI(fileData, fileType);
}
return new Promise((resolve, reject) => {
const image = this._makeImage();
image.src = dataURI;
image.onload = () => {
const newSize = this.getResizedWidthHeight(image.width, image.height);
if (newSize.width === image.width && newSize.height === image.height) {
// No change
resolve(this.convertDataURIToBinary(dataURI));
} else {
const resizedDataURI = this.resize(image, newSize.width, newSize.height).toDataURL();
resolve(this.convertDataURIToBinary(resizedDataURI));
}
};
image.onerror = () => {
reject('Image load failed');
};
});
}

// TODO consolidate with scratch-vm/src/util/base64-util.js
// From https://gist.github.com/borismus/1032746
convertDataURIToBinary (dataURI) {
const BASE64_MARKER = ';base64,';
const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
const base64 = dataURI.substring(base64Index);
const raw = window.atob(base64);
const rawLength = raw.length;
const array = new Uint8Array(new ArrayBuffer(rawLength));

for (let i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
}

convertBinaryToDataURI (arrayBuffer, contentType) {
return `data:${contentType};base64,${base64js.fromByteArray(new Uint8Array(arrayBuffer))}`;
}
}

module.exports = BitmapAdapter;
63 changes: 0 additions & 63 deletions src/bitmap-importer.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
const importBitmap = require('./bitmap-importer');
const SVGRenderer = require('./svg-renderer');
const BitmapAdapter = require('./bitmap-adapter');
const {inlineSvgFonts} = require('./font-inliner');
const convertFonts = require('./font-converter');
// /**
// * Export for NPM & Node.js
// * @type {RenderWebGL}
// */
module.exports = {
BitmapAdapter: BitmapAdapter,
convertFonts: convertFonts,
inlineSvgFonts: inlineSvgFonts,
importBitmap: importBitmap,
SVGRenderer: SVGRenderer
};
102 changes: 102 additions & 0 deletions test/bitmapAdapter_getResized.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Test getResizedWidthHeight function of bitmap adapter class

const test = require('tap').test;
const BitmapAdapter = require('../src/bitmap-adapter');

test('zero', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(0, 0);
t.equals(0, size.width);
t.equals(0, size.height);
t.end();
});

// Double (as if it is bitmap resolution 1)
test('smallImg', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(50, 50);
t.equals(100, size.width);
t.equals(100, size.height);
t.end();
});

// Double (as if it is bitmap resolution 1)
test('stageSizeImage', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(480, 360);
t.equals(960, size.width);
t.equals(720, size.height);
t.end();
});

// Don't resize
test('mediumHeightImage', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(50, 700);
t.equals(50, size.width);
t.equals(700, size.height);
t.end();
});

// Don't resize
test('mediumWidthImage', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(700, 50);
t.equals(700, size.width);
t.equals(50, size.height);
t.end();
});

// Don't resize
test('mediumImage', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(700, 700);
t.equals(700, size.width);
t.equals(700, size.height);
t.end();
});

// Don't resize
test('doubleStageSizeImage', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(960, 720);
t.equals(960, size.width);
t.equals(720, size.height);
t.end();
});

// Fit to stage width
test('wideImage', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(1000, 50);
t.equals(960, size.width);
t.equals(960 / 1000 * 50, size.height);
t.end();
});

// Fit to stage height
test('tallImage', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(50, 1000);
t.equals(720, size.height);
t.equals(720 / 1000 * 50, size.width);
t.end();
});

// Fit to stage height
test('largeImageHeightConstraint', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(1000, 1000);
t.equals(720, size.height);
t.equals(720 / 1000 * 1000, size.width);
t.end();
});

// Fit to stage width
test('largeImageWidthConstraint', t => {
const bitmapAdapter = new BitmapAdapter();
const size = bitmapAdapter.getResizedWidthHeight(2000, 1000);
t.equals(960, size.width);
t.equals(960 / 2000 * 1000, size.height);
t.end();
});

0 comments on commit 5f15b96

Please sign in to comment.