From 12a331b4bc62201473c51ed5c72a181a7f2ed746 Mon Sep 17 00:00:00 2001 From: Alexander Shalamov Date: Mon, 14 Oct 2019 18:31:02 +0300 Subject: [PATCH] Implement 'images in labels' feature (#8904) * Set 'isSDF' flag per icon quad * Simplify formatted expression parsing Allow omission of empty section options objects. This enables following syntax ["format", "first section\n", "second section\n", ["image", "smiley"], "third section"] * Add Image to Format expression Image could be part of Format expression and evaluated ResolvedImage is a optional member of evaluated Formatted type. * Add formatted text's image section image to deps * Add images to TaggedString's section * Pass images to text shaping * Use {text|icon}-size vertex attribute field to encode 'isSDF' flag This change uses higher bit to indicate whether quad's vertices belong to a quad whose texture should be rasterized using SDF function. Size scaling factor is changed from 256 to 128, which reduces resolution a bit, however, as the scaling result is now being rounded, final result should be the same. * Shape images in labels * Draw color icons and sdf text with one program / draw call * Align images and scaled glyphs in vertical layout * Allow image breaking * Update render test expectations for tests using font-scale Fixes misaligned text boxes * Update format expression tests to account for image sections * Update quads unit test * Update shaping unit test * Update unit test for format expression API * Fix linter errors * Don't scale images in labels with text size * Basic render test * Multiline test * Vertical layout with images * Line placement test * Expression test for image section * Format expression unit and API test * Shaping unit test * Add benchmarks for symbol layer with icons * Use explicit 'symbol_text_and_icon' shader name instead of 'symbol' Add is_text back, in follow-up pr shader can be renamed to sdf and raster, and can be used to draw sdf and raster icons in one pass. * Don't use mod for unpacking flag * Use linear interpolation for images in label for zoom dependent text size * Add back glyphMap to shaping Glyph map is used as a fallback whenever glyph is missing in glyph positions, e.g., space character that may not be backed by a texture, yet, has a glyph metrics. * Render test for constant text-size * Render test for image justification * Take into account image padding and image pixel ratio * Add render test to verify that linear filtering is used Test that linear filtering is used for icons in text when text-size is zoom-dependent. * Fix size of images in text at integer zoom levels * Update shaping unit test and remove unused parameter from shapeLines --- bench/benchmarks/layers.js | 19 + bench/versions/benchmarks.js | 3 +- src/data/bucket/symbol_bucket.js | 35 +- src/render/draw_symbol.js | 52 +- src/render/glyph_atlas.js | 2 +- src/render/image_atlas.js | 25 +- src/render/program/program_uniforms.js | 3 +- src/render/program/symbol_program.js | 70 ++- src/shaders/shaders.js | 3 + src/shaders/symbol_icon.vertex.glsl | 9 +- src/shaders/symbol_sdf.vertex.glsl | 10 +- .../symbol_text_and_icon.fragment.glsl | 68 ++ src/shaders/symbol_text_and_icon.vertex.glsl | 116 ++++ .../expression/definitions/coercion.js | 2 +- .../expression/definitions/format.js | 108 ++-- src/style-spec/expression/types/formatted.js | 18 +- src/symbol/quads.js | 188 +++--- src/symbol/shaping.js | 348 ++++++++--- src/symbol/symbol_layout.js | 81 +-- src/symbol/symbol_size.js | 2 +- test/expected/text-shaping-default.json | 169 +++-- .../text-shaping-images-horizontal.json | 275 +++++++++ .../text-shaping-images-vertical.json | 160 +++++ test/expected/text-shaping-linebreak.json | 330 +++++++--- test/expected/text-shaping-newline.json | 330 +++++++--- .../text-shaping-newlines-in-middle.json | 330 +++++++--- test/expected/text-shaping-null.json | 73 ++- test/expected/text-shaping-spacing.json | 169 +++-- .../text-shaping-zero-width-space.json | 459 ++++++++++---- test/fixtures/fontstack-glyphs.json | 579 ++++++++++++------ .../expression-tests/format/basic/test.json | 4 + .../format/coercion/test.json | 3 + .../format/data-driven-font/test.json | 2 + .../format/image-sections/test.json | 54 ++ .../format/implicit-coerce/test.json | 3 + .../format/implicit-omit/test.json | 3 + .../format/implicit/test.json | 3 + .../text-field/formatted-arabic/expected.png | Bin 5873 -> 5872 bytes .../expected.png | Bin 0 -> 2845 bytes .../formatted-images-constant-size/style.json | 59 ++ .../formatted-images-line/expected.png | Bin 0 -> 5300 bytes .../formatted-images-line/style.json | 82 +++ .../formatted-images-multiline/expected.png | Bin 0 -> 8513 bytes .../formatted-images-multiline/style.json | 75 +++ .../expected.png | Bin 0 -> 7765 bytes .../style.json | 103 ++++ .../formatted-images-vertical/expected.png | Bin 0 -> 6352 bytes .../formatted-images-vertical/style.json | 46 ++ .../expected.png | Bin 0 -> 3908 bytes .../style.json | 69 +++ .../text-field/formatted-images/expected.png | Bin 0 -> 4153 bytes .../text-field/formatted-images/style.json | 64 ++ .../formatted-text-color/expected.png | Bin 4735 -> 4778 bytes .../text-field/formatted/expected.png | Bin 4867 -> 4813 bytes .../fixture/text-field-format.input.json | 64 +- ...ext-field-format.output-api-supported.json | 20 +- .../fixture/text-field-format.output.json | 20 +- test/unit/symbol/quads.test.js | 15 +- test/unit/symbol/shaping.test.js | 89 ++- 59 files changed, 3773 insertions(+), 1041 deletions(-) create mode 100644 src/shaders/symbol_text_and_icon.fragment.glsl create mode 100644 src/shaders/symbol_text_and_icon.vertex.glsl create mode 100644 test/expected/text-shaping-images-horizontal.json create mode 100644 test/expected/text-shaping-images-vertical.json create mode 100644 test/integration/expression-tests/format/image-sections/test.json create mode 100644 test/integration/render-tests/text-field/formatted-images-constant-size/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-images-constant-size/style.json create mode 100644 test/integration/render-tests/text-field/formatted-images-line/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-images-line/style.json create mode 100644 test/integration/render-tests/text-field/formatted-images-multiline/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-images-multiline/style.json create mode 100644 test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/style.json create mode 100644 test/integration/render-tests/text-field/formatted-images-vertical/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-images-vertical/style.json create mode 100644 test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/style.json create mode 100644 test/integration/render-tests/text-field/formatted-images/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-images/style.json diff --git a/bench/benchmarks/layers.js b/bench/benchmarks/layers.js index e56c5f0d2fb..a251a07a40a 100644 --- a/bench/benchmarks/layers.js +++ b/bench/benchmarks/layers.js @@ -220,3 +220,22 @@ export class LayerSymbol extends LayerBenchmark { }); } } + +export class LayerSymbolWithIcons extends LayerBenchmark { + constructor() { + super(); + + this.layerStyle = Object.assign({}, style, { + layers: generateLayers({ + 'id': 'symbollayer', + 'type': 'symbol', + 'source': 'composite', + 'source-layer': 'poi_label', + 'layout': { + 'icon-image': 'dot-11', + 'text-field': ['format', ['get', 'name_en'], ['image', 'dot-11']] + } + }) + }); + } +} diff --git a/bench/versions/benchmarks.js b/bench/versions/benchmarks.js index 44eba7b9d5b..6cab2a05299 100644 --- a/bench/versions/benchmarks.js +++ b/bench/versions/benchmarks.js @@ -9,7 +9,7 @@ import WorkerTransfer from '../benchmarks/worker_transfer'; import Paint from '../benchmarks/paint'; import PaintStates from '../benchmarks/paint_states'; import {PropertyLevelRemove, FeatureLevelRemove, SourceLevelRemove} from '../benchmarks/remove_paint_state'; -import {LayerBackground, LayerCircle, LayerFill, LayerFillExtrusion, LayerHeatmap, LayerHillshade, LayerLine, LayerRaster, LayerSymbol} from '../benchmarks/layers'; +import {LayerBackground, LayerCircle, LayerFill, LayerFillExtrusion, LayerHeatmap, LayerHillshade, LayerLine, LayerRaster, LayerSymbol, LayerSymbolWithIcons} from '../benchmarks/layers'; import Load from '../benchmarks/map_load'; import Validate from '../benchmarks/style_validate'; import StyleLayerCreate from '../benchmarks/style_layer_create'; @@ -63,6 +63,7 @@ register('LayerHillshade', new LayerHillshade()); register('LayerLine', new LayerLine()); register('LayerRaster', new LayerRaster()); register('LayerSymbol', new LayerSymbol()); +register('LayerSymbolWithIcons', new LayerSymbolWithIcons()); register('Load', new Load()); register('LayoutDDS', new LayoutDDS()); register('SymbolLayout', new SymbolLayout(style, styleLocations.map(location => location.tileID[0]))); diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 4746935a53c..925e8e70499 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -33,6 +33,7 @@ const vectorTileFeatureTypes = mvt.VectorTileFeature.types; import {verticalizedCharacterMap} from '../../util/verticalize_punctuation'; import Anchor from '../../symbol/anchor'; import {getSizeData} from '../../symbol/symbol_size'; +import {MAX_PACKED_SIZE} from '../../symbol/symbol_layout'; import {register} from '../../util/web_worker_transfer'; import EvaluationParameters from '../../style/evaluation_parameters'; import Formatted from '../../style-spec/expression/types/formatted'; @@ -101,7 +102,9 @@ const shaderOpacityAttributes = [ {name: 'a_fade_opacity', components: 1, type: 'Uint8', offset: 0} ]; -function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex) { +function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex, isSDF: boolean) { + const aSizeX = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[0])) : 0; + const aSizeY = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[1])) : 0; array.emplaceBack( // a_pos_offset anchorX, @@ -112,8 +115,8 @@ function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex) { // a_data tx, // x coordinate of symbol on glyph atlas texture ty, // y coordinate of symbol on glyph atlas texture - sizeVertex ? sizeVertex[0] : 0, - sizeVertex ? sizeVertex[1] : 0 + (aSizeX << 1) + (isSDF ? 1 : 0), + aSizeY ); } @@ -271,6 +274,7 @@ class SymbolBucket implements Bucket { index: number; sdfIcons: boolean; + iconsInText: boolean; iconsNeedLinear: boolean; bucketInstanceId: number; justReloaded: boolean; @@ -381,7 +385,9 @@ class SymbolBucket implements Bucket { const textField = layout.get('text-field'); const iconImage = layout.get('icon-image'); const hasText = - (textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) && + (textField.value.kind !== 'constant' || + (textField.value.value instanceof Formatted && !textField.value.value.isEmpty()) || + textField.value.value.toString().length > 0) && (textFont.value.kind !== 'constant' || textFont.value.value.length > 0); // we should always resolve the icon-image value if the property was defined in the style // this allows us to fire the styleimagemissing event if image evaluation returns null @@ -470,10 +476,15 @@ class SymbolBucket implements Bucket { const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; this.allowVerticalPlacement = this.writingModes && this.writingModes.indexOf(WritingMode.vertical) >= 0; for (const section of text.sections) { - const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString()); - const sectionFont = section.fontStack || fontStack; - const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {}; - this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, this.allowVerticalPlacement, doesAllowVerticalWritingMode); + if (!section.image) { + const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString()); + const sectionFont = section.fontStack || fontStack; + const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {}; + this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, this.allowVerticalPlacement, doesAllowVerticalWritingMode); + } else { + // Add section image to the list of dependencies. + icons[section.image.name] = true; + } } } } @@ -587,10 +598,10 @@ class SymbolBucket implements Bucket { const index = segment.vertexLength; const y = symbol.glyphOffset[1]; - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex); - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex); - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex); - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex, symbol.isSDF); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex, symbol.isSDF); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex, symbol.isSDF); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, symbol.isSDF); addDynamicAttributes(dynamicLayoutVertexArray, labelAnchor, angle); diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index ff8c06cdaee..fb452721628 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -20,7 +20,8 @@ import {evaluateVariableOffset} from '../symbol/symbol_layout'; import { symbolIconUniformValues, - symbolSDFUniformValues + symbolSDFUniformValues, + symbolTextAndIconUniformValues } from './program/symbol_program'; import type Painter from './painter'; @@ -43,7 +44,9 @@ type SymbolTileRenderState = { buffers: SymbolBuffers, uniformValues: any, atlasTexture: Texture, + atlasTextureIcon: Texture | null, atlasInterpolation: any, + atlasInterpolationIcon: any, isSDF: boolean, hasHalo: boolean } @@ -208,6 +211,16 @@ function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, var bucket.text.dynamicLayoutVertexBuffer.updateData(dynamicTextLayoutVertexArray); } +function getSymbolProgramName(isSDF: boolean, isText: boolean, bucket: SymbolBucket) { + if (bucket.iconsInText && isText) { + return 'symbolTextAndIcon'; + } else if (isSDF) { + return 'symbolSDF'; + } else { + return 'symbolIcon'; + } +} + function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate, translateAnchor, rotationAlignment, pitchAlignment, keepUpright, stencilMode, colorMode) { @@ -244,28 +257,33 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const isSDF = isText || bucket.sdfIcons; const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; + const transformed = pitchWithMap || tr.pitch !== 0; if (!program) { - program = painter.useProgram(isSDF ? 'symbolSDF' : 'symbolIcon', programConfiguration); + program = painter.useProgram(getSymbolProgramName(isSDF, isText, bucket), programConfiguration); size = symbolSize.evaluateSizeForZoom(sizeData, tr.zoom); } - context.activeTexture.set(gl.TEXTURE0); - let texSize: [number, number]; + let texSizeIcon: [number, number] = [0, 0]; let atlasTexture; let atlasInterpolation; + let atlasTextureIcon = null; + let atlasInterpolationIcon; if (isText) { atlasTexture = tile.glyphAtlasTexture; atlasInterpolation = gl.LINEAR; texSize = tile.glyphAtlasTexture.size; - + if (bucket.iconsInText) { + texSizeIcon = tile.imageAtlasTexture.size; + atlasTextureIcon = tile.imageAtlasTexture; + const zoomDependentSize = sizeData.kind === 'composite' || sizeData.kind === 'camera'; + atlasInterpolationIcon = transformed || painter.options.rotating || painter.options.zooming || zoomDependentSize ? gl.LINEAR : gl.NEAREST; + } } else { const iconScaled = layer.layout.get('icon-size').constantOr(0) !== 1 || bucket.iconsNeedLinear; - const iconTransformed = pitchWithMap || tr.pitch !== 0; - atlasTexture = tile.imageAtlasTexture; - atlasInterpolation = isSDF || painter.options.rotating || painter.options.zooming || iconScaled || iconTransformed ? + atlasInterpolation = isSDF || painter.options.rotating || painter.options.zooming || iconScaled || transformed ? gl.LINEAR : gl.NEAREST; texSize = tile.imageAtlasTexture.size; @@ -292,10 +310,15 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate let uniformValues; if (isSDF) { - uniformValues = symbolSDFUniformValues(sizeData.kind, + if (!bucket.iconsInText) { + uniformValues = symbolSDFUniformValues(sizeData.kind, size, rotateInShader, pitchWithMap, painter, matrix, uLabelPlaneMatrix, uglCoordMatrix, isText, texSize, true); - + } else { + uniformValues = symbolTextAndIconUniformValues(sizeData.kind, + size, rotateInShader, pitchWithMap, painter, matrix, + uLabelPlaneMatrix, uglCoordMatrix, texSize, texSizeIcon); + } } else { uniformValues = symbolIconUniformValues(sizeData.kind, size, rotateInShader, pitchWithMap, painter, matrix, @@ -307,7 +330,9 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate buffers, uniformValues, atlasTexture, + atlasTextureIcon, atlasInterpolation, + atlasInterpolationIcon, isSDF, hasHalo }; @@ -337,7 +362,14 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate for (const segmentState of tileRenderState) { const state = segmentState.state; + context.activeTexture.set(gl.TEXTURE0); state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE); + if (state.atlasTextureIcon) { + context.activeTexture.set(gl.TEXTURE1); + if (state.atlasTextureIcon) { + state.atlasTextureIcon.bind(state.atlasInterpolationIcon, gl.CLAMP_TO_EDGE); + } + } if (state.isSDF) { const uniformValues = ((state.uniformValues: any): UniformValues); diff --git a/src/render/glyph_atlas.js b/src/render/glyph_atlas.js index 584b8816fbd..a6bcd71a0db 100644 --- a/src/render/glyph_atlas.js +++ b/src/render/glyph_atlas.js @@ -8,7 +8,7 @@ import type {GlyphMetrics, StyleGlyph} from '../style/style_glyph'; const padding = 1; -type Rect = { +export type Rect = { x: number, y: number, w: number, diff --git a/src/render/image_atlas.js b/src/render/image_atlas.js index 270be1191ee..ebf78823be9 100644 --- a/src/render/image_atlas.js +++ b/src/render/image_atlas.js @@ -8,7 +8,8 @@ import type {StyleImage} from '../style/style_image'; import type ImageManager from './image_manager'; import type Texture from './texture'; -const padding = 1; +const IMAGE_PADDING = 1; +export {IMAGE_PADDING}; type Rect = { x: number, @@ -30,15 +31,15 @@ export class ImagePosition { get tl(): [number, number] { return [ - this.paddedRect.x + padding, - this.paddedRect.y + padding + this.paddedRect.x + IMAGE_PADDING, + this.paddedRect.y + IMAGE_PADDING ]; } get br(): [number, number] { return [ - this.paddedRect.x + this.paddedRect.w - padding, - this.paddedRect.y + this.paddedRect.h - padding + this.paddedRect.x + this.paddedRect.w - IMAGE_PADDING, + this.paddedRect.y + this.paddedRect.h - IMAGE_PADDING ]; } @@ -48,8 +49,8 @@ export class ImagePosition { get displaySize(): [number, number] { return [ - (this.paddedRect.w - padding * 2) / this.pixelRatio, - (this.paddedRect.h - padding * 2) / this.pixelRatio + (this.paddedRect.w - IMAGE_PADDING * 2) / this.pixelRatio, + (this.paddedRect.h - IMAGE_PADDING * 2) / this.pixelRatio ]; } } @@ -76,14 +77,14 @@ export default class ImageAtlas { for (const id in icons) { const src = icons[id]; const bin = iconPositions[id].paddedRect; - RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x: bin.x + padding, y: bin.y + padding}, src.data); + RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x: bin.x + IMAGE_PADDING, y: bin.y + IMAGE_PADDING}, src.data); } for (const id in patterns) { const src = patterns[id]; const bin = patternPositions[id].paddedRect; - const x = bin.x + padding, - y = bin.y + padding, + const x = bin.x + IMAGE_PADDING, + y = bin.y + IMAGE_PADDING, w = src.data.width, h = src.data.height; @@ -106,8 +107,8 @@ export default class ImageAtlas { const bin = { x: 0, y: 0, - w: src.data.width + 2 * padding, - h: src.data.height + 2 * padding, + w: src.data.width + 2 * IMAGE_PADDING, + h: src.data.height + 2 * IMAGE_PADDING, }; bins.push(bin); positions[id] = new ImagePosition(bin, src); diff --git a/src/render/program/program_uniforms.js b/src/render/program/program_uniforms.js index b44bd7d802b..9ccc73bfd64 100644 --- a/src/render/program/program_uniforms.js +++ b/src/render/program/program_uniforms.js @@ -10,7 +10,7 @@ import {heatmapUniforms, heatmapTextureUniforms} from './heatmap_program'; import {hillshadeUniforms, hillshadePrepareUniforms} from './hillshade_program'; import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms} from './line_program'; import {rasterUniforms} from './raster_program'; -import {symbolIconUniforms, symbolSDFUniforms} from './symbol_program'; +import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program'; import {backgroundUniforms, backgroundPatternUniforms} from './background_program'; export const programUniforms = { @@ -36,6 +36,7 @@ export const programUniforms = { raster: rasterUniforms, symbolIcon: symbolIconUniforms, symbolSDF: symbolSDFUniforms, + symbolTextAndIcon: symbolTextAndIconUniforms, background: backgroundUniforms, backgroundPattern: backgroundPatternUniforms }; diff --git a/src/render/program/symbol_program.js b/src/render/program/symbol_program.js index 273a261d706..98d6d5bb93f 100644 --- a/src/render/program/symbol_program.js +++ b/src/render/program/symbol_program.js @@ -54,6 +54,30 @@ export type SymbolSDFUniformsType = {| 'u_is_halo': Uniform1f |}; +export type symbolTextAndIconUniformsType = {| + 'u_is_size_zoom_constant': Uniform1i, + 'u_is_size_feature_constant': Uniform1i, + 'u_size_t': Uniform1f, + 'u_size': Uniform1f, + 'u_camera_to_center_distance': Uniform1f, + 'u_pitch': Uniform1f, + 'u_rotate_symbol': Uniform1i, + 'u_aspect_ratio': Uniform1f, + 'u_fade_change': Uniform1f, + 'u_matrix': UniformMatrix4f, + 'u_label_plane_matrix': UniformMatrix4f, + 'u_coord_matrix': UniformMatrix4f, + 'u_is_text': Uniform1f, + 'u_pitch_with_map': Uniform1i, + 'u_texsize': Uniform2f, + 'u_texsize_icon': Uniform2f, + 'u_texture': Uniform1i, + 'u_texture_icon': Uniform1i, + 'u_gamma_scale': Uniform1f, + 'u_device_pixel_ratio': Uniform1f, + 'u_is_halo': Uniform1f +|}; + const symbolIconUniforms = (context: Context, locations: UniformLocations): SymbolIconUniformsType => ({ 'u_is_size_zoom_constant': new Uniform1i(context, locations.u_is_size_zoom_constant), 'u_is_size_feature_constant': new Uniform1i(context, locations.u_is_size_feature_constant), @@ -95,6 +119,30 @@ const symbolSDFUniforms = (context: Context, locations: UniformLocations): Symbo 'u_is_halo': new Uniform1f(context, locations.u_is_halo) }); +const symbolTextAndIconUniforms = (context: Context, locations: UniformLocations): symbolTextAndIconUniformsType => ({ + 'u_is_size_zoom_constant': new Uniform1i(context, locations.u_is_size_zoom_constant), + 'u_is_size_feature_constant': new Uniform1i(context, locations.u_is_size_feature_constant), + 'u_size_t': new Uniform1f(context, locations.u_size_t), + 'u_size': new Uniform1f(context, locations.u_size), + 'u_camera_to_center_distance': new Uniform1f(context, locations.u_camera_to_center_distance), + 'u_pitch': new Uniform1f(context, locations.u_pitch), + 'u_rotate_symbol': new Uniform1i(context, locations.u_rotate_symbol), + 'u_aspect_ratio': new Uniform1f(context, locations.u_aspect_ratio), + 'u_fade_change': new Uniform1f(context, locations.u_fade_change), + 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), + 'u_label_plane_matrix': new UniformMatrix4f(context, locations.u_label_plane_matrix), + 'u_coord_matrix': new UniformMatrix4f(context, locations.u_coord_matrix), + 'u_is_text': new Uniform1f(context, locations.u_is_text), + 'u_pitch_with_map': new Uniform1i(context, locations.u_pitch_with_map), + 'u_texsize': new Uniform2f(context, locations.u_texsize), + 'u_texsize_icon': new Uniform2f(context, locations.u_texsize_icon), + 'u_texture': new Uniform1i(context, locations.u_texture), + 'u_texture_icon': new Uniform1i(context, locations.u_texture_icon), + 'u_gamma_scale': new Uniform1f(context, locations.u_gamma_scale), + 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), + 'u_is_halo': new Uniform1f(context, locations.u_is_halo) +}); + const symbolIconUniformValues = ( functionType: string, size: ?{uSizeT: number, uSize: number}, @@ -153,4 +201,24 @@ const symbolSDFUniformValues = ( }); }; -export {symbolIconUniforms, symbolSDFUniforms, symbolIconUniformValues, symbolSDFUniformValues}; +const symbolTextAndIconUniformValues = ( + functionType: string, + size: ?{uSizeT: number, uSize: number}, + rotateInShader: boolean, + pitchWithMap: boolean, + painter: Painter, + matrix: Float32Array, + labelPlaneMatrix: Float32Array, + glCoordMatrix: Float32Array, + texSizeSDF: [number, number], + texSizeIcon: [number, number] +): UniformValues => { + return extend(symbolSDFUniformValues(functionType, size, + rotateInShader, pitchWithMap, painter, matrix, labelPlaneMatrix, + glCoordMatrix, true, texSizeSDF, true), { + 'u_texsize_icon': texSizeIcon, + 'u_texture_icon': 1 + }); +}; + +export {symbolIconUniforms, symbolSDFUniforms, symbolIconUniformValues, symbolSDFUniformValues, symbolTextAndIconUniformValues, symbolTextAndIconUniforms}; diff --git a/src/shaders/shaders.js b/src/shaders/shaders.js index 13b20267df1..221728dfddd 100644 --- a/src/shaders/shaders.js +++ b/src/shaders/shaders.js @@ -52,6 +52,8 @@ import symbolIconFrag from './symbol_icon.fragment.glsl'; import symbolIconVert from './symbol_icon.vertex.glsl'; import symbolSDFFrag from './symbol_sdf.fragment.glsl'; import symbolSDFVert from './symbol_sdf.vertex.glsl'; +import symbolTextAndIconFrag from './symbol_text_and_icon.fragment.glsl'; +import symbolTextAndIconVert from './symbol_text_and_icon.vertex.glsl'; export const prelude = compile(preludeFrag, preludeVert); export const background = compile(backgroundFrag, backgroundVert); @@ -78,6 +80,7 @@ export const lineSDF = compile(lineSDFFrag, lineSDFVert); export const raster = compile(rasterFrag, rasterVert); export const symbolIcon = compile(symbolIconFrag, symbolIconVert); export const symbolSDF = compile(symbolSDFFrag, symbolSDFVert); +export const symbolTextAndIcon = compile(symbolTextAndIconFrag, symbolTextAndIconVert); // Expand #pragmas to #ifdefs. diff --git a/src/shaders/symbol_icon.vertex.glsl b/src/shaders/symbol_icon.vertex.glsl index d0b24e73d9f..056ce24e4d2 100644 --- a/src/shaders/symbol_icon.vertex.glsl +++ b/src/shaders/symbol_icon.vertex.glsl @@ -38,15 +38,14 @@ void main() { vec2 a_tex = a_data.xy; vec2 a_size = a_data.zw; + float a_size_min = floor(a_size[0] * 0.5); highp float segment_angle = -a_projected_pos[2]; - float size; + if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = mix(a_size[0], a_size[1], u_size_t) / 256.0; + size = mix(a_size_min, a_size[1], u_size_t) / 128.0; } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = a_size[0] / 256.0; - } else if (!u_is_size_zoom_constant && u_is_size_feature_constant) { - size = u_size; + size = a_size_min / 128.0; } else { size = u_size; } diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index 5f80016b87f..6c9ef11f833 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -50,15 +50,14 @@ void main() { vec2 a_tex = a_data.xy; vec2 a_size = a_data.zw; + float a_size_min = floor(a_size[0] * 0.5); highp float segment_angle = -a_projected_pos[2]; float size; if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = mix(a_size[0], a_size[1], u_size_t) / 256.0; + size = mix(a_size_min, a_size[1], u_size_t) / 128.0; } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = a_size[0] / 256.0; - } else if (!u_is_size_zoom_constant && u_is_size_feature_constant) { - size = u_size; + size = a_size_min / 128.0; } else { size = u_size; } @@ -104,11 +103,10 @@ void main() { gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), 0.0, 1.0); float gamma_scale = gl_Position.w; - vec2 tex = a_tex / u_texsize; vec2 fade_opacity = unpack_opacity(a_fade_opacity); float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); - v_data0 = vec2(tex.x, tex.y); + v_data0 = a_tex / u_texsize; v_data1 = vec3(gamma_scale, size, interpolated_fade_opacity); } diff --git a/src/shaders/symbol_text_and_icon.fragment.glsl b/src/shaders/symbol_text_and_icon.fragment.glsl new file mode 100644 index 00000000000..6220563c415 --- /dev/null +++ b/src/shaders/symbol_text_and_icon.fragment.glsl @@ -0,0 +1,68 @@ +#define SDF_PX 8.0 + +#define SDF 1.0 +#define ICON 0.0 + +uniform bool u_is_halo; +uniform sampler2D u_texture; +uniform sampler2D u_texture_icon; +uniform highp float u_gamma_scale; +uniform lowp float u_device_pixel_ratio; + +varying vec4 v_data0; +varying vec4 v_data1; + +#pragma mapbox: define highp vec4 fill_color +#pragma mapbox: define highp vec4 halo_color +#pragma mapbox: define lowp float opacity +#pragma mapbox: define lowp float halo_width +#pragma mapbox: define lowp float halo_blur + +void main() { + #pragma mapbox: initialize highp vec4 fill_color + #pragma mapbox: initialize highp vec4 halo_color + #pragma mapbox: initialize lowp float opacity + #pragma mapbox: initialize lowp float halo_width + #pragma mapbox: initialize lowp float halo_blur + + float fade_opacity = v_data1[2]; + + if (v_data1.w == ICON) { + vec2 tex_icon = v_data0.zw; + lowp float alpha = opacity * fade_opacity; + gl_FragColor = texture2D(u_texture_icon, tex_icon) * alpha; + +#ifdef OVERDRAW_INSPECTOR + gl_FragColor = vec4(1.0); +#endif + return; + } + + vec2 tex = v_data0.xy; + + float EDGE_GAMMA = 0.105 / u_device_pixel_ratio; + + float gamma_scale = v_data1.x; + float size = v_data1.y; + + float fontScale = size / 24.0; + + lowp vec4 color = fill_color; + highp float gamma = EDGE_GAMMA / (fontScale * u_gamma_scale); + lowp float buff = (256.0 - 64.0) / 256.0; + if (u_is_halo) { + color = halo_color; + gamma = (halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale); + buff = (6.0 - halo_width / fontScale) / SDF_PX; + } + + lowp float dist = texture2D(u_texture, tex).a; + highp float gamma_scaled = gamma * gamma_scale; + highp float alpha = smoothstep(buff - gamma_scaled, buff + gamma_scaled, dist); + + gl_FragColor = color * (alpha * opacity * fade_opacity); + +#ifdef OVERDRAW_INSPECTOR + gl_FragColor = vec4(1.0); +#endif +} diff --git a/src/shaders/symbol_text_and_icon.vertex.glsl b/src/shaders/symbol_text_and_icon.vertex.glsl new file mode 100644 index 00000000000..647310fc9c9 --- /dev/null +++ b/src/shaders/symbol_text_and_icon.vertex.glsl @@ -0,0 +1,116 @@ +const float PI = 3.141592653589793; + +attribute vec4 a_pos_offset; +attribute vec4 a_data; +attribute vec3 a_projected_pos; +attribute float a_fade_opacity; + +// contents of a_size vary based on the type of property value +// used for {text,icon}-size. +// For constants, a_size is disabled. +// For source functions, we bind only one value per vertex: the value of {text,icon}-size evaluated for the current feature. +// For composite functions: +// [ text-size(lowerZoomStop, feature), +// text-size(upperZoomStop, feature) ] +uniform bool u_is_size_zoom_constant; +uniform bool u_is_size_feature_constant; +uniform highp float u_size_t; // used to interpolate between zoom stops when size is a composite function +uniform highp float u_size; // used when size is both zoom and feature constant +uniform mat4 u_matrix; +uniform mat4 u_label_plane_matrix; +uniform mat4 u_coord_matrix; +uniform bool u_is_text; +uniform bool u_pitch_with_map; +uniform highp float u_pitch; +uniform bool u_rotate_symbol; +uniform highp float u_aspect_ratio; +uniform highp float u_camera_to_center_distance; +uniform float u_fade_change; +uniform vec2 u_texsize; +uniform vec2 u_texsize_icon; + +varying vec4 v_data0; +varying vec4 v_data1; + +#pragma mapbox: define highp vec4 fill_color +#pragma mapbox: define highp vec4 halo_color +#pragma mapbox: define lowp float opacity +#pragma mapbox: define lowp float halo_width +#pragma mapbox: define lowp float halo_blur + +void main() { + #pragma mapbox: initialize highp vec4 fill_color + #pragma mapbox: initialize highp vec4 halo_color + #pragma mapbox: initialize lowp float opacity + #pragma mapbox: initialize lowp float halo_width + #pragma mapbox: initialize lowp float halo_blur + + vec2 a_pos = a_pos_offset.xy; + vec2 a_offset = a_pos_offset.zw; + + vec2 a_tex = a_data.xy; + vec2 a_size = a_data.zw; + + float a_size_min = floor(a_size[0] * 0.5); + float is_sdf = a_size[0] - 2.0 * a_size_min; + + highp float segment_angle = -a_projected_pos[2]; + float size; + + if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { + size = mix(a_size_min, a_size[1], u_size_t) / 128.0; + } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { + size = a_size_min / 128.0; + } else { + size = u_size; + } + + vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1); + highp float camera_to_anchor_distance = projectedPoint.w; + // If the label is pitched with the map, layout is done in pitched space, + // which makes labels in the distance smaller relative to viewport space. + // We counteract part of that effect by multiplying by the perspective ratio. + // If the label isn't pitched with the map, we do layout in viewport space, + // which makes labels in the distance larger relative to the features around + // them. We counteract part of that effect by dividing by the perspective ratio. + highp float distance_ratio = u_pitch_with_map ? + camera_to_anchor_distance / u_camera_to_center_distance : + u_camera_to_center_distance / camera_to_anchor_distance; + highp float perspective_ratio = clamp( + 0.5 + 0.5 * distance_ratio, + 0.0, // Prevents oversized near-field symbols in pitched/overzoomed tiles + 4.0); + + size *= perspective_ratio; + + float fontScale = size / 24.0; + + highp float symbol_rotation = 0.0; + if (u_rotate_symbol) { + // Point labels with 'rotation-alignment: map' are horizontal with respect to tile units + // To figure out that angle in projected space, we draw a short horizontal line in tile + // space, project it, and measure its angle in projected space. + vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1); + + vec2 a = projectedPoint.xy / projectedPoint.w; + vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w; + + symbol_rotation = atan((b.y - a.y) / u_aspect_ratio, b.x - a.x); + } + + highp float angle_sin = sin(segment_angle + symbol_rotation); + highp float angle_cos = cos(segment_angle + symbol_rotation); + mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos); + + vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0); + gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), 0.0, 1.0); + float gamma_scale = gl_Position.w; + + vec2 fade_opacity = unpack_opacity(a_fade_opacity); + float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; + float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); + + v_data0.xy = a_tex / u_texsize; + v_data0.zw = a_tex / u_texsize_icon; + v_data1 = vec4(gamma_scale, size, interpolated_fade_opacity, is_sdf); +} diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index 9f30b0722b0..8f82185794f 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -118,7 +118,7 @@ class Coercion implements Expression { serialize() { if (this.type.kind === 'formatted') { - return new FormatExpression([{text: this.args[0], scale: null, font: null, textColor: null}]).serialize(); + return new FormatExpression([{content: this.args[0], scale: null, font: null, textColor: null}]).serialize(); } if (this.type.kind === 'resolvedImage') { diff --git a/src/style-spec/expression/definitions/format.js b/src/style-spec/expression/definitions/format.js index 6d9694cbe21..002cc8e41ab 100644 --- a/src/style-spec/expression/definitions/format.js +++ b/src/style-spec/expression/definitions/format.js @@ -1,8 +1,8 @@ // @flow -import {NumberType, ValueType, FormattedType, array, StringType, ColorType} from '../types'; +import {NumberType, ValueType, FormattedType, array, StringType, ColorType, ResolvedImageType} from '../types'; import Formatted, {FormattedSection} from '../types/formatted'; -import {toString} from '../values'; +import {toString, typeOf} from '../values'; import type {Expression} from '../expression'; import type EvaluationContext from '../evaluation_context'; @@ -10,7 +10,9 @@ import type ParsingContext from '../parsing_context'; import type {Type} from '../types'; type FormattedSectionExpression = { - text: Expression, + // Content of a section may be Image expression or other + // type of expression that is coercable to 'string'. + content: Expression, scale: Expression | null; font: Expression | null; textColor: Expression | null; @@ -26,65 +28,83 @@ export default class FormatExpression implements Expression { } static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { - if (args.length < 3) { - return context.error(`Expected at least two arguments.`); + if (args.length < 2) { + return context.error(`Expected at least one argument.`); } - if ((args.length - 1) % 2 !== 0) { - return context.error(`Expected an even number of arguments.`); + const firstArg = args[1]; + if (!Array.isArray(firstArg) && typeof firstArg === 'object') { + return context.error(`First argument must be an image or text section.`); } const sections: Array = []; - for (let i = 1; i < args.length - 1; i += 2) { - const text = context.parse(args[i], 1, ValueType); - if (!text) return null; - const kind = text.type.kind; - if (kind !== 'string' && kind !== 'value' && kind !== 'null') - return context.error(`Formatted text type must be 'string', 'value', or 'null'.`); - - const options = (args[i + 1]: any); - if (typeof options !== "object" || Array.isArray(options)) - return context.error(`Format options argument must be an object.`); - - let scale = null; - if (options['font-scale']) { - scale = context.parse(options['font-scale'], 1, NumberType); - if (!scale) return null; + let nextTokenMayBeObject = false; + for (let i = 1; i <= args.length - 1; ++i) { + const arg = (args[i]: any); + + if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) { + nextTokenMayBeObject = false; + + let scale = null; + if (arg['font-scale']) { + scale = context.parse(arg['font-scale'], 1, NumberType); + if (!scale) return null; + } + + let font = null; + if (arg['text-font']) { + font = context.parse(arg['text-font'], 1, array(StringType)); + if (!font) return null; + } + + let textColor = null; + if (arg['text-color']) { + textColor = context.parse(arg['text-color'], 1, ColorType); + if (!textColor) return null; + } + + const lastExpression = sections[sections.length - 1]; + lastExpression.scale = scale; + lastExpression.font = font; + lastExpression.textColor = textColor; + } else { + const content = context.parse(args[i], 1, ValueType); + if (!content) return null; + + const kind = content.type.kind; + if (kind !== 'string' && kind !== 'value' && kind !== 'null' && kind !== 'resolvedImage') + return context.error(`Formatted text type must be 'string', 'value', 'image' or 'null'.`); + + nextTokenMayBeObject = true; + sections.push({content, scale: null, font: null, textColor: null}); } - - let font = null; - if (options['text-font']) { - font = context.parse(options['text-font'], 1, array(StringType)); - if (!font) return null; - } - - let textColor = null; - if (options['text-color']) { - textColor = context.parse(options['text-color'], 1, ColorType); - if (!textColor) return null; - } - sections.push({text, scale, font, textColor}); } return new FormatExpression(sections); } evaluate(ctx: EvaluationContext) { - return new Formatted( - this.sections.map(section => - new FormattedSection( - toString(section.text.evaluate(ctx)), + const evaluateSection = section => { + const evaluatedContent = section.content.evaluate(ctx); + if (typeOf(evaluatedContent) === ResolvedImageType) { + return new FormattedSection('', evaluatedContent, null, null, null); + } + + return new FormattedSection( + toString(evaluatedContent), + null, section.scale ? section.scale.evaluate(ctx) : null, section.font ? section.font.evaluate(ctx).join(',') : null, section.textColor ? section.textColor.evaluate(ctx) : null - ) - ) - ); + ); + }; + + return new Formatted(this.sections.map(evaluateSection)); } eachChild(fn: (Expression) => void) { for (const section of this.sections) { - fn(section.text); + fn(section.content); if (section.scale) { fn(section.scale); } @@ -106,7 +126,7 @@ export default class FormatExpression implements Expression { serialize() { const serialized = ["format"]; for (const section of this.sections) { - serialized.push(section.text.serialize()); + serialized.push(section.content.serialize()); const options = {}; if (section.scale) { options['font-scale'] = section.scale.serialize(); diff --git a/src/style-spec/expression/types/formatted.js b/src/style-spec/expression/types/formatted.js index 9029a89527d..224594f5360 100644 --- a/src/style-spec/expression/types/formatted.js +++ b/src/style-spec/expression/types/formatted.js @@ -2,15 +2,18 @@ import {stringContainsRTLText} from "../../../util/script_detection"; import type Color from '../../util/color'; +import type ResolvedImage from '../types/resolved_image'; export class FormattedSection { text: string; + image: ResolvedImage | null; scale: number | null; fontStack: string | null; textColor: Color | null; - constructor(text: string, scale: number | null, fontStack: string | null, textColor: Color | null) { + constructor(text: string, image: ResolvedImage | null, scale: number | null, fontStack: string | null, textColor: Color | null) { this.text = text; + this.image = image; this.scale = scale; this.fontStack = fontStack; this.textColor = textColor; @@ -25,7 +28,13 @@ export default class Formatted { } static fromString(unformatted: string): Formatted { - return new Formatted([new FormattedSection(unformatted, null, null, null)]); + return new Formatted([new FormattedSection(unformatted, null, null, null, null)]); + } + + isEmpty(): boolean { + if (this.sections.length === 0) return true; + return !this.sections.some(section => section.text.length !== 0 || + (section.image && section.image.name.length !== 0)); } static factory(text: Formatted | string): Formatted { @@ -37,6 +46,7 @@ export default class Formatted { } toString(): string { + if (this.sections.length === 0) return ''; return this.sections.map(section => section.text).join(''); } @@ -52,6 +62,10 @@ export default class Formatted { serialize(): Array { const serialized = ["format"]; for (const section of this.sections) { + if (section.image) { + serialized.push(["image", section.image.name]); + continue; + } serialized.push(section.text); const options = {}; if (section.fontStack) { diff --git a/src/symbol/quads.js b/src/symbol/quads.js index 6d3b857f717..c30ea9a931f 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -6,9 +6,11 @@ import {GLYPH_PBF_BORDER} from '../style/parse_glyph_pbf'; import type Anchor from './anchor'; import type {PositionedIcon, Shaping} from './shaping'; +import {SHAPING_DEFAULT_OFFSET} from './shaping'; +import {IMAGE_PADDING} from '../render/image_atlas'; import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type {Feature} from '../style-spec/expression'; -import type {GlyphPosition} from '../render/glyph_atlas'; +import type {StyleImage} from '../style/style_image'; import ONE_EM from './one_em'; /** @@ -37,7 +39,8 @@ export type SymbolQuad = { }, writingMode: any | void, glyphOffset: [number, number], - sectionIndex: number + sectionIndex: number, + isSDF: boolean }; /** @@ -46,13 +49,14 @@ export type SymbolQuad = { */ export function getIconQuads( shapedIcon: PositionedIcon, - iconRotate: number): Array { + iconRotate: number, + isSDFIcon: boolean): Array { const image = shapedIcon.image; // If you have a 10px icon that isn't perfectly aligned to the pixel grid it will cover 11 actual // pixels. The quad needs to be padded to account for this, otherwise they'll look slightly clipped // on one edge in some cases. - const border = 1; + const border = IMAGE_PADDING; // Expand the box to respect the 1 pixel border in the atlas image. We're using `image.paddedRect - border` // instead of image.displaySize because we only pad with one pixel for retina images as well, and the @@ -86,7 +90,7 @@ export function getIconQuads( } // Icon quad is padded, so texture coordinates also need to be padded. - return [{tl, tr, bl, br, tex: image.paddedRect, writingMode: undefined, glyphOffset: [0, 0], sectionIndex: 0}]; + return [{tl, tr, bl, br, tex: image.paddedRect, writingMode: undefined, glyphOffset: [0, 0], sectionIndex: 0, isSDF: isSDFIcon}]; } /** @@ -99,93 +103,105 @@ export function getGlyphQuads(anchor: Anchor, layer: SymbolStyleLayer, alongLine: boolean, feature: Feature, - positions: {[string]: {[number]: GlyphPosition}}, + imageMap: {[string]: StyleImage}, allowVerticalPlacement: boolean): Array { const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}) * Math.PI / 180; - - const positionedGlyphs = shaping.positionedGlyphs; const quads = []; - for (let k = 0; k < positionedGlyphs.length; k++) { - const positionedGlyph = positionedGlyphs[k]; - const glyphPositions = positions[positionedGlyph.fontStack]; - const glyph = glyphPositions && glyphPositions[positionedGlyph.glyph]; - if (!glyph) continue; - - const rect = glyph.rect; - if (!rect) continue; - - // The rects have an addditional buffer that is not included in their size. - const glyphPadding = 1.0; - const rectBuffer = GLYPH_PBF_BORDER + glyphPadding; - - const halfAdvance = glyph.metrics.advance * positionedGlyph.scale / 2; - - const glyphOffset = alongLine ? - [positionedGlyph.x + halfAdvance, positionedGlyph.y] : - [0, 0]; - - let builtInOffset = alongLine ? - [0, 0] : - [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1]]; - - const rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical; - - let verticalizedLabelOffset = [0, 0]; - if (rotateVerticalGlyph) { - // Vertical POI labels that are rotated 90deg CW and whose glyphs must preserve upright orientation - // need to be rotated 90deg CCW. After a quad is rotated, it is translated to the original built-in offset. - verticalizedLabelOffset = builtInOffset; - builtInOffset = [0, 0]; - } - - const x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0]; - const y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1]; - const x2 = x1 + rect.w * positionedGlyph.scale; - const y2 = y1 + rect.h * positionedGlyph.scale; - - const tl = new Point(x1, y1); - const tr = new Point(x2, y1); - const bl = new Point(x1, y2); - const br = new Point(x2, y2); - - if (rotateVerticalGlyph) { - // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em) - // In horizontal orientation, the y values for glyphs are below the midline - // and we use a "yOffset" of -17 to pull them up to the middle. - // By rotating counter-clockwise around the point at the center of the left - // edge of a 24x24 layout box centered below the midline, we align the center - // of the glyphs with the horizontal midline, so the yOffset is no longer - // necessary, but we also pull the glyph to the left along the x axis. - // The y coordinate includes baseline yOffset, thus needs to be accounted - // for when glyph is rotated and translated. - const center = new Point(-halfAdvance, halfAdvance - shaping.yOffset); - const verticalRotation = -Math.PI / 2; - - // xHalfWidhtOffsetcorrection is a difference between full-width and half-width - // advance, should be 0 for full-width glyphs and will pull up half-width glyphs. - const xHalfWidhtOffsetcorrection = ONE_EM / 2 - halfAdvance; - const xOffsetCorrection = new Point(5 - shaping.yOffset - xHalfWidhtOffsetcorrection, 0); - const verticalOffsetCorrection = new Point(...verticalizedLabelOffset); - tl._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); - tr._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); - bl._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); - br._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); + for (const line of shaping.positionedLines) { + for (const positionedGlyph of line.positionedGlyphs) { + if (!positionedGlyph.rect) continue; + const textureRect = positionedGlyph.rect || {}; + + // The rects have an additional buffer that is not included in their size. + const glyphPadding = 1.0; + let rectBuffer = GLYPH_PBF_BORDER + glyphPadding; + let isSDF = true; + let pixelRatio = 1.0; + let lineOffset = 0.0; + + const rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical; + const halfAdvance = positionedGlyph.metrics.advance * positionedGlyph.scale / 2; + + // Align images and scaled glyphs in the middle of a vertical line. + if (allowVerticalPlacement && shaping.verticalizable) { + const scaledGlyphOffset = (positionedGlyph.scale - 1) * ONE_EM; + const imageOffset = (ONE_EM - positionedGlyph.metrics.width * positionedGlyph.scale) / 2; + lineOffset = line.lineOffset / 2 - (positionedGlyph.imageName ? -imageOffset : scaledGlyphOffset); + } + + if (positionedGlyph.imageName) { + const image = imageMap[positionedGlyph.imageName]; + isSDF = image.sdf; + pixelRatio = image.pixelRatio; + rectBuffer = IMAGE_PADDING / pixelRatio; + } + + const glyphOffset = alongLine ? + [positionedGlyph.x + halfAdvance, positionedGlyph.y] : + [0, 0]; + + let builtInOffset = alongLine ? + [0, 0] : + [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] - lineOffset]; + + let verticalizedLabelOffset = [0, 0]; + if (rotateVerticalGlyph) { + // Vertical POI labels that are rotated 90deg CW and whose glyphs must preserve upright orientation + // need to be rotated 90deg CCW. After a quad is rotated, it is translated to the original built-in offset. + verticalizedLabelOffset = builtInOffset; + builtInOffset = [0, 0]; + } + + const x1 = (positionedGlyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0]; + const y1 = (-positionedGlyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1]; + const x2 = x1 + textureRect.w * positionedGlyph.scale / pixelRatio; + const y2 = y1 + textureRect.h * positionedGlyph.scale / pixelRatio; + + const tl = new Point(x1, y1); + const tr = new Point(x2, y1); + const bl = new Point(x1, y2); + const br = new Point(x2, y2); + + if (rotateVerticalGlyph) { + // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em) + // In horizontal orientation, the y values for glyphs are below the midline + // and we use a "yOffset" of -17 to pull them up to the middle. + // By rotating counter-clockwise around the point at the center of the left + // edge of a 24x24 layout box centered below the midline, we align the center + // of the glyphs with the horizontal midline, so the yOffset is no longer + // necessary, but we also pull the glyph to the left along the x axis. + // The y coordinate includes baseline yOffset, thus needs to be accounted + // for when glyph is rotated and translated. + const center = new Point(-halfAdvance, halfAdvance - SHAPING_DEFAULT_OFFSET); + const verticalRotation = -Math.PI / 2; + + // xHalfWidhtOffsetCorrection is a difference between full-width and half-width + // advance, should be 0 for full-width glyphs and will pull up half-width glyphs. + const xHalfWidhtOffsetCorrection = ONE_EM / 2 - halfAdvance; + const yImageOffsetCorrection = positionedGlyph.imageName ? xHalfWidhtOffsetCorrection : 0.0; + const halfWidhtOffsetCorrection = new Point(5 - SHAPING_DEFAULT_OFFSET - xHalfWidhtOffsetCorrection, -yImageOffsetCorrection); + const verticalOffsetCorrection = new Point(...verticalizedLabelOffset); + tl._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + tr._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + bl._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + br._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + } + + if (textRotate) { + const sin = Math.sin(textRotate), + cos = Math.cos(textRotate), + matrix = [cos, -sin, sin, cos]; + + tl._matMult(matrix); + tr._matMult(matrix); + bl._matMult(matrix); + br._matMult(matrix); + } + + quads.push({tl, tr, bl, br, tex: textureRect, writingMode: shaping.writingMode, glyphOffset, sectionIndex: positionedGlyph.sectionIndex, isSDF}); } - - if (textRotate) { - const sin = Math.sin(textRotate), - cos = Math.cos(textRotate), - matrix = [cos, -sin, sin, cos]; - - tl._matMult(matrix); - tr._matMult(matrix); - bl._matMult(matrix); - br._matMult(matrix); - } - - quads.push({tl, tr, bl, br, tex: rect, writingMode: shaping.writingMode, glyphOffset, sectionIndex: positionedGlyph.sectionIndex}); } return quads; diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index 0c95738fb87..47541b98c16 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -9,10 +9,14 @@ import { import verticalizePunctuation from '../util/verticalize_punctuation'; import {plugin as rtlTextPlugin} from '../source/rtl_text_plugin'; import ONE_EM from './one_em'; +import {warnOnce} from '../util/util'; -import type {StyleGlyph} from '../style/style_glyph'; +import type {StyleGlyph, GlyphMetrics} from '../style/style_glyph'; +import {GLYPH_PBF_BORDER} from '../style/parse_glyph_pbf'; import type {ImagePosition} from '../render/image_atlas'; -import Formatted from '../style-spec/expression/types/formatted'; +import {IMAGE_PADDING} from '../render/image_atlas'; +import type {Rect, GlyphPosition} from '../render/glyph_atlas'; +import Formatted, {FormattedSection} from '../style-spec/expression/types/formatted'; const WritingMode = { horizontal: 1, @@ -20,57 +24,107 @@ const WritingMode = { horizontalOnly: 3 }; -export {shapeText, shapeIcon, fitIconToText, getAnchorAlignment, WritingMode}; +const SHAPING_DEFAULT_OFFSET = -17; +export {shapeText, shapeIcon, fitIconToText, getAnchorAlignment, WritingMode, SHAPING_DEFAULT_OFFSET}; // The position of a glyph relative to the text's anchor point. export type PositionedGlyph = { glyph: number, + imageName: string | null, x: number, y: number, vertical: boolean, scale: number, fontStack: string, - sectionIndex: number + sectionIndex: number, + metrics: GlyphMetrics, + rect: Rect | null +}; + +export type PositionedLine = { + positionedGlyphs: Array, + lineOffset: number }; // A collection of positioned glyphs and some metadata export type Shaping = { - positionedGlyphs: Array, + positionedLines: Array, top: number, bottom: number, left: number, right: number, writingMode: 1 | 2, - lineCount: number, text: string, - yOffset: number, + iconsInText: boolean, + verticalizable: boolean }; +function isEmpty(positionedLines: Array) { + for (const line of positionedLines) { + if (line.positionedGlyphs.length !== 0) { + return false; + } + } + return true; +} + export type SymbolAnchor = 'center' | 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; export type TextJustify = 'left' | 'center' | 'right'; +// Max number of images in label is 6401 U+E000–U+F8FF that covers +// Basic Multilingual Plane Unicode Private Use Area (PUA). +const PUAbegin = 0xE000; +const PUAend = 0xF8FF; + +class SectionOptions { + // Text options + scale: number; + fontStack: string; + // Image options + imageName: string | null; + + constructor() { + this.scale = 1.0; + this.fontStack = ""; + this.imageName = null; + } + + static forText(scale: number | null, fontStack: string) { + const textOptions = new SectionOptions(); + textOptions.scale = scale || 1; + textOptions.fontStack = fontStack; + return textOptions; + } + + static forImage(imageName: string) { + const imageOptions = new SectionOptions(); + imageOptions.imageName = imageName; + return imageOptions; + } + +} + class TaggedString { text: string; sectionIndex: Array // maps each character in 'text' to its corresponding entry in 'sections' - sections: Array<{ scale: number, fontStack: string }> + sections: Array + imageSectionID: number | null; constructor() { this.text = ""; this.sectionIndex = []; this.sections = []; + this.imageSectionID = null; } static fromFeature(text: Formatted, defaultFontStack: string) { const result = new TaggedString(); for (let i = 0; i < text.sections.length; i++) { const section = text.sections[i]; - result.sections.push({ - scale: section.scale || 1, - fontStack: section.fontStack || defaultFontStack - }); - result.text += section.text; - for (let j = 0; j < section.text.length; j++) { - result.sectionIndex.push(i); + if (!section.image) { + result.addTextSection(section, defaultFontStack); + } else { + result.addImageSection(section); } } return result; @@ -80,7 +134,7 @@ class TaggedString { return this.text.length; } - getSection(index: number): { scale: number, fontStack: string } { + getSection(index: number): SectionOptions { return this.sections[this.sectionIndex[index]]; } @@ -128,6 +182,43 @@ class TaggedString { getMaxScale() { return this.sectionIndex.reduce((max, index) => Math.max(max, this.sections[index].scale), 0); } + + addTextSection(section: FormattedSection, defaultFontStack: string) { + this.text += section.text; + this.sections.push(SectionOptions.forText(section.scale, section.fontStack || defaultFontStack)); + const index = this.sections.length - 1; + for (let i = 0; i < section.text.length; ++i) { + this.sectionIndex.push(index); + } + } + + addImageSection(section: FormattedSection) { + const imageName = section.image ? section.image.name : ''; + if (imageName.length === 0) { + warnOnce(`Can't add FormattedSection with an empty image.`); + return; + } + + const nextImageSectionCharCode = this.getNextImageSectionCharCode(); + if (!nextImageSectionCharCode) { + warnOnce(`Reached maximum number of images ${PUAend - PUAbegin + 2}`); + return; + } + + this.text += String.fromCharCode(nextImageSectionCharCode); + this.sections.push(SectionOptions.forImage(imageName)); + this.sectionIndex.push(this.sections.length - 1); + } + + getNextImageSectionCharCode(): number | null { + if (!this.imageSectionID) { + this.imageSectionID = PUAbegin; + return this.imageSectionID; + } + + if (this.imageSectionID >= PUAend) return null; + return ++this.imageSectionID; + } } function breakLines(input: TaggedString, lineBreakPoints: Array): Array { @@ -146,7 +237,9 @@ function breakLines(input: TaggedString, lineBreakPoints: Array): Array< } function shapeText(text: Formatted, - glyphs: {[string]: {[number]: ?StyleGlyph}}, + glyphMap: {[string]: {[number]: ?StyleGlyph}}, + glyphPositions: {[string]: {[number]: GlyphPosition}}, + imagePositions: {[string]: ImagePosition}, defaultFontStack: string, maxWidth: number, lineHeight: number, @@ -156,7 +249,9 @@ function shapeText(text: Formatted, translate: [number, number], writingMode: 1 | 2, allowVerticalPlacement: boolean, - symbolPlacement: string): Shaping | false { + symbolPlacement: string, + layoutTextSize: number, + layoutTextSizeThisZoom: number): Shaping | false { const logicalInput = TaggedString.fromFeature(text, defaultFontStack); if (writingMode === WritingMode.vertical) { @@ -171,7 +266,7 @@ function shapeText(text: Formatted, lines = []; const untaggedLines = processBidirectionalText(logicalInput.toString(), - determineLineBreaks(logicalInput, spacing, maxWidth, glyphs, symbolPlacement)); + determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap, imagePositions, symbolPlacement, layoutTextSize)); for (const line of untaggedLines) { const taggedLine = new TaggedString(); taggedLine.text = line; @@ -188,7 +283,7 @@ function shapeText(text: Formatted, const processedLines = processStyledBidirectionalText(logicalInput.text, logicalInput.sectionIndex, - determineLineBreaks(logicalInput, spacing, maxWidth, glyphs, symbolPlacement)); + determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap, imagePositions, symbolPlacement, layoutTextSize)); for (const line of processedLines) { const taggedLine = new TaggedString(); taggedLine.text = line[0]; @@ -197,24 +292,24 @@ function shapeText(text: Formatted, lines.push(taggedLine); } } else { - lines = breakLines(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphs, symbolPlacement)); + lines = breakLines(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap, imagePositions, symbolPlacement, layoutTextSize)); } - const positionedGlyphs = []; + const positionedLines = []; const shaping = { - positionedGlyphs, + positionedLines, text: logicalInput.toString(), top: translate[1], bottom: translate[1], left: translate[0], right: translate[0], writingMode, - lineCount: lines.length, - yOffset: -17 // the y offset *should* be part of the font metadata + iconsInText: false, + verticalizable: false }; - shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify, writingMode, spacing, allowVerticalPlacement); - if (!positionedGlyphs.length) return false; + shapeLines(shaping, glyphMap, glyphPositions, imagePositions, lines, lineHeight, textAnchor, textJustify, writingMode, spacing, allowVerticalPlacement, layoutTextSizeThisZoom); + if (isEmpty(positionedLines)) return false; return shaping; } @@ -251,19 +346,35 @@ const breakable: {[number]: boolean} = { // See https://github.com/mapbox/mapbox-gl-js/issues/3658 }; +function getGlyphAdvance(codePoint: number, + section: SectionOptions, + glyphMap: {[string]: {[number]: ?StyleGlyph}}, + imagePositions: {[string]: ImagePosition}, + spacing: number, + layoutTextSize: number): number { + if (!section.imageName) { + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[codePoint]; + if (!glyph) return 0; + return glyph.metrics.advance * section.scale + spacing; + } else { + const imagePosition = imagePositions[section.imageName]; + if (!imagePosition) return 0; + return imagePosition.displaySize[0] * section.scale * ONE_EM / layoutTextSize + spacing; + } +} + function determineAverageLineWidth(logicalInput: TaggedString, spacing: number, maxWidth: number, - glyphMap: {[string]: {[number]: ?StyleGlyph}}) { + glyphMap: {[string]: {[number]: ?StyleGlyph}}, + imagePositions: {[string]: ImagePosition}, + layoutTextSize: number) { let totalWidth = 0; for (let index = 0; index < logicalInput.length(); index++) { const section = logicalInput.getSection(index); - const positions = glyphMap[section.fontStack]; - const glyph = positions && positions[logicalInput.getCharCode(index)]; - if (!glyph) - continue; - totalWidth += glyph.metrics.advance * section.scale + spacing; + totalWidth += getGlyphAdvance(logicalInput.getCharCode(index), section, glyphMap, imagePositions, spacing, layoutTextSize); } const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); @@ -361,7 +472,9 @@ function determineLineBreaks(logicalInput: TaggedString, spacing: number, maxWidth: number, glyphMap: {[string]: {[number]: ?StyleGlyph}}, - symbolPlacement: string): Array { + imagePositions: {[string]: ImagePosition}, + symbolPlacement: string, + layoutTextSize: number): Array { if (symbolPlacement !== 'point') return []; @@ -369,7 +482,7 @@ function determineLineBreaks(logicalInput: TaggedString, return []; const potentialLineBreaks = []; - const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap); + const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap, imagePositions, layoutTextSize); const hasServerSuggestedBreakpoints = logicalInput.text.indexOf("\u200b") >= 0; @@ -378,17 +491,13 @@ function determineLineBreaks(logicalInput: TaggedString, for (let i = 0; i < logicalInput.length(); i++) { const section = logicalInput.getSection(i); const codePoint = logicalInput.getCharCode(i); - const positions = glyphMap[section.fontStack]; - const glyph = positions && positions[codePoint]; - - if (glyph && !whitespace[codePoint]) - currentX += glyph.metrics.advance * section.scale + spacing; + if (!whitespace[codePoint]) currentX += getGlyphAdvance(codePoint, section, glyphMap, imagePositions, spacing, layoutTextSize); // Ideographic characters, spaces, and word-breaking punctuation that often appear without // surrounding spaces. if ((i < logicalInput.length() - 1)) { const ideographicBreak = charAllowsIdeographicBreaking(codePoint); - if (breakable[codePoint] || ideographicBreak) { + if (breakable[codePoint] || ideographicBreak || section.imageName) { potentialLineBreaks.push( evaluateBreak( @@ -446,79 +555,139 @@ function getAnchorAlignment(anchor: SymbolAnchor) { function shapeLines(shaping: Shaping, glyphMap: {[string]: {[number]: ?StyleGlyph}}, + glyphPositions: {[string]: {[number]: GlyphPosition}}, + imagePositions: {[string]: ImagePosition}, lines: Array, lineHeight: number, textAnchor: SymbolAnchor, textJustify: TextJustify, writingMode: 1 | 2, spacing: number, - allowVerticalPlacement: boolean) { + allowVerticalPlacement: boolean, + layoutTextSizeThisZoom: number) { let x = 0; - let y = shaping.yOffset; + let y = SHAPING_DEFAULT_OFFSET; let maxLineLength = 0; - const positionedGlyphs = shaping.positionedGlyphs; + let maxLineHeight = 0; const justify = textJustify === 'right' ? 1 : textJustify === 'left' ? 0 : 0.5; + let lineIndex = 0; for (const line of lines) { line.trim(); const lineMaxScale = line.getMaxScale(); + const maxLineOffset = (lineMaxScale - 1) * ONE_EM; + const positionedLine = {positionedGlyphs: [], lineOffset: 0}; + shaping.positionedLines[lineIndex] = positionedLine; + const positionedGlyphs = positionedLine.positionedGlyphs; + let lineOffset = 0.0; if (!line.length()) { y += lineHeight; // Still need a line feed after empty line + ++lineIndex; continue; } - const lineStartIndex = positionedGlyphs.length; for (let i = 0; i < line.length(); i++) { const section = line.getSection(i); const sectionIndex = line.getSectionIndex(i); const codePoint = line.getCharCode(i); - // We don't know the baseline, but since we're laying out - // at 24 points, we can calculate how much it will move when - // we scale up or down. - const baselineOffset = (lineMaxScale - section.scale) * 24; - const positions = glyphMap[section.fontStack]; - const glyph = positions && positions[codePoint]; - - if (!glyph) continue; - - if (writingMode === WritingMode.horizontal || + let baselineOffset = 0.0; + let metrics = null; + let rect = null; + let imageName = null; + let verticalAdvance = ONE_EM; + const vertical = !(writingMode === WritingMode.horizontal || // Don't verticalize glyphs that have no upright orientation if vertical placement is disabled. (!allowVerticalPlacement && !charHasUprightVerticalOrientation(codePoint)) || - // If vertical placement is ebabled, don't verticalize glyphs that + // If vertical placement is enabled, don't verticalize glyphs that // are from complex text layout script, or whitespaces. - (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))) { - positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: false, scale: section.scale, fontStack: section.fontStack, sectionIndex}); - x += glyph.metrics.advance * section.scale + spacing; + (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))); + + if (!section.imageName) { + const positions = glyphPositions[section.fontStack]; + const glyphPosition = positions && positions[codePoint]; + if (glyphPosition && glyphPosition.rect) { + rect = glyphPosition.rect; + metrics = glyphPosition.metrics; + } else { + const glyphs = glyphMap[section.fontStack]; + const glyph = glyphs && glyphs[codePoint]; + if (!glyph) continue; + metrics = glyph.metrics; + } + + // We don't know the baseline, but since we're laying out + // at 24 points, we can calculate how much it will move when + // we scale up or down. + baselineOffset = (lineMaxScale - section.scale) * ONE_EM; } else { - positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: true, scale: section.scale, fontStack: section.fontStack, sectionIndex}); - x += ONE_EM * section.scale + spacing; + const imagePosition = imagePositions[section.imageName]; + if (!imagePosition) continue; + imageName = section.imageName; + shaping.iconsInText = shaping.iconsInText || true; + rect = imagePosition.paddedRect; + const size = imagePosition.displaySize; + // If needed, allow to set scale factor for an image using + // alias "image-scale" that could be alias for "font-scale" + // when FormattedSection is an image section. + section.scale = section.scale * ONE_EM / layoutTextSizeThisZoom; + + metrics = {width: size[0], + height: size[1], + left: IMAGE_PADDING, + top: -GLYPH_PBF_BORDER, + advance: vertical ? size[1] : size[0]}; + + // Difference between one EM and an image size. + // Aligns bottom of an image to a baseline level. + const imageOffset = ONE_EM - size[1] * section.scale; + baselineOffset = maxLineOffset + imageOffset; + verticalAdvance = metrics.advance; + + // Difference between height of an image and one EM at max line scale. + // Pushes current line down if an image size is over 1 EM at max line scale. + const offset = vertical ? size[0] * section.scale - ONE_EM * lineMaxScale : + size[1] * section.scale - ONE_EM * lineMaxScale; + if (offset > 0 && offset > lineOffset) { + lineOffset = offset; + } + } + + if (!vertical) { + positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + baselineOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); + x += metrics.advance * section.scale + spacing; + } else { + shaping.verticalizable = true; + positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + baselineOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); + x += verticalAdvance * section.scale + spacing; } } // Only justify if we placed at least one glyph - if (positionedGlyphs.length !== lineStartIndex) { + if (positionedGlyphs.length !== 0) { const lineLength = x - spacing; maxLineLength = Math.max(lineLength, maxLineLength); - - justifyLine(positionedGlyphs, glyphMap, lineStartIndex, positionedGlyphs.length - 1, justify); + justifyLine(positionedGlyphs, 0, positionedGlyphs.length - 1, justify, lineOffset); } x = 0; - y += lineHeight * lineMaxScale; + const currentLineHeight = lineHeight * lineMaxScale + lineOffset; + positionedLine.lineOffset = Math.max(lineOffset, maxLineOffset); + y += currentLineHeight; + maxLineHeight = Math.max(currentLineHeight, maxLineHeight); + ++lineIndex; } + // Calculate the bounding box and justify / align text block. + const height = y - SHAPING_DEFAULT_OFFSET; const {horizontalAlign, verticalAlign} = getAnchorAlignment(textAnchor); - align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, lines.length); - - // Calculate the bounding box - const height = y - shaping.yOffset; + align(shaping.positionedLines, justify, horizontalAlign, verticalAlign, maxLineLength, maxLineHeight, lineHeight, height, lines.length); shaping.top += -verticalAlign * height; shaping.bottom = shaping.top + height; @@ -528,39 +697,46 @@ function shapeLines(shaping: Shaping, // justify right = 1, left = 0, center = 0.5 function justifyLine(positionedGlyphs: Array, - glyphMap: {[string]: {[number]: ?StyleGlyph}}, start: number, end: number, - justify: 1 | 0 | 0.5) { - if (!justify) + justify: 1 | 0 | 0.5, + lineOffset: number) { + if (!justify && !lineOffset) return; const lastPositionedGlyph = positionedGlyphs[end]; - const positions = glyphMap[lastPositionedGlyph.fontStack]; - const glyph = positions && positions[lastPositionedGlyph.glyph]; - if (glyph) { - const lastAdvance = glyph.metrics.advance * lastPositionedGlyph.scale; - const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; - - for (let j = start; j <= end; j++) { - positionedGlyphs[j].x -= lineIndent; - } + const lastAdvance = lastPositionedGlyph.metrics.advance * lastPositionedGlyph.scale; + const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; + + for (let j = start; j <= end; j++) { + positionedGlyphs[j].x -= lineIndent; + positionedGlyphs[j].y += lineOffset; } } -function align(positionedGlyphs: Array, +function align(positionedLines: Array, justify: number, horizontalAlign: number, verticalAlign: number, maxLineLength: number, + maxLineHeight: number, lineHeight: number, + blockHeight: number, lineCount: number) { const shiftX = (justify - horizontalAlign) * maxLineLength; - const shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight; + let shiftY = 0; - for (let j = 0; j < positionedGlyphs.length; j++) { - positionedGlyphs[j].x += shiftX; - positionedGlyphs[j].y += shiftY; + if (maxLineHeight !== lineHeight) { + shiftY = -blockHeight * verticalAlign - SHAPING_DEFAULT_OFFSET; + } else { + shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight; + } + + for (const line of positionedLines) { + for (const positionedGlyph of line.positionedGlyphs) { + positionedGlyph.x += shiftX; + positionedGlyph.y += shiftY; + } } } diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index f07d1a04348..28c182ba5c8 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -188,10 +188,13 @@ export function performSymbolLayout(bucket: SymbolBucket, const lineHeight = layout.get('text-line-height') * ONE_EM; const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; const keepUpright = layout.get('text-keep-upright'); + const textSize = layout.get('text-size'); for (const feature of bucket.features) { const fontstack = layout.get('text-font').evaluate(feature, {}).join(','); - const glyphPositionMap = glyphPositions; + const layoutTextSizeThisZoom = textSize.evaluate(feature, {}); + const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}); + const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}); const shapedTextOrientations = { horizontal: {}, @@ -234,8 +237,8 @@ export function performSymbolLayout(bucket: SymbolBucket, // Vertical POI label placement is meant to be used for scripts that support vertical // writing mode, thus, default left justification is used. If Latin // scripts would need to be supported, this should take into account other justifications. - shapedTextOrientations.vertical = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, - 'left', spacingIfAllowed, textOffset, WritingMode.vertical, true, symbolPlacement); + shapedTextOrientations.vertical = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, + 'left', spacingIfAllowed, textOffset, WritingMode.vertical, true, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); } }; @@ -256,11 +259,11 @@ export function performSymbolLayout(bucket: SymbolBucket, } else { // If using text-variable-anchor for the layer, we use a center anchor for all shapings and apply // the offsets for the anchor in the placement step. - const shaping = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, 'center', - justification, spacingIfAllowed, textOffset, WritingMode.horizontal, false, symbolPlacement); + const shaping = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, 'center', + justification, spacingIfAllowed, textOffset, WritingMode.horizontal, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); if (shaping) { shapedTextOrientations.horizontal[justification] = shaping; - singleLine = shaping.lineCount === 1; + singleLine = shaping.positionedLines.length === 1; } } } @@ -272,8 +275,8 @@ export function performSymbolLayout(bucket: SymbolBucket, } // Horizontal point or line label. - const shaping = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, - textOffset, WritingMode.horizontal, false, symbolPlacement); + const shaping = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, + textOffset, WritingMode.horizontal, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); if (shaping) shapedTextOrientations.horizontal[textJustify] = shaping; // Vertical point label (if allowVerticalPlacement is enabled). @@ -281,14 +284,14 @@ export function performSymbolLayout(bucket: SymbolBucket, // Verticalized line label. if (allowsVerticalWritingMode(unformattedText) && textAlongLine && keepUpright) { - shapedTextOrientations.vertical = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, textJustify, - spacingIfAllowed, textOffset, WritingMode.vertical, false, symbolPlacement); + shapedTextOrientations.vertical = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, textJustify, + spacingIfAllowed, textOffset, WritingMode.vertical, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); } } - } let shapedIcon; + let isSDFIcon = false; if (feature.icon && feature.icon.name) { const image = imageMap[feature.icon.name]; if (image) { @@ -296,6 +299,7 @@ export function performSymbolLayout(bucket: SymbolBucket, imagePositions[feature.icon.name], layout.get('icon-offset').evaluate(feature, {}), layout.get('icon-anchor').evaluate(feature, {})); + isSDFIcon = image.sdf; if (bucket.sdfIcons === undefined) { bucket.sdfIcons = image.sdf; } else if (bucket.sdfIcons !== image.sdf) { @@ -309,8 +313,10 @@ export function performSymbolLayout(bucket: SymbolBucket, } } - if (Object.keys(shapedTextOrientations.horizontal).length || shapedIcon) { - addFeature(bucket, feature, shapedTextOrientations, shapedIcon, glyphPositionMap, sizes, textOffset); + const shapedText = getDefaultHorizontalShaping(shapedTextOrientations.horizontal) || shapedTextOrientations.vertical; + bucket.iconsInText = shapedText ? shapedText.iconsInText : false; + if (shapedText || shapedIcon) { + addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon); } } @@ -345,12 +351,12 @@ function addFeature(bucket: SymbolBucket, feature: SymbolFeature, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, - glyphPositionMap: {[string]: {[number]: GlyphPosition}}, + imageMap: {[string]: StyleImage}, sizes: Sizes, - textOffset: [number, number]) { - const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}); - const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}); - + layoutTextSize: number, + layoutIconSize: number, + textOffset: [number, number], + isSDFIcon: boolean) { // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. // bucket calculates text-size at a high zoom level so that all tiles can @@ -399,11 +405,11 @@ function addFeature(bucket: SymbolBucket, return; } - addSymbol(bucket, anchor, line, shapedTextOrientations, shapedIcon, verticallyShapedIcon, bucket.layers[0], + addSymbol(bucket, anchor, line, shapedTextOrientations, shapedIcon, imageMap, verticallyShapedIcon, bucket.layers[0], bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex, bucket.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, - feature, glyphPositionMap, sizes); + feature, sizes, isSDFIcon); }; if (symbolPlacement === 'line') { @@ -463,11 +469,14 @@ function addFeature(bucket: SymbolBucket, } } -const MAX_PACKED_SIZE = 65535; +const MAX_GLYPH_ICON_SIZE = 255; +const MAX_PACKED_SIZE = MAX_GLYPH_ICON_SIZE * SIZE_PACK_FACTOR; +export {MAX_PACKED_SIZE}; function addTextVertices(bucket: SymbolBucket, anchor: Point, shapedText: Shaping, + imageMap: {[string]: StyleImage}, layer: SymbolStyleLayer, textAlongLine: boolean, feature: SymbolFeature, @@ -476,11 +485,10 @@ function addTextVertices(bucket: SymbolBucket, writingMode: number, placementTypes: Array<'vertical' | 'center' | 'left' | 'right'>, placedTextSymbolIndices: {[string]: number}, - glyphPositionMap: {[string]: {[number]: GlyphPosition}}, placedIconIndex: number, sizes: Sizes) { const glyphQuads = getGlyphQuads(anchor, shapedText, textOffset, - layer, textAlongLine, feature, glyphPositionMap, bucket.allowVerticalPlacement); + layer, textAlongLine, feature, imageMap, bucket.allowVerticalPlacement); const sizeData = bucket.textSizeData; let textSizeData = null; @@ -490,7 +498,7 @@ function addTextVertices(bucket: SymbolBucket, SIZE_PACK_FACTOR * layer.layout.get('text-size').evaluate(feature, {}) ]; if (textSizeData[0] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= 256. Reduce your "text-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); } } else if (sizeData.kind === 'composite') { textSizeData = [ @@ -498,7 +506,7 @@ function addTextVertices(bucket: SymbolBucket, SIZE_PACK_FACTOR * sizes.compositeTextSizes[1].evaluate(feature, {}) ]; if (textSizeData[0] > MAX_PACKED_SIZE || textSizeData[1] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= 256. Reduce your "text-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); } } @@ -543,6 +551,7 @@ function addSymbol(bucket: SymbolBucket, line: Array, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, + imageMap: {[string]: StyleImage}, verticallyShapedIcon: PositionedIcon | void, layer: SymbolStyleLayer, collisionBoxArray: CollisionBoxArray, @@ -558,8 +567,8 @@ function addSymbol(bucket: SymbolBucket, iconAlongLine: boolean, iconOffset: [number, number], feature: SymbolFeature, - glyphPositionMap: {[string]: {[number]: GlyphPosition}}, - sizes: Sizes) { + sizes: Sizes, + isSDFIcon: boolean) { const lineArray = bucket.addToLineVertexArray(anchor, line); let textCollisionFeature, iconCollisionFeature, verticalTextCollisionFeature, verticalIconCollisionFeature; @@ -599,8 +608,8 @@ function addSymbol(bucket: SymbolBucket, // For more info check `updateVariableAnchors` in `draw_symbol.js` . if (shapedIcon) { const iconRotate = layer.layout.get('icon-rotate').evaluate(feature, {}); - const iconQuads = getIconQuads(shapedIcon, iconRotate); - const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate) : undefined; + const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon); + const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate, isSDFIcon) : undefined; iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, /*align boxes to line*/false, bucket.overscaling, iconRotate); numIconVertices = iconQuads.length * 4; @@ -613,7 +622,7 @@ function addSymbol(bucket: SymbolBucket, SIZE_PACK_FACTOR * layer.layout.get('icon-size').evaluate(feature, {}) ]; if (iconSizeData[0] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= 256. Reduce your "icon-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); } } else if (sizeData.kind === 'composite') { iconSizeData = [ @@ -621,7 +630,7 @@ function addSymbol(bucket: SymbolBucket, SIZE_PACK_FACTOR * sizes.compositeIconSizes[1].evaluate(feature, {}) ]; if (iconSizeData[0] > MAX_PACKED_SIZE || iconSizeData[1] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= 256. Reduce your "icon-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); } } @@ -673,12 +682,12 @@ function addSymbol(bucket: SymbolBucket, textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textAlongLine, bucket.overscaling, textRotate); } - const singleLine = shaping.lineCount === 1; + const singleLine = shaping.positionedLines.length === 1; numHorizontalGlyphVertices += addTextVertices( - bucket, anchor, shaping, layer, textAlongLine, feature, textOffset, lineArray, + bucket, anchor, shaping, imageMap, layer, textAlongLine, feature, textOffset, lineArray, shapedTextOrientations.vertical ? WritingMode.horizontal : WritingMode.horizontalOnly, singleLine ? (Object.keys(shapedTextOrientations.horizontal): any) : [justification], - placedTextSymbolIndices, glyphPositionMap, placedIconSymbolIndex, sizes); + placedTextSymbolIndices, placedIconSymbolIndex, sizes); if (singleLine) { break; @@ -687,8 +696,8 @@ function addSymbol(bucket: SymbolBucket, if (shapedTextOrientations.vertical) { numVerticalGlyphVertices += addTextVertices( - bucket, anchor, shapedTextOrientations.vertical, layer, textAlongLine, feature, - textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, glyphPositionMap, verticalPlacedIconSymbolIndex, sizes); + bucket, anchor, shapedTextOrientations.vertical, imageMap, layer, textAlongLine, feature, + textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes); } const textBoxStartIndex = textCollisionFeature ? textCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; diff --git a/src/symbol/symbol_size.js b/src/symbol/symbol_size.js index 42a6355ad00..dab8f4b8989 100644 --- a/src/symbol/symbol_size.js +++ b/src/symbol/symbol_size.js @@ -8,7 +8,7 @@ import EvaluationParameters from '../style/evaluation_parameters'; import type {PropertyValue, PossiblyEvaluatedPropertyValue} from '../style/properties'; import type {InterpolationType} from '../style-spec/expression/definitions/interpolate'; -const SIZE_PACK_FACTOR = 256; +const SIZE_PACK_FACTOR = 128; export {getSizeData, evaluateSizeForFeature, evaluateSizeForZoom, SIZE_PACK_FACTOR}; diff --git a/test/expected/text-shaping-default.json b/test/expected/text-shaping-default.json index bf75ee0d5bb..c46e29fb6c1 100644 --- a/test/expected/text-shaping-default.json +++ b/test/expected/text-shaping-default.json @@ -1,49 +1,124 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde", @@ -52,6 +127,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 1 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-images-horizontal.json b/test/expected/text-shaping-images-horizontal.json new file mode 100644 index 00000000000..b0178b19f2b --- /dev/null +++ b/test/expected/text-shaping-images-horizontal.json @@ -0,0 +1,275 @@ +{ + "positionedLines": [ + { + "positionedGlyphs": [ + { + "glyph": 70, + "imageName": null, + "x": -53, + "y": -34.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 18, + "left": 2, + "top": -8, + "advance": 12 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 111, + "imageName": null, + "x": -41, + "y": -34.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 13, + "height": 15, + "left": 1, + "top": -12, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 111, + "imageName": null, + "x": -27, + "y": -34.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 13, + "height": 15, + "left": 1, + "top": -12, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 57344, + "imageName": "square", + "x": -13, + "y": -31.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 1, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 57345, + "imageName": "wide", + "x": 8, + "y": -31.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 2, + "metrics": { + "width": 30, + "height": 14, + "left": 1, + "top": -3, + "advance": 30 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 16 + } + } + ], + "lineOffset": 0 + }, + { + "positionedGlyphs": [ + { + "glyph": 57346, + "imageName": "tall", + "x": -42, + "y": -10.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 4, + "metrics": { + "width": 14, + "height": 30, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 32 + } + }, + { + "glyph": 57347, + "imageName": "square", + "x": -21, + "y": 13.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 5, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 32, + "imageName": null, + "x": 0, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 0, + "height": 0, + "left": 0, + "top": -26, + "advance": 6 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": 6, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 97, + "imageName": null, + "x": 20, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 114, + "imageName": null, + "x": 33, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 8, + "height": 14, + "left": 2, + "top": -12, + "advance": 9 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 21 + } + ], + "text": "Foo\n bar", + "top": -34.5, + "bottom": 34.5, + "left": -53, + "right": 53, + "writingMode": 1, + "iconsInText": true, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-images-vertical.json b/test/expected/text-shaping-images-vertical.json new file mode 100644 index 00000000000..1aef4d6fe64 --- /dev/null +++ b/test/expected/text-shaping-images-vertical.json @@ -0,0 +1,160 @@ +{ + "positionedLines": [ + { + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -33, + "y": -13.5, + "vertical": true, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 57344, + "imageName": "square", + "x": -9, + "y": -10.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 1, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 57345, + "imageName": "wide", + "x": 12, + "y": -10.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 2, + "metrics": { + "width": 30, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 16 + } + } + ], + "lineOffset": 21 + }, + { + "positionedGlyphs": [ + { + "glyph": 57346, + "imageName": "tall", + "x": -43.5, + "y": -10.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 4, + "metrics": { + "width": 14, + "height": 30, + "left": 1, + "top": -3, + "advance": 30 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 32 + } + }, + { + "glyph": 57347, + "imageName": "square", + "x": 1.5, + "y": 13.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 5, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 22.5, + "y": 10.5, + "vertical": true, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 + } + ], + "text": "三​三", + "top": -34.5, + "bottom": 34.5, + "left": -45, + "right": 45, + "writingMode": 2, + "iconsInText": true, + "verticalizable": true +} \ No newline at end of file diff --git a/test/expected/text-shaping-linebreak.json b/test/expected/text-shaping-linebreak.json index f0f9b4430b4..c0e69fa3c66 100644 --- a/test/expected/text-shaping-linebreak.json +++ b/test/expected/text-shaping-linebreak.json @@ -1,94 +1,244 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 98, - "x": -19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 97, - "x": -32.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde abcde", @@ -97,6 +247,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 2 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-newline.json b/test/expected/text-shaping-newline.json index a02ed316308..2eb5df9bba8 100644 --- a/test/expected/text-shaping-newline.json +++ b/test/expected/text-shaping-newline.json @@ -1,94 +1,244 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 98, - "x": -19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 97, - "x": -32.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde\nabcde", @@ -97,6 +247,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 2 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-newlines-in-middle.json b/test/expected/text-shaping-newlines-in-middle.json index 27f1ea28f35..dfcfd638756 100644 --- a/test/expected/text-shaping-newlines-in-middle.json +++ b/test/expected/text-shaping-newlines-in-middle.json @@ -1,94 +1,248 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 98, - "x": -19.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [], + "lineOffset": 0 }, { - "glyph": 99, - "x": -5.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 97, - "x": -32.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde\n\nabcde", @@ -97,6 +251,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 3 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-null.json b/test/expected/text-shaping-null.json index febc8a01e0b..52a63e9835c 100644 --- a/test/expected/text-shaping-null.json +++ b/test/expected/text-shaping-null.json @@ -1,22 +1,55 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 104, - "x": -10, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 105, - "x": 4, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 104, + "imageName": null, + "x": -10, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 19, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 105, + "imageName": null, + "x": 4, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 4, + "height": 18, + "left": 1, + "top": -8, + "advance": 6 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "hi\u0000", @@ -25,6 +58,6 @@ "left": -10, "right": 10, "writingMode": 1, - "yOffset": -17, - "lineCount": 1 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-spacing.json b/test/expected/text-shaping-spacing.json index 960771413aa..53161f0c8b7 100644 --- a/test/expected/text-shaping-spacing.json +++ b/test/expected/text-shaping-spacing.json @@ -1,49 +1,124 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -38.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -22.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 8.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 25.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -38.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -22.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 8.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 25.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde", @@ -52,6 +127,6 @@ "left": -38.5, "right": 38.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 1 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-zero-width-space.json b/test/expected/text-shaping-zero-width-space.json index 85959cc53db..6fc1c2a38a2 100644 --- a/test/expected/text-shaping-zero-width-space.json +++ b/test/expected/text-shaping-zero-width-space.json @@ -1,130 +1,341 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 19977, - "x": -63, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -63, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -42, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -21, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 0, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 21, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 42, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 19977, - "x": -42, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -63, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -42, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -21, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 0, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 21, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 42, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 19977, - "x": -21, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 0, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 21, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 42, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -63, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -42, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -21, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 0, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 21, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 42, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -21, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 0, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -21, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 0, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "三三​三三​三三​三三三三三三​三三", @@ -133,6 +344,6 @@ "left": -63, "right": 63, "writingMode": 1, - "yOffset": -17, - "lineCount": 3 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/fixtures/fontstack-glyphs.json b/test/fixtures/fontstack-glyphs.json index 7dbbcee7e49..bb8c7967f28 100644 --- a/test/fixtures/fontstack-glyphs.json +++ b/test/fixtures/fontstack-glyphs.json @@ -7,7 +7,8 @@ "left": 0, "top": -26, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "33": { "id": 33, @@ -17,7 +18,8 @@ "left": 1, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "34": { "id": 34, @@ -27,7 +29,8 @@ "left": 1, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "35": { "id": 35, @@ -37,7 +40,8 @@ "left": 0, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "36": { "id": 36, @@ -47,7 +51,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "37": { "id": 37, @@ -57,7 +62,8 @@ "left": 1, "top": -8, "advance": 19 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "38": { "id": 38, @@ -67,7 +73,8 @@ "left": 1, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "39": { "id": 39, @@ -77,7 +84,8 @@ "left": 1, "top": -8, "advance": 5 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "40": { "id": 40, @@ -87,7 +95,8 @@ "left": 0, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "41": { "id": 41, @@ -97,7 +106,8 @@ "left": 0, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "42": { "id": 42, @@ -107,7 +117,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "43": { "id": 43, @@ -117,7 +128,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "44": { "id": 44, @@ -127,7 +139,8 @@ "left": 0, "top": -23, "advance": 5 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "45": { "id": 45, @@ -137,7 +150,8 @@ "left": 0, "top": -18, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "46": { "id": 46, @@ -147,7 +161,8 @@ "left": 1, "top": -23, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "47": { "id": 47, @@ -157,7 +172,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "48": { "id": 48, @@ -167,7 +183,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "49": { "id": 49, @@ -177,7 +194,8 @@ "left": 2, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "50": { "id": 50, @@ -187,7 +205,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "51": { "id": 51, @@ -197,7 +216,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "52": { "id": 52, @@ -207,7 +227,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "53": { "id": 53, @@ -217,7 +238,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "54": { "id": 54, @@ -227,7 +249,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "55": { "id": 55, @@ -237,7 +260,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "56": { "id": 56, @@ -247,7 +271,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "57": { "id": 57, @@ -257,7 +282,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "58": { "id": 58, @@ -267,7 +293,8 @@ "left": 1, "top": -12, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "59": { "id": 59, @@ -277,7 +304,8 @@ "left": 0, "top": -12, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "60": { "id": 60, @@ -287,7 +315,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "61": { "id": 61, @@ -297,7 +326,8 @@ "left": 1, "top": -14, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "62": { "id": 62, @@ -307,7 +337,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "63": { "id": 63, @@ -317,7 +348,8 @@ "left": 0, "top": -8, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "64": { "id": 64, @@ -327,7 +359,8 @@ "left": 1, "top": -8, "advance": 21 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "65": { "id": 65, @@ -337,7 +370,8 @@ "left": 0, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "66": { "id": 66, @@ -347,7 +381,8 @@ "left": 2, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "67": { "id": 67, @@ -357,7 +392,8 @@ "left": 1, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "68": { "id": 68, @@ -367,7 +403,8 @@ "left": 2, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "69": { "id": 69, @@ -377,7 +414,8 @@ "left": 2, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "70": { "id": 70, @@ -387,7 +425,8 @@ "left": 2, "top": -8, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "71": { "id": 71, @@ -397,7 +436,8 @@ "left": 1, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "72": { "id": 72, @@ -407,7 +447,8 @@ "left": 2, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "73": { "id": 73, @@ -417,7 +458,8 @@ "left": 2, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "74": { "id": 74, @@ -427,7 +469,8 @@ "left": -2, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "75": { "id": 75, @@ -437,7 +480,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "76": { "id": 76, @@ -447,7 +491,8 @@ "left": 2, "top": -8, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "77": { "id": 77, @@ -457,7 +502,8 @@ "left": 2, "top": -8, "advance": 21 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "78": { "id": 78, @@ -467,7 +513,8 @@ "left": 2, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "79": { "id": 79, @@ -477,7 +524,8 @@ "left": 1, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "80": { "id": 80, @@ -487,7 +535,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "81": { "id": 81, @@ -497,7 +546,8 @@ "left": 1, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "82": { "id": 82, @@ -507,7 +557,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "83": { "id": 83, @@ -517,7 +568,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "84": { "id": 84, @@ -527,7 +579,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "85": { "id": 85, @@ -537,7 +590,8 @@ "left": 2, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "86": { "id": 86, @@ -547,7 +601,8 @@ "left": 0, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "87": { "id": 87, @@ -557,7 +612,8 @@ "left": 0, "top": -8, "advance": 22 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "88": { "id": 88, @@ -567,7 +623,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "89": { "id": 89, @@ -577,7 +634,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "90": { "id": 90, @@ -587,7 +645,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "91": { "id": 91, @@ -597,7 +656,8 @@ "left": 1, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "92": { "id": 92, @@ -607,7 +667,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "93": { "id": 93, @@ -617,7 +678,8 @@ "left": 0, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "94": { "id": 94, @@ -627,7 +689,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "95": { "id": 95, @@ -637,7 +700,8 @@ "left": -1, "top": -28, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "96": { "id": 96, @@ -647,7 +711,8 @@ "left": 4, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "97": { "id": 97, @@ -657,7 +722,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "98": { "id": 98, @@ -667,7 +733,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "99": { "id": 99, @@ -677,7 +744,8 @@ "left": 1, "top": -12, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "100": { "id": 100, @@ -687,7 +755,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "101": { "id": 101, @@ -697,7 +766,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "102": { "id": 102, @@ -707,7 +777,8 @@ "left": 0, "top": -7, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "103": { "id": 103, @@ -717,7 +788,8 @@ "left": 0, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "104": { "id": 104, @@ -727,7 +799,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "105": { "id": 105, @@ -737,7 +810,8 @@ "left": 1, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "106": { "id": 106, @@ -747,7 +821,8 @@ "left": -2, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "107": { "id": 107, @@ -757,7 +832,8 @@ "left": 2, "top": -7, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "108": { "id": 108, @@ -767,7 +843,8 @@ "left": 2, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "109": { "id": 109, @@ -777,7 +854,8 @@ "left": 2, "top": -12, "advance": 22 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "110": { "id": 110, @@ -787,7 +865,8 @@ "left": 2, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "111": { "id": 111, @@ -797,7 +876,8 @@ "left": 1, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "112": { "id": 112, @@ -807,7 +887,8 @@ "left": 2, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "113": { "id": 113, @@ -817,7 +898,8 @@ "left": 1, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "114": { "id": 114, @@ -827,7 +909,8 @@ "left": 2, "top": -12, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "115": { "id": 115, @@ -837,7 +920,8 @@ "left": 1, "top": -12, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "116": { "id": 116, @@ -847,7 +931,8 @@ "left": 0, "top": -10, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "117": { "id": 117, @@ -857,7 +942,8 @@ "left": 1, "top": -13, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "118": { "id": 118, @@ -867,7 +953,8 @@ "left": 0, "top": -13, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "119": { "id": 119, @@ -877,7 +964,8 @@ "left": 0, "top": -13, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "120": { "id": 120, @@ -887,7 +975,8 @@ "left": 0, "top": -13, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "121": { "id": 121, @@ -897,7 +986,8 @@ "left": 0, "top": -13, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "122": { "id": 122, @@ -907,7 +997,8 @@ "left": 0, "top": -13, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "123": { "id": 123, @@ -917,7 +1008,8 @@ "left": 0, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "124": { "id": 124, @@ -927,7 +1019,8 @@ "left": 5, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "125": { "id": 125, @@ -937,7 +1030,8 @@ "left": 0, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "126": { "id": 126, @@ -947,7 +1041,8 @@ "left": 1, "top": -16, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "160": { "id": 160, @@ -957,7 +1052,8 @@ "left": 0, "top": -26, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "161": { "id": 161, @@ -967,7 +1063,8 @@ "left": 1, "top": -12, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "162": { "id": 162, @@ -977,7 +1074,8 @@ "left": 2, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "163": { "id": 163, @@ -987,7 +1085,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "164": { "id": 164, @@ -997,7 +1096,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "165": { "id": 165, @@ -1007,7 +1107,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "166": { "id": 166, @@ -1017,7 +1118,8 @@ "left": 5, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "167": { "id": 167, @@ -1027,7 +1129,8 @@ "left": 1, "top": -7, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "168": { "id": 168, @@ -1037,7 +1140,8 @@ "left": 3, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "169": { "id": 169, @@ -1047,7 +1151,8 @@ "left": 1, "top": -8, "advance": 19 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "170": { "id": 170, @@ -1057,7 +1162,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "171": { "id": 171, @@ -1067,7 +1173,8 @@ "left": 0, "top": -14, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "172": { "id": 172, @@ -1077,7 +1184,8 @@ "left": 1, "top": -16, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "173": { "id": 173, @@ -1087,7 +1195,8 @@ "left": 0, "top": -18, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "174": { "id": 174, @@ -1097,7 +1206,8 @@ "left": 1, "top": -8, "advance": 19 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "175": { "id": 175, @@ -1107,7 +1217,8 @@ "left": -1, "top": -6, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "176": { "id": 176, @@ -1117,7 +1228,8 @@ "left": 1, "top": -8, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "177": { "id": 177, @@ -1127,7 +1239,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "178": { "id": 178, @@ -1137,7 +1250,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "179": { "id": 179, @@ -1147,7 +1261,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "180": { "id": 180, @@ -1157,7 +1272,8 @@ "left": 4, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "181": { "id": 181, @@ -1167,7 +1283,8 @@ "left": 2, "top": -13, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "182": { "id": 182, @@ -1177,7 +1294,8 @@ "left": 1, "top": -7, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "183": { "id": 183, @@ -1187,7 +1305,8 @@ "left": 1, "top": -15, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "184": { "id": 184, @@ -1197,7 +1316,8 @@ "left": 0, "top": -26, "advance": 5 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "185": { "id": 185, @@ -1207,7 +1327,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "186": { "id": 186, @@ -1217,7 +1338,8 @@ "left": 0, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "187": { "id": 187, @@ -1227,7 +1349,8 @@ "left": 0, "top": -14, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "188": { "id": 188, @@ -1237,7 +1360,8 @@ "left": 0, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "189": { "id": 189, @@ -1247,7 +1371,8 @@ "left": 0, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "190": { "id": 190, @@ -1257,7 +1382,8 @@ "left": 0, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "191": { "id": 191, @@ -1267,7 +1393,8 @@ "left": 0, "top": -12, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "192": { "id": 192, @@ -1277,7 +1404,8 @@ "left": 0, "top": -3, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "193": { "id": 193, @@ -1287,7 +1415,8 @@ "left": 0, "top": -3, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "194": { "id": 194, @@ -1297,7 +1426,8 @@ "left": 0, "top": -3, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "195": { "id": 195, @@ -1307,7 +1437,8 @@ "left": 0, "top": -4, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "196": { "id": 196, @@ -1317,7 +1448,8 @@ "left": 0, "top": -4, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "197": { "id": 197, @@ -1327,7 +1459,8 @@ "left": 0, "top": -4, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "198": { "id": 198, @@ -1337,7 +1470,8 @@ "left": -1, "top": -8, "advance": 20 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "199": { "id": 199, @@ -1347,7 +1481,8 @@ "left": 1, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "200": { "id": 200, @@ -1357,7 +1492,8 @@ "left": 2, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "201": { "id": 201, @@ -1367,7 +1503,8 @@ "left": 2, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "202": { "id": 202, @@ -1377,7 +1514,8 @@ "left": 2, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "203": { "id": 203, @@ -1387,7 +1525,8 @@ "left": 2, "top": -4, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "204": { "id": 204, @@ -1397,7 +1536,8 @@ "left": -1, "top": -3, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "205": { "id": 205, @@ -1407,7 +1547,8 @@ "left": 1, "top": -3, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "206": { "id": 206, @@ -1417,7 +1558,8 @@ "left": -1, "top": -3, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "207": { "id": 207, @@ -1427,7 +1569,8 @@ "left": -1, "top": -4, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "208": { "id": 208, @@ -1437,7 +1580,8 @@ "left": 0, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "209": { "id": 209, @@ -1447,7 +1591,8 @@ "left": 2, "top": -4, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "210": { "id": 210, @@ -1457,7 +1602,8 @@ "left": 1, "top": -3, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "211": { "id": 211, @@ -1467,7 +1613,8 @@ "left": 1, "top": -3, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "212": { "id": 212, @@ -1477,7 +1624,8 @@ "left": 1, "top": -3, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "213": { "id": 213, @@ -1487,7 +1635,8 @@ "left": 1, "top": -4, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "214": { "id": 214, @@ -1497,7 +1646,8 @@ "left": 1, "top": -4, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "215": { "id": 215, @@ -1507,7 +1657,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "216": { "id": 216, @@ -1517,7 +1668,8 @@ "left": 1, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "217": { "id": 217, @@ -1527,7 +1679,8 @@ "left": 2, "top": -3, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "218": { "id": 218, @@ -1537,7 +1690,8 @@ "left": 2, "top": -3, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "219": { "id": 219, @@ -1547,7 +1701,8 @@ "left": 2, "top": -3, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "220": { "id": 220, @@ -1557,7 +1712,8 @@ "left": 2, "top": -4, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "221": { "id": 221, @@ -1567,7 +1723,8 @@ "left": 0, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "222": { "id": 222, @@ -1577,7 +1734,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "223": { "id": 223, @@ -1587,7 +1745,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "224": { "id": 224, @@ -1597,7 +1756,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "225": { "id": 225, @@ -1607,7 +1767,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "226": { "id": 226, @@ -1617,7 +1778,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "227": { "id": 227, @@ -1627,7 +1789,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "228": { "id": 228, @@ -1637,7 +1800,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "229": { "id": 229, @@ -1647,7 +1811,8 @@ "left": 1, "top": -6, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "230": { "id": 230, @@ -1657,7 +1822,8 @@ "left": 1, "top": -12, "advance": 20 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "231": { "id": 231, @@ -1667,7 +1833,8 @@ "left": 1, "top": -12, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "232": { "id": 232, @@ -1677,7 +1844,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "233": { "id": 233, @@ -1687,7 +1855,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "234": { "id": 234, @@ -1697,7 +1866,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "235": { "id": 235, @@ -1707,7 +1877,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "236": { "id": 236, @@ -1717,7 +1888,8 @@ "left": -1, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "237": { "id": 237, @@ -1727,7 +1899,8 @@ "left": 1, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "238": { "id": 238, @@ -1737,7 +1910,8 @@ "left": -1, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "239": { "id": 239, @@ -1747,7 +1921,8 @@ "left": -1, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "240": { "id": 240, @@ -1757,7 +1932,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "241": { "id": 241, @@ -1767,7 +1943,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "242": { "id": 242, @@ -1777,7 +1954,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "243": { "id": 243, @@ -1787,7 +1965,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "244": { "id": 244, @@ -1797,7 +1976,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "245": { "id": 245, @@ -1807,7 +1987,8 @@ "left": 1, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "246": { "id": 246, @@ -1817,7 +1998,8 @@ "left": 1, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "247": { "id": 247, @@ -1827,7 +2009,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "248": { "id": 248, @@ -1837,7 +2020,8 @@ "left": 1, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "249": { "id": 249, @@ -1847,7 +2031,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "250": { "id": 250, @@ -1857,7 +2042,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "251": { "id": 251, @@ -1867,7 +2053,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "252": { "id": 252, @@ -1877,7 +2064,8 @@ "left": 1, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "253": { "id": 253, @@ -1887,7 +2075,8 @@ "left": 0, "top": -7, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "254": { "id": 254, @@ -1897,7 +2086,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "255": { "id": 255, @@ -1907,7 +2097,8 @@ "left": 0, "top": -8, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "256": { "id": 256, @@ -1917,7 +2108,8 @@ "left": 0, "top": -5, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "19977": { "id": 19977, @@ -1927,6 +2119,7 @@ "left": 2, "top": -8, "advance": 21 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} } } diff --git a/test/integration/expression-tests/format/basic/test.json b/test/integration/expression-tests/format/basic/test.json index d4a9a32010b..3f6eeaddc55 100644 --- a/test/integration/expression-tests/format/basic/test.json +++ b/test/integration/expression-tests/format/basic/test.json @@ -40,24 +40,28 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null }, { "text": "b", + "image": null, "scale": 2, "fontStack": null, "textColor": null }, { "text": "c", + "image": null, "scale": null, "fontStack": "a,b", "textColor": null }, { "text": "d", + "image": null, "scale": null, "fontStack": null, "textColor": {"r":0,"g":1,"b":0,"a":1} diff --git a/test/integration/expression-tests/format/coercion/test.json b/test/integration/expression-tests/format/coercion/test.json index 60980b55672..213eb961575 100644 --- a/test/integration/expression-tests/format/coercion/test.json +++ b/test/integration/expression-tests/format/coercion/test.json @@ -21,18 +21,21 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null }, { "text": "1", + "image": null, "scale": null, "fontStack": null, "textColor": null }, { "text": "true", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/data-driven-font/test.json b/test/integration/expression-tests/format/data-driven-font/test.json index dc0793d3169..c81fff8e8f1 100644 --- a/test/integration/expression-tests/format/data-driven-font/test.json +++ b/test/integration/expression-tests/format/data-driven-font/test.json @@ -22,6 +22,7 @@ "sections": [ { "text": "a", + "image": null, "scale": 1.5, "fontStack": null, "textColor": null @@ -32,6 +33,7 @@ "sections": [ { "text": "a", + "image": null, "scale": 0.5, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/image-sections/test.json b/test/integration/expression-tests/format/image-sections/test.json new file mode 100644 index 00000000000..63f40ec477c --- /dev/null +++ b/test/integration/expression-tests/format/image-sections/test.json @@ -0,0 +1,54 @@ +{ + "expression": [ + "format", + ["image", "monument-15"], + ["image", "beach-11"] + ], + "inputs": [ + [ + {"availableImages": ["monument-15"]}, + {} + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "", + "image": {"name": "monument-15", "available": true}, + "scale": null, + "fontStack": null, + "textColor": null + }, + { + "text": "", + "image": {"name": "beach-11", "available": false}, + "scale": null, + "fontStack": null, + "textColor": null + } + ] + } + ], + "serialized": [ + "format", + [ + "image", + "monument-15" + ], + {}, + [ + "image", + "beach-11" + ], + {} + ] + } +} diff --git a/test/integration/expression-tests/format/implicit-coerce/test.json b/test/integration/expression-tests/format/implicit-coerce/test.json index 24533043cdd..6b0877bc6a9 100644 --- a/test/integration/expression-tests/format/implicit-coerce/test.json +++ b/test/integration/expression-tests/format/implicit-coerce/test.json @@ -20,6 +20,7 @@ "sections": [ { "text": "", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -30,6 +31,7 @@ "sections": [ { "text": "0", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -40,6 +42,7 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/implicit-omit/test.json b/test/integration/expression-tests/format/implicit-omit/test.json index 01d9f9e9d6f..c586958ede4 100644 --- a/test/integration/expression-tests/format/implicit-omit/test.json +++ b/test/integration/expression-tests/format/implicit-omit/test.json @@ -20,6 +20,7 @@ "sections": [ { "text": "", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -30,6 +31,7 @@ "sections": [ { "text": "0", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -40,6 +42,7 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/implicit/test.json b/test/integration/expression-tests/format/implicit/test.json index f9e2a151cce..097bdf814e4 100644 --- a/test/integration/expression-tests/format/implicit/test.json +++ b/test/integration/expression-tests/format/implicit/test.json @@ -20,6 +20,7 @@ "sections": [ { "text": "", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -30,6 +31,7 @@ "sections": [ { "text": "0", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -40,6 +42,7 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/render-tests/text-field/formatted-arabic/expected.png b/test/integration/render-tests/text-field/formatted-arabic/expected.png index fa904bda07515d25e6a5fa18e23ef3f05b11d6c3..cbb7afa2252e5d6eab3a7f3c32e59596d46b4384 100644 GIT binary patch literal 5872 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 literal 5873 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 liNO+B|EK_0pF#lm&9EplQ+#ius`=24)_u~Kn diff --git a/test/integration/render-tests/text-field/formatted-images-constant-size/expected.png b/test/integration/render-tests/text-field/formatted-images-constant-size/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..cc27f0bcdce30250e01bf8c7e3ed9001330eebbb GIT binary patch literal 2845 zcma)8X;c!37RFssP(c*;B{f7+F}K`uK~!={G9oimoOy#KC1d7VDsBM+Ik}Bl?nQ|T z^U^Gx5@m7HUfSYNTA^fPW2Q7}mSa8V{hj&o&iU@W=Y046`@VDUrSFdk2La810000K z8G*rX*P_2dTXQ>)1g4Au0KnIgnBe#vl_jChg_^Y#z{;!q<@|j93~e-8U9S$C0#FWd zyBKs_<+o3SoxR8H4(`9?IBA+jp_o5>ZYDD0UH9$1PYgSd1-Yc=h?py5+Zcd@PkQl9 z#@^@m{P@ZDj|Z1-=^JGI+V%BStbuy)|AUEV?dna;wd`ZL$Gq;3uRG~AZe6T?csOC@ z_9*tpXTHS|QeTzdQrr>*kb6Vzsm8d>CODvWZ zy(O0ISZM-3TwaO(6P&v$V)_{F;Hw>Oq!mh|(uIMEC6ASp^T_UNuQB1rDg)v<5c~rlqT8rQIg43g`Hjy6x)W| z342Q%%YeO?qQ*r>eRR)ONeJRIw#yd>)c0asd7+l<5P}`&h0MMRdn$g`*CW!S)wn7K z`9Zt_J}JqllxDmOR`FyUwCRY)wVEAu_^bf04tZ|CkZ9{Im@}d_ThYEJl6N`c9sL5=TTGOh!SfRK3Yz64j%-i>YkZ3 zS`A~CPdsF;Uqx*KG#$r09^XQEXXwvK;!6L8$+_d7)7E=j&)f3xTK8%b-Y+>Bc9-CvP&AuqA4n z@J>EB`abdVba~+I#B)6()h8%F$4*=|j~hxCUaOt@lrD7T$E|e0bZ9o(e+SrRUaqM) z0cTj9=UaBJ1or9rc2$3152F%kDG|<7w6zV8m6#NK4VpF`M=VOaKSr|0%6FlRPy1H& zJ&<_NDMZ@Z=p5OC`GqUS1z$6A@-*0OpELr>QOgy0utoN-m6`uzAter0!bQfs0QS(& zBh<~}psXxg5K98MZ$t>X7fN2p7Ke~-fKC3>;x%FNYPi|jhW29BG!73oe6 z+I2==w@nCN$?*b*4T6aHJIZqB^if;Eu zp*RD|Y%Vn4;E?>-{<6ntHT@IeX_O=92PROp!}J{ZJ;@Thg<_g$Bha};Vu{ZAv$cAA zKCYOV!A?k0WiGYD#Q6@s_92pCpv1}pH;(C*0cSYAbb>$okO=Ri@3e&IqU4T?VMI1f zgcui()t3Rqn#qkK=EnYM<5rJsSx4jDH1P>t}Q(=6<#LfVjaqpD>1lSYq{k-DC8L3 z*$ti#>l}Uy@#G3)mh8XJ$jZ_CTD(`1?FMpcrv?zVE$12^@h(0I`g<=&f!6x2#k*m$ zbc-AYa|b^lygQJT9UrkO`84W82&^u{%do)P*qO4p{-|^#%@ojj zvqnus3pO_m1CGY!23&aBXLOf?CheJ_=gJr~nOoG*p|g3QQCmnpK%e9Xyg$|w=f|+P zVvHL3mQ=$voM!vRGg$kRTvw@NWQ{l6lwwYQY=1>;NsdEt-Upgr5}0p^9}yM0h@}-) zHFqRQ@POz{HO?bX$6Itz9+0cyWjwrTW7psnZY{CcxWjQSqKWl@=t8wE|E!90_JrM% zTdc+-hxHOs0zGq4gOVZ284oEVK{KHE=LC)?WT(Pk{HPyjg6Of@bdWS-1(W@TrcEr> zDRq*So+{E9-REEY;|37lZ>bPy@A3g( zMB{Qb25G10S;9~s=&3fv>PkvxX7#PZcr#Ju>8<0bfjOM(4K2jpJKXPE4wW-lW*NNYY4W zl}BHT+)4Mgmy@w`gvYF*1qT=}cg@&?Utl;6vU>XyQLb@8Np{QfhLuAL<(VREs!M~| z?i-QWzO;cWK%b`yjF;RUOPMqXWDYHn$a@aN2SUKaJRQ(iqD;7En~$FJ+U(!dB*vD zJdza&XT2ngeFw`aq@z^-^%RYU8Z)*Nywu`MvJPQiRFG%jkPE#H4Y(&g?y>nz9?72_ zoZT)kF<-IrYLy$L4{kE@cgJ~1Q%2`M=t(V-b@d;ZIQhtz zpqUn25vhCk$s?L;?@ji>pw8xz_AFik4>rxx*H}V!C@#}eCm1^?|LJJ`{fzwsBj2}J asB+6&LNrN)UJJ1%xyH}RImZm}9q?X+o= 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 literal 0 HcmV?d00001 diff --git a/test/integration/render-tests/text-field/formatted-images-multiline/style.json b/test/integration/render-tests/text-field/formatted-images-multiline/style.json new file mode 100644 index 00000000000..7f71170e9b0 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-multiline/style.json @@ -0,0 +1,75 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "multiline": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 30 ] + } + } + ] + } + }, + "multiline_with_scale": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, -18 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "multiline", + "type": "symbol", + "source": "multiline", + "layout": { + "text-max-width": 4, + "text-field": ["format", "London", ["image", "london-underground.national-rail"], + "Berlin", ["image", "s-bahn.u-bahn"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "multiline_with_scale", + "type": "symbol", + "source": "multiline_with_scale", + "layout": { + "text-max-width": 8, + "text-field": ["format", "Paris", ["image", "rer.transilien"], + "USA", ["image", "interstate_1"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/expected.png b/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..18696d91b952bab2db60dc9e16e85e23f5bc853b GIT binary patch literal 7765 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 literal 0 HcmV?d00001 diff --git a/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/style.json b/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/style.json new file mode 100644 index 00000000000..343c4e05fa9 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/style.json @@ -0,0 +1,69 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128, + "allowed": 0.014 + } + }, + "center": [ 0, 0 ], + "zoom": 12, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald@2x", + "layers": [ + { + "id": "text_zoom_constant", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0, -1], + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 11, + 8, + 13, + 34 + ], + "text-field": ["format", "Zoom", ["image", "rer"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "text_zoom_dependent", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0, 1], + "text-size": 21, + "text-field": ["format", "Zoom", ["image", "rer"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images/expected.png b/test/integration/render-tests/text-field/formatted-images/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..dd6707b6877932f40ac29cd2c8b99d6a1c26d09f GIT binary patch literal 4153 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 Qj9&r(Q>-P1Y=FD_KNT9U6#xJL literal 0 HcmV?d00001 diff --git a/test/integration/render-tests/text-field/formatted-images/style.json b/test/integration/render-tests/text-field/formatted-images/style.json new file mode 100644 index 00000000000..bcae63e6548 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 20 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald@2x", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "icon-image": ["image", "london-overground"], + "icon-offset": [0, -20], + "text-field": ["format", "London", ["image", "london-underground"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "images", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0, 2], + "text-size": 20, + "text-field": ["format", ["image", "london-overground"], + ["image", "london-underground"], + ["image", "dlr"], + ["image", "u-bahn"] + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-text-color/expected.png b/test/integration/render-tests/text-field/formatted-text-color/expected.png index 41c8bde1166e4bcb0980c7684cfb37124000a37f..03a96d89cde27824cc3419630584036fc89c2c00 100644 GIT binary patch delta 4665 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 W1aNWgs`8sk<$PdxK#yN)#eV~Uuc2-L literal 4867 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 Xr#bW*!Mx^IOY88#*ofXR%;o but found number instead.", - "line": 126 + "message": "layers[7].layout.text-field[1][0]: Unknown expression \"Helvetica\". If you wanted a literal array, use [\"literal\", [...]].", + "line": 111 + }, + { + "message": "layers[10].layout.text-field[1]: Expected array but found number instead.", + "line": 147 }, { "message": "glyphs: Styles must reference glyphs hosted by Mapbox", diff --git a/test/unit/style-spec/fixture/text-field-format.output.json b/test/unit/style-spec/fixture/text-field-format.output.json index c8bdaf47bfe..a8423368e43 100644 --- a/test/unit/style-spec/fixture/text-field-format.output.json +++ b/test/unit/style-spec/fixture/text-field-format.output.json @@ -1,18 +1,22 @@ [ { - "message": "layers[0].layout.text-field: Expected at least two arguments.", - "line": 31 + "message": "layers[1].layout.text-field: First argument must be an image or text section.", + "line": 42 }, { - "message": "layers[4].layout.text-field[1]: Expected number but found string instead.", - "line": 78 + "message": "layers[2].layout.text-field: Expected at least one argument.", + "line": 53 }, { - "message": "layers[5].layout.text-field[1][0]: Unknown expression \"Helvetica\". If you wanted a literal array, use [\"literal\", [...]].", - "line": 90 + "message": "layers[6].layout.text-field[1]: Expected number but found string instead.", + "line": 99 }, { - "message": "layers[8].layout.text-field[1]: Expected array but found number instead.", - "line": 126 + "message": "layers[7].layout.text-field[1][0]: Unknown expression \"Helvetica\". If you wanted a literal array, use [\"literal\", [...]].", + "line": 111 + }, + { + "message": "layers[10].layout.text-field[1]: Expected array but found number instead.", + "line": 147 } ] \ No newline at end of file diff --git a/test/unit/symbol/quads.test.js b/test/unit/symbol/quads.test.js index 68dea30b067..f97a7959fd6 100644 --- a/test/unit/symbol/quads.test.js +++ b/test/unit/symbol/quads.test.js @@ -15,7 +15,7 @@ test('getIconQuads', (t) => { bottom: 5.5, left: -7.5, image - }, 0), [{ + }, 0, true), [{ tl: {x: -8.5, y: -6.5}, tr: {x: 8.5, y: -6.5}, bl: {x: -8.5, y: 6.5}, @@ -23,6 +23,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: true, sectionIndex: 0 }], 'icon-anchor: center'); @@ -32,7 +33,7 @@ test('getIconQuads', (t) => { bottom: 11, left: -15, image - }, 0), [{ + }, 0, false), [{ tl: {x: -17, y: -13}, tr: {x: 17, y: -13}, bl: {x: -17, y: 13}, @@ -40,6 +41,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }], 'icon-anchor: center icon, icon-scale: 2'); @@ -49,7 +51,7 @@ test('getIconQuads', (t) => { bottom: 11, left: -15, image - }, 0), [{ + }, 0, false), [{ tl: {x: -16, y: -1}, tr: {x: 1, y: -1}, bl: {x: -16, y: 12}, @@ -57,6 +59,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }], 'icon-anchor: top-right'); @@ -66,7 +69,7 @@ test('getIconQuads', (t) => { bottom: 5.5, left: -30, image - }, 0), [{ + }, 0, false), [{ tl: {x: -34, y: -6.5}, tr: {x: 34, y: -6.5}, bl: {x: -34, y: 6.5}, @@ -74,6 +77,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }], 'icon-text-fit: both'); @@ -87,7 +91,7 @@ test('getIconQuads', (t) => { bottom: 5.5, left: -7.5, image - }, 0), [{ + }, 0, false), [{ tl: {x: -8.5, y: -6.5}, tr: {x: 8.5, y: -6.5}, bl: {x: -8.5, y: 6.5}, @@ -95,6 +99,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }]); t.end(); diff --git a/test/unit/symbol/shaping.test.js b/test/unit/symbol/shaping.test.js index b4dd8ebed10..33278229af2 100644 --- a/test/unit/symbol/shaping.test.js +++ b/test/unit/symbol/shaping.test.js @@ -2,7 +2,8 @@ import {test} from '../../util/test'; import fs from 'fs'; import path from 'path'; import * as shaping from '../../../src/symbol/shaping'; -import Formatted from '../../../src/style-spec/expression/types/formatted'; +import Formatted, {FormattedSection} from '../../../src/style-spec/expression/types/formatted'; +import ResolvedImage from '../../../src/style-spec/expression/types/resolved_image'; import {ImagePosition} from '../../../src/render/image_atlas'; const WritingMode = shaping.WritingMode; @@ -13,46 +14,63 @@ if (typeof process !== 'undefined' && process.env !== undefined) { test('shaping', (t) => { const oneEm = 24; + const layoutTextSize = 16; + const layoutTextSizeThisZoom = 16; const fontStack = 'Test'; const glyphs = { 'Test': JSON.parse(fs.readFileSync(path.join(__dirname, '/../../fixtures/fontstack-glyphs.json'))) }; + const glyphPositions = glyphs; + + const images = { + 'square': new ImagePosition({x: 0, y: 0, w: 16, h: 16}, {pixelRatio: 1, version: 1}), + 'tall': new ImagePosition({x: 0, y: 0, w: 16, h: 32}, {pixelRatio: 1, version: 1}), + 'wide': new ImagePosition({x: 0, y: 0, w: 32, h: 16}, {pixelRatio: 1, version: 1}), + }; + + const sectionForImage = (name) => { + return new FormattedSection('', ResolvedImage.fromString(name), null, null, null); + }; + + const sectionForText = (name, scale) => { + return new FormattedSection(name, null, scale, null, null); + }; let shaped; JSON.parse('{}'); - shaped = shaping.shapeText(Formatted.fromString(`hi${String.fromCharCode(0)}`), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString(`hi${String.fromCharCode(0)}`), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-null.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-null.json')))); // Default shaping. - shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-default.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-default.json')))); // Letter spacing. - shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0.125 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0.125 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-spacing.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-spacing.json')))); // Line break. - shaped = shaping.shapeText(Formatted.fromString('abcde abcde'), glyphs, fontStack, 4 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde abcde'), glyphs, glyphPositions, images, fontStack, 4 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-linebreak.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, require('../../expected/text-shaping-linebreak.json')); const expectedNewLine = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-newline.json'))); - shaped = shaping.shapeText(Formatted.fromString('abcde\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde\nabcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-newline.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedNewLine); - shaped = shaping.shapeText(Formatted.fromString('abcde\r\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); - t.deepEqual(shaped.positionedGlyphs, expectedNewLine.positionedGlyphs); + shaped = shaping.shapeText(Formatted.fromString('abcde\r\nabcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + t.deepEqual(shaped.positionedLines, expectedNewLine.positionedLines); const expectedNewLinesInMiddle = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-newlines-in-middle.json'))); - shaped = shaping.shapeText(Formatted.fromString('abcde\n\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde\n\nabcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-newlines-in-middle.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedNewLinesInMiddle); @@ -60,21 +78,62 @@ test('shaping', (t) => { // a position is ideal for breaking. const expectedZeroWidthSpaceBreak = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-zero-width-space.json'))); - shaped = shaping.shapeText(Formatted.fromString('三三\u200b三三\u200b三三\u200b三三三三三三\u200b三三'), glyphs, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('三三\u200b三三\u200b三三\u200b三三三三三三\u200b三三'), glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-zero-width-space.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedZeroWidthSpaceBreak); // Null shaping. - shaped = shaping.shapeText(Formatted.fromString(''), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString(''), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); t.equal(false, shaped); - shaped = shaping.shapeText(Formatted.fromString(String.fromCharCode(0)), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString(String.fromCharCode(0)), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); t.equal(false, shaped); // https://github.com/mapbox/mapbox-gl-js/issues/3254 - shaped = shaping.shapeText(Formatted.fromString(' foo bar\n'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); - const shaped2 = shaping.shapeText(Formatted.fromString('foo bar'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); - t.same(shaped.positionedGlyphs, shaped2.positionedGlyphs); + shaped = shaping.shapeText(Formatted.fromString(' foo bar\n'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + const shaped2 = shaping.shapeText(Formatted.fromString('foo bar'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + t.same(shaped.positionedLines, shaped2.positionedLines); + + t.test('basic image shaping', (t) => { + const shaped = shaping.shapeText(new Formatted([sectionForImage('square')]), glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + t.same(shaped.top, -12); // 1em line height + t.same(shaped.left, -10.5); // 16 - 2px border * 1.5 scale factor + t.end(); + }); + + t.test('images in horizontal layout', (t) => { + const expectedImagesHorizontal = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-images-horizontal.json'))); + const horizontalFormatted = new Formatted([ + sectionForText('Foo'), + sectionForImage('square'), + sectionForImage('wide'), + sectionForText('\n'), + sectionForImage('tall'), + sectionForImage('square'), + sectionForText(' bar'), + ]); + const shaped = shaping.shapeText(horizontalFormatted, glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-images-horizontal.json'), JSON.stringify(shaped, null, 2)); + t.deepEqual(shaped, expectedImagesHorizontal); + t.end(); + }); + + t.test('images in vertical layout', (t) => { + const expectedImagesVertical = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-images-vertical.json'))); + const horizontalFormatted = new Formatted([ + sectionForText('三'), + sectionForImage('square'), + sectionForImage('wide'), + sectionForText('\u200b'), + sectionForImage('tall'), + sectionForImage('square'), + sectionForText('三'), + ]); + const shaped = shaping.shapeText(horizontalFormatted, glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.vertical, true, 'point', layoutTextSize, layoutTextSizeThisZoom); + if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-images-vertical.json'), JSON.stringify(shaped, null, 2)); + t.deepEqual(shaped, expectedImagesVertical); + t.end(); + }); t.end(); });