diff --git a/src/import/load-costume.js b/src/import/load-costume.js index bf8505b7a..31eac9dbc 100644 --- a/src/import/load-costume.js +++ b/src/import/load-costume.js @@ -11,12 +11,8 @@ const loadVector_ = function (costume, costumeAsset, runtime, rotationCenter, op svgString = runtime.v2SvgAdapter.toString(); // Put back into storage const storage = runtime.storage; - costumeAsset.encodeTextData(svgString, storage.DataFormat.SVG); - costume.assetId = storage.builtinHelper.cache( - storage.AssetType.ImageVector, - storage.DataFormat.SVG, - costumeAsset.data - ); + costume.asset.encodeTextData(svgString, storage.DataFormat.SVG, true); + costume.assetId = costume.asset.assetId; costume.md5 = `${costume.assetId}.${costume.dataFormat}`; } // createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's @@ -63,11 +59,14 @@ const loadBitmap_ = function (costume, costumeAsset, runtime, rotationCenter) { } else if (dataURI) { // Put back into storage const storage = runtime.storage; - costume.assetId = storage.builtinHelper.cache( + costume.asset = storage.createAsset( storage.AssetType.ImageBitmap, storage.DataFormat.PNG, - runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI) + runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI), + null, + true // generate md5 ); + costume.assetId = costume.asset.assetId; costume.md5 = `${costume.assetId}.${costume.dataFormat}`; } // Regardless of if conversion succeeds, convert it to bitmap resolution 2, @@ -163,9 +162,12 @@ const loadCostume = function (md5ext, costume, runtime, optVersion) { const md5 = idParts[0]; const ext = idParts[1].toLowerCase(); const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap; - - return runtime.storage.load(assetType, md5, ext).then(costumeAsset => { - costume.dataFormat = ext; + costume.dataFormat = ext; + return ( + (costume.asset && Promise.resolve(costume.asset)) || + runtime.storage.load(assetType, md5, ext) + ).then(costumeAsset => { + costume.asset = costumeAsset; return loadCostumeFromAsset(costume, costumeAsset, runtime, optVersion); }) .catch(e => { diff --git a/src/import/load-sound.js b/src/import/load-sound.js index 8f4f7a1a6..548eb3276 100644 --- a/src/import/load-sound.js +++ b/src/import/load-sound.js @@ -55,11 +55,14 @@ const loadSound = function (sound, runtime, sprite) { const idParts = StringUtil.splitFirst(sound.md5, '.'); const md5 = idParts[0]; const ext = idParts[1].toLowerCase(); - return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext) - .then(soundAsset => { - sound.dataFormat = ext; - return loadSoundFromAsset(sound, soundAsset, runtime, sprite); - }); + sound.dataFormat = ext; + return ( + (sound.asset && Promise.resolve(sound.asset)) || + runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext) + ).then(soundAsset => { + sound.asset = soundAsset; + return loadSoundFromAsset(sound, soundAsset, runtime, sprite); + }); }; module.exports = { diff --git a/src/serialization/deserialize-assets.js b/src/serialization/deserialize-assets.js index 6a49b9a2e..a3bb6e408 100644 --- a/src/serialization/deserialize-assets.js +++ b/src/serialization/deserialize-assets.js @@ -22,37 +22,29 @@ const deserializeSound = function (sound, runtime, zip, assetFileName) { return Promise.resolve(null); } - const assetId = sound.assetId; - - // TODO Is there a faster way to check that this asset - // has already been initialized? - if (storage.get(assetId)) { - // This sound has already been cached. - return Promise.resolve(null); - } if (!zip) { // Zip will not be provided if loading project json from server return Promise.resolve(null); } + const soundFile = zip.file(fileName); if (!soundFile) { log.error(`Could not find sound file associated with the ${sound.name} sound.`); return Promise.resolve(null); } - const dataFormat = sound.dataFormat.toLowerCase() === 'mp3' ? - storage.DataFormat.MP3 : storage.DataFormat.WAV; + if (!JSZip.support.uint8array) { log.error('JSZip uint8array is not supported in this browser.'); return Promise.resolve(null); } - return soundFile.async('uint8array').then(data => { - storage.builtinHelper.cache( - storage.AssetType.Sound, - dataFormat, - data, - assetId - ); - }); + const dataFormat = sound.dataFormat.toLowerCase() === 'mp3' ? + storage.DataFormat.MP3 : storage.DataFormat.WAV; + return soundFile.async('uint8array').then(data => storage.createAsset( + storage.AssetType.Sound, + dataFormat, + data, + sound.assetId + )); }; /** @@ -79,14 +71,6 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName) { return Promise.resolve(null); } - - // TODO Is there a faster way to check that this asset - // has already been initialized? - if (storage.get(assetId)) { - // This costume has already been cached. - return Promise.resolve(null); - } - if (!zip) { // Zip will not be provided if loading project json from server return Promise.resolve(null); } @@ -110,15 +94,13 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName) { return Promise.resolve(null); } - return costumeFile.async('uint8array').then(data => { - storage.builtinHelper.cache( - assetType, - // TODO eventually we want to map non-png's to their actual file types? - costumeFormat, - data, - assetId - ); - }); + return costumeFile.async('uint8array').then(data => storage.createAsset( + assetType, + // TODO eventually we want to map non-png's to their actual file types? + costumeFormat, + data, + assetId + )); }; module.exports = { diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index e5f8dd453..fb5653355 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -452,7 +452,11 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) // the file name of the costume should be the baseLayerID followed by the file ext const assetFileName = `${costumeSource.baseLayerID}.${ext}`; costumePromises.push(deserializeCostume(costume, runtime, zip, assetFileName) - .then(() => loadCostume(costume.md5, costume, runtime, 2 /* optVersion */))); + .then(asset => { + costume.asset = asset; + return loadCostume(costume.md5, costume, runtime, 2 /* optVersion */); + }) + ); } } // Sounds from JSON @@ -486,7 +490,10 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) // followed by the file ext const assetFileName = `${soundSource.soundID}.${ext}`; soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName) - .then(() => loadSound(sound, runtime, sprite))); + .then(asset => { + sound.asset = asset; + return loadSound(sound, runtime, sprite); + })); } } diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index ad5982f78..7012aa949 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -844,7 +844,10 @@ const parseScratchObject = function (object, runtime, extensions, zip) { // any translation that needs to happen will happen in the process // of building up the costume object into an sb3 format return deserializeCostume(costume, runtime, zip) - .then(() => loadCostume(costumeMd5Ext, costume, runtime)); + .then(asset => { + costume.asset = asset; + return loadCostume(costumeMd5Ext, costume, runtime); + }); // Only attempt to load the costume after the deserialization // process has been completed }); @@ -869,7 +872,10 @@ const parseScratchObject = function (object, runtime, extensions, zip) { // any translation that needs to happen will happen in the process // of building up the costume object into an sb3 format return deserializeSound(sound, runtime, zip) - .then(() => loadSound(sound, runtime, sprite)); + .then(asset => { + sound.asset = asset; + return loadSound(sound, runtime, sprite); + }); // Only attempt to load the sound after the deserialization // process has been completed. }); diff --git a/src/serialization/serialize-assets.js b/src/serialization/serialize-assets.js index ac444f11a..c1d063949 100644 --- a/src/serialization/serialize-assets.js +++ b/src/serialization/serialize-assets.js @@ -16,12 +16,10 @@ const serializeAssets = function (runtime, assetType, optTargetId) { const currAssets = currTarget.sprite[assetType]; for (let j = 0; j < currAssets.length; j++) { const currAsset = currAssets[j]; - const assetId = currAsset.assetId; - const storage = runtime.storage; - const storedAsset = storage.get(assetId); + const asset = currAsset.asset; assetDescs.push({ - fileName: `${assetId}.${storedAsset.dataFormat}`, - fileContent: storedAsset.data}); + fileName: `${asset.assetId}.${asset.dataFormat}`, + fileContent: asset.data}); } } return assetDescs; diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index f02f41a0e..e922d4d89 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -146,14 +146,14 @@ class Sprite { newSprite.costumes = this.costumes_.map(costume => { const newCostume = Object.assign({}, costume); - const costumeAsset = this.runtime.storage.get(costume.assetId); + const costumeAsset = costume.asset; assetPromises.push(loadCostumeFromAsset(newCostume, costumeAsset, this.runtime)); return newCostume; }); newSprite.sounds = this.sounds.map(sound => { const newSound = Object.assign({}, sound); - const soundAsset = this.runtime.storage.get(sound.assetId); + const soundAsset = sound.asset; assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime, newSprite)); return newSound; }); diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 43a5e34a9..96ab30411 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -690,11 +690,14 @@ class VirtualMachine extends EventEmitter { // is updated as below. sound.format = ''; const storage = this.runtime.storage; - sound.assetId = storage.builtinHelper.cache( + sound.asset = storage.createAsset( storage.AssetType.Sound, storage.DataFormat.WAV, - soundEncoding + soundEncoding, + null, + true // generate md5 ); + sound.assetId = sound.asset.assetId; sound.dataFormat = storage.DataFormat.WAV; sound.md5 = `${sound.assetId}.${sound.dataFormat}`; } @@ -731,16 +734,16 @@ class VirtualMachine extends EventEmitter { * a dataURI if it's a PNG or JPG, or null if it couldn't be found or decoded. */ getCostume (costumeIndex) { - const id = this.editingTarget.getCostumes()[costumeIndex].assetId; - if (!id || !this.runtime || !this.runtime.storage) return null; - const format = this.runtime.storage.get(id).dataFormat; + const asset = this.editingTarget.getCostumes()[costumeIndex].asset; + if (!asset || !this.runtime || !this.runtime.storage) return null; + const format = asset.dataFormat; if (format === this.runtime.storage.DataFormat.SVG) { - return this.runtime.storage.get(id).decodeText(); + return asset.decodeText(); } else if (format === this.runtime.storage.DataFormat.PNG || format === this.runtime.storage.DataFormat.JPG) { - return this.runtime.storage.get(id).encodeDataURI(); + return asset.encodeDataURI(); } - log.error(`Unhandled format: ${this.runtime.storage.get(id).dataFormat}`); + log.error(`Unhandled format: ${asset.dataFormat}`); return null; } @@ -781,14 +784,17 @@ class VirtualMachine extends EventEmitter { const reader = new FileReader(); reader.addEventListener('loadend', () => { const storage = this.runtime.storage; - costume.assetId = storage.builtinHelper.cache( - storage.AssetType.ImageBitmap, - storage.DataFormat.PNG, - Buffer.from(reader.result) - ); costume.dataFormat = storage.DataFormat.PNG; costume.bitmapResolution = bitmapResolution; costume.size = [bitmap.width, bitmap.height]; + costume.asset = storage.createAsset( + storage.AssetType.ImageBitmap, + costume.dataFormat, + Buffer.from(reader.result), + null, // id + true // generate md5 + ); + costume.assetId = costume.asset.assetId; costume.md5 = `${costume.assetId}.${costume.dataFormat}`; this.emitTargetsUpdate(); }); @@ -812,16 +818,19 @@ class VirtualMachine extends EventEmitter { costume.size = this.runtime.renderer.getSkinSize(costume.skinId); } const storage = this.runtime.storage; - costume.assetId = storage.builtinHelper.cache( - storage.AssetType.ImageVector, - storage.DataFormat.SVG, - (new TextEncoder()).encode(svg) - ); // If we're in here, we've edited an svg in the vector editor, // so the dataFormat should be 'svg' costume.dataFormat = storage.DataFormat.SVG; - costume.md5 = `${costume.assetId}.${costume.dataFormat}`; costume.bitmapResolution = 1; + costume.asset = storage.createAsset( + storage.AssetType.ImageVector, + costume.dataFormat, + (new TextEncoder()).encode(svg), + null, + true // generate md5 + ); + costume.assetId = costume.asset.assetId; + costume.md5 = `${costume.assetId}.${costume.dataFormat}`; this.emitTargetsUpdate(); } diff --git a/test/integration/offline-custom-assets.js b/test/integration/offline-custom-assets.js index 929500971..c67deb852 100644 --- a/test/integration/offline-custom-assets.js +++ b/test/integration/offline-custom-assets.js @@ -10,7 +10,6 @@ const test = require('tap').test; const AdmZip = require('adm-zip'); const ScratchStorage = require('scratch-storage'); const VirtualMachine = require('../../src/index'); -const StringUtil = require('../../src/util/string-util'); const projectUri = path.resolve(__dirname, '../fixtures/offline-custom-assets.sb2'); const projectZip = AdmZip(projectUri); @@ -52,11 +51,8 @@ test('offline-custom-assets', t => { t.equals(costumes.length, 1); const customCostume = costumes[0]; t.equals(customCostume.name, 'A_Test_Costume'); - const costumeMd5Ext = customCostume.md5; - const costumeIdParts = StringUtil.splitFirst(costumeMd5Ext, '.'); - const costumeMd5 = costumeIdParts[0]; - const storedCostume = vm.runtime.storage.get(costumeMd5); + const storedCostume = customCostume.asset; t.type(storedCostume, 'object'); t.deepEquals(storedCostume.data, costumeData); @@ -64,10 +60,7 @@ test('offline-custom-assets', t => { t.equals(sounds.length, 1); const customSound = sounds[0]; t.equals(customSound.name, 'A_Test_Recording'); - const soundMd5Ext = customSound.md5; - const soundIdParts = StringUtil.splitFirst(soundMd5Ext, '.'); - const soundMd5 = soundIdParts[0]; - const storedSound = vm.runtime.storage.get(soundMd5); + const storedSound = customSound.asset; t.type(storedSound, 'object'); t.deepEquals(storedSound.data, soundData);