From b4035a741cbfa3f37121288c414f9d250f5933d4 Mon Sep 17 00:00:00 2001 From: stanislawpuda-tomtom <85107223+stanislawpuda-tomtom@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:26:57 +0100 Subject: [PATCH] Vertical alignment in `format` expression (#5043) * add support for `top` alingment * add support for "center" alignment * add comments * render tests * handle vertical text * cleanout * update top alignment * cleanout * refactor shaping * Fix vertical alignment * fix bbox size * adjust line spacing * vertical placement * cleanout * handle text offset * minor fix * const glyph height * align when no big image * use section.scale when multiplying on image * fix big image case * fix vertical * simplify imageOffset calculation * code simplification * improve readability * cleaning - removing dead code * fix only images case * cleaning + update render tests * bump maplibre-style-spec version * update bundle size * use style spec type * fix verticalAlign rendering tests * improve justifyLine readabilty * refactor getRectAndMetrics * use switch in getVerticalAlignFactor * split calculateLineContentSize fn to improve readability * Revert "split calculateLineContentSize fn to improve readability" This reverts commit b87ecddb86dfdbc99ec6810371286ecb7e3fdd64. * clearing * improve calculateLineContentSize return type * move isVertical outside shaping fn * remove redundant imageName variable * small shapeLines code simplification * split shapeLines fn * fix lint * add tests for vertical align * fix lint in test file * move type definitions to the top of the file * simplify shapeLines adding sectionAttributes object * add return type to TaggedString.getMaxImageSize * add changelog * update render test files "baseline" -> "bottom" --------- Co-authored-by: Zbigniew Matysek --- CHANGELOG.md | 1 + src/symbol/shaping.ts | 296 +++++++++--- test/build/min.test.ts | 2 +- .../expected.png | Bin 0 -> 22608 bytes .../formatted-vertical-align-line/style.json | 452 ++++++++++++++++++ .../expected.png | Bin 0 -> 6889 bytes .../style.json | 45 ++ .../formatted-vertical-align/expected.png | Bin 0 -> 24129 bytes .../formatted-vertical-align/style.json | 119 +++++ .../symbol-shaping/shaping.test.ts | 38 +- 10 files changed, 871 insertions(+), 82 deletions(-) create mode 100644 test/integration/render/tests/text-field/formatted-vertical-align-line/expected.png create mode 100644 test/integration/render/tests/text-field/formatted-vertical-align-line/style.json create mode 100644 test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/expected.png create mode 100644 test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/style.json create mode 100644 test/integration/render/tests/text-field/formatted-vertical-align/expected.png create mode 100644 test/integration/render/tests/text-field/formatted-vertical-align/style.json diff --git a/CHANGELOG.md b/CHANGELOG.md index fa82a46fca..270e4fb553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### ✨ Features and improvements +- Add support for `vertical-align` in `format` expression ([specification](https://maplibre.org/maplibre-style-spec/expressions/#format))([#5043](https://github.com/maplibre/maplibre-gl-js/pull/5043)). - _...Add new stuff here..._ ### 🐞 Bug fixes diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index 567f7b526d..717a7da1a5 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -14,7 +14,7 @@ import {TextFit} from '../style/style_image'; import type {ImagePosition} from '../render/image_atlas'; import {IMAGE_PADDING} from '../render/image_atlas'; import type {Rect, GlyphPosition} from '../render/glyph_atlas'; -import {type Formatted, type FormattedSection} from '@maplibre/maplibre-gl-style-spec'; +import {type Formatted, type FormattedSection, type VerticalAlign} from '@maplibre/maplibre-gl-style-spec'; enum WritingMode { none = 0, @@ -58,6 +58,18 @@ export type Shaping = { verticalizable: boolean; }; +type ShapingSectionAttributes = { + rect: Rect | null; + metrics: GlyphMetrics; + baselineOffset: number; + imageOffset?: number; +}; + +type LineShapingSize = { + verticalLineContentWidth: number; + horizontalLineContentHeight: number; +}; + function isEmpty(positionedLines: Array) { for (const line of positionedLines) { if (line.positionedGlyphs.length !== 0) { @@ -81,23 +93,28 @@ class SectionOptions { fontStack: string; // Image options imageName: string | null; + // Common options + verticalAlign: VerticalAlign; constructor() { this.scale = 1.0; this.fontStack = ''; this.imageName = null; + this.verticalAlign = 'bottom'; } - static forText(scale: number | null, fontStack: string) { + static forText(scale: number | null, fontStack: string, verticalAlign: VerticalAlign | null) { const textOptions = new SectionOptions(); textOptions.scale = scale || 1; textOptions.fontStack = fontStack; + textOptions.verticalAlign = verticalAlign || 'bottom'; return textOptions; } - static forImage(imageName: string) { + static forImage(imageName: string, verticalAlign: VerticalAlign | null) { const imageOptions = new SectionOptions(); imageOptions.imageName = imageName; + imageOptions.verticalAlign = verticalAlign || 'bottom'; return imageOptions; } @@ -182,9 +199,28 @@ class TaggedString { return this.sectionIndex.reduce((max, index) => Math.max(max, this.sections[index].scale), 0); } + getMaxImageSize(imagePositions: {[_: string]: ImagePosition}): { + maxImageWidth: number; + maxImageHeight: number; + } { + let maxImageWidth = 0; + let maxImageHeight = 0; + for (let i = 0; i < this.length(); i++) { + const section = this.getSection(i); + if (section.imageName) { + const imagePosition = imagePositions[section.imageName]; + if (!imagePosition) continue; + const size = imagePosition.displaySize; + maxImageWidth = Math.max(maxImageWidth, size[0]); + maxImageHeight = Math.max(maxImageHeight, size[1]); + } + } + return {maxImageWidth, maxImageHeight}; + } + addTextSection(section: FormattedSection, defaultFontStack: string) { this.text += section.text; - this.sections.push(SectionOptions.forText(section.scale, section.fontStack || defaultFontStack)); + this.sections.push(SectionOptions.forText(section.scale, section.fontStack || defaultFontStack, section.verticalAlign)); const index = this.sections.length - 1; for (let i = 0; i < section.text.length; ++i) { this.sectionIndex.push(index); @@ -205,7 +241,7 @@ class TaggedString { } this.text += String.fromCharCode(nextImageSectionCharCode); - this.sections.push(SectionOptions.forImage(imageName)); + this.sections.push(SectionOptions.forImage(imageName, section.verticalAlign)); this.sectionIndex.push(this.sections.length - 1); } @@ -585,6 +621,68 @@ function getAnchorAlignment(anchor: SymbolAnchor) { return {horizontalAlign, verticalAlign}; } +function calculateLineContentSize( + imagePositions: {[_: string]: ImagePosition}, + line: TaggedString, + layoutTextSizeFactor: number +): LineShapingSize { + const maxGlyphSize = line.getMaxScale() * ONE_EM; + const {maxImageWidth, maxImageHeight} = line.getMaxImageSize(imagePositions); + + const horizontalLineContentHeight = Math.max(maxGlyphSize, maxImageHeight * layoutTextSizeFactor); + const verticalLineContentWidth = Math.max(maxGlyphSize, maxImageWidth * layoutTextSizeFactor); + + return {verticalLineContentWidth, horizontalLineContentHeight}; +} + +function getVerticalAlignFactor( + verticalAlign: VerticalAlign +) { + switch (verticalAlign) { + case 'top': + return 0; + case 'center': + return 0.5; + default: + return 1; + } +} + +function getRectAndMetrics( + glyphPosition: GlyphPosition, + glyphMap: { + [_: string]: { + [_: number]: StyleGlyph; + }; + }, + section: SectionOptions, + codePoint: number +): GlyphPosition | null { + if (glyphPosition && glyphPosition.rect) { + return glyphPosition; + } + + const glyphs = glyphMap[section.fontStack]; + const glyph = glyphs && glyphs[codePoint]; + if (!glyph) return null; + + const metrics = glyph.metrics; + return {rect: null, metrics}; +} + +function isLineVertical( + writingMode: WritingMode.horizontal | WritingMode.vertical, + allowVerticalPlacement: boolean, + codePoint: number +): boolean { + return !(writingMode === WritingMode.horizontal || + // Don't verticalize glyphs that have no upright orientation if vertical placement is disabled. + (!allowVerticalPlacement && !charHasUprightVerticalOrientation(codePoint)) || + // If vertical placement is enabled, don't verticalize glyphs that + // are from complex text layout script, or whitespaces. + (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))); +} + function shapeLines(shaping: Shaping, glyphMap: { [_: string]: { @@ -607,7 +705,7 @@ function shapeLines(shaping: Shaping, layoutTextSizeThisZoom: number) { let x = 0; - let y = SHAPING_DEFAULT_OFFSET; + let y = 0; let maxLineLength = 0; let maxLineHeight = 0; @@ -615,17 +713,17 @@ function shapeLines(shaping: Shaping, const justify = textJustify === 'right' ? 1 : textJustify === 'left' ? 0 : 0.5; + const layoutTextSizeFactor = ONE_EM / layoutTextSizeThisZoom; 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; + let imageOffset = 0.0; if (!line.length()) { y += lineHeight; // Still need a line feed after empty line @@ -633,78 +731,50 @@ function shapeLines(shaping: Shaping, continue; } + const lineShapingSize = calculateLineContentSize(imagePositions, line, layoutTextSizeFactor); + for (let i = 0; i < line.length(); i++) { const section = line.getSection(i); const sectionIndex = line.getSectionIndex(i); const codePoint = line.getCharCode(i); - 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 enabled, don't verticalize glyphs that - // are from complex text layout script, or whitespaces. - (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))); + const vertical = isLineVertical(writingMode, allowVerticalPlacement, codePoint); + + let sectionAttributes: ShapingSectionAttributes; 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; + sectionAttributes = shapeTextSection(section, codePoint, vertical, lineShapingSize, glyphMap, glyphPositions); + if (!sectionAttributes) continue; } else { - const imagePosition = imagePositions[section.imageName]; - if (!imagePosition) continue; - imageName = section.imageName; - shaping.iconsInText = shaping.iconsInText || true; - rect = imagePosition.paddedRect; - const size = imagePosition.displaySize; + shaping.iconsInText = true; // 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; - } + section.scale = section.scale * layoutTextSizeFactor; + + sectionAttributes = shapeImageSection(section, vertical, lineMaxScale, lineShapingSize, imagePositions); + if (!sectionAttributes) continue; + imageOffset = Math.max(imageOffset, sectionAttributes.imageOffset); } + const {rect, metrics, baselineOffset} = sectionAttributes; + positionedGlyphs.push({ + glyph: codePoint, + imageName: section.imageName, + x, + y: y + baselineOffset + SHAPING_DEFAULT_OFFSET, + vertical, + scale: section.scale, + fontStack: section.fontStack, + sectionIndex, + metrics, + rect + }); + 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}); + const verticalAdvance = section.imageName ? metrics.advance : ONE_EM; x += verticalAdvance * section.scale + spacing; } } @@ -713,35 +783,107 @@ function shapeLines(shaping: Shaping, if (positionedGlyphs.length !== 0) { const lineLength = x - spacing; maxLineLength = Math.max(lineLength, maxLineLength); - justifyLine(positionedGlyphs, 0, positionedGlyphs.length - 1, justify, lineOffset); + justifyLine(positionedGlyphs, 0, positionedGlyphs.length - 1, justify); } x = 0; - const currentLineHeight = lineHeight * lineMaxScale + lineOffset; - positionedLine.lineOffset = Math.max(lineOffset, maxLineOffset); + const maxLineOffset = (lineMaxScale - 1) * ONE_EM; + positionedLine.lineOffset = Math.max(imageOffset, maxLineOffset); + const currentLineHeight = lineHeight * lineMaxScale + imageOffset; 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(shaping.positionedLines, justify, horizontalAlign, verticalAlign, maxLineLength, maxLineHeight, lineHeight, height, lines.length); + align(shaping.positionedLines, justify, horizontalAlign, verticalAlign, maxLineLength, maxLineHeight, lineHeight, y, lines.length); - shaping.top += -verticalAlign * height; - shaping.bottom = shaping.top + height; + // Calculate the bounding box + // shaping.top & shaping.left already include text offset (text-radial-offset or text-offset) + shaping.top += -verticalAlign * y; + shaping.bottom = shaping.top + y; shaping.left += -horizontalAlign * maxLineLength; shaping.right = shaping.left + maxLineLength; } +function shapeTextSection( + section: SectionOptions, + codePoint: number, + vertical: boolean, + lineShapingSize: LineShapingSize, + glyphMap: { + [_: string]: { + [_: number]: StyleGlyph; + }; + }, + glyphPositions: { + [_: string]: { + [_: number]: GlyphPosition; + }; + }, +): ShapingSectionAttributes | null { + const positions = glyphPositions[section.fontStack]; + const glyphPosition = positions && positions[codePoint]; + + const rectAndMetrics = getRectAndMetrics(glyphPosition, glyphMap, section, codePoint); + + if (rectAndMetrics === null) return null; + + let baselineOffset: number; + if (vertical) { + baselineOffset = lineShapingSize.verticalLineContentWidth - section.scale * ONE_EM; + } else { + const verticalAlignFactor = getVerticalAlignFactor(section.verticalAlign); + baselineOffset = (lineShapingSize.horizontalLineContentHeight - section.scale * ONE_EM) * verticalAlignFactor; + } + + return { + rect: rectAndMetrics.rect, + metrics: rectAndMetrics.metrics, + baselineOffset + }; +} + +function shapeImageSection( + section: SectionOptions, + vertical: boolean, + lineMaxScale: number, + lineShapingSize: LineShapingSize, + imagePositions: {[_: string]: ImagePosition}, +): ShapingSectionAttributes | null { + const imagePosition = imagePositions[section.imageName]; + if (!imagePosition) return null; + const rect = imagePosition.paddedRect; + const size = imagePosition.displaySize; + + const metrics = {width: size[0], + height: size[1], + left: IMAGE_PADDING, + top: -GLYPH_PBF_BORDER, + advance: vertical ? size[1] : size[0]}; + + let baselineOffset: number; + if (vertical) { + baselineOffset = lineShapingSize.verticalLineContentWidth - size[1] * section.scale; + } else { + const verticalAlignFactor = getVerticalAlignFactor(section.verticalAlign); + baselineOffset = (lineShapingSize.horizontalLineContentHeight - size[1] * section.scale) * verticalAlignFactor; + } + + // 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 imageOffset = (vertical ? size[0] : size[1]) * section.scale - ONE_EM * lineMaxScale; + + return {rect, metrics, baselineOffset, imageOffset}; +} + // justify right = 1, left = 0, center = 0.5 function justifyLine(positionedGlyphs: Array, start: number, end: number, - justify: 1 | 0 | 0.5, - lineOffset: number) { - if (!justify && !lineOffset) + justify: 1 | 0 | 0.5) { + if (justify === 0) return; const lastPositionedGlyph = positionedGlyphs[end]; @@ -750,10 +892,12 @@ function justifyLine(positionedGlyphs: Array, for (let j = start; j <= end; j++) { positionedGlyphs[j].x -= lineIndent; - positionedGlyphs[j].y += lineOffset; } } +/** + * Aligns the lines based on horizontal and vertical alignment. + */ function align(positionedLines: Array, justify: number, horizontalAlign: number, @@ -769,7 +913,7 @@ function align(positionedLines: Array, if (maxLineHeight !== lineHeight) { shiftY = -blockHeight * verticalAlign - SHAPING_DEFAULT_OFFSET; } else { - shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight; + shiftY = -verticalAlign * lineCount * lineHeight + 0.5 * lineHeight; } for (const line of positionedLines) { diff --git a/test/build/min.test.ts b/test/build/min.test.ts index 3d61808d9f..e2e31841fc 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -38,7 +38,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 908628; + const expectedBytes = 909795; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/integration/render/tests/text-field/formatted-vertical-align-line/expected.png b/test/integration/render/tests/text-field/formatted-vertical-align-line/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..ce55a4688c7a146e4e2339f70f492c9f3355ee5c GIT binary patch literal 22608 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 EAJigPg#Z8m literal 0 HcmV?d00001 diff --git a/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/style.json b/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/style.json new file mode 100644 index 0000000000..af4e8491b7 --- /dev/null +++ b/test/integration/render/tests/text-field/formatted-vertical-align-vertical-text/style.json @@ -0,0 +1,45 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "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", + "layers": [ + { + "id": "vertical", + "type": "symbol", + "source": "point", + "layout": { + "text-writing-mode": ["vertical"], + "text-field": ["format", "H", { "vertical-align": "top" }, "H", { "vertical-align": "center" }, "H", ["image", "interstate_1"], { "vertical-align": "center" }, ["image", "government_icon"], { "vertical-align": "top" }, ["image", "government_icon"], { "vertical-align": "center" }, ["image", "government_icon"], "ッ",{"font-scale": 1.8}], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/text-field/formatted-vertical-align/expected.png b/test/integration/render/tests/text-field/formatted-vertical-align/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3223aca65336fe9dc4564b17df01a372cc58d0 GIT binary patch literal 24129 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 literal 0 HcmV?d00001 diff --git a/test/integration/render/tests/text-field/formatted-vertical-align/style.json b/test/integration/render/tests/text-field/formatted-vertical-align/style.json new file mode 100644 index 0000000000..563f582d2e --- /dev/null +++ b/test/integration/render/tests/text-field/formatted-vertical-align/style.json @@ -0,0 +1,119 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 512, + "width": 512 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "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", + "layers": [ + { + "id": "point-label-top", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "top" }, + " ", + {}, + "ÓÑt yg", + { "font-scale": 1.2, "vertical-align": "top" }, + " ", + {}, + "TOP", + { "font-scale": 0.8, "vertical-align": "top" } + ], + "text-size": 48, + "text-offset": [0, -2], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + }, + { + "id": "point-label-center", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "center" }, + " ", + {}, + "ÓÑt yg", + { "font-scale": 1.2, "vertical-align": "center" }, + " ", + {}, + "CENTER", + { "font-scale": 0.8, "vertical-align": "center" } + ], + "text-size": 48, + "text-offset": [0, 0], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + }, + { + "id": "point-label-bottom", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "government_icon"], + { "vertical-align": "bottom" }, + " ", + {}, + "ÓÑt yg", + { "font-scale": 1.2, "vertical-align": "bottom" }, + " ", + {}, + "BOTTOM", + { "font-scale": 0.8, "vertical-align": "bottom" } + ], + "text-size": 48, + "text-offset": [0, 2], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + }, + "paint": { + "text-color": "#000000" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/symbol-shaping/shaping.test.ts b/test/integration/symbol-shaping/shaping.test.ts index e226edf4b9..090f993a03 100644 --- a/test/integration/symbol-shaping/shaping.test.ts +++ b/test/integration/symbol-shaping/shaping.test.ts @@ -2,7 +2,7 @@ import {describe, test, expect} from 'vitest'; import fs from 'fs'; import path from 'path'; import {WritingMode, shapeText, type Shaping} from '../../../src/symbol/shaping'; -import {ResolvedImage, Formatted, FormattedSection} from '@maplibre/maplibre-gl-style-spec'; +import {ResolvedImage, Formatted, FormattedSection, type VerticalAlign} from '@maplibre/maplibre-gl-style-spec'; import {ImagePosition} from '../../../src/render/image_atlas'; import type {StyleImage} from '../../../src/style/style_image'; import type {StyleGlyph} from '../../../src/style/style_glyph'; @@ -22,12 +22,12 @@ if (typeof process !== 'undefined' && process.env !== undefined) { UPDATE = !!process.env.UPDATE; } -function sectionForImage(name: string) { - return new FormattedSection('', ResolvedImage.fromString(name), null, null, null, null); +function sectionForImage(name: string, verticalAlign?: VerticalAlign) { + return new FormattedSection('', ResolvedImage.fromString(name), null, null, null, verticalAlign); } -function sectionForText(name: string, scale?: number) { - return new FormattedSection(name, null, scale, null, null, null); +function sectionForText(name: string, scale?: number, verticalAlign?: VerticalAlign) { + return new FormattedSection(name, null, scale, null, null, verticalAlign); } describe('shaping', () => { @@ -160,4 +160,32 @@ describe('shaping', () => { if (UPDATE) fs.writeFileSync(path.resolve(__dirname, './tests/text-shaping-images-vertical.json'), JSON.stringify(shaped, null, 2)); expect(shaped).toEqual(expectedImagesVertical); }); + + test('text vertical align', () => { + const shaped = shapeText(new Formatted([ + sectionForText('A', 3), + sectionForText('A', 1, 'top'), + sectionForText('A', 1, 'center'), + sectionForText('A', 1, 'bottom'), + ]), glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, layoutTextSize, layoutTextSizeThisZoom); + const positionedGlyphs = (shaped as Shaping).positionedLines[0].positionedGlyphs; + expect(positionedGlyphs[0].y).toBe(-36); + expect(positionedGlyphs[1].y).toBe(-36); + expect(positionedGlyphs[2].y).toBe(-12); + expect(positionedGlyphs[3].y).toBe(12); + }); + + test('image vertical align', () => { + const shaped = shapeText(new Formatted([ + sectionForText('A', 3), + sectionForImage('square', 'top'), + sectionForImage('square', 'center'), + sectionForImage('square', 'bottom'), + ]), glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, layoutTextSize, layoutTextSizeThisZoom); + const positionedGlyphs = (shaped as Shaping).positionedLines[0].positionedGlyphs; + expect(positionedGlyphs[0].y).toBe(-36); + expect(positionedGlyphs[1].y).toBe(-36); + expect(positionedGlyphs[2].y).toBe(-10.5); + expect(positionedGlyphs[3].y).toBe(15); + }); });