From 8bfaf69d6e0802917a94121770d9e37debd07996 Mon Sep 17 00:00:00 2001 From: DD Liu <liudi08@gmail.com> Date: Thu, 23 Aug 2018 14:57:08 -0400 Subject: [PATCH] Inject fonts thumbnails (#2948) --- src/containers/sprite-selector-item.jsx | 55 ++++++++++++++++--- .../containers/sprite-selector-item.test.jsx | 9 +++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx index b2fb8f877..01e617c8e 100644 --- a/src/containers/sprite-selector-item.jsx +++ b/src/containers/sprite-selector-item.jsx @@ -6,15 +6,20 @@ import {connect} from 'react-redux'; import {setHoveredSprite} from '../reducers/hovered-target'; import {updateAssetDrag} from '../reducers/asset-drag'; import {getEventXY} from '../lib/touch-utils'; +import VM from 'scratch-vm'; +import {SVGRenderer} from 'scratch-svg-renderer'; import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx'; const dragThreshold = 3; // Same as the block drag threshold +// Contains 'font-family', but doesn't only contain 'font-family="none"' +const HAS_FONT_REGEXP = 'font-family(?!="none")'; class SpriteSelectorItem extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'getCostumeUrl', 'handleClick', 'handleDelete', 'handleDuplicate', @@ -25,6 +30,34 @@ class SpriteSelectorItem extends React.Component { 'handleMouseMove', 'handleMouseUp' ]); + this.svgRenderer = new SVGRenderer(); + // Asset ID of the SVG currently in SVGRenderer + this.svgRendererAssetId = null; + } + getCostumeUrl () { + if (this.props.costumeURL) return this.props.costumeURL; + if (!this.props.assetId) return null; + + const storage = this.props.vm.runtime.storage; + const asset = storage.get(this.props.assetId); + // If the SVG refers to fonts, they must be inlined in order to display correctly in the img tag. + // Avoid parsing the SVG when possible, since it's expensive. + if (asset.assetType === storage.AssetType.ImageVector) { + // If the asset ID has not changed, no need to re-parse + if (this.svgRendererAssetId === this.props.assetId) { + return this.cachedUrl; + } + + const svgString = this.props.vm.runtime.storage.get(this.props.assetId).decodeText(); + if (svgString.match(HAS_FONT_REGEXP)) { + this.svgRendererAssetId = this.props.assetId; + this.svgRenderer.loadString(svgString); + const svgText = this.svgRenderer.toString(true /* shouldInjectFonts */); + this.cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; + return this.cachedUrl; + } + } + return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI(); } handleMouseUp () { this.initialOffset = null; @@ -49,7 +82,7 @@ class SpriteSelectorItem extends React.Component { const dy = currentOffset.y - this.initialOffset.y; if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) { this.props.onDrag({ - img: this.props.costumeURL, + img: this.getCostumeUrl(), currentOffset: currentOffset, dragging: true, dragType: this.props.dragType, @@ -103,11 +136,14 @@ class SpriteSelectorItem extends React.Component { onExportButtonClick, dragPayload, receivedBlocks, + costumeURL, + vm, /* eslint-enable no-unused-vars */ ...props } = this.props; return ( <SpriteSelectorItemComponent + costumeURL={this.getCostumeUrl()} onClick={this.handleClick} onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null} onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null} @@ -139,14 +175,15 @@ SpriteSelectorItem.propTypes = { onDuplicateButtonClick: PropTypes.func, onExportButtonClick: PropTypes.func, receivedBlocks: PropTypes.bool.isRequired, - selected: PropTypes.bool + selected: PropTypes.bool, + vm: PropTypes.instanceOf(VM).isRequired }; -const mapStateToProps = (state, {assetId, costumeURL, id}) => ({ - costumeURL: costumeURL || (assetId && state.scratchGui.vm.runtime.storage.get(assetId).encodeDataURI()), +const mapStateToProps = (state, {id}) => ({ dragging: state.scratchGui.assetDrag.dragging, receivedBlocks: state.scratchGui.hoveredTarget.receivedBlocks && - state.scratchGui.hoveredTarget.sprite === id + state.scratchGui.hoveredTarget.sprite === id, + vm: state.scratchGui.vm }); const mapDispatchToProps = dispatch => ({ dispatchSetHoveredSprite: spriteId => { @@ -155,8 +192,12 @@ const mapDispatchToProps = dispatch => ({ onDrag: data => dispatch(updateAssetDrag(data)) }); - -export default connect( +const ConnectedComponent = connect( mapStateToProps, mapDispatchToProps )(SpriteSelectorItem); + +export { + ConnectedComponent as default, + HAS_FONT_REGEXP // Exposed for testing +}; diff --git a/test/unit/containers/sprite-selector-item.test.jsx b/test/unit/containers/sprite-selector-item.test.jsx index f133c02a2..25200a1fb 100644 --- a/test/unit/containers/sprite-selector-item.test.jsx +++ b/test/unit/containers/sprite-selector-item.test.jsx @@ -4,6 +4,7 @@ import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; import SpriteSelectorItem from '../../../src/containers/sprite-selector-item'; +import {HAS_FONT_REGEXP} from '../../../src/containers/sprite-selector-item'; import CloseButton from '../../../src/components/close-button/close-button'; describe('SpriteSelectorItem Container', () => { @@ -55,4 +56,12 @@ describe('SpriteSelectorItem Container', () => { wrapper.find(CloseButton).simulate('click'); expect(onDeleteButtonClick).toHaveBeenCalledWith(1337); }); + + test('Has font regexp works', () => { + expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); + expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); + expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); + + expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy(); + }); }); -- GitLab