diff --git a/package.json b/package.json
index 432d01e293f9dd922af101a449522c02fc98c7da..be23d9f7dcaffdb218866d6240207eb63d0fab6f 100644
--- a/package.json
+++ b/package.json
@@ -92,9 +92,10 @@
     "scratch-blocks": "0.1.0-prerelease.1524267558",
     "scratch-l10n": "2.0.20180108132626",
     "scratch-paint": "0.2.0-prerelease.20180423143518",
-    "scratch-render": "0.1.0-prerelease.1523453612",
+    "scratch-render": "0.1.0-prerelease.20180423214437",
+    "scratch-svg-renderer": "0.1.0-prerelease.20180423193917",
     "scratch-storage": "0.4.0",
-    "scratch-vm": "0.1.0-prerelease.1524254811",
+    "scratch-vm": "0.1.0-prerelease.1524520946",
     "selenium-webdriver": "3.6.0",
     "startaudiocontext": "1.2.1",
     "style-loader": "^0.20.0",
diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx
index 8af363c09312afda80c554e75ecd0627b923c362..66fbc1fe9e20bf3afdc3944daa45091e94fe6d1b 100644
--- a/src/components/stage-selector/stage-selector.jsx
+++ b/src/components/stage-selector/stage-selector.jsx
@@ -32,16 +32,19 @@ const messages = defineMessages({
     addBackdropFromFile: {
         id: 'gui.stageSelector.addBackdropFromFile',
         description: 'Button to add a stage in the target pane from file',
-        defaultMessage: 'Coming Soon'
+        defaultMessage: 'Upload Backdrop'
     }
 });
 
 const StageSelector = props => {
     const {
         backdropCount,
+        fileInputRef,
         intl,
         selected,
         url,
+        onBackdropFileUploadClick,
+        onBackdropFileUpload,
         onClick,
         onNewBackdropClick,
         onSurpriseBackdropClick,
@@ -81,7 +84,11 @@ const StageSelector = props => {
                 moreButtons={[
                     {
                         title: intl.formatMessage(messages.addBackdropFromFile),
-                        img: fileUploadIcon
+                        img: fileUploadIcon,
+                        onClick: onBackdropFileUploadClick,
+                        fileAccept: '.svg, .png, .jpg, .jpeg', // Bitmap coming soon
+                        fileChange: onBackdropFileUpload,
+                        fileInput: fileInputRef
                     }, {
                         title: intl.formatMessage(messages.addBackdropFromSurprise),
                         img: surpriseIcon,
@@ -102,7 +109,10 @@ const StageSelector = props => {
 
 StageSelector.propTypes = {
     backdropCount: PropTypes.number.isRequired,
+    fileInputRef: PropTypes.func,
     intl: intlShape.isRequired,
+    onBackdropFileUpload: PropTypes.func,
+    onBackdropFileUploadClick: PropTypes.func,
     onClick: PropTypes.func,
     onEmptyBackdropClick: PropTypes.func,
     onNewBackdropClick: PropTypes.func,
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index 7cc7a1ed25963029ea60a6fd3decfb69c52722df..4be489f6948e718c7fe8c2cecb90c44db388726a 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -89,9 +89,11 @@ const spriteShape = PropTypes.shape({
     costume: PropTypes.shape({
         url: PropTypes.string,
         name: PropTypes.string.isRequired,
-        bitmapResolution: PropTypes.number.isRequired,
-        rotationCenterX: PropTypes.number.isRequired,
-        rotationCenterY: PropTypes.number.isRequired
+        // The following are optional because costumes uploaded from disk
+        // will not have these properties available
+        bitmapResolution: PropTypes.number,
+        rotationCenterX: PropTypes.number,
+        rotationCenterY: PropTypes.number
     }),
     direction: PropTypes.number,
     id: PropTypes.string,
diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx
index 43a98bd430321e6d894279509ba39758a00de867..6042ceaaac8003eb03e4594faa2a91eb5b563b77 100644
--- a/src/containers/costume-tab.jsx
+++ b/src/containers/costume-tab.jsx
@@ -9,6 +9,7 @@ import PaintEditorWrapper from './paint-editor-wrapper.jsx';
 import CostumeLibrary from './costume-library.jsx';
 import BackdropLibrary from './backdrop-library.jsx';
 import {connect} from 'react-redux';
+import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
 
 import {
     closeCostumeLibrary,
@@ -48,9 +49,14 @@ const messages = defineMessages({
         description: 'Button to add a surprise costume in the editor tab',
         id: 'gui.costumeTab.addSurpriseCostume'
     },
+    addFileBackdropMsg: {
+        defaultMessage: 'Upload Backdrop',
+        description: 'Button to add a backdrop by uploading a file in the editor tab',
+        id: 'gui.costumeTab.addFileBackdrop'
+    },
     addFileCostumeMsg: {
-        defaultMessage: 'Coming Soon',
-        description: 'Button to add a file upload costume in the editor tab',
+        defaultMessage: 'Upload Costume',
+        description: 'Button to add a costume by uploading a file in the editor tab',
         id: 'gui.costumeTab.addFileCostume'
     },
     addCameraCostumeMsg: {
@@ -67,9 +73,13 @@ class CostumeTab extends React.Component {
             'handleSelectCostume',
             'handleDeleteCostume',
             'handleDuplicateCostume',
+            'handleNewCostume',
             'handleNewBlankCostume',
             'handleSurpriseCostume',
-            'handleSurpriseBackdrop'
+            'handleSurpriseBackdrop',
+            'handleFileUploadClick',
+            'handleCostumeUpload',
+            'setFileInput'
         ]);
         const {
             editingTarget,
@@ -121,6 +131,9 @@ class CostumeTab extends React.Component {
     handleDuplicateCostume (costumeIndex) {
         this.props.vm.duplicateCostume(costumeIndex);
     }
+    handleNewCostume (costume) {
+        this.props.vm.addCostume(costume.md5, costume);
+    }
     handleNewBlankCostume () {
         const emptyItem = costumeLibraryContent.find(item => (
             item.name === 'Empty'
@@ -128,35 +141,50 @@ class CostumeTab extends React.Component {
         const name = this.props.vm.editingTarget.isStage ? `backdrop1` : `costume1`;
         const vmCostume = {
             name: name,
+            md5: emptyItem.md5,
             rotationCenterX: emptyItem.info[0],
             rotationCenterY: emptyItem.info[1],
             bitmapResolution: emptyItem.info.length > 2 ? emptyItem.info[2] : 1,
             skinId: null
         };
 
-        this.props.vm.addCostume(emptyItem.md5, vmCostume);
+        this.handleNewCostume(vmCostume);
     }
     handleSurpriseCostume () {
         const item = costumeLibraryContent[Math.floor(Math.random() * costumeLibraryContent.length)];
         const vmCostume = {
             name: item.name,
+            md5: item.md5,
             rotationCenterX: item.info[0],
             rotationCenterY: item.info[1],
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        this.props.vm.addCostume(item.md5, vmCostume);
+        this.handleNewCostume(vmCostume);
     }
     handleSurpriseBackdrop () {
         const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
         const vmCostume = {
             name: item.name,
+            md5: item.md5,
             rotationCenterX: item.info[0] && item.info[0] / 2,
             rotationCenterY: item.info[1] && item.info[1] / 2,
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        this.props.vm.addCostume(item.md5, vmCostume);
+        this.handleNewCostume(vmCostume);
+    }
+    handleCostumeUpload (e) {
+        const storage = this.props.vm.runtime.storage;
+        handleFileUpload(e.target, (buffer, fileType, fileName) => {
+            costumeUpload(buffer, fileType, fileName, storage, this.handleNewCostume);
+        });
+    }
+    handleFileUploadClick () {
+        this.fileInput.click();
+    }
+    setFileInput (input) {
+        this.fileInput = input;
     }
     formatCostumeDetails (size) {
         // Round up width and height for scratch-flash compatibility
@@ -185,6 +213,7 @@ class CostumeTab extends React.Component {
         }
 
         const addLibraryMessage = target.isStage ? messages.addLibraryBackdropMsg : messages.addLibraryCostumeMsg;
+        const addFileMessage = target.isStage ? messages.addFileBackdropMsg : messages.addFileCostumeMsg;
         const addSurpriseFunc = target.isStage ? this.handleSurpriseBackdrop : this.handleSurpriseCostume;
         const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick;
         const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon;
@@ -208,8 +237,12 @@ class CostumeTab extends React.Component {
                         img: cameraIcon
                     },
                     {
-                        title: intl.formatMessage(messages.addFileCostumeMsg),
-                        img: fileUploadIcon
+                        title: intl.formatMessage(addFileMessage),
+                        img: fileUploadIcon,
+                        onClick: this.handleFileUploadClick,
+                        fileAccept: '.svg, .png, .jpg, .jpeg', // coming soon
+                        fileChange: this.handleCostumeUpload,
+                        fileInput: this.setFileInput
                     },
                     {
                         title: intl.formatMessage(messages.addSurpriseCostumeMsg),
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 94e72125d15f62333c10de5fee7e704f8a425626..ab4c9062bdebc5fa1b5055919a0df50bc1c3f3a3 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -16,6 +16,7 @@ import SoundEditor from './sound-editor.jsx';
 import SoundLibrary from './sound-library.jsx';
 
 import soundLibraryContent from '../lib/libraries/sounds.json';
+import {handleFileUpload, soundUpload} from '../lib/file-uploader.js';
 
 import {connect} from 'react-redux';
 
@@ -106,41 +107,13 @@ class SoundTab extends React.Component {
     }
 
     handleSoundUpload (e) {
-        const thisFileInput = e.target;
-        let thisFile = null;
-        const reader = new FileReader();
-        reader.onload = () => {
-            // 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
-            thisFileInput.value = null;
-            // Cache the sound in storage
-            const soundBuffer = reader.result;
-            const storage = this.props.vm.runtime.storage;
+        const storage = this.props.vm.runtime.storage;
+        const handleSound = newSound => this.props.vm.addSound(newSound)
+            .then(() => this.handleNewSound());
 
-            const fileType = thisFile.type; // what file type does the browser think this is
-            const soundFormat = fileType === 'audio/mp3' ? storage.DataFormat.MP3 : storage.DataFormat.WAV;
-            const md5 = storage.builtinHelper.cache(
-                storage.AssetType.Sound,
-                soundFormat,
-                new Uint8Array(soundBuffer),
-            );
-            // Add the sound to vm
-            const newSound = {
-                format: '',
-                name: 'sound1',
-                dataFormat: soundFormat,
-                md5: `${md5}.${soundFormat}`
-            };
-
-            this.props.vm.addSound(newSound).then(() => {
-                this.handleNewSound();
-            });
-        };
-        if (thisFileInput.files) {
-            thisFile = thisFileInput.files[0];
-            reader.readAsArrayBuffer(thisFile);
-        }
+        handleFileUpload(e.target, (buffer, fileType, fileName) => {
+            soundUpload(buffer, fileType, fileName, storage, handleSound);
+        });
     }
 
     setFileInput (input) {
diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx
index bf749c12a08fa7d997e484aa19b98d252e2a9d30..f3497a54cd9f2e187ffc5a5586b20f7492027f0c 100644
--- a/src/containers/stage-selector.jsx
+++ b/src/containers/stage-selector.jsx
@@ -10,47 +10,64 @@ import StageSelectorComponent from '../components/stage-selector/stage-selector.
 
 import backdropLibraryContent from '../lib/libraries/backdrops.json';
 import costumeLibraryContent from '../lib/libraries/costumes.json';
+import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
 
 class StageSelector extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
             'handleClick',
+            'handleNewBackdrop',
             'handleSurpriseBackdrop',
             'handleEmptyBackdrop',
-            'addBackdropFromLibraryItem'
+            'addBackdropFromLibraryItem',
+            'handleFileUploadClick',
+            'handleBackdropUpload',
+            'setFileInput'
         ]);
     }
     addBackdropFromLibraryItem (item) {
         const vmBackdrop = {
             name: item.name,
+            md5: item.md5,
             rotationCenterX: item.info[0] && item.info[0] / 2,
             rotationCenterY: item.info[1] && item.info[1] / 2,
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        return this.props.vm.addBackdrop(item.md5, vmBackdrop);
+        this.handleNewBackdrop(vmBackdrop);
     }
-    handleClick (e) {
-        e.preventDefault();
+    handleClick () {
         this.props.onSelect(this.props.id);
     }
+    handleNewBackdrop (backdrop) {
+        this.props.vm.addBackdrop(backdrop.md5, backdrop).then(() =>
+            this.props.onActivateTab(COSTUMES_TAB_INDEX));
+    }
     handleSurpriseBackdrop () {
         // @todo should this not add a backdrop you already have?
         const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
-        this.addBackdropFromLibraryItem(item).then(() => {
-            this.props.onActivateTab(COSTUMES_TAB_INDEX);
-        });
+        this.addBackdropFromLibraryItem(item);
     }
     handleEmptyBackdrop () {
         // @todo this is brittle, will need to be refactored for localized libraries
         const emptyItem = costumeLibraryContent.find(item => item.name === 'Empty');
         if (emptyItem) {
-            this.addBackdropFromLibraryItem(emptyItem).then(() => {
-                this.props.onActivateTab(COSTUMES_TAB_INDEX);
-            });
+            this.addBackdropFromLibraryItem(emptyItem);
         }
     }
+    handleBackdropUpload (e) {
+        const storage = this.props.vm.runtime.storage;
+        handleFileUpload(e.target, (buffer, fileType, fileName) => {
+            costumeUpload(buffer, fileType, fileName, storage, this.handleNewBackdrop);
+        });
+    }
+    handleFileUploadClick () {
+        this.fileInput.click();
+    }
+    setFileInput (input) {
+        this.fileInput = input;
+    }
     render () {
         const {
             /* eslint-disable no-unused-vars */
@@ -63,9 +80,13 @@ class StageSelector extends React.Component {
         } = this.props;
         return (
             <StageSelectorComponent
+                fileInputRef={this.setFileInput}
+                onBackdropFileUpload={this.handleBackdropUpload}
+                onBackdropFileUploadClick={this.handleFileUploadClick}
                 onClick={this.handleClick}
                 onEmptyBackdropClick={this.handleEmptyBackdrop}
                 onSurpriseBackdropClick={this.handleSurpriseBackdrop}
+
                 {...componentProps}
             />
         );
diff --git a/src/lib/file-uploader.js b/src/lib/file-uploader.js
new file mode 100644
index 0000000000000000000000000000000000000000..cea0aaeb11abc1258672bd0220abd0522ea76b33
--- /dev/null
+++ b/src/lib/file-uploader.js
@@ -0,0 +1,173 @@
+import {importBitmap} from 'scratch-svg-renderer';
+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
+ */
+const handleFileUpload = function (fileInput, onload) {
+    let thisFile = null;
+    const reader = new FileReader();
+    reader.onload = () => {
+        // 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;
+        const fileType = thisFile.type;
+        const fileName = extractFileName(thisFile.name);
+
+        onload(reader.result, fileType, fileName);
+    };
+    if (fileInput.files) {
+        thisFile = fileInput.files[0];
+        reader.readAsArrayBuffer(thisFile);
+    }
+};
+
+/**
+ * @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
+ */
+
+/**
+ * Cache an asset (costume, sound) in 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 {string} fileName The name of the asset
+ * @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 cacheAsset = function (storage, fileName, assetType, dataFormat, data) {
+    const md5 = storage.builtinHelper.cache(
+        assetType,
+        dataFormat,
+        data
+    );
+
+    return {
+        name: fileName,
+        dataFormat: dataFormat,
+        md5: `${md5}.${dataFormat}`
+    };
+};
+
+/**
+ * Handles loading a costume or a backdrop using the provided, context-relevant information.
+ * @param {ArrayBuffer} fileData The costume data to load
+ * @param {string} fileType The MIME type of this file
+ * @param {string} costumeName The user-readable name to use for the costume.
+ * @param {ScratchStorage} storage 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
+ */
+const costumeUpload = function (fileData, fileType, costumeName, storage, handleCostume) {
+    let costumeFormat = null;
+    let assetType = null;
+    switch (fileType) {
+    case 'image/svg+xml': {
+        costumeFormat = storage.DataFormat.SVG;
+        assetType = storage.AssetType.ImageVector;
+        break;
+    }
+    case 'image/jpeg': {
+        costumeFormat = storage.DataFormat.JPG;
+        assetType = storage.AssetType.ImageBitmap;
+        break;
+    }
+    case 'image/png': {
+        costumeFormat = storage.DataFormat.PNG;
+        assetType = storage.AssetType.ImageBitmap;
+        break;
+    }
+    default:
+        return;
+    }
+
+    const addCostumeFromBuffer = function (error, costumeBuffer) {
+        if (error) {
+            log.warn(`An error occurred while trying to extract image data: ${error}`);
+            return;
+        }
+
+        const vmCostume = cacheAsset(storage, costumeName, assetType, costumeFormat, costumeBuffer);
+        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(null, new Uint8Array(fileData));
+    } else {
+        // otherwise it's a bitmap
+        importBitmap(fileData, addCostumeFromBuffer);
+    }
+};
+
+/**
+ * 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 {string} soundName The user-readable name to use for the sound.
+  * @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
+ */
+const soundUpload = function (fileData, fileType, soundName, storage, handleSound) {
+    let soundFormat;
+    switch (fileType) {
+    case 'audio/mp3': {
+        soundFormat = storage.DataFormat.MP3;
+        break;
+    }
+    case 'audio/wav': { // TODO support audio/x-wav? Do we see this in the wild?
+        soundFormat = storage.DataFormat.WAV;
+        break;
+    }
+    default:
+        return;
+    }
+
+    const vmSound = cacheAsset(
+        storage,
+        soundName,
+        storage.AssetType.Sound,
+        soundFormat,
+        new Uint8Array(fileData));
+
+    handleSound(vmSound);
+};
+
+export {
+    handleFileUpload,
+    costumeUpload,
+    soundUpload
+};