Spaces:
Runtime error
Runtime error
| import {BitmapAdapter, sanitizeSvg} from 'scratch-svg-renderer'; | |
| import randomizeSpritePosition from './randomize-sprite-position.js'; | |
| import bmpConverter from './bmp-converter'; | |
| import gifDecoder from './gif-decoder'; | |
| import fixSVG from './tw-svg-fixer'; | |
| import convertAudioToWav from './tw-convert-audio-wav.js'; | |
| /** | |
| * Extract the file name given a string of the form fileName + ext | |
| * @param {string} nameExt File name + extension (e.g. 'my_image.png') | |
| * @return {string} The name without the extension, or the full name if | |
| * there was no '.' in the string (e.g. 'my_image') | |
| */ | |
| const extractFileName = function (nameExt) { | |
| // There could be multiple dots, but get the stuff before the first . | |
| const nameParts = nameExt.split('.', 1); // we only care about the first . | |
| return nameParts[0]; | |
| }; | |
| /** | |
| * Handle a file upload given the input element that contains the file, | |
| * and a function to handle loading the file. | |
| * @param {Input} fileInput The <input/> element that contains the file being loaded | |
| * @param {Function} onload The function that handles loading the file | |
| * @param {Function} onerror The function that handles any error loading the file | |
| */ | |
| const handleFileUpload = function (fileInput, onload, onerror) { | |
| const readFile = (i, files) => { | |
| if (i === files.length) { | |
| // Reset the file input value now that we have everything we need | |
| // so that the user can upload the same sound multiple times if | |
| // they choose | |
| fileInput.value = null; | |
| return; | |
| } | |
| const file = files[i]; | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| const fileType = file.type; | |
| const fileName = extractFileName(file.name); | |
| onload(reader.result, fileType, fileName, i, files.length); | |
| readFile(i + 1, files); | |
| }; | |
| reader.onerror = onerror; | |
| reader.readAsArrayBuffer(file); | |
| }; | |
| readFile(0, fileInput.files); | |
| }; | |
| /** | |
| * @typedef VMAsset | |
| * @property {string} name The user-readable name of this asset - This will | |
| * automatically get translated to a fresh name if this one already exists in the | |
| * scope of this vm asset (e.g. if a sound already exists with the same name for | |
| * the same target) | |
| * @property {string} dataFormat The data format of this asset, typically | |
| * the extension to be used for that particular asset, e.g. 'svg' for vector images | |
| * @property {string} md5 The md5 hash of the asset data, followed by '.'' and dataFormat | |
| * @property {string} The md5 hash of the asset data // TODO remove duplication.... | |
| */ | |
| /** | |
| * Create an asset (costume, sound) with storage and return an object representation | |
| * of the asset to track in the VM. | |
| * @param {ScratchStorage} storage The storage to cache the asset in | |
| * @param {AssetType} assetType A ScratchStorage AssetType indicating what kind of | |
| * asset this is. | |
| * @param {string} dataFormat The format of this data (typically the file extension) | |
| * @param {UInt8Array} data The asset data buffer | |
| * @return {VMAsset} An object representing this asset and relevant information | |
| * which can be used to look up the data in storage | |
| */ | |
| const createVMAsset = function (storage, assetType, dataFormat, data) { | |
| const asset = storage.createAsset( | |
| assetType, | |
| dataFormat, | |
| data, | |
| null, | |
| true // generate md5 | |
| ); | |
| return { | |
| name: null, // Needs to be set by caller | |
| dataFormat: dataFormat, | |
| asset: asset, | |
| md5: `${asset.assetId}.${dataFormat}`, | |
| assetId: asset.assetId | |
| }; | |
| }; | |
| /** | |
| * Handles loading a costume or a backdrop using the provided, context-relevant information. | |
| * @param {ArrayBuffer | string} fileData The costume data to load (this can be a base64 string | |
| * iff the image is a bitmap) | |
| * @param {string} fileType The MIME type of this file | |
| * @param {VM} vm The ScratchStorage instance to cache the costume data | |
| * @param {Function} handleCostume The function to execute on the costume object returned after | |
| * caching this costume in storage - This function should be responsible for | |
| * adding the costume to the VM and handling other UI flow that should come after adding the costume | |
| * @param {Function} handleError The function to execute if there is an error parsing the costume | |
| */ | |
| const costumeUpload = function (fileData, fileType, vm, handleCostume, handleError = () => {}) { | |
| const storage = vm.runtime.storage; | |
| let costumeFormat = null; | |
| let assetType = null; | |
| switch (fileType) { | |
| case 'image/svg+xml': { | |
| // run svg bytes through scratch-svg-renderer's sanitization code | |
| fileData = sanitizeSvg.sanitizeByteStream(fileData); | |
| costumeFormat = storage.DataFormat.SVG; | |
| assetType = storage.AssetType.ImageVector; | |
| fileData = fixSVG(fileData); | |
| break; | |
| } | |
| case 'image/jpeg': { | |
| costumeFormat = storage.DataFormat.JPG; | |
| assetType = storage.AssetType.ImageBitmap; | |
| break; | |
| } | |
| case 'image/bmp': { | |
| // Convert .bmp files to .png to compress them. .bmps are completely uncompressed, | |
| // and would otherwise take up a lot of storage space and take much longer to upload and download. | |
| bmpConverter(fileData).then(dataUrl => { | |
| costumeUpload(dataUrl, 'image/png', vm, handleCostume); | |
| }); | |
| return; // Return early because we're triggering another proper costumeUpload | |
| } | |
| case 'image/png': { | |
| costumeFormat = storage.DataFormat.PNG; | |
| assetType = storage.AssetType.ImageBitmap; | |
| break; | |
| } | |
| case 'image/webp': { | |
| // Scratch does not natively support webp, so convert to png | |
| // see image/bmp logic above | |
| bmpConverter(fileData, 'image/webp').then(dataUrl => { | |
| costumeUpload(dataUrl, 'image/png', vm, handleCostume); | |
| }); | |
| return; | |
| } | |
| case 'image/gif': { | |
| let costumes = []; | |
| gifDecoder(fileData, (frameNumber, dataUrl, numFrames) => { | |
| costumeUpload(dataUrl, 'image/png', vm, costumes_ => { | |
| costumes = costumes.concat(costumes_); | |
| if (frameNumber === numFrames - 1) { | |
| handleCostume(costumes); | |
| } | |
| }, handleError); | |
| }); | |
| return; // Abandon this load, do not try to load gif itself | |
| } | |
| default: | |
| handleError(`Encountered unexpected file type: ${fileType}`); | |
| return; | |
| } | |
| const bitmapAdapter = new BitmapAdapter(); | |
| if (bitmapAdapter.setStageSize) { | |
| const width = vm.runtime.stageWidth; | |
| const height = vm.runtime.stageHeight; | |
| bitmapAdapter.setStageSize(width, height); | |
| } | |
| const addCostumeFromBuffer = function (dataBuffer) { | |
| const vmCostume = createVMAsset( | |
| storage, | |
| assetType, | |
| costumeFormat, | |
| dataBuffer | |
| ); | |
| handleCostume([vmCostume]); | |
| }; | |
| if (costumeFormat === storage.DataFormat.SVG) { | |
| // Must pass in file data as a Uint8Array, | |
| // passing in an array buffer causes the sprite/costume | |
| // thumbnails to not display because the data URI for the costume | |
| // is invalid | |
| addCostumeFromBuffer(new Uint8Array(fileData)); | |
| } else { | |
| // otherwise it's a bitmap | |
| bitmapAdapter.importBitmap(fileData, fileType).then(addCostumeFromBuffer) | |
| .catch(handleError); | |
| } | |
| }; | |
| /** | |
| * Handles loading a sound using the provided, context-relevant information. | |
| * @param {ArrayBuffer} fileData The sound data to load | |
| * @param {string} fileType The MIME type of this file; This function will exit | |
| * early if the fileType is unexpected. | |
| * @param {ScratchStorage} storage The ScratchStorage instance to cache the sound data | |
| * @param {Function} handleSound The function to execute on the sound object of type VMAsset | |
| * This function should be responsible for adding the sound to the VM | |
| * as well as handling other UI flow that should come after adding the sound | |
| * @param {Function} handleError The function to execute if there is an error parsing the sound | |
| */ | |
| const soundUpload = function (fileData, fileType, storage, handleSound, handleError) { | |
| let soundFormat; | |
| switch (fileType) { | |
| case 'audio/mp3': | |
| case 'audio/mpeg': { | |
| soundFormat = storage.DataFormat.MP3; | |
| break; | |
| } | |
| case 'audio/wav': | |
| case 'audio/wave': | |
| case 'audio/x-wav': | |
| case 'audio/x-pn-wav': { | |
| soundFormat = storage.DataFormat.WAV; | |
| break; | |
| } | |
| case 'audio/ogg': { | |
| soundFormat = storage.DataFormat.OGG; | |
| break; | |
| } | |
| case 'audio/x-flac': | |
| case 'audio/flac': { | |
| soundFormat = storage.DataFormat.FLAC; | |
| break; | |
| } | |
| default: | |
| convertAudioToWav(fileData) | |
| .then(fixed => { | |
| soundUpload(fixed, 'audio/wav', storage, handleSound, handleError); | |
| }) | |
| .catch(handleError); | |
| return; | |
| } | |
| const vmSound = createVMAsset( | |
| storage, | |
| storage.AssetType.Sound, | |
| soundFormat, | |
| new Uint8Array(fileData)); | |
| handleSound(vmSound); | |
| }; | |
| /** | |
| * Handles loading a sound using the provided, context-relevant information. | |
| * @param {ArrayBuffer} fileData The sound data to load | |
| * @param {string} fileType The MIME type of this file. | |
| * @param {ScratchStorage} storage The ScratchStorage instance to cache the sound data | |
| * @param {Function} handleFile The function to execute on the sound object of type VMAsset | |
| * This function should be responsible for adding the sound to the VM | |
| * as well as handling other UI flow that should come after adding the sound | |
| * @param {Function} handleError The function to execute if there is an error parsing the sound | |
| */ | |
| const externalFileUpload = function (fileData, fileType, storage, handleFile, handleError) { | |
| // TODO: we should handle TXT and JSON differently | |
| const vmFile = createVMAsset( | |
| storage, | |
| storage.AssetType.ExternalFile, | |
| storage.DataFormat.TXT, | |
| new Uint8Array(fileData)); | |
| handleFile(vmFile); | |
| }; | |
| const spriteUpload = function (fileData, fileType, spriteName, vm, handleSprite, handleError = () => {}) { | |
| switch (fileType) { | |
| case '': | |
| case 'application/zip': { // We think this is a .sprite2 or .sprite3 file | |
| handleSprite(new Uint8Array(fileData)); | |
| return; | |
| } | |
| case 'image/svg+xml': | |
| case 'image/png': | |
| case 'image/bmp': | |
| case 'image/jpeg': | |
| case 'image/webp': | |
| case 'image/gif': { | |
| // Make a sprite from an image by making it a costume first | |
| costumeUpload(fileData, fileType, vm, vmCostumes => { | |
| vmCostumes.forEach((costume, i) => { | |
| costume.name = `${spriteName}${i ? i + 1 : ''}`; | |
| }); | |
| const newSprite = { | |
| name: spriteName, | |
| isStage: false, | |
| x: 0, // x/y will be randomized below | |
| y: 0, | |
| visible: true, | |
| size: 100, | |
| rotationStyle: 'all around', | |
| direction: 90, | |
| draggable: false, | |
| currentCostume: 0, | |
| blocks: {}, | |
| variables: {}, | |
| costumes: vmCostumes, | |
| sounds: [] // TODO are all of these necessary? | |
| }; | |
| randomizeSpritePosition(newSprite); | |
| // TODO probably just want sprite upload to handle this object directly | |
| handleSprite(JSON.stringify(newSprite)); | |
| }, handleError); | |
| return; | |
| } | |
| default: { | |
| handleError(`Encountered unexpected file type: ${fileType}`); | |
| return; | |
| } | |
| } | |
| }; | |
| export { | |
| handleFileUpload, | |
| costumeUpload, | |
| soundUpload, | |
| spriteUpload, | |
| externalFileUpload | |
| }; | |