Spaces:
Running
Running
File size: 11,606 Bytes
f2bee8a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
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;
}
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
};
|