diff --git a/src/import/.load-costume.js.swp b/src/import/.load-costume.js.swp new file mode 100644 index 00000000000..d5692b3a83d Binary files /dev/null and b/src/import/.load-costume.js.swp differ diff --git a/src/import/load-costume.js b/src/import/load-costume.js index a355edddc8f..d02b69e1c9e 100644 --- a/src/import/load-costume.js +++ b/src/import/load-costume.js @@ -84,7 +84,140 @@ const loadCostume = function (md5ext, costume, runtime) { }); }; +/** + * Load an "old text" costume's asset into memory asynchronously. + * "Old text" costumes are ones who have a text part from Scratch 1.4. + * See the issue LLK/scratch-vm#672 for more information. + * Do not call this unless there is a renderer attached. + * @param {string} baseMD5ext - the MD5 and extension of the base layer of the costume to be loaded. + * @param {string} textMD5ext - the MD5 and extension of the text layer of the costume to be loaded. + * @param {!object} costume - the Scratch costume object. + * @property {int} skinId - the ID of the costume's render skin, once installed. + * @property {number} rotationCenterX - the X component of the costume's origin. + * @property {number} rotationCenterY - the Y component of the costume's origin. + * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. + * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. + * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error. + */ +const loadOldTextCostume = function (baseMD5ext, textMD5ext, costume, runtime) { + // @todo should [bitmapResolution] (in the documentation comment) not be optional? After all, the resulting image + // is always a bitmap. + + if (!runtime.storage) { + log.error('No storage module present; cannot load costume asset: ', baseMD5ext, textMD5ext); + return Promise.resolve(costume); + } + + const [baseMD5, baseExt] = StringUtil.splitFirst(baseMD5ext, '.'); + const [textMD5, textExt] = StringUtil.splitFirst(textMD5ext, '.'); + + if (baseExt === 'svg' || textExt === 'svg') { + log.error('Old text costumes should never be SVGs'); + return Promise.resolve(costume); + } + + const assetType = runtime.storage.AssetType.ImageBitmap; + + // @todo should this be in a separate function, which could also be used by loadCostume? + const rotationCenter = [ + costume.rotationCenterX / costume.bitmapResolution, + costume.rotationCenterY / costume.bitmapResolution + ]; + + // @todo what should the assetId be? Probably unset, since we'll be doing image processing (which will produce + // a completely new image)? + // @todo what about the dataFormat? This depends on how the image processing is implemented. + + return Promise.all([ + runtime.storage.load(assetType, baseMD5, baseExt), + runtime.storage.load(assetType, textMD5, textExt) + ]) + .then(costumeAssets => ( + new Promise((resolve, reject) => { + const baseImageElement = new Image(); + const textImageElement = new Image(); + + let loadedOne = false; + + const onError = function () { + // eslint-disable-next-line no-use-before-define + removeEventListeners(); + reject(); + }; + const onLoad = function () { + if (loadedOne) { + // eslint-disable-next-line no-use-before-define + removeEventListeners(); + resolve([baseImageElement, textImageElement]); + } else { + loadedOne = true; + } + }; + + const removeEventListeners = function () { + baseImageElement.removeEventListener('error', onError); + textImageElement.removeEventListener('error', onError); + baseImageElement.removeEventListener('load', onLoad); + textImageElement.removeEventListener('load', onLoad); + }; + + baseImageElement.addEventListener('error', onError); + textImageElement.addEventListener('error', onError); + baseImageElement.addEventListener('load', onLoad); + textImageElement.addEventListener('load', onLoad); + + const [baseAsset, textAsset] = costumeAssets; + + baseImageElement.src = baseAsset.encodeDataURI(); + textImageElement.src = textAsset.encodeDataURI(); + }) + )) + .then(imageElements => { + const [baseImageElement, textImageElement] = imageElements; + + const canvas = document.createElement('canvas'); + canvas.width = baseImageElement.width; + canvas.height = baseImageElement.height; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(baseImageElement, 0, 0); + ctx.drawImage(textImageElement, 0, 0); + + return new Promise((resolve, reject) => { + canvas.toBlob(blob => { + const reader = new FileReader(); + const onError = function () { + // eslint-disable-next-line no-use-before-define + removeEventListeners(); + reject(); + }; + const onLoad = function () { + // eslint-disable-next-line no-use-before-define + removeEventListeners(); + costume.assetId = runtime.storage.builtinHelper.cache( + assetType, + runtime.storage.DataFormat.PNG, + new Buffer(reader.result) + ); + costume.skinId = runtime.renderer.createBitmapSkin( + canvas, costume.bitmapResolution, rotationCenter + ); + resolve(costume); + }; + const removeEventListeners = function () { + reader.removeEventListener('error', onError); + reader.removeEventListener('load', onLoad); + }; + reader.addEventListener('error', onError); + reader.addEventListener('load', onLoad); + reader.readAsArrayBuffer(blob); + }, 'image/png'); + }); + }); +}; + module.exports = { loadCostume, - loadCostumeFromAsset + loadCostumeFromAsset, + loadOldTextCostume }; diff --git a/src/serialization/.sb2.js.swp b/src/serialization/.sb2.js.swp new file mode 100644 index 00000000000..f01fd2e6044 Binary files /dev/null and b/src/serialization/.sb2.js.swp differ diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 1102c005e44..66424b8a244 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -14,7 +14,7 @@ const uid = require('../util/uid'); const specMap = require('./sb2_specmap'); const Variable = require('../engine/variable'); -const {loadCostume} = require('../import/load-costume.js'); +const {loadCostume, loadOldTextCostume} = require('../import/load-costume.js'); const {loadSound} = require('../import/load-sound.js'); /** @@ -226,7 +226,14 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) { rotationCenterY: costumeSource.rotationCenterY, skinId: null }; - costumePromises.push(loadCostume(costumeSource.baseLayerMD5, costume, runtime)); + + if ('textLayerMD5' in costumeSource) { + costumePromises.push( + loadOldTextCostume(costumeSource.baseLayerMD5, costumeSource.textLayerMD5, costume, runtime) + ); + } else { + costumePromises.push(loadCostume(costumeSource.baseLayerMD5, costume, runtime)); + } } } // Sounds from JSON