diff --git a/.eslintrc b/.eslintrc index ab07a74e6ca..e1f71a14410 100644 --- a/.eslintrc +++ b/.eslintrc @@ -38,6 +38,9 @@ "no-useless-escape": "off", "indent": ["error", 4, { "flatTernaryExpressions": true, + "CallExpression": { + "arguments": "off" + }, "FunctionDeclaration": { "parameters": "off" }, diff --git a/flow-typed/pbf.js b/flow-typed/pbf.js index c9af60f593d..ca92463be8b 100644 --- a/flow-typed/pbf.js +++ b/flow-typed/pbf.js @@ -1,6 +1,24 @@ declare module "pbf" { + declare type ReadFunction = (tag: number, result: T, pbf: Pbf) => void; + declare class Pbf { - constructor(ArrayBuffer | $ArrayBufferView): Pbf; + constructor(buf?: ArrayBuffer | Uint8Array): Pbf; + + readFields(readField: ReadFunction, result: T, end?: number): T; + readMessage(readField: ReadFunction, result: T): T; + + readFixed32(): number; + readSFixed32(): number; + readFixed64(): number; + readSFixed64(): number; + readFloat(): number; + readDouble(): number; + readVarint(): number; + readVarint64(): number; + readSVarint(): number; + readBoolean(): boolean; + readString(): string; + readBytes(): Uint8Array; } declare module.exports: typeof Pbf diff --git a/flow-typed/shelf-pack.js b/flow-typed/shelf-pack.js new file mode 100644 index 00000000000..42c391562f1 --- /dev/null +++ b/flow-typed/shelf-pack.js @@ -0,0 +1,24 @@ +declare module "@mapbox/shelf-pack" { + declare type Bin = { + id: number|string, + x: number, + y: number, + w: number, + h: number + }; + + declare class ShelfPack { + w: number; + h: number; + + constructor(w: number, h: number, options?: {autoResize: boolean}): ShelfPack; + + pack(bins: Array<{w: number, h: number}>, options?: {inPlace: boolean}): Array; + packOne(w: number, h: number, id?: number|string): Bin; + + ref(bin: Bin): number; + unref(bin: Bin): number; + } + + declare module.exports: typeof ShelfPack; +} diff --git a/src/data/bucket.js b/src/data/bucket.js index 90395406225..f71498d9db7 100644 --- a/src/data/bucket.js +++ b/src/data/bucket.js @@ -11,6 +11,7 @@ export type BucketParameters = { index: number, layers: Array, zoom: number, + pixelRatio: number, overscaling: number, collisionBoxArray: CollisionBoxArray } diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index f11ee205ab9..eefa5cef7fb 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -38,6 +38,10 @@ import type { import type StyleLayer from '../../style/style_layer'; import type {Shaping, PositionedIcon} from '../../symbol/shaping'; import type {SymbolQuad} from '../../symbol/quads'; +import type {StyleImage} from '../../style/style_image'; +import type {StyleGlyph} from '../../style/style_glyph'; +import type {ImagePosition} from '../../render/image_atlas'; +import type {GlyphPosition} from '../../render/glyph_atlas'; type SymbolBucketParameters = BucketParameters & { sdfIcons: boolean, @@ -320,7 +324,6 @@ class SymbolBucket implements Bucket { index: number; sdfIcons: boolean; iconsNeedLinear: boolean; - fontstack: string; textSizeData: any; iconSizeData: any; placedGlyphArray: StructArray; @@ -329,6 +332,7 @@ class SymbolBucket implements Bucket { lineVertexArray: StructArray; features: Array; symbolInstances: Array; + pixelRatio: number; tilePixelRatio: number; compareText: {[string]: Array}; @@ -345,7 +349,7 @@ class SymbolBucket implements Bucket { this.index = options.index; this.sdfIcons = options.sdfIcons; this.iconsNeedLinear = options.iconsNeedLinear; - this.fontstack = options.fontstack; + this.pixelRatio = options.pixelRatio; // deserializing a bucket created on a worker thread if (options.text) { @@ -463,7 +467,6 @@ class SymbolBucket implements Bucket { iconsNeedLinear: this.iconsNeedLinear, textSizeData: this.textSizeData, iconSizeData: this.iconSizeData, - fontstack: this.fontstack, placedGlyphArray: this.placedGlyphArray.serialize(transferables), placedIconArray: this.placedIconArray.serialize(transferables), glyphOffsetArray: this.glyphOffsetArray.serialize(transferables), @@ -486,7 +489,10 @@ class SymbolBucket implements Bucket { this.collisionBox.destroy(); } - prepare(stacks: any, icons: any) { + prepare(glyphMap: {[string]: {[number]: ?StyleGlyph}}, + glyphPositions: {[string]: {[number]: GlyphPosition}}, + imageMap: {[string]: StyleImage}, + imagePositions: {[string]: ImagePosition}) { this.symbolInstances = []; const tileSize = 512 * this.overscaling; @@ -498,15 +504,17 @@ class SymbolBucket implements Bucket { const oneEm = 24; const lineHeight = layout['text-line-height'] * oneEm; - const fontstack = this.fontstack = layout['text-font'].join(','); + + const fontstack = layout['text-font'].join(','); const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; + const glyphs = glyphMap[fontstack] || {}; + const glyphPositionMap = glyphPositions[fontstack] || {}; for (const feature of this.features) { - let shapedTextOrientations; + const shapedTextOrientations = {}; const text = feature.text; if (text) { - const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(text); const textOffset = this.layers[0].getLayoutValue('text-offset', {zoom: this.zoom}, feature.properties).map((t)=> t * oneEm); const spacing = this.layers[0].getLayoutValue('text-letter-spacing', {zoom: this.zoom}, feature.properties) * oneEm; const spacingIfAllowed = scriptDetection.allowsLetterSpacing(text) ? spacing : 0; @@ -516,44 +524,32 @@ class SymbolBucket implements Bucket { this.layers[0].getLayoutValue('text-max-width', {zoom: this.zoom}, feature.properties) * oneEm : 0; - shapedTextOrientations = { - [WritingMode.horizontal]: shapeText(text, - stacks[fontstack], - maxWidth, - lineHeight, - textAnchor, - textJustify, - spacingIfAllowed, - textOffset, - oneEm, - WritingMode.horizontal), - [WritingMode.vertical]: allowsVerticalWritingMode && textAlongLine && shapeText(text, - stacks[fontstack], - maxWidth, - lineHeight, - textAnchor, - textJustify, - spacingIfAllowed, - textOffset, - oneEm, - WritingMode.vertical) + const applyShaping = (text: string, writingMode: 1 | 2) => { + return shapeText( + text, glyphs, maxWidth, lineHeight, textAnchor, textJustify, + spacingIfAllowed, textOffset, oneEm, writingMode); }; - } else { - shapedTextOrientations = {}; + + shapedTextOrientations[WritingMode.horizontal] = applyShaping(text, WritingMode.horizontal); + + if (scriptDetection.allowsVerticalWritingMode(text) && textAlongLine) { + shapedTextOrientations[WritingMode.vertical] = applyShaping(text, WritingMode.vertical); + } } let shapedIcon; if (feature.icon) { - const image = icons[feature.icon]; + const image = imageMap[feature.icon]; if (image) { - shapedIcon = shapeIcon(image, + shapedIcon = shapeIcon( + imagePositions[feature.icon], this.layers[0].getLayoutValue('icon-offset', {zoom: this.zoom}, feature.properties)); if (this.sdfIcons === undefined) { this.sdfIcons = image.sdf; } else if (this.sdfIcons !== image.sdf) { util.warnOnce('Style sheet warning: Cannot mix SDF and non-SDF icons in one buffer'); } - if (!image.isNativePixelRatio) { + if (image.pixelRatio !== this.pixelRatio) { this.iconsNeedLinear = true; } else if (layout['icon-rotate'] !== 0 || !this.layers[0].isLayoutValueFeatureConstant('icon-rotate')) { this.iconsNeedLinear = true; @@ -562,7 +558,7 @@ class SymbolBucket implements Bucket { } if (shapedTextOrientations[WritingMode.horizontal] || shapedIcon) { - this.addFeature(feature, shapedTextOrientations, shapedIcon); + this.addFeature(feature, shapedTextOrientations, shapedIcon, glyphPositionMap); } } } @@ -575,7 +571,10 @@ class SymbolBucket implements Bucket { * source.) * @private */ - addFeature(feature: SymbolFeature, shapedTextOrientations: ShapedTextOrientations, shapedIcon: PositionedIcon | void) { + addFeature(feature: SymbolFeature, + shapedTextOrientations: ShapedTextOrientations, + shapedIcon: PositionedIcon | void, + glyphPositionMap: {[number]: GlyphPosition}) { const layoutTextSize = this.layers[0].getLayoutValue('text-size', {zoom: this.zoom + 1}, feature.properties); const layoutIconSize = this.layers[0].getLayoutValue('icon-size', {zoom: this.zoom + 1}, feature.properties); @@ -628,7 +627,7 @@ class SymbolBucket implements Bucket { addToBuffers, this.collisionBoxArray, feature.index, feature.sourceLayerIndex, this.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, - {zoom: this.zoom}, feature.properties); + {zoom: this.zoom}, feature.properties, glyphPositionMap); }; if (symbolPlacement === 'line') { @@ -1004,7 +1003,8 @@ class SymbolBucket implements Bucket { iconAlongLine: boolean, iconOffset: [number, number], globalProperties: Object, - featureProperties: Object) { + featureProperties: Object, + glyphPositionMap: {[number]: GlyphPosition}) { let textCollisionFeature, iconCollisionFeature; let iconQuads = []; @@ -1014,7 +1014,7 @@ class SymbolBucket implements Bucket { if (!shapedTextOrientations[writingMode]) continue; glyphQuads = glyphQuads.concat(addToBuffers ? getGlyphQuads(anchor, shapedTextOrientations[writingMode], - layer, textAlongLine, globalProperties, featureProperties) : + layer, textAlongLine, globalProperties, featureProperties, glyphPositionMap) : []); textCollisionFeature = new CollisionFeature(collisionBoxArray, line, diff --git a/src/render/draw_line.js b/src/render/draw_line.js index 68395cddc67..2c61feec721 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -69,13 +69,15 @@ function drawLineTile(program, painter, tile, bucket, layer, coord, programConfi gl.uniform1f(program.uniforms.u_sdfgamma, painter.lineAtlas.width / (Math.min(widthA, widthB) * 256 * browser.devicePixelRatio) / 2); } else if (image) { - imagePosA = painter.spriteAtlas.getPattern(image.from); - imagePosB = painter.spriteAtlas.getPattern(image.to); + imagePosA = painter.imageManager.getPattern(image.from); + imagePosB = painter.imageManager.getPattern(image.to); if (!imagePosA || !imagePosB) return; gl.uniform2f(program.uniforms.u_pattern_size_a, imagePosA.displaySize[0] * image.fromScale / tileRatio, imagePosB.displaySize[1]); gl.uniform2f(program.uniforms.u_pattern_size_b, imagePosB.displaySize[0] * image.toScale / tileRatio, imagePosB.displaySize[1]); - gl.uniform2fv(program.uniforms.u_texsize, painter.spriteAtlas.getPixelSize()); + + const {width, height} = painter.imageManager.getPixelSize(); + gl.uniform2fv(program.uniforms.u_texsize, [width, height]); } gl.uniform2f(program.uniforms.u_gl_units_to_pixels, 1 / painter.transform.pixelsToGLUnits[0], 1 / painter.transform.pixelsToGLUnits[1]); @@ -95,7 +97,7 @@ function drawLineTile(program, painter, tile, bucket, layer, coord, programConfi } else if (image) { gl.uniform1i(program.uniforms.u_image, 0); gl.activeTexture(gl.TEXTURE0); - painter.spriteAtlas.bind(gl, true); + painter.imageManager.bind(gl); gl.uniform2fv(program.uniforms.u_pattern_tl_a, (imagePosA: any).tl); gl.uniform2fv(program.uniforms.u_pattern_br_a, (imagePosA: any).br); diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index a8531396e70..0cd70a58236 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -64,10 +64,8 @@ function drawSymbols(painter: Painter, sourceCache: SourceCache, layer: SymbolSt function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate, translateAnchor, rotationAlignment, pitchAlignment, keepUpright) { - if (!isText && painter.style.sprite && !painter.style.sprite.loaded()) - return; - const gl = painter.gl; + const tr = painter.transform; const rotateWithMap = rotationAlignment === 'map'; const pitchWithMap = pitchAlignment === 'map'; @@ -85,7 +83,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate gl.disable(gl.DEPTH_TEST); } - let program, prevFontstack; + let program; for (const coord of coords) { const tile = sourceCache.getTile(coord); @@ -99,11 +97,29 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; - if (!program || bucket.fontstack !== prevFontstack) { + if (!program) { program = painter.useProgram(isSDF ? 'symbolSDF' : 'symbolIcon', programConfiguration); programConfiguration.setUniforms(gl, program, layer, {zoom: painter.transform.zoom}); - setSymbolDrawState(program, painter, layer, coord.z, isText, isSDF, rotateInShader, pitchWithMap, bucket.fontstack, bucket.iconsNeedLinear, sizeData); + setSymbolDrawState(program, painter, layer, isText, rotateInShader, pitchWithMap, sizeData); + } + + gl.activeTexture(gl.TEXTURE0); + gl.uniform1i(program.uniforms.u_texture, 0); + + if (isText) { + tile.glyphAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + gl.uniform2fv(program.uniforms.u_texsize, tile.glyphAtlasTexture.size); + } else { + const iconScaled = !layer.isLayoutValueFeatureConstant('icon-size') || + !layer.isLayoutValueZoomConstant('icon-size') || + layer.getLayoutValue('icon-size', { zoom: tr.zoom }) !== 1 || + bucket.iconsNeedLinear; + const iconTransformed = pitchWithMap || tr.pitch !== 0; + + tile.iconAtlasTexture.bind(isSDF || painter.options.rotating || painter.options.zooming || iconScaled || iconTransformed ? + gl.LINEAR : gl.NEAREST, gl.CLAMP_TO_EDGE); + gl.uniform2fv(program.uniforms.u_texsize, tile.iconAtlasTexture.size); } painter.enableTileClippingMask(coord); @@ -125,44 +141,20 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate gl.uniform1f(program.uniforms.u_collision_y_stretch, (tile.collisionTile: any).yStretch); drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, pitchWithMap); - - prevFontstack = bucket.fontstack; } if (!depthOn) gl.enable(gl.DEPTH_TEST); } -function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, rotateInShader, pitchWithMap, fontstack, iconsNeedLinear, sizeData) { +function setSymbolDrawState(program, painter, layer, isText, rotateInShader, pitchWithMap, sizeData) { const gl = painter.gl; const tr = painter.transform; gl.uniform1i(program.uniforms.u_pitch_with_map, pitchWithMap ? 1 : 0); - gl.activeTexture(gl.TEXTURE0); - gl.uniform1i(program.uniforms.u_texture, 0); - gl.uniform1f(program.uniforms.u_is_text, isText ? 1 : 0); - if (isText) { - // use the fonstack used when parsing the tile, not the fontstack - // at the current zoom level (layout['text-font']). - const glyphAtlas = fontstack && painter.glyphSource.getGlyphAtlas(fontstack); - if (!glyphAtlas) return; - - glyphAtlas.updateTexture(gl); - gl.uniform2f(program.uniforms.u_texsize, glyphAtlas.width, glyphAtlas.height); - } else { - const mapMoving = painter.options.rotating || painter.options.zooming; - const iconSizeScaled = !layer.isLayoutValueFeatureConstant('icon-size') || - !layer.isLayoutValueZoomConstant('icon-size') || - layer.getLayoutValue('icon-size', { zoom: tr.zoom }) !== 1; - const iconScaled = iconSizeScaled || iconsNeedLinear; - const iconTransformed = pitchWithMap || tr.pitch !== 0; - painter.spriteAtlas.bind(gl, isSDF || mapMoving || iconScaled || iconTransformed); - gl.uniform2fv(program.uniforms.u_texsize, painter.spriteAtlas.getPixelSize()); - } - gl.activeTexture(gl.TEXTURE1); painter.frameHistory.bind(gl); gl.uniform1i(program.uniforms.u_fadetexture, 1); diff --git a/src/render/glyph_atlas.js b/src/render/glyph_atlas.js new file mode 100644 index 00000000000..b83c4a2221a --- /dev/null +++ b/src/render/glyph_atlas.js @@ -0,0 +1,75 @@ +// @flow + +const ShelfPack = require('@mapbox/shelf-pack'); +const {AlphaImage} = require('../util/image'); + +import type {GlyphMetrics, StyleGlyph} from '../style/style_glyph'; + +const padding = 1; + +type Rect = { + x: number, + y: number, + w: number, + h: number +}; + +export type GlyphPosition = { + rect: Rect, + metrics: GlyphMetrics +}; + +export type GlyphAtlas = { + image: AlphaImage, + positions: {[string]: {[number]: GlyphPosition}} +}; + +function makeGlyphAtlas(stacks: {[string]: {[number]: ?StyleGlyph}}): GlyphAtlas { + const image = AlphaImage.create({width: 0, height: 0}); + const positions = {}; + + const pack = new ShelfPack(0, 0, {autoResize: true}); + + for (const stack in stacks) { + const glyphs = stacks[stack]; + const stackPositions = positions[stack] = {}; + + for (const id in glyphs) { + const src = glyphs[+id]; + if (src && src.bitmap.width !== 0 && src.bitmap.height !== 0) { + const bin = pack.packOne( + src.bitmap.width + 2 * padding, + src.bitmap.height + 2 * padding); + + AlphaImage.resize(image, { + width: pack.w, + height: pack.h + }); + + AlphaImage.copy( + src.bitmap, + image, + { x: 0, y: 0 }, + { + x: bin.x + padding, + y: bin.y + padding + }, + src.bitmap); + + stackPositions[id] = { rect: bin, metrics: src.metrics }; + } + } + } + + // pack.shrink(); + // resizeAlphaImage(image, { + // width: pack.w, + // height: pack.h + // }); + + return {image, positions}; +} + +module.exports = { + makeGlyphAtlas +}; diff --git a/src/render/glyph_manager.js b/src/render/glyph_manager.js new file mode 100644 index 00000000000..ad13298e4e4 --- /dev/null +++ b/src/render/glyph_manager.js @@ -0,0 +1,147 @@ +// @flow + +const loadGlyphRange = require('../style/load_glyph_range'); +const TinySDF = require('@mapbox/tiny-sdf'); +const isChar = require('../util/is_char_in_unicode_block'); +const {asyncAll} = require('../util/util'); +const {AlphaImage} = require('../util/image'); + +import type {StyleGlyph} from '../style/style_glyph'; +import type {RequestTransformFunction} from '../ui/map'; + +type Entry = { + // null means we've requested the range, but the glyph wasn't included in the result. + glyphs: {[id: number]: StyleGlyph | null}, + requests: {[range: number]: Array>}, + tinySDF?: TinySDF +}; + +class GlyphManager { + requestTransform: RequestTransformFunction; + localIdeographFontFamily: ?string; + entries: {[string]: Entry}; + url: ?string; + + constructor(requestTransform: RequestTransformFunction, localIdeographFontFamily: ?string) { + this.requestTransform = requestTransform; + this.localIdeographFontFamily = localIdeographFontFamily; + this.entries = {}; + } + + setURL(url: ?string) { + this.url = url; + } + + getGlyphs(glyphs: {[stack: string]: Array}, callback: Callback<{[stack: string]: {[id: number]: ?StyleGlyph}}>) { + const all = []; + + for (const stack in glyphs) { + for (const id of glyphs[stack]) { + all.push({stack, id}); + } + } + + asyncAll(all, ({stack, id}, callback: Callback<{stack: string, id: number, glyph: ?StyleGlyph}>) => { + let entry = this.entries[stack]; + if (!entry) { + entry = this.entries[stack] = { + glyphs: {}, + requests: {} + }; + } + + let glyph = entry.glyphs[id]; + if (glyph !== undefined) { + callback(null, {stack, id, glyph}); + return; + } + + glyph = this._tinySDF(entry, stack, id); + if (glyph) { + callback(null, {stack, id, glyph}); + return; + } + + const range = Math.floor(id / 256); + if (range * 256 > 65535) { + callback(new Error('glyphs > 65535 not supported')); + return; + } + + let requests = entry.requests[range]; + if (!requests) { + requests = entry.requests[range] = []; + loadGlyphRange(stack, range, (this.url: any), this.requestTransform, + (err, response: ?{[number]: StyleGlyph | null}) => { + if (response) { + for (const id in response) { + entry.glyphs[+id] = response[+id]; + } + } + for (const cb of requests) { + cb(err, response); + } + delete entry.requests[range]; + }); + } + + requests.push((err, result: ?{[number]: StyleGlyph | null}) => { + if (err) { + callback(err); + } else if (result) { + callback(null, {stack, id, glyph: result[id] || null}); + } + }); + }, (err, glyphs: ?Array<{stack: string, id: number, glyph: ?StyleGlyph}>) => { + if (err) { + callback(err); + } else if (glyphs) { + const result = {}; + + for (const {stack, id, glyph} of glyphs) { + (result[stack] || (result[stack] = {}))[id] = glyph; + } + + callback(null, result); + } + }); + } + + _tinySDF(entry: Entry, stack: string, id: number): ?StyleGlyph { + const family = this.localIdeographFontFamily; + if (!family) { + return; + } + + if (!isChar['CJK Unified Ideographs'](id) && !isChar['Hangul Syllables'](id)) { // eslint-disable-line new-cap + return; + } + + let tinySDF = entry.tinySDF; + if (!tinySDF) { + let fontWeight = '400'; + if (/bold/i.test(stack)) { + fontWeight = '900'; + } else if (/medium/i.test(stack)) { + fontWeight = '500'; + } else if (/light/i.test(stack)) { + fontWeight = '200'; + } + tinySDF = entry.tinySDF = new TinySDF(24, 3, 8, .25, family, fontWeight); + } + + return { + id, + bitmap: AlphaImage.create({width: 30, height: 30}, tinySDF.draw(String.fromCharCode(id))), + metrics: { + width: 24, + height: 24, + left: 0, + top: -8, + advance: 24 + } + }; + } +} + +module.exports = GlyphManager; diff --git a/src/render/image_atlas.js b/src/render/image_atlas.js new file mode 100644 index 00000000000..5bbb9148e69 --- /dev/null +++ b/src/render/image_atlas.js @@ -0,0 +1,101 @@ +// @flow + +const ShelfPack = require('@mapbox/shelf-pack'); +const {RGBAImage} = require('../util/image'); + +import type {StyleImage} from '../style/style_image'; + +const padding = 1; + +type Rect = { + x: number, + y: number, + w: number, + h: number +}; + +export type ImagePosition = { + pixelRatio: number, + textureRect: Rect, + tl: [number, number], + br: [number, number], + displaySize: [number, number] +}; + +// This wants to be a class, but is sent to workers, so must be a plain JSON blob. +function imagePosition(rect: Rect, {pixelRatio}: StyleImage): ImagePosition { + const textureRect = { + x: rect.x + padding, + y: rect.y + padding, + w: rect.w - padding * 2, + h: rect.h - padding * 2 + }; + return { + pixelRatio, + textureRect, + + // Redundant calculated members. + tl: [ + textureRect.x, + textureRect.y + ], + br: [ + textureRect.x + textureRect.w, + textureRect.y + textureRect.h + ], + displaySize: [ + textureRect.w / pixelRatio, + textureRect.h / pixelRatio + ] + }; +} + +export type ImageAtlas = { + image: RGBAImage, + positions: {[string]: ImagePosition} +}; + +function makeImageAtlas(images: {[string]: StyleImage}): ImageAtlas { + const image = RGBAImage.create({width: 0, height: 0}); + const positions = {}; + + const pack = new ShelfPack(0, 0, {autoResize: true}); + + for (const id in images) { + const src = images[id]; + + const bin = pack.packOne( + src.data.width + 2 * padding, + src.data.height + 2 * padding); + + RGBAImage.resize(image, { + width: pack.w, + height: pack.h + }); + + RGBAImage.copy( + src.data, + image, + { x: 0, y: 0 }, + { + x: bin.x + padding, + y: bin.y + padding + }, + src.data); + + positions[id] = imagePosition(bin, src); + } + + // pack.shrink(); + // resizeImageData(image, { + // width: pack.w, + // height: pack.h + // }); + + return {image, positions}; +} + +module.exports = { + imagePosition, + makeImageAtlas +}; diff --git a/src/render/image_manager.js b/src/render/image_manager.js new file mode 100644 index 00000000000..252b5632251 --- /dev/null +++ b/src/render/image_manager.js @@ -0,0 +1,193 @@ +// @flow + +const ShelfPack = require('@mapbox/shelf-pack'); +const {RGBAImage} = require('../util/image'); +const {imagePosition} = require('./image_atlas'); +const Texture = require('./texture'); +const assert = require('assert'); + +import type {StyleImage} from '../style/style_image'; +import type {ImagePosition} from './image_atlas'; +import type {Bin} from '@mapbox/shelf-pack'; + +type Pattern = { + bin: Bin, + position: ImagePosition +}; + +// When copied into the atlas texture, image data is padded by one pixel on each side. Icon +// images are padded with fully transparent pixels, while pattern images are padded with a +// copy of the image data wrapped from the opposite side. In both cases, this ensures the +// correct behavior of GL_LINEAR texture sampling mode. +const padding = 1; + +/* + ImageManager does two things: + + 1. Tracks requests for icon images from tile workers and sends responses when the requests are fulfilled. + 2. Builds a texture atlas for pattern images. + + These are disparate responsibilities and should eventually be handled by different classes. When we implement + data-driven support for `*-pattern`, we'll likely use per-bucket pattern atlases, and that would be a good time + to refactor this. +*/ +class ImageManager { + images: {[string]: StyleImage}; + loaded: boolean; + requestors: Array<{ids: Array, callback: Callback<{[string]: StyleImage}>}>; + + shelfPack: ShelfPack; + patterns: {[string]: Pattern}; + atlasImage: RGBAImage; + atlasTexture: ?Texture; + dirty: boolean; + + constructor() { + this.images = {}; + this.loaded = false; + this.requestors = []; + + this.shelfPack = new ShelfPack(64, 64, {autoResize: true}); + this.patterns = {}; + this.atlasImage = RGBAImage.create({width: 64, height: 64}); + this.dirty = true; + } + + isLoaded() { + return this.loaded; + } + + setLoaded(loaded: boolean) { + if (this.loaded === loaded) { + return; + } + + this.loaded = loaded; + + if (loaded) { + for (const {ids, callback} of this.requestors) { + this._notify(ids, callback); + } + this.requestors = []; + } + } + + getImage(id: string): ?StyleImage { + return this.images[id]; + } + + addImage(id: string, image: StyleImage) { + assert(!this.images[id]); + this.images[id] = image; + } + + removeImage(id: string) { + assert(this.images[id]); + delete this.images[id]; + + const pattern = this.patterns[id]; + if (pattern) { + this.shelfPack.unref(pattern.bin); + delete this.patterns[id]; + } + } + + getImages(ids: Array, callback: Callback<{[string]: StyleImage}>) { + // If the sprite has been loaded, or if all the icon dependencies are already present + // (i.e. if they've been addeded via runtime styling), then notify the requestor immediately. + // Otherwise, delay notification until the sprite is loaded. At that point, if any of the + // dependencies are still unavailable, we'll just assume they are permanently missing. + let hasAllDependencies = true; + if (!this.isLoaded()) { + for (const id of ids) { + if (!this.images[id]) { + hasAllDependencies = false; + } + } + } + if (this.isLoaded() || hasAllDependencies) { + this._notify(ids, callback); + } else { + this.requestors.push({ids, callback}); + } + } + + _notify(ids: Array, callback: Callback<{[string]: StyleImage}>) { + const response = {}; + + for (const id of ids) { + const image = this.images[id]; + if (image) { + response[id] = image; + } + } + + callback(null, response); + } + + // Pattern stuff + + getPixelSize() { + return { + width: this.shelfPack.w, + height: this.shelfPack.h + }; + } + + getPattern(id: string): ?ImagePosition { + const pattern = this.patterns[id]; + if (pattern) { + return pattern.position; + } + + const image = this.getImage(id); + if (!image) { + return null; + } + + const width = image.data.width + padding * 2; + const height = image.data.height + padding * 2; + + const bin = this.shelfPack.packOne(width, height); + if (!bin) { + return null; + } + + RGBAImage.resize(this.atlasImage, this.getPixelSize()); + + const src = image.data; + const dst = this.atlasImage; + + const x = bin.x + padding; + const y = bin.y + padding; + const w = src.width; + const h = src.height; + + RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h }); + + // Add 1 pixel wrapped padding on each side of the image. + RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x: x, y: y - 1 }, { width: w, height: 1 }); // T + RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x, y: y + h }, { width: w, height: 1 }); // B + RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y: y }, { width: 1, height: h }); // L + RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y: y }, { width: 1, height: h }); // R + + this.dirty = true; + + const position = imagePosition(bin, image); + this.patterns[id] = { bin, position }; + return position; + } + + bind(gl: WebGLRenderingContext) { + if (!this.atlasTexture) { + this.atlasTexture = new Texture(gl, this.atlasImage, gl.RGBA); + } else if (this.dirty) { + this.atlasTexture.update(this.atlasImage); + this.dirty = false; + } + + this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + } +} + +module.exports = ImageManager; diff --git a/src/render/painter.js b/src/render/painter.js index 219e95be187..50400f12028 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -32,8 +32,8 @@ import type TileCoord from '../source/tile_coord'; import type Style from '../style/style'; import type StyleLayer from '../style/style_layer'; import type LineAtlas from './line_atlas'; -import type SpriteAtlas from '../symbol/sprite_atlas'; -import type GlyphSource from '../symbol/glyph_source'; +import type ImageManager from './image_manager'; +import type GlyphManager from './glyph_manager'; type PainterOptions = { showOverdrawInspector: boolean, @@ -76,8 +76,8 @@ class Painter { style: Style; options: PainterOptions; lineAtlas: LineAtlas; - spriteAtlas: SpriteAtlas; - glyphSource: GlyphSource; + imageManager: ImageManager; + glyphManager: GlyphManager; depthRange: number; isOpaquePass: boolean; currentLayer: number; @@ -249,11 +249,8 @@ class Painter { this.options = options; this.lineAtlas = style.lineAtlas; - - this.spriteAtlas = style.spriteAtlas; - this.spriteAtlas.setSprite(style.sprite); - - this.glyphSource = style.glyphSource; + this.imageManager = style.imageManager; + this.glyphManager = style.glyphManager; this.frameHistory.record(Date.now(), this.transform.zoom, style.getTransition().duration); diff --git a/src/render/pattern.js b/src/render/pattern.js index deaa965ad3a..5d9edaf0e61 100644 --- a/src/render/pattern.js +++ b/src/render/pattern.js @@ -22,16 +22,16 @@ type CrossFaded = { */ exports.isPatternMissing = function(image: CrossFaded, painter: Painter): boolean { if (!image) return false; - const imagePosA = painter.spriteAtlas.getPattern(image.from); - const imagePosB = painter.spriteAtlas.getPattern(image.to); + const imagePosA = painter.imageManager.getPattern(image.from); + const imagePosB = painter.imageManager.getPattern(image.to); return !imagePosA || !imagePosB; }; exports.prepare = function (image: CrossFaded, painter: Painter, program: Program) { const gl = painter.gl; - const imagePosA = painter.spriteAtlas.getPattern(image.from); - const imagePosB = painter.spriteAtlas.getPattern(image.to); + const imagePosA = painter.imageManager.getPattern(image.from); + const imagePosB = painter.imageManager.getPattern(image.to); assert(imagePosA && imagePosB); gl.uniform1i(program.uniforms.u_image, 0); @@ -39,7 +39,8 @@ exports.prepare = function (image: CrossFaded, painter: Painter, program gl.uniform2fv(program.uniforms.u_pattern_br_a, (imagePosA: any).br); gl.uniform2fv(program.uniforms.u_pattern_tl_b, (imagePosB: any).tl); gl.uniform2fv(program.uniforms.u_pattern_br_b, (imagePosB: any).br); - gl.uniform2fv(program.uniforms.u_texsize, painter.spriteAtlas.getPixelSize()); + const {width, height} = painter.imageManager.getPixelSize(); + gl.uniform2fv(program.uniforms.u_texsize, [width, height]); gl.uniform1f(program.uniforms.u_mix, image.t); gl.uniform2fv(program.uniforms.u_pattern_size_a, (imagePosA: any).displaySize); gl.uniform2fv(program.uniforms.u_pattern_size_b, (imagePosB: any).displaySize); @@ -47,7 +48,7 @@ exports.prepare = function (image: CrossFaded, painter: Painter, program gl.uniform1f(program.uniforms.u_scale_b, image.toScale); gl.activeTexture(gl.TEXTURE0); - painter.spriteAtlas.bind(gl, true); + painter.imageManager.bind(gl); }; exports.setTile = function (tile: {coord: TileCoord, tileSize: number}, painter: Painter, program: Program) { diff --git a/src/render/texture.js b/src/render/texture.js new file mode 100644 index 00000000000..54b8a2ccd0e --- /dev/null +++ b/src/render/texture.js @@ -0,0 +1,73 @@ +// @flow + +import type {RGBAImage, AlphaImage} from '../util/image'; + +export type TextureFormat = + | $PropertyType + | $PropertyType; +export type TextureFilter = + | $PropertyType + | $PropertyType; +export type TextureWrap = + | $PropertyType + | $PropertyType + | $PropertyType + +class Texture { + gl: WebGLRenderingContext; + size: Array; + texture: WebGLTexture; + format: TextureFormat; + filter: ?TextureFilter; + wrap: ?TextureWrap; + + constructor(gl: WebGLRenderingContext, image: RGBAImage | AlphaImage, format: TextureFormat) { + this.gl = gl; + + const {width, height} = image; + this.size = [width, height]; + this.format = format; + + this.texture = gl.createTexture(); + this.update(image); + } + + update(image: RGBAImage | AlphaImage) { + const {width, height, data} = image; + this.size = [width, height]; + + const {gl} = this; + gl.bindTexture(gl.TEXTURE_2D, this.texture); + + if (this.format === gl.RGBA) { + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, (true: any)); + } + + gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, gl.UNSIGNED_BYTE, data); + } + + bind(filter: TextureFilter, wrap: TextureWrap) { + const {gl} = this; + gl.bindTexture(gl.TEXTURE_2D, this.texture); + + if (filter !== this.filter) { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); + this.filter = filter; + } + + if (wrap !== this.wrap) { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap); + this.wrap = wrap; + } + } + + destroy() { + const {gl} = this; + gl.deleteTexture(this.texture); + this.texture = (null: any); + } +} + +module.exports = Texture; diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index d73f4fb112b..1d6caca168b 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -5,6 +5,7 @@ const util = require('../util/util'); const window = require('../util/window'); const EXTENT = require('../data/extent'); const ResourceType = require('../util/ajax').ResourceType; +const browser = require('../util/browser'); import type {Source} from './source'; import type Map from '../ui/map'; @@ -195,6 +196,7 @@ class GeoJSONSource extends Evented implements Source { maxZoom: this.maxzoom, tileSize: this.tileSize, source: this.id, + pixelRatio: browser.devicePixelRatio, overscaling: tile.coord.z > this.maxzoom ? Math.pow(2, tile.coord.z - this.maxzoom) : 1, angle: this.map.transform.angle, pitch: this.map.transform.pitch, diff --git a/src/source/tile.js b/src/source/tile.js index 034106e0b95..71341d08b89 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -11,6 +11,7 @@ const featureFilter = require('../style-spec/feature_filter'); const CollisionTile = require('../symbol/collision_tile'); const CollisionBoxArray = require('../symbol/collision_box'); const Throttler = require('../util/throttler'); +const Texture = require('../render/texture'); const CLOCK_SKEW_RETRY_TIMEOUT = 30000; @@ -19,6 +20,7 @@ import type StyleLayer from '../style/style_layer'; import type TileCoord from './tile_coord'; import type {WorkerTileResult} from './worker_source'; import type Point from '@mapbox/point-geometry'; +import type {RGBAImage, AlphaImage} from '../util/image'; export type TileState = | 'loading' // Tile data is in the process of loading. @@ -42,6 +44,10 @@ class Tile { tileSize: number; sourceMaxZoom: number; buckets: {[string]: Bucket}; + iconAtlasImage: ?RGBAImage; + iconAtlasTexture: Texture; + glyphAtlasImage: ?AlphaImage; + glyphAtlasTexture: Texture; expirationTime: any; expiredRequestCount: number; state: TileState; @@ -133,6 +139,13 @@ class Tile { this.collisionTile = CollisionTile.deserialize(data.collisionTile, this.collisionBoxArray); this.featureIndex = FeatureIndex.deserialize(data.featureIndex, this.rawTileData, this.collisionTile); this.buckets = deserializeBucket(data.buckets, painter.style); + + if (data.iconAtlasImage) { + this.iconAtlasImage = data.iconAtlasImage; + } + if (data.glyphAtlasImage) { + this.glyphAtlasImage = data.glyphAtlasImage; + } } /** @@ -161,6 +174,13 @@ class Tile { // Add new symbol buckets util.extend(this.buckets, deserializeBucket(data.buckets, style)); + + if (data.iconAtlasImage) { + this.iconAtlasImage = data.iconAtlasImage; + } + if (data.glyphAtlasImage) { + this.glyphAtlasImage = data.glyphAtlasImage; + } } /** @@ -174,6 +194,13 @@ class Tile { } this.buckets = {}; + if (this.iconAtlasTexture) { + this.iconAtlasTexture.destroy(); + } + if (this.glyphAtlasTexture) { + this.glyphAtlasTexture.destroy(); + } + this.collisionBoxArray = null; this.collisionTile = null; this.featureIndex = null; @@ -257,6 +284,16 @@ class Tile { bucket.uploaded = true; } } + + if (this.iconAtlasImage) { + this.iconAtlasTexture = new Texture(gl, this.iconAtlasImage, gl.RGBA); + this.iconAtlasImage = null; + } + + if (this.glyphAtlasImage) { + this.glyphAtlasTexture = new Texture(gl, this.glyphAtlasImage, gl.ALPHA); + this.glyphAtlasImage = null; + } } queryRenderedFeatures(layers: {[string]: StyleLayer}, diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index d21be2ebd68..0136645dcf9 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -6,6 +6,7 @@ const loadTileJSON = require('./load_tilejson'); const normalizeURL = require('../util/mapbox').normalizeTileURL; const TileBounds = require('./tile_bounds'); const ResourceType = require('../util/ajax').ResourceType; +const browser = require('../util/browser'); import type {Source} from './source'; import type TileCoord from './tile_coord'; @@ -104,6 +105,7 @@ class VectorTileSource extends Evented implements Source { tileSize: this.tileSize * overscaling, type: this.type, source: this.id, + pixelRatio: browser.devicePixelRatio, overscaling: overscaling, angle: this.map.transform.angle, pitch: this.map.transform.pitch, diff --git a/src/source/worker_source.js b/src/source/worker_source.js index 08df0bf03ad..35dc0863292 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -8,6 +8,7 @@ import type {SerializedFeatureIndex} from '../data/feature_index'; import type {SerializedCollisionTile} from '../symbol/collision_tile'; import type {SerializedStructArray} from '../util/struct_array'; import type {RequestParameters} from '../util/ajax'; +import type {RGBAImage, AlphaImage} from '../util/image'; export type TileParameters = { source: string, @@ -28,11 +29,14 @@ export type WorkerTileParameters = TileParameters & { zoom: number, maxZoom: number, tileSize: number, + pixelRatio: number, overscaling: number, } & PlacementConfig; export type WorkerTileResult = { buckets: Array, + iconAtlasImage: RGBAImage, + glyphAtlasImage: AlphaImage, featureIndex: SerializedFeatureIndex, collisionTile: SerializedCollisionTile, collisionBoxArray: SerializedStructArray, diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 94d42f7ba76..20f29538fa3 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -7,11 +7,15 @@ const DictionaryCoder = require('../util/dictionary_coder'); const SymbolBucket = require('../data/bucket/symbol_bucket'); const util = require('../util/util'); const assert = require('assert'); +const {makeImageAtlas} = require('../render/image_atlas'); +const {makeGlyphAtlas} = require('../render/glyph_atlas'); import type TileCoord from './tile_coord'; import type {Bucket} from '../data/bucket'; import type Actor from '../util/actor'; import type StyleLayerIndex from '../style/style_layer_index'; +import type {StyleImage} from '../style/style_image'; +import type {StyleGlyph} from '../style/style_glyph'; import type { WorkerTileParameters, WorkerTileCallback, @@ -21,6 +25,7 @@ class WorkerTile { coord: TileCoord; uid: string; zoom: number; + pixelRatio: number; tileSize: number; source: string; overscaling: number; @@ -43,6 +48,7 @@ class WorkerTile { this.coord = params.coord; this.uid = params.uid; this.zoom = params.zoom; + this.pixelRatio = params.pixelRatio; this.tileSize = params.tileSize; this.source = params.source; this.overscaling = params.overscaling; @@ -106,6 +112,7 @@ class WorkerTile { index: featureIndex.bucketLayerIDs.length, layers: family, zoom: this.zoom, + pixelRatio: this.pixelRatio, overscaling: this.overscaling, collisionBoxArray: this.collisionBoxArray }); @@ -115,20 +122,6 @@ class WorkerTile { } } - - const done = (collisionTile) => { - this.status = 'done'; - - const transferables = []; - - callback(null, { - buckets: serializeBuckets(util.values(buckets), transferables), - featureIndex: featureIndex.serialize(transferables), - collisionTile: collisionTile.serialize(transferables), - collisionBoxArray: this.collisionBoxArray.serialize() - }, transferables); - }; - // Symbol buckets must be placed in reverse order. this.symbolBuckets = []; for (let i = layerIndex.symbolOrder.length - 1; i >= 0; i--) { @@ -139,18 +132,42 @@ class WorkerTile { } } - if (this.symbolBuckets.length === 0) { - return done(new CollisionTile(this.angle, this.pitch, this.cameraToCenterDistance, this.cameraToTileDistance, this.collisionBoxArray)); + let error: ?Error; + let glyphMap: ?{[string]: {[number]: ?StyleGlyph}}; + let imageMap: ?{[string]: StyleImage}; + + const stacks = util.mapObject(options.glyphDependencies, (glyphs) => Object.keys(glyphs).map(Number)); + if (Object.keys(stacks).length) { + actor.send('getGlyphs', {uid: this.uid, stacks}, (err, result) => { + if (!error) { + error = err; + glyphMap = result; + maybePrepare.call(this); + } + }); + } else { + glyphMap = {}; + } + + const icons = Object.keys(options.iconDependencies); + if (icons.length) { + actor.send('getImages', {icons}, (err, result) => { + if (!error) { + error = err; + imageMap = result; + maybePrepare.call(this); + } + }); + } else { + imageMap = {}; } - let deps = 0; - let icons = Object.keys(options.iconDependencies); - let stacks = util.mapObject(options.glyphDependencies, (glyphs) => Object.keys(glyphs).map(Number)); + maybePrepare.call(this); - const gotDependency = (err) => { - if (err) return callback(err); - deps++; - if (deps === 2) { + function maybePrepare() { + if (error) { + return callback(error); + } else if (glyphMap && imageMap) { const collisionTile = new CollisionTile( this.angle, this.pitch, @@ -158,33 +175,34 @@ class WorkerTile { this.cameraToTileDistance, this.collisionBoxArray); + const glyphAtlas = makeGlyphAtlas(glyphMap); + const imageAtlas = makeImageAtlas(imageMap); + for (const bucket of this.symbolBuckets) { recalculateLayers(bucket, this.zoom); - bucket.prepare(stacks, icons); + bucket.prepare(glyphMap, glyphAtlas.positions, + imageMap, imageAtlas.positions); + bucket.place(collisionTile, this.showCollisionBoxes); } - done(collisionTile); + this.status = 'done'; + + const transferables = [ + glyphAtlas.image.data.buffer, + imageAtlas.image.data.buffer + ]; + + callback(null, { + buckets: serializeBuckets(util.values(buckets), transferables), + featureIndex: featureIndex.serialize(transferables), + collisionTile: collisionTile.serialize(transferables), + collisionBoxArray: this.collisionBoxArray.serialize(), + glyphAtlasImage: glyphAtlas.image, + iconAtlasImage: imageAtlas.image + }, transferables); } - }; - - if (Object.keys(stacks).length) { - actor.send('getGlyphs', {uid: this.uid, stacks: stacks}, (err, newStacks) => { - stacks = newStacks; - gotDependency(err); - }); - } else { - gotDependency(); - } - - if (icons.length) { - actor.send('getIcons', {icons: icons}, (err, newIcons) => { - icons = newIcons; - gotDependency(err); - }); - } else { - gotDependency(); } } diff --git a/src/style/image_sprite.js b/src/style/image_sprite.js deleted file mode 100644 index 89a37714829..00000000000 --- a/src/style/image_sprite.js +++ /dev/null @@ -1,95 +0,0 @@ -// @flow - -const Evented = require('../util/evented'); -const ajax = require('../util/ajax'); -const browser = require('../util/browser'); -const normalizeURL = require('../util/mapbox').normalizeSpriteURL; - -import type {RequestTransformFunction} from '../ui/map'; - -class SpritePosition { - x: number; - y: number; - width: number; - height: number; - pixelRatio: number; - sdf: boolean; - - constructor() { - this.x = 0; - this.y = 0; - this.width = 0; - this.height = 0; - this.pixelRatio = 1; - this.sdf = false; - } -} - -class ImageSprite extends Evented { - base: string; - retina: boolean; - - transformRequestFn: RequestTransformFunction; - data: ?{[string]: SpritePosition}; - imgData: ?ImageData; - - constructor(base: string, transformRequestCallback: RequestTransformFunction, eventedParent?: Evented) { - super(); - this.base = base; - this.retina = browser.devicePixelRatio > 1; - this.setEventedParent(eventedParent); - this.transformRequestFn = transformRequestCallback; - - const format = this.retina ? '@2x' : ''; - let url = normalizeURL(base, format, '.json'); - const jsonRequest = transformRequestCallback(url, ajax.ResourceType.SpriteJSON); - ajax.getJSON(jsonRequest, (err, data) => { - if (err) { - this.fire('error', {error: err}); - } else if (data) { - this.data = (data: any); - if (this.imgData) this.fire('data', {dataType: 'style'}); - } - }); - url = normalizeURL(base, format, '.png'); - const imageRequest = transformRequestCallback(url, ajax.ResourceType.SpriteImage); - ajax.getImage(imageRequest, (err, img) => { - if (err) { - this.fire('error', {error: err}); - } else if (img) { - this.imgData = browser.getImageData(img); - if (this.data) this.fire('data', {dataType: 'style'}); - } - }); - } - - toJSON() { - return this.base; - } - - loaded() { - return !!(this.data && this.imgData); - } - - resize(/*gl*/) { - if (browser.devicePixelRatio > 1 !== this.retina) { - const newSprite = new ImageSprite(this.base, this.transformRequestFn); - newSprite.on('data', () => { - this.data = newSprite.data; - this.imgData = newSprite.imgData; - this.retina = newSprite.retina; - }); - } - } - - getSpritePosition(name: string) { - if (!this.loaded()) return new SpritePosition(); - - const pos = this.data && this.data[name]; - if (pos && this.imgData) return pos; - - return new SpritePosition(); - } -} - -module.exports = ImageSprite; diff --git a/src/style/load_glyph_range.js b/src/style/load_glyph_range.js new file mode 100644 index 00000000000..e9eb5169f16 --- /dev/null +++ b/src/style/load_glyph_range.js @@ -0,0 +1,37 @@ +// @flow + +const {normalizeGlyphsURL} = require('../util/mapbox'); +const ajax = require('../util/ajax'); +const parseGlyphPBF = require('./parse_glyph_pbf'); + +import type {StyleGlyph} from './style_glyph'; +import type {RequestTransformFunction} from '../ui/map'; + +module.exports = function (fontstack: string, + range: number, + urlTemplate: string, + requestTransform: RequestTransformFunction, + callback: Callback<{[number]: StyleGlyph | null}>) { + const begin = range * 256; + const end = begin + 255; + + const request = requestTransform( + normalizeGlyphsURL(urlTemplate) + .replace('{fontstack}', fontstack) + .replace('{range}', `${begin}-${end}`), + ajax.ResourceType.Glyphs); + + ajax.getArrayBuffer(request, (err, response) => { + if (err) { + callback(err); + } else if (response) { + const glyphs = {}; + + for (const glyph of parseGlyphPBF(response.data)) { + glyphs[glyph.id] = glyph; + } + + callback(null, glyphs); + } + }); +}; diff --git a/src/style/load_sprite.js b/src/style/load_sprite.js new file mode 100644 index 00000000000..a8c23dbf226 --- /dev/null +++ b/src/style/load_sprite.js @@ -0,0 +1,54 @@ +// @flow + +const ajax = require('../util/ajax'); +const browser = require('../util/browser'); +const {normalizeSpriteURL} = require('../util/mapbox'); +const {RGBAImage} = require('../util/image'); + +import type {StyleImage} from './style_image'; +import type {RequestTransformFunction} from '../ui/map'; + +module.exports = function(baseURL: ?string, + transformRequestCallback: RequestTransformFunction, + callback: Callback<{[string]: StyleImage}>) { + if (!baseURL) { + callback(null, {}); + return; + } + + let json: any, image, error; + const format = browser.devicePixelRatio > 1 ? '@2x' : ''; + + ajax.getJSON(transformRequestCallback(normalizeSpriteURL(baseURL, format, '.json'), ajax.ResourceType.SpriteJSON), (err, data) => { + if (!error) { + error = err; + json = data; + maybeComplete(); + } + }); + + ajax.getImage(transformRequestCallback(normalizeSpriteURL(baseURL, format, '.png'), ajax.ResourceType.SpriteImage), (err, img) => { + if (!error) { + error = err; + image = img; + maybeComplete(); + } + }); + + function maybeComplete() { + if (error) { + callback(error); + } else if (json && image) { + const result = {}; + + for (const id in json) { + const {width, height, x, y, sdf, pixelRatio} = json[id]; + const data = RGBAImage.create({width, height}); + RGBAImage.copy(browser.getImageData(image), data, {x, y}, {x: 0, y: 0}, {width, height}); + result[id] = {data, pixelRatio, sdf}; + } + + callback(null, result); + } + } +}; diff --git a/src/style/parse_glyph_pbf.js b/src/style/parse_glyph_pbf.js new file mode 100644 index 00000000000..add4ad167a7 --- /dev/null +++ b/src/style/parse_glyph_pbf.js @@ -0,0 +1,41 @@ +// @flow + +const {AlphaImage} = require('../util/image'); +const Protobuf = require('pbf'); +const border = 3; + +import type {StyleGlyph} from './style_glyph'; + +function readFontstacks(tag: number, glyphs: Array, pbf: Protobuf) { + if (tag === 1) { + pbf.readMessage(readFontstack, glyphs); + } +} + +function readFontstack(tag: number, glyphs: Array, pbf: Protobuf) { + if (tag === 3) { + const {id, bitmap, width, height, left, top, advance} = pbf.readMessage(readGlyph, {}); + glyphs.push({ + id, + bitmap: AlphaImage.create({ + width: width + 2 * border, + height: height + 2 * border + }, bitmap), + metrics: {width, height, left, top, advance} + }); + } +} + +function readGlyph(tag: number, glyph: Object, pbf: Protobuf) { + if (tag === 1) glyph.id = pbf.readVarint(); + else if (tag === 2) glyph.bitmap = pbf.readBytes(); + else if (tag === 3) glyph.width = pbf.readVarint(); + else if (tag === 4) glyph.height = pbf.readVarint(); + else if (tag === 5) glyph.left = pbf.readSVarint(); + else if (tag === 6) glyph.top = pbf.readSVarint(); + else if (tag === 7) glyph.advance = pbf.readVarint(); +} + +module.exports = function (data: ArrayBuffer | Uint8Array): Array { + return new Protobuf(data).readFields(readFontstacks, []); +}; diff --git a/src/style/style.js b/src/style/style.js index e5b3a845982..c5dcd2917c1 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -3,10 +3,10 @@ const assert = require('assert'); const Evented = require('../util/evented'); const StyleLayer = require('./style_layer'); -const ImageSprite = require('./image_sprite'); +const loadSprite = require('./load_sprite'); +const ImageManager = require('../render/image_manager'); +const GlyphManager = require('../render/glyph_manager'); const Light = require('./light'); -const GlyphSource = require('../symbol/glyph_source'); -const SpriteAtlas = require('../symbol/sprite_atlas'); const LineAtlas = require('../render/line_atlas'); const util = require('../util/util'); const ajax = require('../util/ajax'); @@ -29,7 +29,8 @@ const rtlTextPlugin = require('../source/rtl_text_plugin'); import type Map from '../ui/map'; import type Transform from '../geo/transform'; import type {Source} from '../source/source'; -import type {IconMap} from '../symbol/sprite_atlas'; +import type {StyleImage} from './style_image'; +import type {StyleGlyph} from './style_glyph'; const supportedDiffOperations = util.pick(diff.operations, [ 'addLayer', @@ -72,10 +73,9 @@ class Style extends Evented { stylesheet: StyleSpecification; animationLoop: AnimationLoop; dispatcher: Dispatcher; - sprite: ImageSprite; - spriteAtlas: SpriteAtlas; + imageManager: ImageManager; + glyphManager: GlyphManager; lineAtlas: LineAtlas; - glyphSource: GlyphSource; light: Light; _layers: {[string]: StyleLayer}; @@ -95,11 +95,20 @@ class Style extends Evented { constructor(stylesheet: StyleSpecification, map: Map, options: StyleOptions) { super(); + + options = util.extend({ + validate: typeof stylesheet === 'string' ? !mapbox.isMapboxURL(stylesheet) : true + }, options); + + const transformRequest = (url, resourceType) => { + return this.map ? this.map._transformRequest(url, resourceType) : { url }; + }; + this.map = map; this.animationLoop = (map && map.animationLoop) || new AnimationLoop(); this.dispatcher = new Dispatcher(getWorkerPool(), this); - this.spriteAtlas = new SpriteAtlas(1024, 1024); - this.spriteAtlas.setEventedParent(this); + this.imageManager = new ImageManager(); + this.glyphManager = new GlyphManager(transformRequest, options.localIdeographFontFamily); this.lineAtlas = new LineAtlas(256, 512); this._layers = {}; @@ -112,10 +121,6 @@ class Style extends Evented { this._resetUpdates(); - options = util.extend({ - validate: typeof stylesheet === 'string' ? !mapbox.isMapboxURL(stylesheet) : true - }, options); - this.setEventedParent(map); this.fire('dataloading', {dataType: 'style'}); @@ -127,10 +132,6 @@ class Style extends Evented { } }); - const transformRequest = (url, resourceType) => { - return this.map ? this.map._transformRequest(url, resourceType) : { url }; - }; - const stylesheetLoaded = (err, stylesheet: ?StyleSpecification) => { if (err) { this.fire('error', {error: err}); @@ -146,11 +147,19 @@ class Style extends Evented { this.addSource(id, stylesheet.sources[id], options); } - if (stylesheet.sprite) { - this.sprite = new ImageSprite(stylesheet.sprite, transformRequest, this); - } + loadSprite(stylesheet.sprite, transformRequest, (err, images) => { + if (err) { + this.fire('error', err); + } else if (images) { + for (const id in images) { + this.imageManager.addImage(id, images[id]); + } + } + + this.imageManager.setLoaded(true); + }); - this.glyphSource = new GlyphSource(stylesheet.glyphs, options.localIdeographFontFamily, transformRequest, this); + this.glyphManager.setURL(stylesheet.glyphs); this._resolve(); this.fire('data', {dataType: 'style'}); this.fire('style.load'); @@ -221,7 +230,7 @@ class Style extends Evented { if (!this.sourceCaches[id].loaded()) return false; - if (this.sprite && !this.sprite.loaded()) + if (!this.imageManager.isLoaded()) return false; return true; @@ -426,6 +435,22 @@ class Style extends Evented { return true; } + addImage(id: string, image: StyleImage) { + if (this.imageManager.getImage(id)) { + return this.fire('error', {error: new Error('An image with this name already exists.')}); + } + this.imageManager.addImage(id, image); + this.fire('data', {dataType: 'style'}); + } + + removeImage(id: string) { + if (!this.imageManager.getImage(id)) { + return this.fire('error', {error: new Error('No image with this name exists.')}); + } + this.imageManager.removeImage(id); + this.fire('data', {dataType: 'style'}); + } + addSource(id: string, source: SourceSpecification, options?: {validate?: boolean}) { this._checkLoaded(); @@ -930,36 +955,12 @@ class Style extends Evented { // Callbacks from web workers - getIcons(mapId: string, params: {icons: any}, callback: Callback) { - const updateSpriteAtlas = () => { - this.spriteAtlas.setSprite(this.sprite); - this.spriteAtlas.addIcons(params.icons, callback); - }; - if (!this.sprite || this.sprite.loaded()) { - updateSpriteAtlas(); - } else { - this.sprite.on('data', updateSpriteAtlas); - } + getImages(mapId: string, params: {icons: Array}, callback: Callback<{[string]: StyleImage}>) { + this.imageManager.getImages(params.icons, callback); } - getGlyphs(mapId: string, params: {stacks: {[string]: Array}, uid: number}, callback: Callback<{}>) { - const stacks = params.stacks; - let remaining = Object.keys(stacks).length; - const allGlyphs = {}; - - for (const fontName in stacks) { - this.glyphSource.getSimpleGlyphs(fontName, stacks[fontName], params.uid, done); - } - - function done(err, glyphs, fontName) { - if (err) console.error(err); - - allGlyphs[fontName] = glyphs; - remaining--; - - if (remaining === 0) - callback(null, allGlyphs); - } + getGlyphs(mapId: string, params: {stacks: {[string]: Array}}, callback: Callback<{[string]: {[number]: ?StyleGlyph}}>) { + this.glyphManager.getGlyphs(params.stacks, callback); } } diff --git a/src/style/style_glyph.js b/src/style/style_glyph.js new file mode 100644 index 00000000000..009bf125796 --- /dev/null +++ b/src/style/style_glyph.js @@ -0,0 +1,17 @@ +// @flow + +import type {AlphaImage} from '../util/image'; + +export type GlyphMetrics = { + width: number, + height: number, + left: number, + top: number, + advance: number +}; + +export type StyleGlyph = { + id: number, + bitmap: AlphaImage, + metrics: GlyphMetrics +}; diff --git a/src/style/style_image.js b/src/style/style_image.js new file mode 100644 index 00000000000..90e8b6317e1 --- /dev/null +++ b/src/style/style_image.js @@ -0,0 +1,9 @@ +// @flow + +import type {RGBAImage} from '../util/image'; + +export type StyleImage = { + data: RGBAImage, + pixelRatio: number, + sdf: boolean +}; diff --git a/src/symbol/glyph_atlas.js b/src/symbol/glyph_atlas.js deleted file mode 100644 index 7dd7599e2a0..00000000000 --- a/src/symbol/glyph_atlas.js +++ /dev/null @@ -1,182 +0,0 @@ -// @flow - -const ShelfPack = require('@mapbox/shelf-pack'); -const util = require('../util/util'); - -const SIZE_GROWTH_RATE = 4; -const DEFAULT_SIZE = 128; -// must be "DEFAULT_SIZE * SIZE_GROWTH_RATE ^ n" for some integer n -const MAX_SIZE = 2048; - -import type {Glyph} from '../util/glyphs'; - -export type Rect = { - x: number, - y: number, - w: number, - h: number -}; - -class GlyphAtlas { - width: number; - height: number; - atlas: ShelfPack; - index: {[string]: Rect}; - ids: {[string]: Array}; - data: Uint8Array; - dirty: boolean; - gl: WebGLRenderingContext; - texture: WebGLTexture; - - constructor() { - this.width = DEFAULT_SIZE; - this.height = DEFAULT_SIZE; - - this.atlas = new ShelfPack(this.width, this.height); - this.index = {}; - this.ids = {}; - this.data = new Uint8Array(this.width * this.height); - } - - getGlyphs() { - const glyphs = {}; - let split, - name, - id; - - for (const key in this.ids) { - split = key.split('#'); - name = split[0]; - id = split[1]; - - if (!glyphs[name]) glyphs[name] = []; - glyphs[name].push(id); - } - - return glyphs; - } - - getRects() { - const rects = {}; - let split, - name, - id; - - for (const key in this.ids) { - split = key.split('#'); - name = split[0]; - id = split[1]; - - if (!rects[name]) rects[name] = {}; - rects[name][id] = this.index[key]; - } - - return rects; - } - - addGlyph(id: number, name: string, glyph: Glyph, buffer: number): ?Rect { - if (!glyph) return null; - - const key = `${name}#${glyph.id}`; - - // The glyph is already in this texture. - if (this.index[key]) { - if (this.ids[key].indexOf(id) < 0) { - this.ids[key].push(id); - } - return this.index[key]; - } - - // The glyph bitmap has zero width. - if (!glyph.bitmap) { - return null; - } - - const bufferedWidth = glyph.width + buffer * 2; - const bufferedHeight = glyph.height + buffer * 2; - - // Add a 1px border around every image. - const padding = 1; - const packWidth = bufferedWidth + 2 * padding; - const packHeight = bufferedHeight + 2 * padding; - - let rect = this.atlas.packOne(packWidth, packHeight); - if (!rect) { - this.resize(); - rect = this.atlas.packOne(packWidth, packHeight); - } - if (!rect) { - util.warnOnce('glyph bitmap overflow'); - return null; - } - - this.index[key] = rect; - this.ids[key] = [id]; - - const target = this.data; - const source = glyph.bitmap; - for (let y = 0; y < bufferedHeight; y++) { - const y1 = this.width * (rect.y + y + padding) + rect.x + padding; - const y2 = bufferedWidth * y; - for (let x = 0; x < bufferedWidth; x++) { - target[y1 + x] = source[y2 + x]; - } - } - - this.dirty = true; - - return rect; - } - - resize() { - const prevWidth = this.width; - const prevHeight = this.height; - - if (prevWidth >= MAX_SIZE || prevHeight >= MAX_SIZE) return; - - if (this.texture) { - if (this.gl) { - this.gl.deleteTexture(this.texture); - } - this.texture = null; - } - - this.width *= SIZE_GROWTH_RATE; - this.height *= SIZE_GROWTH_RATE; - this.atlas.resize(this.width, this.height); - - const buf = new ArrayBuffer(this.width * this.height); - for (let i = 0; i < prevHeight; i++) { - const src = new Uint8Array(this.data.buffer, prevHeight * i, prevWidth); - const dst = new Uint8Array(buf, prevHeight * i * SIZE_GROWTH_RATE, prevWidth); - dst.set(src); - } - this.data = new Uint8Array(buf); - } - - bind(gl: WebGLRenderingContext) { - this.gl = gl; - if (!this.texture) { - this.texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this.texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.width, this.height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, null); - - } else { - gl.bindTexture(gl.TEXTURE_2D, this.texture); - } - } - - updateTexture(gl: WebGLRenderingContext) { - this.bind(gl); - if (this.dirty) { - gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, this.height, gl.ALPHA, gl.UNSIGNED_BYTE, this.data); - this.dirty = false; - } - } -} - -module.exports = GlyphAtlas; diff --git a/src/symbol/glyph_source.js b/src/symbol/glyph_source.js deleted file mode 100644 index 17cebb38d68..00000000000 --- a/src/symbol/glyph_source.js +++ /dev/null @@ -1,237 +0,0 @@ -// @flow - -const normalizeURL = require('../util/mapbox').normalizeGlyphsURL; -const ajax = require('../util/ajax'); -const Glyphs = require('../util/glyphs'); -const GlyphAtlas = require('../symbol/glyph_atlas'); -const Protobuf = require('pbf'); -const TinySDF = require('@mapbox/tiny-sdf'); -const isChar = require('../util/is_char_in_unicode_block'); -const Evented = require('../util/evented'); -const assert = require('assert'); - -import type {Glyph, GlyphStack} from '../util/glyphs'; -import type {Rect} from '../symbol/glyph_atlas'; -import type {RequestTransformFunction} from '../ui/map'; - -// A simplified representation of the glyph containing only the properties needed for shaping. -class SimpleGlyph { - advance: number; - left: number; - top: number; - rect: ?Rect; - - constructor(glyph: Glyph, rect: ?Rect, buffer: number) { - const padding = 1; - this.advance = glyph.advance; - this.left = glyph.left - buffer - padding; - this.top = glyph.top + buffer + padding; - this.rect = rect; - } -} - -export type {SimpleGlyph as SimpleGlyph}; - -/** - * A glyph source has a URL from which to load new glyphs and manages - * GlyphAtlases in which to store glyphs used by the requested fontstacks - * and ranges. - * - * @private - */ -class GlyphSource extends Evented { - url: ?string; - atlases: {[string]: GlyphAtlas}; - stacks: {[string]: { ranges: {[number]: GlyphStack}, cjkGlyphs: {[number]: Glyph} }}; - loading: {[string]: {[number]: Array}}; - localIdeographFontFamily: ?string; - tinySDFs: {[string]: TinySDF}; - transformRequestCallback: RequestTransformFunction; - - /** - * @param {string} url glyph template url - */ - constructor(url: ?string, localIdeographFontFamily: ?string, transformRequestCallback: RequestTransformFunction, eventedParent?: Evented) { - super(); - this.url = url && normalizeURL(url); - this.atlases = {}; - this.stacks = {}; - this.loading = {}; - this.localIdeographFontFamily = localIdeographFontFamily; - this.tinySDFs = {}; - this.setEventedParent(eventedParent); - this.transformRequestCallback = transformRequestCallback; - } - - getSimpleGlyphs(fontstack: string, glyphIDs: Array, uid: number, callback: (err: ?Error, glyphs: {[number]: SimpleGlyph}, fontstack: string) => void) { - if (this.stacks[fontstack] === undefined) { - this.stacks[fontstack] = { ranges: {}, cjkGlyphs: {} }; - } - if (this.atlases[fontstack] === undefined) { - this.atlases[fontstack] = new GlyphAtlas(); - } - - const glyphs: {[number]: SimpleGlyph} = {}; - const stack = this.stacks[fontstack]; - const atlas = this.atlases[fontstack]; - - // the number of pixels the sdf bitmaps are padded by - const buffer = 3; - - const missingRanges: {[number]: Array} = {}; - let remaining = 0; - - const getGlyph = (glyphID) => { - const range = Math.floor(glyphID / 256); - if (this.localIdeographFontFamily && - // eslint-disable-next-line new-cap - (isChar['CJK Unified Ideographs'](glyphID) || - // eslint-disable-next-line new-cap - isChar['Hangul Syllables'](glyphID))) { - if (!stack.cjkGlyphs[glyphID]) { - stack.cjkGlyphs[glyphID] = this.loadCJKGlyph(fontstack, glyphID); - } - - const glyph = stack.cjkGlyphs[glyphID]; - const rect = atlas.addGlyph(uid, fontstack, glyph, buffer); - if (glyph) glyphs[glyphID] = new SimpleGlyph(glyph, rect, buffer); - } else { - if (stack.ranges[range]) { - const glyph = stack.ranges[range].glyphs[glyphID]; - const rect = atlas.addGlyph(uid, fontstack, glyph, buffer); - if (glyph) glyphs[glyphID] = new SimpleGlyph(glyph, rect, buffer); - } else { - if (missingRanges[range] === undefined) { - missingRanges[range] = []; - remaining++; - } - missingRanges[range].push(glyphID); - } - } - /* eslint-enable new-cap */ - }; - - for (const glyphID of glyphIDs) { - getGlyph(glyphID); - } - - if (!remaining) callback(undefined, glyphs, fontstack); - - const onRangeLoaded = (err: ?Error, range: ?number, data: ?Glyphs) => { - if (err) { - this.fire('error', { error: err }); - } else if (typeof range === 'number' && data) { - const stack = this.stacks[fontstack].ranges[range] = data.stacks[0]; - for (let i = 0; i < missingRanges[range].length; i++) { - const glyphID = missingRanges[range][i]; - const glyph = stack.glyphs[glyphID]; - const rect = atlas.addGlyph(uid, fontstack, glyph, buffer); - if (glyph) glyphs[glyphID] = new SimpleGlyph(glyph, rect, buffer); - } - - remaining--; - if (!remaining) callback(undefined, glyphs, fontstack); - } - }; - - for (const r in missingRanges) { - this.loadRange(fontstack, +r, onRangeLoaded); - } - } - - createTinySDF(fontFamily: string, fontWeight: string) { - return new TinySDF(24, 3, 8, .25, fontFamily, fontWeight); - } - - loadCJKGlyph(fontstack: string, glyphID: number): Glyph { - let tinySDF = this.tinySDFs[fontstack]; - if (!tinySDF) { - let fontWeight = '400'; - if (/bold/i.test(fontstack)) { - fontWeight = '900'; - } else if (/medium/i.test(fontstack)) { - fontWeight = '500'; - } else if (/light/i.test(fontstack)) { - fontWeight = '200'; - } - assert(this.localIdeographFontFamily); - tinySDF = this.tinySDFs[fontstack] = this.createTinySDF((this.localIdeographFontFamily: any), fontWeight); - } - - return { - id: glyphID, - bitmap: tinySDF.draw(String.fromCharCode(glyphID)), - width: 24, - height: 24, - left: 0, - top: -8, - advance: 24 - }; - } - - loadPBF(url: string, callback: Callback<{data: ArrayBuffer}>) { - const request = this.transformRequestCallback ? this.transformRequestCallback(url, ajax.ResourceType.Glyphs) : { url }; - ajax.getArrayBuffer(request, callback); - } - - loadRange(fontstack: string, range: number, callback: (err: ?Error, range: ?number, glyphs: ?Glyphs) => void) { - if (range * 256 > 65535) { - callback(new Error('glyphs > 65535 not supported')); - return; - } - - if (this.loading[fontstack] === undefined) { - this.loading[fontstack] = {}; - } - const loading = this.loading[fontstack]; - - if (loading[range]) { - loading[range].push(callback); - } else { - loading[range] = [callback]; - - assert(this.url); - const rangeName = `${range * 256}-${range * 256 + 255}`; - const url = glyphUrl(fontstack, rangeName, (this.url: any)); - - this.loadPBF(url, (err, response) => { - if (err) { - for (const cb of loading[range]) { - cb(err); - } - } else if (response) { - const glyphs = new Glyphs(new Protobuf(response.data)); - for (const cb of loading[range]) { - cb(null, range, glyphs); - } - } - delete loading[range]; - }); - } - } - - getGlyphAtlas(fontstack: string) { - return this.atlases[fontstack]; - } -} - -/** - * Use CNAME sharding to load a specific glyph range over a randomized - * but consistent subdomain. - * @param {string} fontstack comma-joined fonts - * @param {string} range comma-joined range - * @param {url} url templated url - * @param {string} [subdomains=abc] subdomains as a string where each letter is one. - * @returns {string} a url to load that section of glyphs - * @private - */ -function glyphUrl(fontstack, range, url, subdomains) { - subdomains = subdomains || 'abc'; - - return url - .replace('{s}', subdomains[fontstack.length % subdomains.length]) - .replace('{fontstack}', fontstack) - .replace('{range}', range); -} - -module.exports = GlyphSource; diff --git a/src/symbol/quads.js b/src/symbol/quads.js index 1807d4dbe84..62e0af2c829 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -5,6 +5,7 @@ const Point = require('@mapbox/point-geometry'); import type Anchor from './anchor'; import type {PositionedIcon, Shaping} from './shaping'; import type StyleLayer from '../style/style_layer'; +import type {GlyphPosition} from '../render/glyph_atlas'; module.exports = { getIconQuads, @@ -128,7 +129,8 @@ function getGlyphQuads(anchor: Anchor, layer: StyleLayer, alongLine: boolean, globalProperties: Object, - featureProperties: Object): Array { + featureProperties: Object, + positions: {[number]: GlyphPosition}): Array { const oneEm = 24; const textRotate = layer.getLayoutValue('text-rotate', globalProperties, featureProperties) * Math.PI / 180; @@ -140,13 +142,17 @@ function getGlyphQuads(anchor: Anchor, for (let k = 0; k < positionedGlyphs.length; k++) { const positionedGlyph = positionedGlyphs[k]; - const glyph = positionedGlyph.glyph; + const glyph = positions[positionedGlyph.glyph]; if (!glyph) continue; const rect = glyph.rect; if (!rect) continue; - const halfAdvance = glyph.advance / 2; + // The rects have an addditional buffer that is not included in their size; + const glyphPadding = 1.0; + const rectBuffer = 3.0 + glyphPadding; + + const halfAdvance = glyph.metrics.advance / 2; const glyphOffset = alongLine ? [positionedGlyph.x + halfAdvance, positionedGlyph.y] : @@ -157,8 +163,8 @@ function getGlyphQuads(anchor: Anchor, [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1]]; - const x1 = glyph.left - halfAdvance + builtInOffset[0]; - const y1 = -glyph.top + builtInOffset[1]; + const x1 = glyph.metrics.left - rectBuffer - halfAdvance + builtInOffset[0]; + const y1 = -glyph.metrics.top - rectBuffer + builtInOffset[1]; const x2 = x1 + rect.w; const y2 = y1 + rect.h; diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index ee2015a9920..c6e0dcba8c9 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -4,8 +4,8 @@ const scriptDetection = require('../util/script_detection'); const verticalizePunctuation = require('../util/verticalize_punctuation'); const rtlTextPlugin = require('../source/rtl_text_plugin'); -import type {SpriteAtlasElement} from './sprite_atlas'; -import type {SimpleGlyph} from './glyph_source'; +import type {StyleGlyph} from '../style/style_glyph'; +import type {ImagePosition} from '../render/image_atlas'; const WritingMode = { horizontal: 1, @@ -20,17 +20,15 @@ module.exports = { // The position of a glyph relative to the text's anchor point. export type PositionedGlyph = { - codePoint: number, + glyph: number, x: number, y: number, - glyph: SimpleGlyph, vertical: boolean }; // A collection of positioned glyphs and some metadata export type Shaping = { positionedGlyphs: Array, - text: string, top: number, bottom: number, left: number, @@ -56,7 +54,7 @@ function breakLines(text: string, lineBreakPoints: Array) { } function shapeText(text: string, - glyphs: {[number]: SimpleGlyph}, + glyphs: {[number]: ?StyleGlyph}, maxWidth: number, lineHeight: number, textAnchor: TextAnchor, @@ -130,14 +128,14 @@ const breakable: {[number]: boolean} = { function determineAverageLineWidth(logicalInput: string, spacing: number, maxWidth: number, - glyphs: {[number]: SimpleGlyph}) { + glyphs: {[number]: ?StyleGlyph}) { let totalWidth = 0; for (let index = 0; index < logicalInput.length; index++) { const glyph = glyphs[logicalInput.charCodeAt(index)]; if (!glyph) continue; - totalWidth += glyph.advance + spacing; + totalWidth += glyph.metrics.advance + spacing; } const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); @@ -228,7 +226,7 @@ function leastBadBreaks(lastLineBreak: ?Break): Array { function determineLineBreaks(logicalInput: string, spacing: number, maxWidth: number, - glyphs: {[number]: SimpleGlyph}): Array { + glyphs: {[number]: ?StyleGlyph}): Array { if (!maxWidth) return []; @@ -245,7 +243,7 @@ function determineLineBreaks(logicalInput: string, const glyph = glyphs[codePoint]; if (glyph && !whitespace[codePoint]) - currentX += glyph.advance + spacing; + currentX += glyph.metrics.advance + spacing; // Ideographic characters, spaces, and word-breaking punctuation that often appear without // surrounding spaces. @@ -307,7 +305,7 @@ function getAnchorAlignment(textAnchor: TextAnchor) { } function shapeLines(shaping: Shaping, - glyphs: {[number]: SimpleGlyph}, + glyphs: {[number]: ?StyleGlyph}, lines: Array, lineHeight: number, textAnchor: TextAnchor, @@ -344,10 +342,10 @@ function shapeLines(shaping: Shaping, if (!glyph) continue; if (!scriptDetection.charHasUprightVerticalOrientation(codePoint) || writingMode === WritingMode.horizontal) { - positionedGlyphs.push({codePoint, x, y, glyph, vertical: false}); - x += glyph.advance + spacing; + positionedGlyphs.push({glyph: codePoint, x, y, vertical: false}); + x += glyph.metrics.advance + spacing; } else { - positionedGlyphs.push({codePoint, x, y: 0, glyph, vertical: true}); + positionedGlyphs.push({glyph: codePoint, x, y: 0, vertical: true}); x += verticalHeight + spacing; } } @@ -378,18 +376,21 @@ function shapeLines(shaping: Shaping, // justify right = 1, left = 0, center = 0.5 function justifyLine(positionedGlyphs: Array, - glyphs: {[number]: SimpleGlyph}, + glyphs: {[number]: ?StyleGlyph}, start: number, end: number, justify: 1 | 0 | 0.5) { if (!justify) return; - const lastAdvance = glyphs[positionedGlyphs[end].codePoint].advance; - const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; + const glyph = glyphs[positionedGlyphs[end].glyph]; + if (glyph) { + const lastAdvance = glyph.metrics.advance; + const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; - for (let j = start; j <= end; j++) { - positionedGlyphs[j].x -= lineIndent; + for (let j = start; j <= end; j++) { + positionedGlyphs[j].x -= lineIndent; + } } } @@ -410,14 +411,14 @@ function align(positionedGlyphs: Array, } export type PositionedIcon = { - image: any, + image: ImagePosition, top: number, bottom: number, left: number, right: number }; -function shapeIcon(image: SpriteAtlasElement, iconOffset: [number, number]): PositionedIcon { +function shapeIcon(image: ImagePosition, iconOffset: [number, number]): PositionedIcon { const dx = iconOffset[0]; const dy = iconOffset[1]; const x1 = dx - image.displaySize[0] / 2; diff --git a/src/symbol/sprite_atlas.js b/src/symbol/sprite_atlas.js deleted file mode 100644 index 181337966a2..00000000000 --- a/src/symbol/sprite_atlas.js +++ /dev/null @@ -1,325 +0,0 @@ -// @flow - -const ShelfPack = require('@mapbox/shelf-pack'); -const browser = require('../util/browser'); -const util = require('../util/util'); -const Evented = require('../util/evented'); -const padding = 1; - -import type ImageSprite from '../style/image_sprite'; - -type Rect = { - x: number, - y: number, - w: number, - h: number -}; - -type Pos = { - x: number, - y: number, - width: number, - height: number -}; - -type Image = { - rect: Rect, - width: number, - height: number, - pixelRatio: number, - sdf: boolean -}; - -export type SpriteAtlasElement = { - sdf: boolean, - pixelRatio: number, - isNativePixelRatio: boolean, - textureRect: Rect, - tl: [number, number], - br: [number, number], - displaySize: [number, number] -}; - -export type IconMap = {[string]: ?SpriteAtlasElement}; - -// This wants to be a class, but is sent to workers, so must be a plain JSON blob. -function spriteAtlasElement(image): SpriteAtlasElement { - const textureRect = { - x: image.rect.x + padding, - y: image.rect.y + padding, - w: image.rect.w - padding * 2, - h: image.rect.h - padding * 2 - }; - return { - sdf: image.sdf, - pixelRatio: image.pixelRatio, - isNativePixelRatio: image.pixelRatio === browser.devicePixelRatio, - textureRect: textureRect, - - // Redundant calculated members. - tl: [ - textureRect.x, - textureRect.y - ], - br: [ - textureRect.x + textureRect.w, - textureRect.y + textureRect.h - ], - displaySize: [ - textureRect.w / image.pixelRatio, - textureRect.h / image.pixelRatio - ] - }; -} - -// The SpriteAtlas class is responsible for turning a sprite and assorted -// other images added at runtime into a texture that can be consumed by WebGL. -class SpriteAtlas extends Evented { - images: {[string]: Image}; - data: Uint32Array; - texture: WebGLTexture; - filter: number; - width: number; - height: number; - shelfPack: ShelfPack; - dirty: boolean; - sprite: ImageSprite; - - constructor(width: number, height: number) { - super(); - this.images = {}; - this.filter = 0; // WebGL ID - this.width = Math.ceil(width * browser.devicePixelRatio); - this.height = Math.ceil(height * browser.devicePixelRatio); - this.shelfPack = new ShelfPack(this.width, this.height); - this.dirty = true; - } - - getPixelSize() { - return [ - this.width, - this.height - ]; - } - - allocateImage(pixelWidth: number, pixelHeight: number): ?Rect { - const width = pixelWidth + 2 * padding; - const height = pixelHeight + 2 * padding; - - const rect = this.shelfPack.packOne(width, height); - if (!rect) { - util.warnOnce('SpriteAtlas out of space.'); - return null; - } - - return rect; - } - - addImage(name: string, image: {width: number, height: number, data: Uint8ClampedArray}, {pixelRatio, sdf}: {pixelRatio: number, sdf: boolean}) { - if (this.images[name]) { - return this.fire('error', {error: new Error('An image with this name already exists.')}); - } - - const {width, height, data} = image; - const rect = this.allocateImage(width, height); - if (!rect) { - return this.fire('error', {error: new Error('There is not enough space to add this image.')}); - } - - this.images[name] = { - rect, - width, - height, - pixelRatio, - sdf - }; - - this.copy(new Uint32Array(data.buffer), width, rect, {x: 0, y: 0, width, height}, false); - - this.fire('data', {dataType: 'style'}); - } - - removeImage(name: string) { - const image = this.images[name]; - delete this.images[name]; - - if (!image) { - return this.fire('error', {error: new Error('No image with this name exists.')}); - } - - this.shelfPack.unref(image.rect); - this.fire('data', {dataType: 'style'}); - } - - // Return metrics for an icon image. - getIcon(name: string): ?SpriteAtlasElement { - return this._getImage(name, false); - } - - // Return metrics for repeating pattern image. - getPattern(name: string): ?SpriteAtlasElement { - return this._getImage(name, true); - } - - _getImage(name: string, wrap: boolean): ?SpriteAtlasElement { - if (this.images[name]) { - return spriteAtlasElement(this.images[name]); - } - - if (!this.sprite) { - return null; - } - - const pos = this.sprite.getSpritePosition(name); - if (!pos.width || !pos.height) { - return null; - } - - const rect = this.allocateImage(pos.width, pos.height); - if (!rect) { - return null; - } - - const image = { - rect, - width: pos.width, - height: pos.height, - sdf: pos.sdf, - pixelRatio: pos.pixelRatio - }; - this.images[name] = image; - - if (!this.sprite.imgData) { - return null; - } - - const srcImg = new Uint32Array(this.sprite.imgData.data.buffer); - this.copy(srcImg, this.sprite.imgData.width, rect, pos, wrap); - - return spriteAtlasElement(image); - } - - allocate() { - if (!this.data) { - this.data = new Uint32Array(this.width * this.height); - for (let i = 0; i < this.data.length; i++) { - this.data[i] = 0; - } - } - } - - // Copy some portion of srcImage into `SpriteAtlas#data` - copy(srcImg: Uint32Array, srcImgWidth: number, dstPos: Rect, srcPos: Pos, wrap: boolean) { - this.allocate(); - const dstImg = this.data; - - copyBitmap( - /* source buffer */ srcImg, - /* source stride */ srcImgWidth, - /* source x */ srcPos.x, - /* source y */ srcPos.y, - /* dest buffer */ dstImg, - /* dest stride */ this.getPixelSize()[0], - /* dest x */ dstPos.x + padding, - /* dest y */ dstPos.y + padding, - /* icon dimension */ srcPos.width, - /* icon dimension */ srcPos.height, - /* wrap */ wrap - ); - - // Indicates that `SpriteAtlas#data` has changed and needs to be - // reuploaded into the GL texture specified by `SpriteAtlas#texture`. - this.dirty = true; - } - - setSprite(sprite: ImageSprite) { - this.sprite = sprite; - } - - addIcons(icons: Array, callback: Callback) { - const result = {}; - for (const icon of icons) { - result[icon] = this.getIcon(icon); - } - callback(null, result); - } - - bind(gl: WebGLRenderingContext, linear: boolean) { - let first = false; - if (!this.texture) { - this.texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this.texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, (true: any)); - first = true; - } else { - gl.bindTexture(gl.TEXTURE_2D, this.texture); - } - - const filterVal = linear ? gl.LINEAR : gl.NEAREST; - if (filterVal !== this.filter) { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterVal); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterVal); - this.filter = filterVal; - } - - if (this.dirty) { - this.allocate(); - - if (first) { - gl.texImage2D( - gl.TEXTURE_2D, // enum target - 0, // ind level - gl.RGBA, // ind internalformat - this.width, // GLsizei width - this.height, // GLsizei height - 0, // ind border - gl.RGBA, // enum format - gl.UNSIGNED_BYTE, // enum type - new Uint8Array(this.data.buffer) // Object data - ); - } else { - gl.texSubImage2D( - gl.TEXTURE_2D, // enum target - 0, // int level - 0, // int xoffset - 0, // int yoffset - this.width, // long width - this.height, // long height - gl.RGBA, // enum format - gl.UNSIGNED_BYTE, // enum type - new Uint8Array(this.data.buffer) // Object pixels - ); - } - - this.dirty = false; - } - } -} - -module.exports = SpriteAtlas; - -function copyBitmap(src, srcStride, srcX, srcY, dst, dstStride, dstX, dstY, width, height, wrap) { - let srcI = srcY * srcStride + srcX; - let dstI = dstY * dstStride + dstX; - let x, y; - - if (wrap) { - // add 1 pixel wrapped padding on each side of the image - dstI -= dstStride; - for (y = -1; y <= height; y++, dstI += dstStride) { - srcI = ((y + height) % height + srcY) * srcStride + srcX; - for (x = -1; x <= width; x++) { - dst[dstI + x] = src[srcI + ((x + width) % width)]; - } - } - - } else { - for (y = 0; y < height; y++, srcI += srcStride, dstI += dstStride) { - for (x = 0; x < width; x++) { - dst[dstI + x] = src[srcI + x]; - } - } - } -} diff --git a/src/ui/map.js b/src/ui/map.js index f31e3a3a37b..0447fb0310b 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -31,6 +31,7 @@ import type {LngLatBoundsLike} from '../geo/lng_lat_bounds'; import type {RequestParameters} from '../util/ajax'; import type {StyleOptions} from '../style/style'; import type {MapEvent, MapDataEvent} from './events'; +import type {RGBAImage} from '../util/image'; import type ScrollZoomHandler from './handler/scroll_zoom'; import type BoxZoomHandler from './handler/box_zoom'; @@ -1149,7 +1150,7 @@ class Map extends Camera { * @param options.pixelRatio The ratio of pixels in the image to physical pixels on the screen * @param options.sdf Whether the image should be interpreted as an SDF image */ - addImage(name: string, data: HTMLImageElement | {width: number, height: number, data: Uint8ClampedArray}, + addImage(id: string, data: HTMLImageElement | {width: number, height: number, data: Uint8Array | Uint8ClampedArray}, {pixelRatio = 1, sdf = false}: {pixelRatio?: number, sdf?: boolean} = {}) { if (data instanceof HTMLImageElement) { data = browser.getImageData(data); @@ -1158,16 +1159,16 @@ class Map extends Camera { 'Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, ' + 'or object with `width`, `height`, and `data` properties with the same format as `ImageData`')}); } - this.style.spriteAtlas.addImage(name, data, {pixelRatio, sdf}); + this.style.addImage(id, { data: ((data: any): RGBAImage), pixelRatio, sdf }); } /** * Remove an image from the style (such as one used by `icon-image` or `background-pattern`). * - * @param {string} name The name of the image. + * @param id The ID of the image. */ - removeImage(name: string) { - this.style.spriteAtlas.removeImage(name); + removeImage(id: string) { + this.style.removeImage(id); } /** diff --git a/src/util/glyphs.js b/src/util/glyphs.js deleted file mode 100644 index caf2be0966e..00000000000 --- a/src/util/glyphs.js +++ /dev/null @@ -1,53 +0,0 @@ -// @flow - -export type Glyph = { - id: number, - width: number, - height: number, - left: number, - top: number, - advance: number, - bitmap: Uint8ClampedArray -}; - -export type GlyphStack = { - name: string, - range: string, - glyphs: {[number]: Glyph} -}; - -class Glyphs { - stacks: Array; - - constructor(pbf: any, end: any) { - this.stacks = pbf.readFields(readFontstacks, [], end); - } -} - -function readFontstacks(tag, stacks, pbf) { - if (tag === 1) { - const fontstack = pbf.readMessage(readFontstack, {glyphs: {}}); - stacks.push(fontstack); - } -} - -function readFontstack(tag, fontstack, pbf) { - if (tag === 1) fontstack.name = pbf.readString(); - else if (tag === 2) fontstack.range = pbf.readString(); - else if (tag === 3) { - const glyph = pbf.readMessage(readGlyph, {}); - fontstack.glyphs[glyph.id] = glyph; - } -} - -function readGlyph(tag, glyph, pbf) { - if (tag === 1) glyph.id = pbf.readVarint(); - else if (tag === 2) glyph.bitmap = pbf.readBytes(); - else if (tag === 3) glyph.width = pbf.readVarint(); - else if (tag === 4) glyph.height = pbf.readVarint(); - else if (tag === 5) glyph.left = pbf.readSVarint(); - else if (tag === 6) glyph.top = pbf.readSVarint(); - else if (tag === 7) glyph.advance = pbf.readVarint(); -} - -module.exports = Glyphs; diff --git a/src/util/image.js b/src/util/image.js new file mode 100644 index 00000000000..c3cc3d76a42 --- /dev/null +++ b/src/util/image.js @@ -0,0 +1,121 @@ +// @flow + +const assert = require('assert'); + +export type Size = { + width: number, + height: number +}; + +type Point = { + x: number, + y: number +}; + +function createImage({width, height}: Size, channels: number, data?: Uint8Array) { + if (!data) { + data = new Uint8Array(width * height * channels); + } else if (data.length !== width * height * channels) { + throw new RangeError('mismatched image size'); + } + return { width, height, data }; +} + +function resizeImage(image: *, {width, height}: Size, channels: number) { + if (width === image.width && height === image.height) { + return image; + } + + const newImage = createImage({width, height}, channels); + + copyImage(image, newImage, {x: 0, y: 0}, {x: 0, y: 0}, { + width: Math.min(image.width, width), + height: Math.min(image.height, height) + }, channels); + + image.width = width; + image.height = height; + image.data = newImage.data; +} + +function copyImage(srcImg: *, dstImg: *, srcPt: Point, dstPt: Point, size: Size, channels: number) { + if (size.width === 0 || size.height === 0) { + return dstImg; + } + + if (size.width > srcImg.width || + size.height > srcImg.height || + srcPt.x > srcImg.width - size.width || + srcPt.y > srcImg.height - size.height) { + throw new RangeError('out of range source coordinates for image copy'); + } + + if (size.width > dstImg.width || + size.height > dstImg.height || + dstPt.x > dstImg.width - size.width || + dstPt.y > dstImg.height - size.height) { + throw new RangeError('out of range destination coordinates for image copy'); + } + + const srcData = srcImg.data; + const dstData = dstImg.data; + + assert(srcData !== dstData); + + for (let y = 0; y < size.height; y++) { + const srcOffset = ((srcPt.y + y) * srcImg.width + srcPt.x) * channels; + const dstOffset = ((dstPt.y + y) * dstImg.width + dstPt.x) * channels; + for (let i = 0; i < size.width * channels; i++) { + dstData[dstOffset + i] = srcData[srcOffset + i]; + } + } + + return dstImg; +} + +// These "classes" are really just a combination of type (for the properties) +// and namespace (for the static methods). In reality, the type at runtime is +// a plain old object; we can't use instance methods because these values are +// transferred to and from workers. +class AlphaImage { + width: number; + height: number; + data: Uint8Array; + + static create(size: Size, data?: Uint8Array) { + return ((createImage(size, 1, data): any): AlphaImage); + } + + static resize(image: AlphaImage, size: Size) { + resizeImage(image, size, 1); + } + + static copy(srcImg: AlphaImage, dstImg: AlphaImage, srcPt: Point, dstPt: Point, size: Size) { + copyImage(srcImg, dstImg, srcPt, dstPt, size, 1); + } +} + +// Not premultiplied, because ImageData is not premultiplied. +// UNPACK_PREMULTIPLY_ALPHA_WEBGL must be used when uploading to a texture. +class RGBAImage { + width: number; + height: number; + data: Uint8Array; + + static create(size: Size, data?: Uint8Array) { + return ((createImage(size, 4, data): any): RGBAImage); + } + + static resize(image: RGBAImage, size: Size) { + resizeImage(image, size, 4); + } + + static copy(srcImg: RGBAImage | ImageData, dstImg: RGBAImage, srcPt: Point, dstPt: Point, size: Size) { + copyImage(srcImg, dstImg, srcPt, dstPt, size, 4); + } +} + +module.exports = { + AlphaImage, + RGBAImage +}; diff --git a/src/util/util.js b/src/util/util.js index d66bc7ee495..540e65f43d2 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -89,8 +89,8 @@ exports.wrap = function (n: number, min: number, max: number): number { */ exports.asyncAll = function ( array: Array, - fn: (item: Item, fnCallback: (error: Error | null, result: Result) => void) => void, - callback: (error: Error | null, results: Array) => void + fn: (item: Item, fnCallback: Callback) => void, + callback: Callback> ) { if (!array.length) { return callback(null, []); } let remaining = array.length; @@ -99,7 +99,7 @@ exports.asyncAll = function ( array.forEach((item, i) => { fn(item, (err, result) => { if (err) error = err; - results[i] = result; + results[i] = ((result: any): Result); // https://github.com/facebook/flow/issues/2123 if (--remaining === 0) callback(error, results); }); }); diff --git a/test/expected/text-shaping-default.json b/test/expected/text-shaping-default.json index 657e5807f1a..34324492e0f 100644 --- a/test/expected/text-shaping-default.json +++ b/test/expected/text-shaping-default.json @@ -1,73 +1,33 @@ { "positionedGlyphs": [ { - "codePoint": 97, + "glyph": 97, "x": -32.5, "y": -17, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -19.5, "y": -17, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": -17, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 5.5, "y": -17, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 19.5, "y": -17, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false } ], diff --git a/test/expected/text-shaping-linebreak.json b/test/expected/text-shaping-linebreak.json index 13c79a89c73..1948fcecab1 100644 --- a/test/expected/text-shaping-linebreak.json +++ b/test/expected/text-shaping-linebreak.json @@ -1,143 +1,63 @@ { "positionedGlyphs": [ { - "codePoint": 97, + "glyph": 97, "x": -32.5, "y": -29, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -19.5, "y": -29, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": -29, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 5.5, "y": -29, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 19.5, "y": -29, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 97, + "glyph": 97, "x": -32.5, "y": -5, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -19.5, "y": -5, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": -5, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 5.5, "y": -5, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 19.5, "y": -5, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false } ], diff --git a/test/expected/text-shaping-newline.json b/test/expected/text-shaping-newline.json index 69cdf4d3a99..583c5018b1f 100644 --- a/test/expected/text-shaping-newline.json +++ b/test/expected/text-shaping-newline.json @@ -1,143 +1,63 @@ { "positionedGlyphs": [ { - "codePoint": 97, + "glyph": 97, "x": -32.5, "y": -29, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -19.5, "y": -29, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": -29, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 5.5, "y": -29, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 19.5, "y": -29, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 97, + "glyph": 97, "x": -32.5, "y": -5, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -19.5, "y": -5, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": -5, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 5.5, "y": -5, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 19.5, "y": -5, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false } ], diff --git a/test/expected/text-shaping-newlines-in-middle.json b/test/expected/text-shaping-newlines-in-middle.json index 31597924c01..a11952a84a6 100644 --- a/test/expected/text-shaping-newlines-in-middle.json +++ b/test/expected/text-shaping-newlines-in-middle.json @@ -1,143 +1,63 @@ { "positionedGlyphs": [ { - "codePoint": 97, + "glyph": 97, "x": -32.5, "y": -41, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -19.5, "y": -41, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": -41, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 5.5, "y": -41, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 19.5, "y": -41, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 97, + "glyph": 97, "x": -32.5, "y": 7, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -19.5, "y": 7, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": 7, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 5.5, "y": 7, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 19.5, "y": 7, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false } ], diff --git a/test/expected/text-shaping-null.json b/test/expected/text-shaping-null.json index 8810f46836f..65c9cb911aa 100644 --- a/test/expected/text-shaping-null.json +++ b/test/expected/text-shaping-null.json @@ -1,31 +1,15 @@ { "positionedGlyphs": [ { - "codePoint": 104, + "glyph": 104, "x": -10, "y": -17, - "glyph": { - "id": 104, - "width": 11, - "height": 19, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 105, + "glyph": 105, "x": 4, "y": -17, - "glyph": { - "id": 105, - "width": 4, - "height": 18, - "left": 1, - "top": -8, - "advance": 6 - }, "vertical": false } ], @@ -35,4 +19,4 @@ "left": -10, "right": 10, "writingMode": 1 -} +} \ No newline at end of file diff --git a/test/expected/text-shaping-spacing.json b/test/expected/text-shaping-spacing.json index 31cda6bd523..b9b712bf563 100644 --- a/test/expected/text-shaping-spacing.json +++ b/test/expected/text-shaping-spacing.json @@ -1,73 +1,33 @@ { "positionedGlyphs": [ { - "codePoint": 97, + "glyph": 97, "x": -38.5, "y": -17, - "glyph": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false }, { - "codePoint": 98, + "glyph": 98, "x": -22.5, "y": -17, - "glyph": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 99, + "glyph": 99, "x": -5.5, "y": -17, - "glyph": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, "vertical": false }, { - "codePoint": 100, + "glyph": 100, "x": 8.5, "y": -17, - "glyph": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, "vertical": false }, { - "codePoint": 101, + "glyph": 101, "x": 25.5, "y": -17, - "glyph": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, "vertical": false } ], diff --git a/test/fixtures/fontstack-glyphs.json b/test/fixtures/fontstack-glyphs.json index 3c566f6de25..edfc6386a8b 100644 --- a/test/fixtures/fontstack-glyphs.json +++ b/test/fixtures/fontstack-glyphs.json @@ -1,1538 +1,1922 @@ { - "32": { - "id": 32, - "width": 0, - "height": 0, - "left": 0, - "top": -26, - "advance": 6 - }, - "33": { - "id": 33, - "width": 4, - "height": 19, - "left": 1, - "top": -8, - "advance": 6 - }, - "34": { - "id": 34, - "width": 8, - "height": 8, - "left": 1, - "top": -8, - "advance": 9 - }, - "35": { - "id": 35, - "width": 15, - "height": 18, - "left": 0, - "top": -8, - "advance": 15 - }, - "36": { - "id": 36, - "width": 12, - "height": 21, - "left": 1, - "top": -7, - "advance": 13 - }, - "37": { - "id": 37, - "width": 18, - "height": 19, - "left": 1, - "top": -8, - "advance": 19 - }, - "38": { - "id": 38, - "width": 17, - "height": 19, - "left": 1, - "top": -8, - "advance": 17 - }, - "39": { - "id": 39, - "width": 3, - "height": 8, - "left": 1, - "top": -8, - "advance": 5 - }, - "40": { - "id": 40, - "width": 7, - "height": 22, - "left": 0, - "top": -8, - "advance": 7 - }, - "41": { - "id": 41, - "width": 7, - "height": 22, - "left": 0, - "top": -8, - "advance": 7 - }, - "42": { - "id": 42, - "width": 12, - "height": 12, - "left": 1, - "top": -7, - "advance": 13 - }, - "43": { - "id": 43, - "width": 12, - "height": 13, - "left": 1, - "top": -11, - "advance": 13 - }, - "44": { - "id": 44, - "width": 5, - "height": 7, - "left": 0, - "top": -23, - "advance": 5 - }, - "45": { - "id": 45, - "width": 7, - "height": 3, - "left": 0, - "top": -18, - "advance": 7 - }, - "46": { - "id": 46, - "width": 4, - "height": 4, - "left": 1, - "top": -23, - "advance": 6 - }, - "47": { - "id": 47, - "width": 9, - "height": 18, - "left": 0, - "top": -8, - "advance": 8 - }, - "48": { - "id": 48, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "49": { - "id": 49, - "width": 7, - "height": 18, - "left": 2, - "top": -8, - "advance": 13 - }, - "50": { - "id": 50, - "width": 12, - "height": 18, - "left": 1, - "top": -8, - "advance": 13 - }, - "51": { - "id": 51, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "52": { - "id": 52, - "width": 14, - "height": 18, - "left": 0, - "top": -8, - "advance": 13 - }, - "53": { - "id": 53, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "54": { - "id": 54, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "55": { - "id": 55, - "width": 12, - "height": 18, - "left": 1, - "top": -8, - "advance": 13 - }, - "56": { - "id": 56, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "57": { - "id": 57, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "58": { - "id": 58, - "width": 4, - "height": 15, - "left": 1, - "top": -12, - "advance": 6 - }, - "59": { - "id": 59, - "width": 5, - "height": 18, - "left": 0, - "top": -12, - "advance": 6 - }, - "60": { - "id": 60, - "width": 12, - "height": 13, - "left": 1, - "top": -11, - "advance": 13 - }, - "61": { - "id": 61, - "width": 12, - "height": 7, - "left": 1, - "top": -14, - "advance": 13 - }, - "62": { - "id": 62, - "width": 12, - "height": 13, - "left": 1, - "top": -11, - "advance": 13 - }, - "63": { - "id": 63, - "width": 10, - "height": 19, - "left": 0, - "top": -8, - "advance": 10 - }, - "64": { - "id": 64, - "width": 20, - "height": 21, - "left": 1, - "top": -8, - "advance": 21 - }, - "65": { - "id": 65, - "width": 16, - "height": 18, - "left": 0, - "top": -8, - "advance": 15 - }, - "66": { - "id": 66, - "width": 13, - "height": 18, - "left": 2, - "top": -8, - "advance": 15 - }, - "67": { - "id": 67, - "width": 14, - "height": 19, - "left": 1, - "top": -8, - "advance": 15 - }, - "68": { - "id": 68, - "width": 15, - "height": 18, - "left": 2, - "top": -8, - "advance": 17 - }, - "69": { - "id": 69, - "width": 10, - "height": 18, - "left": 2, - "top": -8, - "advance": 13 - }, - "70": { - "id": 70, - "width": 10, - "height": 18, - "left": 2, - "top": -8, - "advance": 12 - }, - "71": { - "id": 71, - "width": 15, - "height": 19, - "left": 1, - "top": -8, - "advance": 17 - }, - "72": { - "id": 72, - "width": 14, - "height": 18, - "left": 2, - "top": -8, - "advance": 17 - }, - "73": { - "id": 73, - "width": 3, - "height": 18, - "left": 2, - "top": -8, - "advance": 6 - }, - "74": { - "id": 74, - "width": 7, - "height": 23, - "left": -2, - "top": -8, - "advance": 6 - }, - "75": { - "id": 75, - "width": 13, - "height": 18, - "left": 2, - "top": -8, - "advance": 14 - }, - "76": { - "id": 76, - "width": 10, - "height": 18, - "left": 2, - "top": -8, - "advance": 12 - }, - "77": { - "id": 77, - "width": 18, - "height": 18, - "left": 2, - "top": -8, - "advance": 21 - }, - "78": { - "id": 78, - "width": 14, - "height": 18, - "left": 2, - "top": -8, - "advance": 18 - }, - "79": { - "id": 79, - "width": 17, - "height": 19, - "left": 1, - "top": -8, - "advance": 18 - }, - "80": { - "id": 80, - "width": 12, - "height": 18, - "left": 2, - "top": -8, - "advance": 14 - }, - "81": { - "id": 81, - "width": 17, - "height": 23, - "left": 1, - "top": -8, - "advance": 18 - }, - "82": { - "id": 82, - "width": 13, - "height": 18, - "left": 2, - "top": -8, - "advance": 14 - }, - "83": { - "id": 83, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "84": { - "id": 84, - "width": 14, - "height": 18, - "left": 0, - "top": -8, - "advance": 13 - }, - "85": { - "id": 85, - "width": 14, - "height": 19, - "left": 2, - "top": -8, - "advance": 17 - }, - "86": { - "id": 86, - "width": 15, - "height": 18, - "left": 0, - "top": -8, - "advance": 14 - }, - "87": { - "id": 87, - "width": 22, - "height": 18, - "left": 0, - "top": -8, - "advance": 22 - }, - "88": { - "id": 88, - "width": 14, - "height": 18, - "left": 0, - "top": -8, - "advance": 13 - }, - "89": { - "id": 89, - "width": 14, - "height": 18, - "left": 0, - "top": -8, - "advance": 13 - }, - "90": { - "id": 90, - "width": 13, - "height": 18, - "left": 0, - "top": -8, - "advance": 13 - }, - "91": { - "id": 91, - "width": 7, - "height": 22, - "left": 1, - "top": -8, - "advance": 7 - }, - "92": { - "id": 92, - "width": 9, - "height": 18, - "left": 0, - "top": -8, - "advance": 8 - }, - "93": { - "id": 93, - "width": 6, - "height": 22, - "left": 0, - "top": -8, - "advance": 7 - }, - "94": { - "id": 94, - "width": 13, - "height": 12, - "left": 0, - "top": -8, - "advance": 13 - }, - "95": { - "id": 95, - "width": 12, - "height": 2, - "left": -1, - "top": -28, - "advance": 10 - }, - "96": { - "id": 96, - "width": 6, - "height": 5, - "left": 4, - "top": -7, - "advance": 13 - }, - "97": { - "id": 97, - "width": 11, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, - "98": { - "id": 98, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, - "99": { - "id": 99, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, - "100": { - "id": 100, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "101": { - "id": 101, - "width": 12, - "height": 15, - "left": 1, - "top": -12, - "advance": 13 - }, - "102": { - "id": 102, - "width": 10, - "height": 19, - "left": 0, - "top": -7, - "advance": 8 - }, - "103": { - "id": 103, - "width": 13, - "height": 20, - "left": 0, - "top": -12, - "advance": 13 - }, - "104": { - "id": 104, - "width": 11, - "height": 19, - "left": 2, - "top": -7, - "advance": 14 - }, - "105": { - "id": 105, - "width": 4, - "height": 18, - "left": 1, - "top": -8, - "advance": 6 - }, - "106": { - "id": 106, - "width": 7, - "height": 24, - "left": -2, - "top": -8, - "advance": 6 - }, - "107": { - "id": 107, - "width": 11, - "height": 19, - "left": 2, - "top": -7, - "advance": 12 - }, - "108": { - "id": 108, - "width": 3, - "height": 19, - "left": 2, - "top": -7, - "advance": 6 - }, - "109": { - "id": 109, - "width": 19, - "height": 14, - "left": 2, - "top": -12, - "advance": 22 - }, - "110": { - "id": 110, - "width": 11, - "height": 14, - "left": 2, - "top": -12, - "advance": 14 - }, - "111": { - "id": 111, - "width": 13, - "height": 15, - "left": 1, - "top": -12, - "advance": 14 - }, - "112": { - "id": 112, - "width": 12, - "height": 20, - "left": 2, - "top": -12, - "advance": 14 - }, - "113": { - "id": 113, - "width": 12, - "height": 20, - "left": 1, - "top": -12, - "advance": 14 - }, - "114": { - "id": 114, - "width": 8, - "height": 14, - "left": 2, - "top": -12, - "advance": 9 - }, - "115": { - "id": 115, - "width": 10, - "height": 15, - "left": 1, - "top": -12, - "advance": 11 - }, - "116": { - "id": 116, - "width": 8, - "height": 17, - "left": 0, - "top": -10, - "advance": 8 - }, - "117": { - "id": 117, - "width": 12, - "height": 14, - "left": 1, - "top": -13, - "advance": 14 - }, - "118": { - "id": 118, - "width": 13, - "height": 13, - "left": 0, - "top": -13, - "advance": 12 - }, - "119": { - "id": 119, - "width": 19, - "height": 13, - "left": 0, - "top": -13, - "advance": 18 - }, - "120": { - "id": 120, - "width": 13, - "height": 13, - "left": 0, - "top": -13, - "advance": 12 - }, - "121": { - "id": 121, - "width": 13, - "height": 19, - "left": 0, - "top": -13, - "advance": 12 - }, - "122": { - "id": 122, - "width": 11, - "height": 13, - "left": 0, - "top": -13, - "advance": 11 - }, - "123": { - "id": 123, - "width": 9, - "height": 22, - "left": 0, - "top": -8, - "advance": 9 - }, - "124": { - "id": 124, - "width": 3, - "height": 25, - "left": 5, - "top": -7, - "advance": 13 - }, - "125": { - "id": 125, - "width": 9, - "height": 22, - "left": 0, - "top": -8, - "advance": 9 - }, - "126": { - "id": 126, - "width": 12, - "height": 4, - "left": 1, - "top": -16, - "advance": 13 - }, - "160": { - "id": 160, - "width": 0, - "height": 0, - "left": 0, - "top": -26, - "advance": 6 - }, - "161": { - "id": 161, - "width": 4, - "height": 19, - "left": 1, - "top": -12, - "advance": 6 - }, - "162": { - "id": 162, - "width": 10, - "height": 19, - "left": 2, - "top": -8, - "advance": 13 - }, - "163": { - "id": 163, - "width": 13, - "height": 18, - "left": 0, - "top": -8, - "advance": 13 - }, - "164": { - "id": 164, - "width": 12, - "height": 11, - "left": 1, - "top": -12, - "advance": 13 - }, - "165": { - "id": 165, - "width": 14, - "height": 18, - "left": 0, - "top": -8, - "advance": 13 - }, - "166": { - "id": 166, - "width": 3, - "height": 25, - "left": 5, - "top": -7, - "advance": 13 - }, - "167": { - "id": 167, - "width": 10, - "height": 20, - "left": 1, - "top": -7, - "advance": 12 - }, - "168": { - "id": 168, - "width": 8, - "height": 3, - "left": 3, - "top": -8, - "advance": 13 - }, - "169": { - "id": 169, - "width": 18, - "height": 19, - "left": 1, - "top": -8, - "advance": 19 - }, - "170": { - "id": 170, - "width": 8, - "height": 9, - "left": 0, - "top": -8, - "advance": 8 - }, - "171": { - "id": 171, - "width": 11, - "height": 11, - "left": 0, - "top": -14, - "advance": 11 - }, - "172": { - "id": 172, - "width": 12, - "height": 7, - "left": 1, - "top": -16, - "advance": 13 - }, - "173": { - "id": 173, - "width": 7, - "height": 3, - "left": 0, - "top": -18, - "advance": 7 - }, - "174": { - "id": 174, - "width": 18, - "height": 19, - "left": 1, - "top": -8, - "advance": 19 - }, - "175": { - "id": 175, - "width": 14, - "height": 2, - "left": -1, - "top": -6, - "advance": 12 - }, - "176": { - "id": 176, - "width": 8, - "height": 8, - "left": 1, - "top": -8, - "advance": 10 - }, - "177": { - "id": 177, - "width": 12, - "height": 16, - "left": 1, - "top": -11, - "advance": 13 - }, - "178": { - "id": 178, - "width": 8, - "height": 12, - "left": 0, - "top": -8, - "advance": 8 - }, - "179": { - "id": 179, - "width": 8, - "height": 12, - "left": 0, - "top": -8, - "advance": 8 - }, - "180": { - "id": 180, - "width": 6, - "height": 5, - "left": 4, - "top": -7, - "advance": 13 - }, - "181": { - "id": 181, - "width": 11, - "height": 19, - "left": 2, - "top": -13, - "advance": 14 - }, - "182": { - "id": 182, - "width": 13, - "height": 23, - "left": 1, - "top": -7, - "advance": 15 - }, - "183": { - "id": 183, - "width": 4, - "height": 5, - "left": 1, - "top": -15, - "advance": 6 - }, - "184": { - "id": 184, - "width": 6, - "height": 6, - "left": 0, - "top": -26, - "advance": 5 - }, - "185": { - "id": 185, - "width": 6, - "height": 12, - "left": 0, - "top": -8, - "advance": 8 - }, - "186": { - "id": 186, - "width": 9, - "height": 9, - "left": 0, - "top": -8, - "advance": 9 - }, - "187": { - "id": 187, - "width": 11, - "height": 11, - "left": 0, - "top": -14, - "advance": 11 - }, - "188": { - "id": 188, - "width": 18, - "height": 19, - "left": 0, - "top": -8, - "advance": 18 - }, - "189": { - "id": 189, - "width": 18, - "height": 19, - "left": 0, - "top": -8, - "advance": 18 - }, - "190": { - "id": 190, - "width": 19, - "height": 19, - "left": 0, - "top": -8, - "advance": 18 - }, - "191": { - "id": 191, - "width": 10, - "height": 19, - "left": 0, - "top": -12, - "advance": 10 - }, - "192": { - "id": 192, - "width": 16, - "height": 23, - "left": 0, - "top": -3, - "advance": 15 - }, - "193": { - "id": 193, - "width": 16, - "height": 23, - "left": 0, - "top": -3, - "advance": 15 - }, - "194": { - "id": 194, - "width": 16, - "height": 23, - "left": 0, - "top": -3, - "advance": 15 - }, - "195": { - "id": 195, - "width": 16, - "height": 22, - "left": 0, - "top": -4, - "advance": 15 - }, - "196": { - "id": 196, - "width": 16, - "height": 22, - "left": 0, - "top": -4, - "advance": 15 - }, - "197": { - "id": 197, - "width": 16, - "height": 22, - "left": 0, - "top": -4, - "advance": 15 - }, - "198": { - "id": 198, - "width": 21, - "height": 18, - "left": -1, - "top": -8, - "advance": 20 - }, - "199": { - "id": 199, - "width": 14, - "height": 24, - "left": 1, - "top": -8, - "advance": 15 - }, - "200": { - "id": 200, - "width": 10, - "height": 23, - "left": 2, - "top": -3, - "advance": 13 - }, - "201": { - "id": 201, - "width": 10, - "height": 23, - "left": 2, - "top": -3, - "advance": 13 - }, - "202": { - "id": 202, - "width": 10, - "height": 23, - "left": 2, - "top": -3, - "advance": 13 - }, - "203": { - "id": 203, - "width": 10, - "height": 22, - "left": 2, - "top": -4, - "advance": 13 - }, - "204": { - "id": 204, - "width": 6, - "height": 23, - "left": -1, - "top": -3, - "advance": 6 - }, - "205": { - "id": 205, - "width": 6, - "height": 23, - "left": 1, - "top": -3, - "advance": 6 - }, - "206": { - "id": 206, - "width": 9, - "height": 23, - "left": -1, - "top": -3, - "advance": 6 - }, - "207": { - "id": 207, - "width": 8, - "height": 22, - "left": -1, - "top": -4, - "advance": 6 - }, - "208": { - "id": 208, - "width": 16, - "height": 18, - "left": 0, - "top": -8, - "advance": 17 - }, - "209": { - "id": 209, - "width": 14, - "height": 22, - "left": 2, - "top": -4, - "advance": 18 - }, - "210": { - "id": 210, - "width": 17, - "height": 24, - "left": 1, - "top": -3, - "advance": 18 - }, - "211": { - "id": 211, - "width": 17, - "height": 24, - "left": 1, - "top": -3, - "advance": 18 - }, - "212": { - "id": 212, - "width": 17, - "height": 24, - "left": 1, - "top": -3, - "advance": 18 - }, - "213": { - "id": 213, - "width": 17, - "height": 23, - "left": 1, - "top": -4, - "advance": 18 - }, - "214": { - "id": 214, - "width": 17, - "height": 23, - "left": 1, - "top": -4, - "advance": 18 - }, - "215": { - "id": 215, - "width": 12, - "height": 11, - "left": 1, - "top": -12, - "advance": 13 - }, - "216": { - "id": 216, - "width": 17, - "height": 19, - "left": 1, - "top": -8, - "advance": 18 - }, - "217": { - "id": 217, - "width": 14, - "height": 24, - "left": 2, - "top": -3, - "advance": 17 - }, - "218": { - "id": 218, - "width": 14, - "height": 24, - "left": 2, - "top": -3, - "advance": 17 - }, - "219": { - "id": 219, - "width": 14, - "height": 24, - "left": 2, - "top": -3, - "advance": 17 - }, - "220": { - "id": 220, - "width": 14, - "height": 23, - "left": 2, - "top": -4, - "advance": 17 - }, - "221": { - "id": 221, - "width": 14, - "height": 23, - "left": 0, - "top": -3, - "advance": 13 - }, - "222": { - "id": 222, - "width": 12, - "height": 18, - "left": 2, - "top": -8, - "advance": 14 - }, - "223": { - "id": 223, - "width": 12, - "height": 20, - "left": 2, - "top": -7, - "advance": 14 - }, - "224": { - "id": 224, - "width": 11, - "height": 20, - "left": 1, - "top": -7, - "advance": 13 - }, - "225": { - "id": 225, - "width": 11, - "height": 20, - "left": 1, - "top": -7, - "advance": 13 - }, - "226": { - "id": 226, - "width": 11, - "height": 20, - "left": 1, - "top": -7, - "advance": 13 - }, - "227": { - "id": 227, - "width": 11, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "228": { - "id": 228, - "width": 11, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "229": { - "id": 229, - "width": 11, - "height": 21, - "left": 1, - "top": -6, - "advance": 13 - }, - "230": { - "id": 230, - "width": 19, - "height": 15, - "left": 1, - "top": -12, - "advance": 20 - }, - "231": { - "id": 231, - "width": 10, - "height": 20, - "left": 1, - "top": -12, - "advance": 11 - }, - "232": { - "id": 232, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 13 - }, - "233": { - "id": 233, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 13 - }, - "234": { - "id": 234, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 13 - }, - "235": { - "id": 235, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 13 - }, - "236": { - "id": 236, - "width": 6, - "height": 19, - "left": -1, - "top": -7, - "advance": 6 - }, - "237": { - "id": 237, - "width": 6, - "height": 19, - "left": 1, - "top": -7, - "advance": 6 - }, - "238": { - "id": 238, - "width": 9, - "height": 19, - "left": -1, - "top": -7, - "advance": 6 - }, - "239": { - "id": 239, - "width": 8, - "height": 18, - "left": -1, - "top": -8, - "advance": 6 - }, - "240": { - "id": 240, - "width": 13, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "241": { - "id": 241, - "width": 11, - "height": 18, - "left": 2, - "top": -8, - "advance": 14 - }, - "242": { - "id": 242, - "width": 13, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "243": { - "id": 243, - "width": 13, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "244": { - "id": 244, - "width": 13, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "245": { - "id": 245, - "width": 13, - "height": 19, - "left": 1, - "top": -8, - "advance": 14 - }, - "246": { - "id": 246, - "width": 13, - "height": 19, - "left": 1, - "top": -8, - "advance": 14 - }, - "247": { - "id": 247, - "width": 12, - "height": 12, - "left": 1, - "top": -12, - "advance": 13 - }, - "248": { - "id": 248, - "width": 13, - "height": 15, - "left": 1, - "top": -12, - "advance": 14 - }, - "249": { - "id": 249, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "250": { - "id": 250, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "251": { - "id": 251, - "width": 12, - "height": 20, - "left": 1, - "top": -7, - "advance": 14 - }, - "252": { - "id": 252, - "width": 12, - "height": 19, - "left": 1, - "top": -8, - "advance": 14 - }, - "253": { - "id": 253, - "width": 13, - "height": 25, - "left": 0, - "top": -7, - "advance": 12 - }, - "254": { - "id": 254, - "width": 12, - "height": 25, - "left": 2, - "top": -7, - "advance": 14 - }, - "255": { - "id": 255, - "width": 13, - "height": 24, - "left": 0, - "top": -8, - "advance": 12 - }, - "256": { - "id": 256, - "width": 16, - "height": 21, - "left": 0, - "top": -5, - "advance": 15 - } -} + "32": { + "id": 32, + "metrics": { + "width": 0, + "height": 0, + "left": 0, + "top": -26, + "advance": 6 + } + }, + "33": { + "id": 33, + "metrics": { + "width": 4, + "height": 19, + "left": 1, + "top": -8, + "advance": 6 + } + }, + "34": { + "id": 34, + "metrics": { + "width": 8, + "height": 8, + "left": 1, + "top": -8, + "advance": 9 + } + }, + "35": { + "id": 35, + "metrics": { + "width": 15, + "height": 18, + "left": 0, + "top": -8, + "advance": 15 + } + }, + "36": { + "id": 36, + "metrics": { + "width": 12, + "height": 21, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "37": { + "id": 37, + "metrics": { + "width": 18, + "height": 19, + "left": 1, + "top": -8, + "advance": 19 + } + }, + "38": { + "id": 38, + "metrics": { + "width": 17, + "height": 19, + "left": 1, + "top": -8, + "advance": 17 + } + }, + "39": { + "id": 39, + "metrics": { + "width": 3, + "height": 8, + "left": 1, + "top": -8, + "advance": 5 + } + }, + "40": { + "id": 40, + "metrics": { + "width": 7, + "height": 22, + "left": 0, + "top": -8, + "advance": 7 + } + }, + "41": { + "id": 41, + "metrics": { + "width": 7, + "height": 22, + "left": 0, + "top": -8, + "advance": 7 + } + }, + "42": { + "id": 42, + "metrics": { + "width": 12, + "height": 12, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "43": { + "id": 43, + "metrics": { + "width": 12, + "height": 13, + "left": 1, + "top": -11, + "advance": 13 + } + }, + "44": { + "id": 44, + "metrics": { + "width": 5, + "height": 7, + "left": 0, + "top": -23, + "advance": 5 + } + }, + "45": { + "id": 45, + "metrics": { + "width": 7, + "height": 3, + "left": 0, + "top": -18, + "advance": 7 + } + }, + "46": { + "id": 46, + "metrics": { + "width": 4, + "height": 4, + "left": 1, + "top": -23, + "advance": 6 + } + }, + "47": { + "id": 47, + "metrics": { + "width": 9, + "height": 18, + "left": 0, + "top": -8, + "advance": 8 + } + }, + "48": { + "id": 48, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "49": { + "id": 49, + "metrics": { + "width": 7, + "height": 18, + "left": 2, + "top": -8, + "advance": 13 + } + }, + "50": { + "id": 50, + "metrics": { + "width": 12, + "height": 18, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "51": { + "id": 51, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "52": { + "id": 52, + "metrics": { + "width": 14, + "height": 18, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "53": { + "id": 53, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "54": { + "id": 54, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "55": { + "id": 55, + "metrics": { + "width": 12, + "height": 18, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "56": { + "id": 56, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "57": { + "id": 57, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "58": { + "id": 58, + "metrics": { + "width": 4, + "height": 15, + "left": 1, + "top": -12, + "advance": 6 + } + }, + "59": { + "id": 59, + "metrics": { + "width": 5, + "height": 18, + "left": 0, + "top": -12, + "advance": 6 + } + }, + "60": { + "id": 60, + "metrics": { + "width": 12, + "height": 13, + "left": 1, + "top": -11, + "advance": 13 + } + }, + "61": { + "id": 61, + "metrics": { + "width": 12, + "height": 7, + "left": 1, + "top": -14, + "advance": 13 + } + }, + "62": { + "id": 62, + "metrics": { + "width": 12, + "height": 13, + "left": 1, + "top": -11, + "advance": 13 + } + }, + "63": { + "id": 63, + "metrics": { + "width": 10, + "height": 19, + "left": 0, + "top": -8, + "advance": 10 + } + }, + "64": { + "id": 64, + "metrics": { + "width": 20, + "height": 21, + "left": 1, + "top": -8, + "advance": 21 + } + }, + "65": { + "id": 65, + "metrics": { + "width": 16, + "height": 18, + "left": 0, + "top": -8, + "advance": 15 + } + }, + "66": { + "id": 66, + "metrics": { + "width": 13, + "height": 18, + "left": 2, + "top": -8, + "advance": 15 + } + }, + "67": { + "id": 67, + "metrics": { + "width": 14, + "height": 19, + "left": 1, + "top": -8, + "advance": 15 + } + }, + "68": { + "id": 68, + "metrics": { + "width": 15, + "height": 18, + "left": 2, + "top": -8, + "advance": 17 + } + }, + "69": { + "id": 69, + "metrics": { + "width": 10, + "height": 18, + "left": 2, + "top": -8, + "advance": 13 + } + }, + "70": { + "id": 70, + "metrics": { + "width": 10, + "height": 18, + "left": 2, + "top": -8, + "advance": 12 + } + }, + "71": { + "id": 71, + "metrics": { + "width": 15, + "height": 19, + "left": 1, + "top": -8, + "advance": 17 + } + }, + "72": { + "id": 72, + "metrics": { + "width": 14, + "height": 18, + "left": 2, + "top": -8, + "advance": 17 + } + }, + "73": { + "id": 73, + "metrics": { + "width": 3, + "height": 18, + "left": 2, + "top": -8, + "advance": 6 + } + }, + "74": { + "id": 74, + "metrics": { + "width": 7, + "height": 23, + "left": -2, + "top": -8, + "advance": 6 + } + }, + "75": { + "id": 75, + "metrics": { + "width": 13, + "height": 18, + "left": 2, + "top": -8, + "advance": 14 + } + }, + "76": { + "id": 76, + "metrics": { + "width": 10, + "height": 18, + "left": 2, + "top": -8, + "advance": 12 + } + }, + "77": { + "id": 77, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + } + }, + "78": { + "id": 78, + "metrics": { + "width": 14, + "height": 18, + "left": 2, + "top": -8, + "advance": 18 + } + }, + "79": { + "id": 79, + "metrics": { + "width": 17, + "height": 19, + "left": 1, + "top": -8, + "advance": 18 + } + }, + "80": { + "id": 80, + "metrics": { + "width": 12, + "height": 18, + "left": 2, + "top": -8, + "advance": 14 + } + }, + "81": { + "id": 81, + "metrics": { + "width": 17, + "height": 23, + "left": 1, + "top": -8, + "advance": 18 + } + }, + "82": { + "id": 82, + "metrics": { + "width": 13, + "height": 18, + "left": 2, + "top": -8, + "advance": 14 + } + }, + "83": { + "id": 83, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "84": { + "id": 84, + "metrics": { + "width": 14, + "height": 18, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "85": { + "id": 85, + "metrics": { + "width": 14, + "height": 19, + "left": 2, + "top": -8, + "advance": 17 + } + }, + "86": { + "id": 86, + "metrics": { + "width": 15, + "height": 18, + "left": 0, + "top": -8, + "advance": 14 + } + }, + "87": { + "id": 87, + "metrics": { + "width": 22, + "height": 18, + "left": 0, + "top": -8, + "advance": 22 + } + }, + "88": { + "id": 88, + "metrics": { + "width": 14, + "height": 18, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "89": { + "id": 89, + "metrics": { + "width": 14, + "height": 18, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "90": { + "id": 90, + "metrics": { + "width": 13, + "height": 18, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "91": { + "id": 91, + "metrics": { + "width": 7, + "height": 22, + "left": 1, + "top": -8, + "advance": 7 + } + }, + "92": { + "id": 92, + "metrics": { + "width": 9, + "height": 18, + "left": 0, + "top": -8, + "advance": 8 + } + }, + "93": { + "id": 93, + "metrics": { + "width": 6, + "height": 22, + "left": 0, + "top": -8, + "advance": 7 + } + }, + "94": { + "id": 94, + "metrics": { + "width": 13, + "height": 12, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "95": { + "id": 95, + "metrics": { + "width": 12, + "height": 2, + "left": -1, + "top": -28, + "advance": 10 + } + }, + "96": { + "id": 96, + "metrics": { + "width": 6, + "height": 5, + "left": 4, + "top": -7, + "advance": 13 + } + }, + "97": { + "id": 97, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + } + }, + "98": { + "id": 98, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + } + }, + "99": { + "id": 99, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + } + }, + "100": { + "id": 100, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "101": { + "id": 101, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + } + }, + "102": { + "id": 102, + "metrics": { + "width": 10, + "height": 19, + "left": 0, + "top": -7, + "advance": 8 + } + }, + "103": { + "id": 103, + "metrics": { + "width": 13, + "height": 20, + "left": 0, + "top": -12, + "advance": 13 + } + }, + "104": { + "id": 104, + "metrics": { + "width": 11, + "height": 19, + "left": 2, + "top": -7, + "advance": 14 + } + }, + "105": { + "id": 105, + "metrics": { + "width": 4, + "height": 18, + "left": 1, + "top": -8, + "advance": 6 + } + }, + "106": { + "id": 106, + "metrics": { + "width": 7, + "height": 24, + "left": -2, + "top": -8, + "advance": 6 + } + }, + "107": { + "id": 107, + "metrics": { + "width": 11, + "height": 19, + "left": 2, + "top": -7, + "advance": 12 + } + }, + "108": { + "id": 108, + "metrics": { + "width": 3, + "height": 19, + "left": 2, + "top": -7, + "advance": 6 + } + }, + "109": { + "id": 109, + "metrics": { + "width": 19, + "height": 14, + "left": 2, + "top": -12, + "advance": 22 + } + }, + "110": { + "id": 110, + "metrics": { + "width": 11, + "height": 14, + "left": 2, + "top": -12, + "advance": 14 + } + }, + "111": { + "id": 111, + "metrics": { + "width": 13, + "height": 15, + "left": 1, + "top": -12, + "advance": 14 + } + }, + "112": { + "id": 112, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -12, + "advance": 14 + } + }, + "113": { + "id": 113, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -12, + "advance": 14 + } + }, + "114": { + "id": 114, + "metrics": { + "width": 8, + "height": 14, + "left": 2, + "top": -12, + "advance": 9 + } + }, + "115": { + "id": 115, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + } + }, + "116": { + "id": 116, + "metrics": { + "width": 8, + "height": 17, + "left": 0, + "top": -10, + "advance": 8 + } + }, + "117": { + "id": 117, + "metrics": { + "width": 12, + "height": 14, + "left": 1, + "top": -13, + "advance": 14 + } + }, + "118": { + "id": 118, + "metrics": { + "width": 13, + "height": 13, + "left": 0, + "top": -13, + "advance": 12 + } + }, + "119": { + "id": 119, + "metrics": { + "width": 19, + "height": 13, + "left": 0, + "top": -13, + "advance": 18 + } + }, + "120": { + "id": 120, + "metrics": { + "width": 13, + "height": 13, + "left": 0, + "top": -13, + "advance": 12 + } + }, + "121": { + "id": 121, + "metrics": { + "width": 13, + "height": 19, + "left": 0, + "top": -13, + "advance": 12 + } + }, + "122": { + "id": 122, + "metrics": { + "width": 11, + "height": 13, + "left": 0, + "top": -13, + "advance": 11 + } + }, + "123": { + "id": 123, + "metrics": { + "width": 9, + "height": 22, + "left": 0, + "top": -8, + "advance": 9 + } + }, + "124": { + "id": 124, + "metrics": { + "width": 3, + "height": 25, + "left": 5, + "top": -7, + "advance": 13 + } + }, + "125": { + "id": 125, + "metrics": { + "width": 9, + "height": 22, + "left": 0, + "top": -8, + "advance": 9 + } + }, + "126": { + "id": 126, + "metrics": { + "width": 12, + "height": 4, + "left": 1, + "top": -16, + "advance": 13 + } + }, + "160": { + "id": 160, + "metrics": { + "width": 0, + "height": 0, + "left": 0, + "top": -26, + "advance": 6 + } + }, + "161": { + "id": 161, + "metrics": { + "width": 4, + "height": 19, + "left": 1, + "top": -12, + "advance": 6 + } + }, + "162": { + "id": 162, + "metrics": { + "width": 10, + "height": 19, + "left": 2, + "top": -8, + "advance": 13 + } + }, + "163": { + "id": 163, + "metrics": { + "width": 13, + "height": 18, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "164": { + "id": 164, + "metrics": { + "width": 12, + "height": 11, + "left": 1, + "top": -12, + "advance": 13 + } + }, + "165": { + "id": 165, + "metrics": { + "width": 14, + "height": 18, + "left": 0, + "top": -8, + "advance": 13 + } + }, + "166": { + "id": 166, + "metrics": { + "width": 3, + "height": 25, + "left": 5, + "top": -7, + "advance": 13 + } + }, + "167": { + "id": 167, + "metrics": { + "width": 10, + "height": 20, + "left": 1, + "top": -7, + "advance": 12 + } + }, + "168": { + "id": 168, + "metrics": { + "width": 8, + "height": 3, + "left": 3, + "top": -8, + "advance": 13 + } + }, + "169": { + "id": 169, + "metrics": { + "width": 18, + "height": 19, + "left": 1, + "top": -8, + "advance": 19 + } + }, + "170": { + "id": 170, + "metrics": { + "width": 8, + "height": 9, + "left": 0, + "top": -8, + "advance": 8 + } + }, + "171": { + "id": 171, + "metrics": { + "width": 11, + "height": 11, + "left": 0, + "top": -14, + "advance": 11 + } + }, + "172": { + "id": 172, + "metrics": { + "width": 12, + "height": 7, + "left": 1, + "top": -16, + "advance": 13 + } + }, + "173": { + "id": 173, + "metrics": { + "width": 7, + "height": 3, + "left": 0, + "top": -18, + "advance": 7 + } + }, + "174": { + "id": 174, + "metrics": { + "width": 18, + "height": 19, + "left": 1, + "top": -8, + "advance": 19 + } + }, + "175": { + "id": 175, + "metrics": { + "width": 14, + "height": 2, + "left": -1, + "top": -6, + "advance": 12 + } + }, + "176": { + "id": 176, + "metrics": { + "width": 8, + "height": 8, + "left": 1, + "top": -8, + "advance": 10 + } + }, + "177": { + "id": 177, + "metrics": { + "width": 12, + "height": 16, + "left": 1, + "top": -11, + "advance": 13 + } + }, + "178": { + "id": 178, + "metrics": { + "width": 8, + "height": 12, + "left": 0, + "top": -8, + "advance": 8 + } + }, + "179": { + "id": 179, + "metrics": { + "width": 8, + "height": 12, + "left": 0, + "top": -8, + "advance": 8 + } + }, + "180": { + "id": 180, + "metrics": { + "width": 6, + "height": 5, + "left": 4, + "top": -7, + "advance": 13 + } + }, + "181": { + "id": 181, + "metrics": { + "width": 11, + "height": 19, + "left": 2, + "top": -13, + "advance": 14 + } + }, + "182": { + "id": 182, + "metrics": { + "width": 13, + "height": 23, + "left": 1, + "top": -7, + "advance": 15 + } + }, + "183": { + "id": 183, + "metrics": { + "width": 4, + "height": 5, + "left": 1, + "top": -15, + "advance": 6 + } + }, + "184": { + "id": 184, + "metrics": { + "width": 6, + "height": 6, + "left": 0, + "top": -26, + "advance": 5 + } + }, + "185": { + "id": 185, + "metrics": { + "width": 6, + "height": 12, + "left": 0, + "top": -8, + "advance": 8 + } + }, + "186": { + "id": 186, + "metrics": { + "width": 9, + "height": 9, + "left": 0, + "top": -8, + "advance": 9 + } + }, + "187": { + "id": 187, + "metrics": { + "width": 11, + "height": 11, + "left": 0, + "top": -14, + "advance": 11 + } + }, + "188": { + "id": 188, + "metrics": { + "width": 18, + "height": 19, + "left": 0, + "top": -8, + "advance": 18 + } + }, + "189": { + "id": 189, + "metrics": { + "width": 18, + "height": 19, + "left": 0, + "top": -8, + "advance": 18 + } + }, + "190": { + "id": 190, + "metrics": { + "width": 19, + "height": 19, + "left": 0, + "top": -8, + "advance": 18 + } + }, + "191": { + "id": 191, + "metrics": { + "width": 10, + "height": 19, + "left": 0, + "top": -12, + "advance": 10 + } + }, + "192": { + "id": 192, + "metrics": { + "width": 16, + "height": 23, + "left": 0, + "top": -3, + "advance": 15 + } + }, + "193": { + "id": 193, + "metrics": { + "width": 16, + "height": 23, + "left": 0, + "top": -3, + "advance": 15 + } + }, + "194": { + "id": 194, + "metrics": { + "width": 16, + "height": 23, + "left": 0, + "top": -3, + "advance": 15 + } + }, + "195": { + "id": 195, + "metrics": { + "width": 16, + "height": 22, + "left": 0, + "top": -4, + "advance": 15 + } + }, + "196": { + "id": 196, + "metrics": { + "width": 16, + "height": 22, + "left": 0, + "top": -4, + "advance": 15 + } + }, + "197": { + "id": 197, + "metrics": { + "width": 16, + "height": 22, + "left": 0, + "top": -4, + "advance": 15 + } + }, + "198": { + "id": 198, + "metrics": { + "width": 21, + "height": 18, + "left": -1, + "top": -8, + "advance": 20 + } + }, + "199": { + "id": 199, + "metrics": { + "width": 14, + "height": 24, + "left": 1, + "top": -8, + "advance": 15 + } + }, + "200": { + "id": 200, + "metrics": { + "width": 10, + "height": 23, + "left": 2, + "top": -3, + "advance": 13 + } + }, + "201": { + "id": 201, + "metrics": { + "width": 10, + "height": 23, + "left": 2, + "top": -3, + "advance": 13 + } + }, + "202": { + "id": 202, + "metrics": { + "width": 10, + "height": 23, + "left": 2, + "top": -3, + "advance": 13 + } + }, + "203": { + "id": 203, + "metrics": { + "width": 10, + "height": 22, + "left": 2, + "top": -4, + "advance": 13 + } + }, + "204": { + "id": 204, + "metrics": { + "width": 6, + "height": 23, + "left": -1, + "top": -3, + "advance": 6 + } + }, + "205": { + "id": 205, + "metrics": { + "width": 6, + "height": 23, + "left": 1, + "top": -3, + "advance": 6 + } + }, + "206": { + "id": 206, + "metrics": { + "width": 9, + "height": 23, + "left": -1, + "top": -3, + "advance": 6 + } + }, + "207": { + "id": 207, + "metrics": { + "width": 8, + "height": 22, + "left": -1, + "top": -4, + "advance": 6 + } + }, + "208": { + "id": 208, + "metrics": { + "width": 16, + "height": 18, + "left": 0, + "top": -8, + "advance": 17 + } + }, + "209": { + "id": 209, + "metrics": { + "width": 14, + "height": 22, + "left": 2, + "top": -4, + "advance": 18 + } + }, + "210": { + "id": 210, + "metrics": { + "width": 17, + "height": 24, + "left": 1, + "top": -3, + "advance": 18 + } + }, + "211": { + "id": 211, + "metrics": { + "width": 17, + "height": 24, + "left": 1, + "top": -3, + "advance": 18 + } + }, + "212": { + "id": 212, + "metrics": { + "width": 17, + "height": 24, + "left": 1, + "top": -3, + "advance": 18 + } + }, + "213": { + "id": 213, + "metrics": { + "width": 17, + "height": 23, + "left": 1, + "top": -4, + "advance": 18 + } + }, + "214": { + "id": 214, + "metrics": { + "width": 17, + "height": 23, + "left": 1, + "top": -4, + "advance": 18 + } + }, + "215": { + "id": 215, + "metrics": { + "width": 12, + "height": 11, + "left": 1, + "top": -12, + "advance": 13 + } + }, + "216": { + "id": 216, + "metrics": { + "width": 17, + "height": 19, + "left": 1, + "top": -8, + "advance": 18 + } + }, + "217": { + "id": 217, + "metrics": { + "width": 14, + "height": 24, + "left": 2, + "top": -3, + "advance": 17 + } + }, + "218": { + "id": 218, + "metrics": { + "width": 14, + "height": 24, + "left": 2, + "top": -3, + "advance": 17 + } + }, + "219": { + "id": 219, + "metrics": { + "width": 14, + "height": 24, + "left": 2, + "top": -3, + "advance": 17 + } + }, + "220": { + "id": 220, + "metrics": { + "width": 14, + "height": 23, + "left": 2, + "top": -4, + "advance": 17 + } + }, + "221": { + "id": 221, + "metrics": { + "width": 14, + "height": 23, + "left": 0, + "top": -3, + "advance": 13 + } + }, + "222": { + "id": 222, + "metrics": { + "width": 12, + "height": 18, + "left": 2, + "top": -8, + "advance": 14 + } + }, + "223": { + "id": 223, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + } + }, + "224": { + "id": 224, + "metrics": { + "width": 11, + "height": 20, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "225": { + "id": 225, + "metrics": { + "width": 11, + "height": 20, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "226": { + "id": 226, + "metrics": { + "width": 11, + "height": 20, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "227": { + "id": 227, + "metrics": { + "width": 11, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "228": { + "id": 228, + "metrics": { + "width": 11, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "229": { + "id": 229, + "metrics": { + "width": 11, + "height": 21, + "left": 1, + "top": -6, + "advance": 13 + } + }, + "230": { + "id": 230, + "metrics": { + "width": 19, + "height": 15, + "left": 1, + "top": -12, + "advance": 20 + } + }, + "231": { + "id": 231, + "metrics": { + "width": 10, + "height": 20, + "left": 1, + "top": -12, + "advance": 11 + } + }, + "232": { + "id": 232, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "233": { + "id": 233, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "234": { + "id": 234, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 13 + } + }, + "235": { + "id": 235, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 13 + } + }, + "236": { + "id": 236, + "metrics": { + "width": 6, + "height": 19, + "left": -1, + "top": -7, + "advance": 6 + } + }, + "237": { + "id": 237, + "metrics": { + "width": 6, + "height": 19, + "left": 1, + "top": -7, + "advance": 6 + } + }, + "238": { + "id": 238, + "metrics": { + "width": 9, + "height": 19, + "left": -1, + "top": -7, + "advance": 6 + } + }, + "239": { + "id": 239, + "metrics": { + "width": 8, + "height": 18, + "left": -1, + "top": -8, + "advance": 6 + } + }, + "240": { + "id": 240, + "metrics": { + "width": 13, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "241": { + "id": 241, + "metrics": { + "width": 11, + "height": 18, + "left": 2, + "top": -8, + "advance": 14 + } + }, + "242": { + "id": 242, + "metrics": { + "width": 13, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "243": { + "id": 243, + "metrics": { + "width": 13, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "244": { + "id": 244, + "metrics": { + "width": 13, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "245": { + "id": 245, + "metrics": { + "width": 13, + "height": 19, + "left": 1, + "top": -8, + "advance": 14 + } + }, + "246": { + "id": 246, + "metrics": { + "width": 13, + "height": 19, + "left": 1, + "top": -8, + "advance": 14 + } + }, + "247": { + "id": 247, + "metrics": { + "width": 12, + "height": 12, + "left": 1, + "top": -12, + "advance": 13 + } + }, + "248": { + "id": 248, + "metrics": { + "width": 13, + "height": 15, + "left": 1, + "top": -12, + "advance": 14 + } + }, + "249": { + "id": 249, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "250": { + "id": 250, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "251": { + "id": 251, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + } + }, + "252": { + "id": 252, + "metrics": { + "width": 12, + "height": 19, + "left": 1, + "top": -8, + "advance": 14 + } + }, + "253": { + "id": 253, + "metrics": { + "width": 13, + "height": 25, + "left": 0, + "top": -7, + "advance": 12 + } + }, + "254": { + "id": 254, + "metrics": { + "width": 12, + "height": 25, + "left": 2, + "top": -7, + "advance": 14 + } + }, + "255": { + "id": 255, + "metrics": { + "width": 13, + "height": 24, + "left": 0, + "top": -8, + "advance": 12 + } + }, + "256": { + "id": 256, + "metrics": { + "width": 16, + "height": 21, + "left": 0, + "top": -5, + "advance": 15 + } + } +} \ No newline at end of file diff --git a/test/unit/data/symbol_bucket.test.js b/test/unit/data/symbol_bucket.test.js index 87fcf8524c5..1173a394798 100644 --- a/test/unit/data/symbol_bucket.test.js +++ b/test/unit/data/symbol_bucket.test.js @@ -8,7 +8,6 @@ const VectorTile = require('@mapbox/vector-tile').VectorTile; const SymbolBucket = require('../../../src/data/bucket/symbol_bucket'); const CollisionTile = require('../../../src/symbol/collision_tile'); const CollisionBoxArray = require('../../../src/symbol/collision_box'); -const GlyphAtlas = require('../../../src/symbol/glyph_atlas'); const StyleLayer = require('../../../src/style/style_layer'); const util = require('../../../src/util/util'); const featureFilter = require('../../../src/style-spec/feature_filter'); @@ -21,11 +20,6 @@ const glyphs = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../fixtures/ /*eslint new-cap: 0*/ const collisionBoxArray = new CollisionBoxArray(); const collision = new CollisionTile(0, 0, 1, 1, collisionBoxArray); -const atlas = new GlyphAtlas(); -for (const id in glyphs) { - glyphs[id].bitmap = true; - glyphs[id].rect = atlas.addGlyph(id, 'Test', glyphs[id], 3); -} const stacks = { 'Test': glyphs }; diff --git a/test/unit/render/glyph_manager.test.js b/test/unit/render/glyph_manager.test.js new file mode 100644 index 00000000000..55d155b24f4 --- /dev/null +++ b/test/unit/render/glyph_manager.test.js @@ -0,0 +1,70 @@ +// @flow + +'use strict'; + +const {test} = require('mapbox-gl-js-test'); +const proxyquire = require('proxyquire'); +const parseGlyphPBF = require('../../../src/style/parse_glyph_pbf'); +const fs = require('fs'); + +const glyphs = {}; +for (const glyph of parseGlyphPBF(fs.readFileSync('./test/fixtures/0-255.pbf'))) { + glyphs[glyph.id] = glyph; +} + +test('GlyphManager requests 0-255 PBF', (t) => { + const identityTransform = (url) => ({url}); + const GlyphManager = proxyquire('../../../src/render/glyph_manager', { + '../style/load_glyph_range': (stack, range, urlTemplate, transform, callback) => { + t.equal(stack, 'Arial Unicode MS'); + t.equal(range, 0); + t.equal(urlTemplate, 'https://localhost/fonts/v1/{fontstack}/{range}.pbf'); + t.equal(transform, identityTransform); + setImmediate(() => callback(null, glyphs)); + } + }); + + const manager = new GlyphManager(identityTransform); + manager.setURL('https://localhost/fonts/v1/{fontstack}/{range}.pbf'); + + manager.getGlyphs({'Arial Unicode MS': [55]}, (err, glyphs) => { + t.ifError(err); + t.equal(glyphs['Arial Unicode MS']['55'].metrics.advance, 12); + t.end(); + }); +}); + +test('GlyphManager requests remote CJK PBF', (t) => { + const GlyphManager = proxyquire('../../../src/render/glyph_manager', { + '../style/load_glyph_range': (stack, range, urlTemplate, transform, callback) => { + setImmediate(() => callback(null, glyphs)); + } + }); + + const manager = new GlyphManager((url) => ({url})); + manager.setURL('https://localhost/fonts/v1/{fontstack}/{range}.pbf'); + + manager.getGlyphs({'Arial Unicode MS': [0x5e73]}, (err, glyphs) => { + t.ifError(err); + t.equal(glyphs['Arial Unicode MS'][0x5e73], null); // The fixture returns a PBF without the glyph we requested + t.end(); + }); +}); + +test('GlyphManager generates CJK PBF locally', (t) => { + const GlyphManager = proxyquire('../../../src/render/glyph_manager', { + '@mapbox/tiny-sdf': class { + // Return empty 30x30 bitmap (24 fontsize + 3 * 2 buffer) + draw() { return new Uint8ClampedArray(900); } + } + }); + + const manager = new GlyphManager((url) => ({url}), 'sans-serif'); + manager.setURL('https://localhost/fonts/v1/{fontstack}/{range}.pbf'); + + manager.getGlyphs({'Arial Unicode MS': [0x5e73]}, (err, glyphs) => { + t.ifError(err); + t.equal(glyphs['Arial Unicode MS'][0x5e73].metrics.advance, 24); + t.end(); + }); +}); diff --git a/test/unit/style/load_glyph_range.test.js b/test/unit/style/load_glyph_range.test.js new file mode 100644 index 00000000000..d24d5081cf1 --- /dev/null +++ b/test/unit/style/load_glyph_range.test.js @@ -0,0 +1,29 @@ +// @flow + +'use strict'; + +const {test} = require('mapbox-gl-js-test'); +const proxyquire = require('proxyquire'); + +test('loadGlyphRange', (t) => { + const transform = t.stub().callsFake((url) => ({url})); + const data = {}; + const getArrayBuffer = t.stub().yields(null, {data}); + const parseGlyphPBF = t.stub().returns([]); + + const loadGlyphRange = proxyquire('../../../src/style/load_glyph_range', { + '../util/ajax': {getArrayBuffer}, + './parse_glyph_pbf': parseGlyphPBF + }); + + loadGlyphRange('Arial Unicode MS', 0, 'https://localhost/fonts/v1/{fontstack}/{range}.pbf', transform, (err) => { + t.ifError(err); + t.ok(transform.calledOnce); + t.deepEqual(transform.getCall(0).args, ['https://localhost/fonts/v1/Arial Unicode MS/0-255.pbf', 'Glyphs']); + t.ok(getArrayBuffer.calledOnce); + t.deepEqual(getArrayBuffer.getCall(0).args[0], {url: 'https://localhost/fonts/v1/Arial Unicode MS/0-255.pbf'}); + t.ok(parseGlyphPBF.calledOnce); + t.deepEqual(parseGlyphPBF.getCall(0).args, [data]); + t.end(); + }); +}); diff --git a/test/unit/symbol/glyph_source.test.js b/test/unit/symbol/glyph_source.test.js deleted file mode 100644 index 93b3a9aeb06..00000000000 --- a/test/unit/symbol/glyph_source.test.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -const test = require('mapbox-gl-js-test').test; -const ajax = require('../../../src/util/ajax'); -const GlyphSource = require('../../../src/symbol/glyph_source'); -const fs = require('fs'); - -const mockTinySDF = { - // Return empty 30x30 bitmap (24 fontsize + 3 * 2 buffer) - draw: function () { return new Uint8ClampedArray(900); } -}; - -function createSource(t, localIdeographFontFamily) { - const aPBF = fs.readFileSync('./test/fixtures/0-255.pbf'); - const source = new GlyphSource("https://localhost/fonts/v1/{fontstack}/{range}.pbf", localIdeographFontFamily); - t.stub(source, 'createTinySDF').returns(mockTinySDF); - // It would be better to mock with FakeXMLHttpRequest, but the binary encoding - // doesn't survive the mocking - source.loadPBF = function(url, callback) { - callback(null, { data: aPBF }); - }; - - return source; -} - - -test('GlyphSource', (t) => { - t.test('requests 0-255 PBF', (t) => { - const source = createSource(t); - source.getSimpleGlyphs("Arial Unicode MS", [55], 0, (err, glyphs, fontName) => { - t.notOk(err); - t.equal(fontName, "Arial Unicode MS"); - t.equal(glyphs['55'].advance, 12); - t.end(); - }); - }); - - t.test('transforms glyph URL before request', (t) => { - t.stub(ajax, 'getArrayBuffer').callsFake((url, cb) => cb()); - const transformSpy = t.stub().callsFake((url) => { return { url }; }); - const source = new GlyphSource("https://localhost/fonts/v1/{fontstack}/{range}.pbf", false, transformSpy); - - source.loadPBF("https://localhost/fonts/v1/Arial Unicode MS/0-255.pbf", () => { - t.ok(transformSpy.calledOnce); - t.equal(transformSpy.getCall(0).args[0], "https://localhost/fonts/v1/Arial Unicode MS/0-255.pbf"); - t.end(); - }); - }); - - t.test('requests remote CJK PBF', (t) => { - const source = createSource(t); - source.getSimpleGlyphs("Arial Unicode MS", [0x5e73], 0, (err, glyphs, fontName) => { - t.notOk(err); - t.equal(fontName, "Arial Unicode MS"); - t.notOk(Object.keys(glyphs).length); // The fixture returns a PBF without the glyph we requested - t.end(); - }); - - }); - - t.test('locally generates CJK PBF', (t) => { - const source = createSource(t, 'sans-serif'); - source.getSimpleGlyphs("Arial Unicode MS", [0x5e73], 0, (err, glyphs, fontName) => { - t.notOk(err); - t.equal(fontName, "Arial Unicode MS"); - t.equal(glyphs['24179'].advance, 24); - t.end(); - }); - }); - - t.end(); -});