forked from scratchfoundation/scratch-vm
-
-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
efa7e9d
commit 1ba70ac
Showing
7 changed files
with
830 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
const EventEmitter = require('events'); | ||
|
||
/** | ||
* @typedef InternalFont | ||
* @property {boolean} system True if the font is built in to the system | ||
* @property {string} family The font's name | ||
* @property {string} fallback Fallback font family list | ||
* @property {Asset} [asset] scratch-storage asset if system: false | ||
*/ | ||
|
||
const AssetUtil = require('../util/tw-asset-util'); | ||
|
||
class FontManager extends EventEmitter { | ||
/** | ||
* @param {Runtime} runtime | ||
*/ | ||
constructor (runtime) { | ||
super(); | ||
|
||
this.runtime = runtime; | ||
|
||
/** | ||
* Maps font family names to font metadata | ||
* @type {Array<InternalFont>} | ||
*/ | ||
this.fonts = []; | ||
|
||
this.suppressEvents = false; | ||
} | ||
|
||
/** | ||
* @param {string} family An unknown font family | ||
* @returns {boolean} true if the family is valid | ||
*/ | ||
isValidFamily (family) { | ||
return /^[a-z0-9\-_ ]+$/i.test(family); | ||
} | ||
|
||
/** | ||
* @param {string} familyList A list of font families | ||
* @returns {boolean} true if the list is valid | ||
*/ | ||
isValidFamilyList (familyList) { | ||
const families = familyList.split(','); | ||
return families.every(i => this.isValidFamily(i)); | ||
} | ||
|
||
changed () { | ||
if (!this.suppressEvents) { | ||
this.emit('change'); | ||
} | ||
} | ||
|
||
/** | ||
* @param {string} family | ||
* @param {string} fallback | ||
*/ | ||
addSystemFont (family, fallback) { | ||
if (!this.isValidFamily(family) || !this.isValidFamilyList(fallback)) { | ||
throw new Error('Invalid family'); | ||
} | ||
this.fonts.push({ | ||
system: true, | ||
family, | ||
fallback | ||
}); | ||
this.changed(); | ||
} | ||
|
||
/** | ||
* @param {string} family | ||
* @param {string} fallback | ||
* @param {Uint8Array|Asset} data Binary data or scratch-storage asset | ||
*/ | ||
addCustomFont (family, fallback, data) { | ||
if (!this.isValidFamily(family) || !this.isValidFamilyList(fallback)) { | ||
throw new Error('Invalid family'); | ||
} | ||
|
||
const storage = this.runtime.storage; | ||
const asset = data instanceof storage.Asset ? | ||
data : | ||
storage.createAsset( | ||
storage.AssetType.Font, | ||
storage.DataFormat.TTF, | ||
data, | ||
null, | ||
true | ||
); | ||
|
||
this.fonts.push({ | ||
system: false, | ||
family, | ||
fallback, | ||
asset | ||
}); | ||
|
||
this.updateRenderer(); | ||
this.changed(); | ||
} | ||
|
||
/** | ||
* @returns {Array<{name: string; family: string;}>} | ||
*/ | ||
getFonts () { | ||
return this.fonts.map(font => ({ | ||
name: font.family, | ||
family: `${font.family}, ${font.fallback}` | ||
})); | ||
} | ||
|
||
/** | ||
* @param {number} index Corresponds to index from getFonts() | ||
*/ | ||
deleteFont (index) { | ||
this.fonts.splice(index, 1); | ||
this.updateRenderer(); | ||
this.changed(); | ||
} | ||
|
||
clear () { | ||
this.fonts = []; | ||
this.updateRenderer(); | ||
this.changed(); | ||
} | ||
|
||
updateRenderer () { | ||
if (!this.runtime.renderer) { | ||
return; | ||
} | ||
const fontfaces = {}; | ||
for (const font of this.fonts) { | ||
if (!font.system) { | ||
const uri = font.asset.encodeDataURI(); | ||
const fontface = `@font-face { font-family: "${font.family}"; src: url("${uri}"); }`; | ||
const family = `${font.family}, ${font.fallback}`; | ||
fontfaces[family] = fontface; | ||
} | ||
} | ||
this.runtime.renderer.setCustomFonts(fontfaces); | ||
} | ||
|
||
/** | ||
* Get data to save in project.json and sb3 files. | ||
* @returns {{json: any; assets: Array<any>;}|null} | ||
*/ | ||
serializeJSON () { | ||
if (this.fonts.length === 0) { | ||
return null; | ||
} | ||
|
||
return this.fonts.map(font => { | ||
const serialized = { | ||
system: font.system, | ||
family: font.family, | ||
fallback: font.fallback | ||
}; | ||
|
||
if (!font.system) { | ||
const asset = font.asset; | ||
serialized.md5ext = `${asset.assetId}.${asset.dataFormat}`; | ||
} | ||
|
||
return serialized; | ||
}); | ||
} | ||
|
||
/** | ||
* @returns {Asset[]} list of scratch-storage assets | ||
*/ | ||
serializeAssets () { | ||
return this.fonts | ||
.filter(i => !i.system) | ||
.map(i => i.asset); | ||
} | ||
|
||
/** | ||
* @param {unknown} json | ||
* @param {JSZip} [zip] | ||
* @param {boolean} [keepExisting] | ||
* @returns {Promise<void>} | ||
*/ | ||
async deserialize (json, zip, keepExisting) { | ||
if (!Array.isArray(json)) { | ||
if (!keepExisting) { | ||
this.clear(); | ||
} | ||
return; | ||
} | ||
|
||
this.suppressEvents = true; | ||
if (!keepExisting) { | ||
this.clear(); | ||
} | ||
|
||
for (const font of json) { | ||
if (!font || typeof font !== 'object') continue; | ||
|
||
const system = font.system; | ||
const family = font.family; | ||
const fallback = font.fallback; | ||
if (typeof system !== 'boolean' || typeof family !== 'string' || typeof fallback !== 'string') continue; | ||
|
||
if (system) { | ||
this.addSystemFont(family, fallback); | ||
} else { | ||
const md5ext = font.md5ext; | ||
if (typeof md5ext !== 'string') continue; | ||
|
||
const asset = await AssetUtil.getByMd5ext( | ||
this.runtime, | ||
zip, | ||
this.runtime.storage.AssetType.Font, | ||
md5ext | ||
); | ||
this.addCustomFont(family, fallback, asset); | ||
} | ||
} | ||
|
||
this.suppressEvents = false; | ||
if (json.length || !keepExisting) { | ||
this.changed(); | ||
} | ||
} | ||
} | ||
|
||
module.exports = FontManager; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
const StringUtil = require('./string-util'); | ||
|
||
class AssetUtil { | ||
/** | ||
* @param {Runtime} runtime runtime with storage attached | ||
* @param {JSZip} zip optional JSZip to search for asset in | ||
* @param {Storage.assetType} assetType scratch-storage asset type | ||
* @param {string} md5ext full md5 with file extension | ||
* @returns {Promise<Storage.Asset>} scratch-storage asset object | ||
*/ | ||
static getByMd5ext (runtime, zip, assetType, md5ext) { | ||
const storage = runtime.storage; | ||
const idParts = StringUtil.splitFirst(md5ext, '.'); | ||
const md5 = idParts[0]; | ||
const ext = idParts[1].toLowerCase(); | ||
|
||
if (zip) { | ||
// Search the root of the zip | ||
let file = zip.file(md5ext); | ||
|
||
// Search subfolders of the zip | ||
// This matches behavior of deserialize-assets.js | ||
if (!file) { | ||
const fileMatch = new RegExp(`^([^/]*/)?${md5ext}$`); | ||
file = zip.file(fileMatch)[0]; | ||
} | ||
|
||
if (file) { | ||
return file.async('uint8array').then(data => runtime.storage.createAsset( | ||
assetType, | ||
ext, | ||
data, | ||
md5, | ||
false | ||
)); | ||
} | ||
} | ||
|
||
return storage.load(assetType, md5, ext); | ||
} | ||
} | ||
|
||
module.exports = AssetUtil; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.