diff --git a/packages/scratch-gui/src/components/sprite-selector/sprite-selector.jsx b/packages/scratch-gui/src/components/sprite-selector/sprite-selector.jsx index 7f8a5fc44c4..b6f0d2efa64 100644 --- a/packages/scratch-gui/src/components/sprite-selector/sprite-selector.jsx +++ b/packages/scratch-gui/src/components/sprite-selector/sprite-selector.jsx @@ -120,7 +120,7 @@ const SpriteSelectorComponent = function (props) { title: intl.formatMessage(messages.addSpriteFromFile), img: fileUploadIcon, onClick: onFileUploadClick, - fileAccept: '.svg, .png, .bmp, .jpg, .jpeg, .sprite2, .sprite3, .gif', + fileAccept: '.svg, .png, .bmp, .jpg, .jpeg, .sprite2, .sprite3, .gif, .webp', fileChange: onSpriteUpload, fileInput: spriteFileInput, fileMultiple: true diff --git a/packages/scratch-gui/src/components/stage-selector/stage-selector.jsx b/packages/scratch-gui/src/components/stage-selector/stage-selector.jsx index 12f3fa97dc6..7fd2ea4c133 100644 --- a/packages/scratch-gui/src/components/stage-selector/stage-selector.jsx +++ b/packages/scratch-gui/src/components/stage-selector/stage-selector.jsx @@ -102,7 +102,7 @@ const StageSelector = props => { title: intl.formatMessage(messages.addBackdropFromFile), img: fileUploadIcon, onClick: onBackdropFileUploadClick, - fileAccept: '.svg, .png, .bmp, .jpg, .jpeg, .gif', + fileAccept: '.svg, .png, .bmp, .jpg, .jpeg, .gif, .webp', fileChange: onBackdropFileUpload, fileInput: fileInputRef, fileMultiple: true diff --git a/packages/scratch-gui/src/containers/costume-tab.jsx b/packages/scratch-gui/src/containers/costume-tab.jsx index 992cbed9e61..69030cbf9ac 100644 --- a/packages/scratch-gui/src/containers/costume-tab.jsx +++ b/packages/scratch-gui/src/containers/costume-tab.jsx @@ -351,7 +351,7 @@ class CostumeTab extends React.Component { title: intl.formatMessage(addFileMessage), img: fileUploadIcon, onClick: this.handleFileUploadClick, - fileAccept: '.svg, .png, .bmp, .jpg, .jpeg, .gif', + fileAccept: '.svg, .png, .bmp, .jpg, .jpeg, .gif, .webp', fileChange: this.handleCostumeUpload, fileInput: this.setFileInput, fileMultiple: true diff --git a/packages/scratch-gui/src/lib/file-uploader.js b/packages/scratch-gui/src/lib/file-uploader.js index c6fd07265ed..ead150ac2f3 100644 --- a/packages/scratch-gui/src/lib/file-uploader.js +++ b/packages/scratch-gui/src/lib/file-uploader.js @@ -1,6 +1,7 @@ import {BitmapAdapter, sanitizeSvg} from '@scratch/scratch-svg-renderer'; import randomizeSpritePosition from './randomize-sprite-position.js'; import bmpConverter from './bmp-converter'; +import webpConverter from './webp-converter'; import gifDecoder from './gif-decoder'; /** diff --git a/packages/scratch-gui/src/lib/webp-converter.js b/packages/scratch-gui/src/lib/webp-converter.js new file mode 100644 index 00000000000..97fa980a69f --- /dev/null +++ b/packages/scratch-gui/src/lib/webp-converter.js @@ -0,0 +1,27 @@ +export default webpImage => new Promise(resolve => { + // If the input is an ArrayBuffer, we need to convert it to a `Blob` and give it a URL so we can use it as an + // `src`. If it's a data URI, we can use it as-is. + const imageUrl = webpImage instanceof String ? + webpImage : + window.URL.createObjectURL(new Blob([webpImage], {type: 'image/webp'})); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + const image = document.createElement('img'); + + image.addEventListener('load', () => { + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + ctx.drawImage(image, 0, 0); + + const dataUrl = canvas.toDataURL('image/png'); + + // Revoke URL. If a blob URL was generated earlier, this allows the blob to be GC'd and prevents a memory leak. + window.URL.revokeObjectURL(imageUrl); + + resolve(dataUrl); + }); + + image.setAttribute('src', imageUrl); +}); diff --git a/packages/scratch-gui/test/fixtures/webpfile.webp b/packages/scratch-gui/test/fixtures/webpfile.webp new file mode 100644 index 00000000000..b92fe178f7d Binary files /dev/null and b/packages/scratch-gui/test/fixtures/webpfile.webp differ diff --git a/packages/scratch-gui/test/integration/costumes.test.js b/packages/scratch-gui/test/integration/costumes.test.js index 1d52f62370d..d01992b00a0 100644 --- a/packages/scratch-gui/test/integration/costumes.test.js +++ b/packages/scratch-gui/test/integration/costumes.test.js @@ -161,6 +161,20 @@ describe('Working with costumes', () => { await expect(logs).toEqual([]); }); + test('Adding a webp from file', async () => { + await loadUri(uri); + await clickText('Costumes'); + const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); + await driver.actions().mouseMove(el) + .perform(); + await driver.sleep(500); // Wait for thermometer menu to come up + const input = await findByXpath('//input[@type="file"]'); + await input.sendKeys(path.resolve(__dirname, '../fixtures/webpfile.webp')); + await clickText('webpfile', scope.costumesTab); + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + test('Adding several costumes with a gif', async () => { await loadUri(uri); await clickText('Costumes');