init
This commit is contained in:
290
scratch-gui/src/lib/file-uploader.js
Normal file
290
scratch-gui/src/lib/file-uploader.js
Normal file
@@ -0,0 +1,290 @@
|
||||
import {BitmapAdapter, sanitizeSvg, fixForVanilla} from '@turbowarp/scratch-svg-renderer';
|
||||
import randomizeSpritePosition from './randomize-sprite-position.js';
|
||||
import bmpConverter from './bmp-converter';
|
||||
import gifDecoder from './gif-decoder';
|
||||
import convertAudioToWav from './tw-convert-audio-wav.js';
|
||||
import log from './log.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': {
|
||||
// fix any vanilla compatibility issues in the SVG first
|
||||
try {
|
||||
fileData = fixForVanilla(fileData);
|
||||
} catch (e) {
|
||||
log.error('fixForVanilla error', e);
|
||||
}
|
||||
|
||||
// run svg bytes through scratch-svg-renderer's sanitization code
|
||||
fileData = sanitizeSvg.sanitizeByteStream(fileData);
|
||||
|
||||
costumeFormat = storage.DataFormat.SVG;
|
||||
assetType = storage.AssetType.ImageVector;
|
||||
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);
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user