diff --git a/include/mbgl/style/image.hpp b/include/mbgl/style/image.hpp index 30206a740d9..35c42e8a39d 100644 --- a/include/mbgl/style/image.hpp +++ b/include/mbgl/style/image.hpp @@ -14,6 +14,12 @@ namespace style { using ImageStretch = std::pair; using ImageStretches = std::vector; +enum class TextFit { + stretchOrShrink, + stretchOnly, + proportional +}; + class ImageContent { public: float left; @@ -34,15 +40,26 @@ class Image { bool sdf, ImageStretches stretchX = {}, ImageStretches stretchY = {}, - const std::optional& content = std::nullopt); + const std::optional& content = std::nullopt, + const std::optional& textFitWidth = std::nullopt, + const std::optional& textFitHeight = std::nullopt); Image(std::string id, PremultipliedImage&& image, float pixelRatio, ImageStretches stretchX = {}, ImageStretches stretchY = {}, - const std::optional& content = std::nullopt) - : Image(std::move(id), std::move(image), pixelRatio, false, std::move(stretchX), std::move(stretchY), content) { - } + const std::optional& content = std::nullopt, + const std::optional& textFitWidth = std::nullopt, + const std::optional& textFitHeight = std::nullopt) + : Image(std::move(id), + std::move(image), + pixelRatio, + false, + std::move(stretchX), + std::move(stretchY), + content, + textFitWidth, + textFitHeight) {} Image(const Image&); std::string getID() const; @@ -63,6 +80,12 @@ class Image { /// The space where text can be fit into this image. const std::optional& getContent() const; + /// The constraints on the horizontal scaling of the image when `icon-text-fit` is used + const std::optional& getTextFitWidth() const; + + /// The constraints on the vertical scaling of the image when `icon-text-fit` is used + const std::optional& getTextFitHeight() const; + class Impl; Immutable baseImpl; explicit Image(Immutable baseImpl_) diff --git a/metrics/cache-style.db b/metrics/cache-style.db index b24c05547e1..7fbe6a9aaad 100644 Binary files a/metrics/cache-style.db and b/metrics/cache-style.db differ diff --git a/metrics/ignores/platform-all.json b/metrics/ignores/platform-all.json index 1ce8ff95803..624a166875d 100644 --- a/metrics/ignores/platform-all.json +++ b/metrics/ignores/platform-all.json @@ -85,6 +85,7 @@ "render-tests/fill-pattern/update-feature-state": "https://github.com/mapbox/mapbox-gl-native/issues/15895", "render-tests/geojson/inline-linestring-fill": "current behavior is arbitrary", "render-tests/icon-text-fit/text-variable-anchor-overlap": "https://github.com/mapbox/mapbox-gl-native/issues/15809", + "render-tests/icon-text-fit/textFit-grid-long": "Needs to be investigated and fixed.", "render-tests/mixed-zoom/z10-z11": "https://github.com/mapbox/mapbox-gl-native/issues/10397", "render-tests/raster-masking/overlapping-zoom": "https://github.com/mapbox/mapbox-gl-native/issues/10195", "render-tests/real-world/bangkok": "https://github.com/mapbox/mapbox-gl-native/issues/10412", diff --git a/metrics/integration/geojson/grid-tall.json b/metrics/integration/geojson/grid-tall.json new file mode 100644 index 00000000000..c1d6e2ad72c --- /dev/null +++ b/metrics/integration/geojson/grid-tall.json @@ -0,0 +1,85 @@ +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "cell": "top-left" + }, + "geometry": { + "type": "Point", + "coordinates": [ -70, 60 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "top-middle" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 60 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "top-right" + }, + "geometry": { + "type": "Point", + "coordinates": [ 70, 60 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "middle-left" + }, + "geometry": { + "type": "Point", + "coordinates": [ -70, 0 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "middle-middle" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "middle-right" + }, + "geometry": { + "type": "Point", + "coordinates": [ 70, 0 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "bottom-left" + }, + "geometry": { + "type": "Point", + "coordinates": [ -70, -60 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "bottom-middle" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, -60 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "bottom-right" + }, + "geometry": { + "type": "Point", + "coordinates": [ 70, -60 ] + } + }] +} diff --git a/metrics/integration/geojson/grid.json b/metrics/integration/geojson/grid.json new file mode 100644 index 00000000000..2f760dd7982 --- /dev/null +++ b/metrics/integration/geojson/grid.json @@ -0,0 +1,85 @@ +{ + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "cell": "top-left" + }, + "geometry": { + "type": "Point", + "coordinates": [ -70, 35 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "top-middle" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 35 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "top-right" + }, + "geometry": { + "type": "Point", + "coordinates": [ 70, 35 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "middle-left" + }, + "geometry": { + "type": "Point", + "coordinates": [ -70, 0 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "middle-middle" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "middle-right" + }, + "geometry": { + "type": "Point", + "coordinates": [ 70, 0 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "bottom-left" + }, + "geometry": { + "type": "Point", + "coordinates": [ -70, -35 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "bottom-middle" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, -35 ] + } + }, { + "type": "Feature", + "properties": { + "cell": "bottom-right" + }, + "geometry": { + "type": "Point", + "coordinates": [ 70, -35 ] + } + }] +} diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-anchors-long/expected.png b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-long/expected.png new file mode 100644 index 00000000000..6248aaec4e2 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-long/expected.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-anchors-long/style.json b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-long/style.json new file mode 100644 index 00000000000..daa6053e117 --- /dev/null +++ b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-long/style.json @@ -0,0 +1,193 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 200, + "height": 150, + "description": "This an adaptation of stretch-nine-part-content but with a long enough text content to stretch beyond aspect ratio." + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://geojson/anchors.json" + } + }, + "sprite": "local://sprites/stretch", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "anchor-center", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "center"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-left", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "left"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "left", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-top-left", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "top-left"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "top-left", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-top", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "top"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "top", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-top-right", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "top-right"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "top-right", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-right", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "right"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "right", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-bottom-left", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "bottom-left"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "bottom-left", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-bottom", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "bottom"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "bottom", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-bottom-right", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "bottom-right"], + "layout": { + "text-field": "123456", + "text-size": 20, + "text-anchor": "bottom-right", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchors", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 2, + "circle-color": "green", + "circle-stroke-color": "white", + "circle-stroke-width": 1 + } + } + ] + } \ No newline at end of file diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-anchors-short/expected.png b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-short/expected.png new file mode 100644 index 00000000000..c9793726035 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-short/expected.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-anchors-short/style.json b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-short/style.json new file mode 100644 index 00000000000..1587d6940b3 --- /dev/null +++ b/metrics/integration/render-tests/icon-text-fit/textFit-anchors-short/style.json @@ -0,0 +1,193 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 200, + "height": 150, + "description": "This an adaptation of stretch-nine-part-content but with '1' as the text and textFitWidth=stretchOnly and textFitHeight=proportional. Because of this, the aspect ratio of the content won't get more narrow than the starting content aspect ratio." + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://geojson/anchors.json" + } + }, + "sprite": "local://sprites/stretch", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "anchor-center", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "center"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-left", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "left"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "left", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-top-left", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "top-left"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "top-left", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-top", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "top"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "top", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-top-right", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "top-right"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "top-right", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-right", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "right"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "right", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-bottom-left", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "bottom-left"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "bottom-left", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-bottom", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "bottom"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "bottom", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchor-bottom-right", + "type": "symbol", + "source": "geojson", + "filter": ["==", "anchor", "bottom-right"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "bottom-right", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "anchors", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 2, + "circle-color": "green", + "circle-stroke-color": "white", + "circle-stroke-width": 1 + } + } + ] + } \ No newline at end of file diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-collision/expected.png b/metrics/integration/render-tests/icon-text-fit/textFit-collision/expected.png new file mode 100644 index 00000000000..c73717653b9 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-collision/expected.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-collision/style.json b/metrics/integration/render-tests/icon-text-fit/textFit-collision/style.json new file mode 100644 index 00000000000..21166a21043 --- /dev/null +++ b/metrics/integration/render-tests/icon-text-fit/textFit-collision/style.json @@ -0,0 +1,99 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": { + "order": "no-match" + }, + "geometry": { + "type": "Point", + "coordinates": [-6.6, 12] + } + }, { + "type": "Feature", + "properties": { + "order": "no-match" + }, + "geometry": { + "type": "Point", + "coordinates": [6.6, 12] + } + }, { + "type": "Feature", + "properties": { + "order": "match" + }, + "geometry": { + "type": "Point", + "coordinates": [-6.6, -11.5] + } + }, { + "type": "Feature", + "properties": { + "order": "match" + }, + "geometry": { + "type": "Point", + "coordinates": [6.6, -11.5] + } + }] + } + } + }, + "sprite": "local://sprites/stretch", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "filter": ["==", "order", "no-match"], + "layout": { + "text-field": "I", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "icon-image": "nine-part-content", + "icon-text-fit": "both" + } + }, + { + "id": "symbol_with_offset", + "type": "symbol", + "source": "geojson", + "filter": ["==", "order", "match"], + "layout": { + "text-field": "I", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "icon-image": "nine-part-textfit-wh-stretchOnly-proportional", + "icon-text-fit": "both" + } + }, + { + "id": "anchors", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 2, + "circle-color": "green", + "circle-stroke-color": "white", + "circle-stroke-width": 1 + } + } + ] + } diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-long-vertical/expected.png b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long-vertical/expected.png new file mode 100644 index 00000000000..4a017e76b60 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long-vertical/expected.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-long-vertical/style.json b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long-vertical/style.json new file mode 100644 index 00000000000..76d81ef8112 --- /dev/null +++ b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long-vertical/style.json @@ -0,0 +1,191 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 360, + "height": 360, + "description": "This draws the matrix of all possible combinations of textFitWidth (columns) and textFitHeight (rows) where they proceed from the top-left in the order stretchOrShrink, stretchOnly, and proportional. This version shows the effect of a long string with a wider aspect ratio than the source image." + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://geojson/grid-tall.json" + } + }, + "sprite": "local://sprites/textFit", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "wh-stretchOrShrink-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-left"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-middle"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-right"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-left"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-middle"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-right"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-left"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-middle"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-right"], + "layout": { + "text-field": "一二三四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + } + ] + } \ No newline at end of file diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected-mac.png b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected-mac.png new file mode 100644 index 00000000000..ca63b0b2546 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected-mac.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected.png b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected.png new file mode 100644 index 00000000000..060dce66232 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected_Alt.png b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected_Alt.png new file mode 100644 index 00000000000..7b6667dc244 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/expected_Alt.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/style.json b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/style.json new file mode 100644 index 00000000000..85fe30f01c3 --- /dev/null +++ b/metrics/integration/render-tests/icon-text-fit/textFit-grid-long/style.json @@ -0,0 +1,182 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 360, + "height": 180, + "description": "This draws the matrix of all possible combinations of textFitWidth (columns) and textFitHeight (rows) where they proceed from the top-left in the order stretchOrShrink, stretchOnly, and proportional. This version shows the effect of a long string with a wider aspect ratio than the source image." + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://geojson/grid.json" + } + }, + "sprite": "local://sprites/textFit", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "wh-stretchOrShrink-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-left"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-middle"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-right"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-left"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-middle"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-right"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-left"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-middle"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-right"], + "layout": { + "text-field": "12345", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + } + ] + } \ No newline at end of file diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-short-vertical/expected.png b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short-vertical/expected.png new file mode 100644 index 00000000000..87ee66964ce Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short-vertical/expected.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-short-vertical/style.json b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short-vertical/style.json new file mode 100644 index 00000000000..b163434ee13 --- /dev/null +++ b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short-vertical/style.json @@ -0,0 +1,191 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 360, + "height": 360, + "description": "This draws the matrix of all possible combinations of textFitWidth (columns) and textFitHeight (rows) where they proceed from the top-left in the order stretchOrShrink, stretchOnly, and proportional. This version shows the effect of a long string with a wider aspect ratio than the source image." + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://geojson/grid-tall.json" + } + }, + "sprite": "local://sprites/textFit", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "wh-stretchOrShrink-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-left"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-middle"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-right"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-left"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-middle"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-right"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-left"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-middle"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-right"], + "layout": { + "text-field": "四", + "text-size": 20, + "text-anchor": "center", + "text-writing-mode": ["vertical"], + "text-font": [ "NotoCJK" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + } + ] + } \ No newline at end of file diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-short/expected.png b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short/expected.png new file mode 100644 index 00000000000..4dff9a99a27 Binary files /dev/null and b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short/expected.png differ diff --git a/metrics/integration/render-tests/icon-text-fit/textFit-grid-short/style.json b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short/style.json new file mode 100644 index 00000000000..432b6cee783 --- /dev/null +++ b/metrics/integration/render-tests/icon-text-fit/textFit-grid-short/style.json @@ -0,0 +1,182 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 360, + "height": 180, + "description": "This draws the matrix of all possible combinations of textFitWidth (columns) and textFitHeight (rows) where they proceed from the top-left in the order stretchOrShrink, stretchOnly, and proportional. This version shows the effect of a short string with a narrower aspect ratio than the source image." + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": "local://geojson/grid.json" + } + }, + "sprite": "local://sprites/textFit", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "wh-stretchOrShrink-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-left"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-middle"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOrShrink", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "top-right"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOrShrink", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-left"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-middle"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-stretchOnly", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "middle-right"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-stretchOnly", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOrShrink-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-left"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOrShrink-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-stretchOnly-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-middle"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-stretchOnly-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, + { + "id": "wh-proportional-proportional", + "type": "symbol", + "source": "geojson", + "filter": ["==", "cell", "bottom-right"], + "layout": { + "text-field": "1", + "text-size": 20, + "text-anchor": "center", + "text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ], + "text-allow-overlap": true, + "text-ignore-placement": true, + "icon-image": "textFit-wh-proportional-proportional", + "icon-text-fit": "both", + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + } + ] + } \ No newline at end of file diff --git a/metrics/integration/sprites/stretch.json b/metrics/integration/sprites/stretch.json index 35b03bec863..c6e5320cdf6 100644 --- a/metrics/integration/sprites/stretch.json +++ b/metrics/integration/sprites/stretch.json @@ -20,6 +20,19 @@ "sdf": false, "content": [10, 10, 30, 30] }, + "nine-part-textfit-wh-stretchOnly-proportional": { + "x": 0, + "y": 0, + "width": 40, + "height": 40, + "pixelRatio": 2, + "stretchX": [[10, 30]], + "stretchY": [[10, 30]], + "sdf": false, + "content": [10, 10, 30, 30], + "textFitWidth": "stretchOnly", + "textFitHeight": "proportional" + }, "fifteen-part": { "x": 50, "y": 0, diff --git a/metrics/integration/sprites/textFit.json b/metrics/integration/sprites/textFit.json new file mode 100644 index 00000000000..550c3f8057c --- /dev/null +++ b/metrics/integration/sprites/textFit.json @@ -0,0 +1,119 @@ +{ + "textFit-wh-stretchOrShrink-stretchOrShrink": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "stretchOrShrink", + "textFitHeight": "stretchOrShrink" + }, + "textFit-wh-stretchOnly-stretchOrShrink": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "stretchOnly", + "textFitHeight": "stretchOrShrink" + }, + "textFit-wh-proportional-stretchOrShrink": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "proportional", + "textFitHeight": "stretchOrShrink" + }, + "textFit-wh-stretchOrShrink-stretchOnly": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "stretchOrShrink", + "textFitHeight": "stretchOnly" + }, + "textFit-wh-stretchOnly-stretchOnly": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "stretchOnly", + "textFitHeight": "stretchOnly" + }, + "textFit-wh-proportional-stretchOnly": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "proportional", + "textFitHeight": "stretchOnly" + }, + "textFit-wh-stretchOrShrink-proportional": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "stretchOrShrink", + "textFitHeight": "proportional" + }, + "textFit-wh-stretchOnly-proportional": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "stretchOnly", + "textFitHeight": "proportional" + }, + "textFit-wh-proportional-proportional": { + "x": 0, + "y": 0, + "width": 100, + "height": 70, + "pixelRatio": 1, + "stretchX": [[9, 91]], + "stretchY": [[9, 61]], + "sdf": false, + "content": [9, 9, 91, 61], + "textFitWidth": "proportional", + "textFitHeight": "proportional" + } +} diff --git a/metrics/integration/sprites/textFit.png b/metrics/integration/sprites/textFit.png new file mode 100644 index 00000000000..90266af2add Binary files /dev/null and b/metrics/integration/sprites/textFit.png differ diff --git a/metrics/ios-render-test-runner-style.json b/metrics/ios-render-test-runner-style.json index e5d9de77be5..2af09d3ff36 100644 --- a/metrics/ios-render-test-runner-style.json +++ b/metrics/ios-render-test-runner-style.json @@ -14,4 +14,4 @@ "probeGFX", "probeNetwork" ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-image/stretchable-content/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-image/stretchable-content/metrics.json index 253fc965aa4..c49819c946f 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-image/stretchable-content/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-image/stretchable-content/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 3, - 3380 + 3696 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-image/stretchable/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-image/stretchable/metrics.json index 253fc965aa4..c49819c946f 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-image/stretchable/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-image/stretchable/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 3, - 3380 + 3696 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-fifteen-part/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-fifteen-part/metrics.json index 37cfb3b5fd3..b96283e28b6 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-fifteen-part/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-fifteen-part/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-@2x/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-@2x/metrics.json index 52972bf07ee..8405b98019c 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-@2x/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-@2x/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content-collision/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content-collision/metrics.json index a96054ffb96..32f0d48b415 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content-collision/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content-collision/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content/metrics.json index a96054ffb96..32f0d48b415 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-content/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-height/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-height/metrics.json index c410a69d195..6ce8b0325d5 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-height/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-height/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-width/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-width/metrics.json index c410a69d195..6ce8b0325d5 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-width/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part-just-width/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part/metrics.json index 5bec05e5841..d6ec82d8f31 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-nine-part/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-three-part/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-three-part/metrics.json index 0cd910a99ac..3b9c68707c1 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-three-part/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-three-part/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-two-part/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-two-part/metrics.json index 3e848e85338..733f6c4009e 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-two-part/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-two-part/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-underscale/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-underscale/metrics.json index 6048fc7e0b6..8193a6c6d74 100644 --- a/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-underscale/metrics.json +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/stretch-underscale/metrics.json @@ -3,7 +3,7 @@ [ "probeNetwork - default - end", 4, - 88322 + 88638 ], [ "probeNetwork - default - start", @@ -32,4 +32,4 @@ ] ] ] -} \ No newline at end of file +} diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-anchors-long/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-anchors-long/metrics.json new file mode 100644 index 00000000000..fce753d4340 --- /dev/null +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-anchors-long/metrics.json @@ -0,0 +1,14 @@ +{ + "network": [ + [ + "probeNetwork - default - end", + 4, + 88638 + ], + [ + "probeNetwork - default - start", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-anchors-short/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-anchors-short/metrics.json new file mode 100644 index 00000000000..fce753d4340 --- /dev/null +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-anchors-short/metrics.json @@ -0,0 +1,14 @@ +{ + "network": [ + [ + "probeNetwork - default - end", + 4, + 88638 + ], + [ + "probeNetwork - default - start", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-collision/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-collision/metrics.json new file mode 100644 index 00000000000..0ab854214ed --- /dev/null +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-collision/metrics.json @@ -0,0 +1,14 @@ +{ + "network": [ + [ + "probeNetwork - default - end", + 3, + 86757 + ], + [ + "probeNetwork - default - start", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-long-vertical/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-long-vertical/metrics.json new file mode 100644 index 00000000000..69a1acb43f1 --- /dev/null +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-long-vertical/metrics.json @@ -0,0 +1,14 @@ +{ + "network": [ + [ + "probeNetwork - default - end", + 5, + 390425 + ], + [ + "probeNetwork - default - start", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-long/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-long/metrics.json new file mode 100644 index 00000000000..b885feb80ca --- /dev/null +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-long/metrics.json @@ -0,0 +1,14 @@ +{ + "network": [ + [ + "probeNetwork - default - end", + 4, + 90025 + ], + [ + "probeNetwork - default - start", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-short-vertical/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-short-vertical/metrics.json new file mode 100644 index 00000000000..b1c125ef6dd --- /dev/null +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-short-vertical/metrics.json @@ -0,0 +1,14 @@ +{ + "network": [ + [ + "probeNetwork - default - end", + 4, + 199330 + ], + [ + "probeNetwork - default - start", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-short/metrics.json b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-short/metrics.json new file mode 100644 index 00000000000..b885feb80ca --- /dev/null +++ b/metrics/linux-gcc8-release/render-tests/icon-text-fit/textFit-grid-short/metrics.json @@ -0,0 +1,14 @@ +{ + "network": [ + [ + "probeNetwork - default - end", + 4, + 90025 + ], + [ + "probeNetwork - default - start", + 0, + 0 + ] + ] +} \ No newline at end of file diff --git a/src/mbgl/renderer/image_atlas.cpp b/src/mbgl/renderer/image_atlas.cpp index 34adbb8281d..e4586ce9005 100644 --- a/src/mbgl/renderer/image_atlas.cpp +++ b/src/mbgl/renderer/image_atlas.cpp @@ -13,7 +13,9 @@ ImagePosition::ImagePosition(const mapbox::Bin& bin, const style::Image::Impl& i version(version_), stretchX(image.stretchX), stretchY(image.stretchY), - content(image.content) {} + content(image.content), + textFitWidth(image.textFitWidth), + textFitHeight(image.textFitHeight) {} namespace { diff --git a/src/mbgl/renderer/image_atlas.hpp b/src/mbgl/renderer/image_atlas.hpp index aaaea1ba107..055c819088f 100644 --- a/src/mbgl/renderer/image_atlas.hpp +++ b/src/mbgl/renderer/image_atlas.hpp @@ -28,6 +28,8 @@ class ImagePosition { style::ImageStretches stretchX; style::ImageStretches stretchY; std::optional content; + std::optional textFitWidth; + std::optional textFitHeight; std::array tl() const { return {{static_cast(paddedRect.x + padding), static_cast(paddedRect.y + padding)}}; diff --git a/src/mbgl/sprite/sprite_parser.cpp b/src/mbgl/sprite/sprite_parser.cpp index 8c7768b5c8b..f6a550e4c4a 100644 --- a/src/mbgl/sprite/sprite_parser.cpp +++ b/src/mbgl/sprite/sprite_parser.cpp @@ -26,7 +26,9 @@ std::unique_ptr createStyleImage(const std::string& id, const bool sdf, style::ImageStretches&& stretchX, style::ImageStretches&& stretchY, - const std::optional& content) { + const std::optional& content, + const std::optional& textFitWidth, + const std::optional& textFitHeight) { // Disallow invalid parameter configurations. if (width <= 0 || height <= 0 || width > 1024 || height > 1024 || ratio <= 0 || ratio > 10 || srcX < 0 || srcY < 0 || srcX >= static_cast(image.size.width) || srcY >= static_cast(image.size.height) || @@ -47,8 +49,15 @@ std::unique_ptr createStyleImage(const std::string& id, PremultipliedImage::copy(image, dstImage, {static_cast(srcX), static_cast(srcY)}, {0, 0}, size); try { - return std::make_unique( - id, std::move(dstImage), static_cast(ratio), sdf, std::move(stretchX), std::move(stretchY), content); + return std::make_unique(id, + std::move(dstImage), + static_cast(ratio), + sdf, + std::move(stretchX), + std::move(stretchY), + content, + textFitWidth, + textFitHeight); } catch (const util::StyleImageException& ex) { Log::Error(Event::Sprite, std::string("Can't create image with invalid metadata: ") + ex.what()); return nullptr; @@ -150,6 +159,40 @@ std::optional getContent(const JSValue& value, const char* return std::nullopt; } +std::optional parseTextFit(const std::string_view& value) { + if (value == "stretchOrShrink") { + return style::TextFit::stretchOrShrink; + } else if (value == "stretchOnly") { + return style::TextFit::stretchOnly; + } else if (value == "proportional") { + return style::TextFit::proportional; + } else { + return std::nullopt; + } +} + +std::optional getTextFit(const JSValue& value, const char* property, const char* name) { + if (value.HasMember(property)) { + const auto& v = value[property]; + if (v.IsString()) { + const auto& valueString = v.GetString(); + const auto textFit = parseTextFit(std::string_view(valueString)); + if (!textFit.has_value()) { + Log::Warning(Event::Sprite, + std::string("Invalid sprite image '") + name + "': value of '" + property + + "' is an invalid value '" + valueString + "'"); + } + return textFit; + } else { + Log::Warning( + Event::Sprite, + std::string("Invalid sprite image '") + name + "': value of '" + property + "' must be a string"); + } + } + + return std::nullopt; +} + } // namespace std::vector> parseSprite(const std::string& id, @@ -189,6 +232,8 @@ std::vector> parseSprite(const std::string& id, style::ImageStretches stretchX = getStretches(value, "stretchX", name.c_str()); style::ImageStretches stretchY = getStretches(value, "stretchY", name.c_str()); std::optional content = getContent(value, "content", name.c_str()); + std::optional textFitWidth = getTextFit(value, "textFitWidth", name.c_str()); + std::optional textFitHeight = getTextFit(value, "textFitHeight", name.c_str()); auto image = createStyleImage(completeName, raster, @@ -200,7 +245,9 @@ std::vector> parseSprite(const std::string& id, sdf, std::move(stretchX), std::move(stretchY), - content); + content, + textFitWidth, + textFitHeight); if (image) { images.push_back(std::move(image->baseImpl)); } diff --git a/src/mbgl/sprite/sprite_parser.hpp b/src/mbgl/sprite/sprite_parser.hpp index d08d19110cb..1df6110c8d7 100644 --- a/src/mbgl/sprite/sprite_parser.hpp +++ b/src/mbgl/sprite/sprite_parser.hpp @@ -17,7 +17,9 @@ std::unique_ptr createStyleImage(const std::string& id, bool sdf, style::ImageStretches&& stretchX = {}, style::ImageStretches&& stretchY = {}, - const std::optional& content = std::nullopt); + const std::optional& content = std::nullopt, + const std::optional& textFitWidth = std::nullopt, + const std::optional& textFitHeight = std::nullopt); // Parses an image and an associated JSON file and returns the sprite objects. std::vector> parseSprite(const std::string& id, diff --git a/src/mbgl/style/image.cpp b/src/mbgl/style/image.cpp index ea4c8ee8d7e..c7dd4263252 100644 --- a/src/mbgl/style/image.cpp +++ b/src/mbgl/style/image.cpp @@ -11,9 +11,18 @@ Image::Image(std::string id, bool sdf, ImageStretches stretchX, ImageStretches stretchY, - const std::optional& content) - : baseImpl(makeMutable( - std::move(id), std::move(image), pixelRatio, sdf, std::move(stretchX), std::move(stretchY), content)) {} + const std::optional& content, + const std::optional& textFitWidth, + const std::optional& textFitHeight) + : baseImpl(makeMutable(std::move(id), + std::move(image), + pixelRatio, + sdf, + std::move(stretchX), + std::move(stretchY), + content, + textFitWidth, + textFitHeight)) {} std::string Image::getID() const { return baseImpl->id; @@ -45,5 +54,12 @@ const std::optional& Image::getContent() const { return baseImpl->content; } +const std::optional& Image::getTextFitWidth() const { + return baseImpl->textFitWidth; +} + +const std::optional& Image::getTextFitHeight() const { + return baseImpl->textFitHeight; +} } // namespace style } // namespace mbgl diff --git a/src/mbgl/style/image_impl.cpp b/src/mbgl/style/image_impl.cpp index 3f60ae09ce3..a2468279a3e 100644 --- a/src/mbgl/style/image_impl.cpp +++ b/src/mbgl/style/image_impl.cpp @@ -38,14 +38,18 @@ Image::Impl::Impl(std::string id_, bool sdf_, ImageStretches stretchX_, ImageStretches stretchY_, - std::optional content_) + std::optional content_, + std::optional textFitWidth_, + std::optional textFitHeight_) : id(std::move(id_)), image(std::move(image_)), pixelRatio(pixelRatio_), sdf(sdf_), stretchX(std::move(stretchX_)), stretchY(std::move(stretchY_)), - content(std::move(content_)) { + content(std::move(content_)), + textFitWidth(std::move(textFitWidth_)), + textFitHeight(std::move(textFitHeight_)) { if (!image.valid()) { throw util::StyleImageException("dimensions may not be zero"); } else if (pixelRatio <= 0) { diff --git a/src/mbgl/style/image_impl.hpp b/src/mbgl/style/image_impl.hpp index 7983c5aa63d..2865972c952 100644 --- a/src/mbgl/style/image_impl.hpp +++ b/src/mbgl/style/image_impl.hpp @@ -17,7 +17,9 @@ class Image::Impl { bool sdf = false, ImageStretches stretchX = {}, ImageStretches stretchY = {}, - std::optional content = std::nullopt); + std::optional content = std::nullopt, + std::optional textFitWidth = std::nullopt, + std::optional textFitHeight = std::nullopt); const std::string id; @@ -35,6 +37,13 @@ class Image::Impl { // The space where text can be fit into this image. const std::optional content; + + // If `icon-text-fit` is used in a layer with this image, this option defines constraints on the horizontal scaling + // of the image. + const std::optional textFitWidth; + // If `icon-text-fit` is used in a layer with this image, this option defines constraints on the vertical scaling of + // the image. + const std::optional textFitHeight; }; } // namespace style diff --git a/src/mbgl/text/collision_feature.cpp b/src/mbgl/text/collision_feature.cpp index 2b11507713e..8687425b61f 100644 --- a/src/mbgl/text/collision_feature.cpp +++ b/src/mbgl/text/collision_feature.cpp @@ -7,19 +7,70 @@ namespace mbgl { CollisionFeature::CollisionFeature(const GeometryCoordinates& line, const Anchor& anchor, - const float top, - const float bottom, - const float left, - const float right, - const std::optional& collisionPadding, + const Shaping& shapedText, const float boxScale, const float padding, const style::SymbolPlacementType placement, - IndexedSubfeature indexedFeature_, + const RefIndexedSubfeature& indexedFeature_, const float overscaling, const float rotate) : indexedFeature(std::move(indexedFeature_)), alongLine(placement != style::SymbolPlacementType::Point) { + initialize(line, + anchor, + shapedText.top, + shapedText.bottom, + shapedText.left, + shapedText.right, + std::nullopt, + boxScale, + padding, + overscaling, + rotate); +} + +CollisionFeature::CollisionFeature(const GeometryCoordinates& line, + const Anchor& anchor, + std::optional shapedIcon, + const float boxScale, + const float padding, + const RefIndexedSubfeature& indexedFeature_, + const float rotate) + : indexedFeature(std::move(indexedFeature_)), + alongLine(false) { + if (shapedIcon) { + const auto& image = shapedIcon->image(); + auto icon = *shapedIcon; + if (image.content && (image.textFitWidth || image.textFitHeight)) { + icon = shapedIcon->applyTextFit(); + } + initialize(line, + anchor, + icon.top(), + icon.bottom(), + icon.left(), + icon.right(), + shapedIcon->collisionPadding(), + boxScale, + padding, + 1, + rotate); + } else { + initialize(line, anchor, 0, 0, 0, 0, std::nullopt, boxScale, padding, 1, rotate); + } +} + +void CollisionFeature::initialize(const GeometryCoordinates& line, + const Anchor& anchor, + float top, + float bottom, + float left, + float right, + const std::optional& collisionPadding, + float boxScale, + float padding, + float overscaling, + float rotate) { if (top == 0 && bottom == 0 && left == 0 && right == 0) return; float y1 = top * boxScale - padding; diff --git a/src/mbgl/text/collision_feature.hpp b/src/mbgl/text/collision_feature.hpp index 29f63ac2591..819cca508ba 100644 --- a/src/mbgl/text/collision_feature.hpp +++ b/src/mbgl/text/collision_feature.hpp @@ -89,20 +89,7 @@ class CollisionFeature { const style::SymbolPlacementType placement, const RefIndexedSubfeature& indexedFeature_, const float overscaling, - const float rotate) - : CollisionFeature(line, - anchor, - shapedText.top, - shapedText.bottom, - shapedText.left, - shapedText.right, - std::nullopt, - boxScale, - padding, - placement, - indexedFeature_, - overscaling, - rotate) {} + const float rotate); // for icons // Icons collision features are always SymbolPlacementType::Point, which @@ -118,40 +105,24 @@ class CollisionFeature { const float boxScale, const float padding, const RefIndexedSubfeature& indexedFeature_, - const float rotate) - : CollisionFeature(line, - anchor, - (shapedIcon ? shapedIcon->top() : 0), - (shapedIcon ? shapedIcon->bottom() : 0), - (shapedIcon ? shapedIcon->left() : 0), - (shapedIcon ? shapedIcon->right() : 0), - (shapedIcon ? shapedIcon->collisionPadding() : std::optional{std::nullopt}), - boxScale, - padding, - style::SymbolPlacementType::Point, - indexedFeature_, - 1, - rotate) {} - - CollisionFeature(const GeometryCoordinates& line, - const Anchor&, - float top, - float bottom, - float left, - float right, - const std::optional& collisionPadding, - float boxScale, - float padding, - style::SymbolPlacementType, - IndexedSubfeature, - float overscaling, - float rotate); + const float rotate); std::vector boxes; IndexedSubfeature indexedFeature; bool alongLine; private: + void initialize(const GeometryCoordinates& line, + const Anchor& anchor, + float top, + float bottom, + float left, + float right, + const std::optional& collisionPadding, + float boxScale, + float padding, + float overscaling, + float rotate); void bboxifyLabel(const GeometryCoordinates& line, GeometryCoordinate& anchorPoint, std::size_t segment, diff --git a/src/mbgl/text/quads.cpp b/src/mbgl/text/quads.cpp index 84b5521b0ef..1c57e4a1d45 100644 --- a/src/mbgl/text/quads.cpp +++ b/src/mbgl/text/quads.cpp @@ -76,9 +76,6 @@ SymbolQuads getIconQuads(const PositionedIcon& shapedIcon, const uint16_t imageWidth = image.paddedRect.w - 2 * border; const uint16_t imageHeight = image.paddedRect.h - 2 * border; - const float iconWidth = shapedIcon.right() - shapedIcon.left(); - const float iconHeight = shapedIcon.bottom() - shapedIcon.top(); - const ImageStretches stretchXFull{{0.0f, imageWidth}}; const ImageStretches stretchYFull{{0.0f, imageHeight}}; const ImageStretches& stretchX = !image.stretchX.empty() ? image.stretchX : stretchXFull; @@ -98,16 +95,23 @@ SymbolQuads getIconQuads(const PositionedIcon& shapedIcon, float fixedOffsetY = 0; float fixedContentHeight = fixedHeight; + auto icon = shapedIcon; + if (hasIconTextFit && image.content) { - auto& content = *image.content; + const auto& content = *image.content; + const auto contentWidth = content.right - content.left; + const auto contentHeight = content.bottom - content.top; + if (image.textFitWidth || image.textFitHeight) { + icon = icon.applyTextFit(); + } stretchOffsetX = sumWithinRange(stretchX, 0, content.left); stretchOffsetY = sumWithinRange(stretchY, 0, content.top); stretchContentWidth = sumWithinRange(stretchX, content.left, content.right); stretchContentHeight = sumWithinRange(stretchY, content.top, content.bottom); fixedOffsetX = content.left - stretchOffsetX; fixedOffsetY = content.top - stretchOffsetY; - fixedContentWidth = content.right - content.left - stretchContentWidth; - fixedContentHeight = content.bottom - content.top - stretchContentHeight; + fixedContentWidth = contentWidth - stretchContentWidth; + fixedContentHeight = contentHeight - stretchContentHeight; } std::optional> matrix{std::nullopt}; @@ -118,21 +122,22 @@ SymbolQuads getIconQuads(const PositionedIcon& shapedIcon, matrix = std::array{{angle_cos, -angle_sin, angle_sin, angle_cos}}; } + const float iconLeft = icon.left(); + const float iconTop = icon.top(); + const float iconWidth = icon.right() - iconLeft; + const float iconHeight = icon.bottom() - iconTop; + auto makeBox = [&](Cut left, Cut top, Cut right, Cut bottom) { - const float leftEm = getEmOffset( - left.stretch - stretchOffsetX, stretchContentWidth, iconWidth, shapedIcon.left()); + const float leftEm = getEmOffset(left.stretch - stretchOffsetX, stretchContentWidth, iconWidth, iconLeft); const float leftPx = getPxOffset(left.fixed - fixedOffsetX, fixedContentWidth, left.stretch, stretchWidth); - const float topEm = getEmOffset( - top.stretch - stretchOffsetY, stretchContentHeight, iconHeight, shapedIcon.top()); + const float topEm = getEmOffset(top.stretch - stretchOffsetY, stretchContentHeight, iconHeight, iconTop); const float topPx = getPxOffset(top.fixed - fixedOffsetY, fixedContentHeight, top.stretch, stretchHeight); - const float rightEm = getEmOffset( - right.stretch - stretchOffsetX, stretchContentWidth, iconWidth, shapedIcon.left()); + const float rightEm = getEmOffset(right.stretch - stretchOffsetX, stretchContentWidth, iconWidth, iconLeft); const float rightPx = getPxOffset(right.fixed - fixedOffsetX, fixedContentWidth, right.stretch, stretchWidth); - const float bottomEm = getEmOffset( - bottom.stretch - stretchOffsetY, stretchContentHeight, iconHeight, shapedIcon.top()); + const float bottomEm = getEmOffset(bottom.stretch - stretchOffsetY, stretchContentHeight, iconHeight, iconTop); const float bottomPx = getPxOffset( bottom.fixed - fixedOffsetY, fixedContentHeight, bottom.stretch, stretchHeight); diff --git a/src/mbgl/text/shaping.cpp b/src/mbgl/text/shaping.cpp index 991792fed79..1d6b30e80de 100644 --- a/src/mbgl/text/shaping.cpp +++ b/src/mbgl/text/shaping.cpp @@ -92,6 +92,45 @@ PositionedIcon PositionedIcon::shapeIcon(const ImagePosition& image, return PositionedIcon{image, top, bottom, left, right, collisionPadding}; } +PositionedIcon PositionedIcon::applyTextFit() const { + if (!_image.textFitWidth && !_image.textFitHeight) { + return *this; + } + const float width = _right - _left; + const float height = _bottom - _top; + + const float contentWidth = _image.content->right - _image.content->left; + const float contentHeight = _image.content->bottom - _image.content->top; + const float contentAspectRatio = contentWidth / contentHeight; + const auto textFitWidth = _image.textFitWidth.value(); + const auto textFitHeight = _image.textFitHeight.value(); + float newLeft = _left; + float newRight = _right; + float newTop = _top; + float newBottom = _bottom; + + if (textFitHeight == style::TextFit::proportional) { + if ((textFitWidth == style::TextFit::stretchOnly && (width / height) < contentAspectRatio) || + textFitWidth == style::TextFit::proportional) { + float newIconWidth = std::ceil(height * contentAspectRatio); + newLeft *= newIconWidth / width; + newRight = newLeft + newIconWidth; + } + } else if (textFitWidth == style::TextFit::proportional) { + if (textFitHeight == style::TextFit::stretchOnly && contentAspectRatio != 0 && + (width / height) > contentAspectRatio) { + float newIconHeight = std::ceil(width / contentAspectRatio); + newTop *= newIconHeight / height; + newBottom = newTop + newIconHeight; + } + } else { + // If neither textFitHeight nor textFitWidth are proportional then + // there is no effect since the content rectangle should be precisely + // matched to the content + } + return PositionedIcon{_image, newTop, newBottom, newLeft, newRight, _collisionPadding}; +} + void PositionedIcon::fitIconToText(const Shaping& shapedText, const style::IconTextFitType textFit, const std::array& padding, diff --git a/src/mbgl/text/shaping.hpp b/src/mbgl/text/shaping.hpp index 8be0322f516..eb9a95f22cb 100644 --- a/src/mbgl/text/shaping.hpp +++ b/src/mbgl/text/shaping.hpp @@ -73,6 +73,10 @@ class PositionedIcon { const std::array& iconOffset, float fontScale); + // Called after a PositionedIcon has already been run through fitIconToText, + // but needs further adjustment to apply textFitWidth and textFitHeight. + PositionedIcon applyTextFit() const; + const ImagePosition& image() const { return _image; } float top() const { return _top; } float bottom() const { return _bottom; } diff --git a/test/sprite/sprite_parser.test.cpp b/test/sprite/sprite_parser.test.cpp index 57c1026d4c9..17e291b55a7 100644 --- a/test/sprite/sprite_parser.test.cpp +++ b/test/sprite/sprite_parser.test.cpp @@ -589,3 +589,87 @@ TEST(Sprite, SpriteParsingNullRatio) { "sprite", })); } + +TEST(Sprite, SpriteParsingTextFit) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + + const auto images = parseSprite("default", image_1x, R"JSON({ + "image": { + "width": 16, + "height": 16, + "x": 0, + "y": 0, + "pixelRatio": 1, + "textFitWidth": "stretchOrShrink", + "textFitHeight": "proportional" + } + })JSON"); + EXPECT_EQ(1u, images.size()); + EXPECT_EQ("image", images[0]->id); + EXPECT_EQ(style::TextFit::stretchOrShrink, images[0]->textFitWidth); + EXPECT_EQ(style::TextFit::proportional, images[0]->textFitHeight); +} + +TEST(Sprite, SpriteParsingNullTextFit) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + + const auto images = parseSprite("default", image_1x, R"JSON({ + "image": { + "width": 16, + "height": 16, + "x": 0, + "y": 0, + "pixelRatio": 1, + "textFitHeight": "stretchOnly" + } + })JSON"); + EXPECT_EQ(1u, images.size()); + EXPECT_EQ("image", images[0]->id); + EXPECT_FALSE(images[0]->textFitWidth.has_value()); + EXPECT_EQ(style::TextFit::stretchOnly, images[0]->textFitHeight); +} + +TEST(Sprite, SpriteParsingInvalidTextFit) { + FixtureLog log; + + const auto image_1x = util::read_file("test/fixtures/annotations/emerald.png"); + + parseSprite("default", image_1x, R"JSON({ + "image": { + "width": 16, + "height": 16, + "x": 0, + "y": 0, + "pixelRatio": 1, + "textFitWidth": "invalid", + "textFitHeight": "stretchOnly" + } + })JSON"); + EXPECT_EQ(1u, + log.count({EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Invalid sprite image 'image': value of 'textFitWidth' " + "is an invalid value 'invalid'"})); + + parseSprite("default", image_1x, R"JSON({ + "image": { + "width": 16, + "height": 16, + "x": 0, + "y": 0, + "pixelRatio": 1, + "textFitHeight": 0 + } + })JSON"); + EXPECT_EQ(1u, + log.count({EventSeverity::Warning, + Event::Sprite, + int64_t(-1), + "Invalid sprite image 'image': value of 'textFitHeight' " + "must be a string"})); +} \ No newline at end of file diff --git a/test/text/shaping.test.cpp b/test/text/shaping.test.cpp index 0f6c5f6adcd..bd502611f69 100644 --- a/test/text/shaping.test.cpp +++ b/test/text/shaping.test.cpp @@ -103,3 +103,125 @@ TEST(Shaping, ZWSP) { ASSERT_EQ(shaping.writingMode, WritingModeType::Horizontal); } } + +void setupShapedText(Shaping& shapedText, float textSize) { + const auto glyph = PositionedGlyph(32, + 0.0f, + 0.0f, + false, + 0, + 1.0, + /*texRect*/ {}, + /*metrics*/ {}, + /*imageID*/ std::nullopt); + shapedText.right = textSize; + shapedText.bottom = textSize; + shapedText.positionedLines.emplace_back(); + shapedText.positionedLines.back().positionedGlyphs.emplace_back(glyph); +} + +void testApplyTextFit(const mapbox::Bin& rectangle, + const style::ImageContent& content, + const std::optional textFitWidth, + const std::optional textFitHeight, + const Shaping& shapedText, + float fontScale, + float expectedRight, + float expectedBottom) { + ImagePosition image = { + rectangle, + style::Image::Impl("test", + PremultipliedImage({static_cast(rectangle.w), static_cast(rectangle.h)}), + 1.0f, + false, + {}, + {}, + content, + textFitWidth, + textFitHeight)}; + auto shapedIcon = PositionedIcon::shapeIcon(image, {0, 0}, style::SymbolAnchorType::TopLeft); + shapedIcon.fitIconToText(shapedText, style::IconTextFitType::Both, {0, 0, 0, 0}, {0, 0}, fontScale); + const auto& icon = shapedIcon.applyTextFit(); + ASSERT_EQ(icon.top(), 0); + ASSERT_EQ(icon.left(), 0); + ASSERT_EQ(icon.right(), expectedRight); + ASSERT_EQ(icon.bottom(), expectedBottom); +} + +TEST(Shaping, applyTextFit) { + float textSize = 4; + float fontScale = 4; + float expectedImageSize = textSize * fontScale; + Shaping shapedText; + setupShapedText(shapedText, textSize); + + { + // applyTextFitHorizontal + // This set of tests against applyTextFit starts with a 100x20 image with a 5,5,95,15 content box + // that has been fitted to a 4*4 text with scale 4, resulting in a 16*16 image. + const auto horizontalRectangle = mapbox::Bin(-1, 100, 20, -1, -1, 0, 0); + const style::ImageContent horizontalContent = {5, 5, 95, 15}; + + { + // applyTextFit: not specified + // No change should happen + testApplyTextFit(horizontalRectangle, + horizontalContent, + std::nullopt, + std::nullopt, + shapedText, + fontScale, + expectedImageSize, + expectedImageSize); + } + + { + // applyTextFit: both stretchOrShrink + // No change should happen + testApplyTextFit(horizontalRectangle, + horizontalContent, + style::TextFit::stretchOrShrink, + style::TextFit::stretchOrShrink, + shapedText, + fontScale, + expectedImageSize, + expectedImageSize); + } + + { + // applyTextFit: stretchOnly, proportional + // Since textFitWidth is stretchOnly, it should be returned to + // the aspect ratio of the content rectangle (9:1) aspect ratio so 144x16. + testApplyTextFit(horizontalRectangle, + horizontalContent, + style::TextFit::stretchOnly, + style::TextFit::proportional, + shapedText, + fontScale, + expectedImageSize * 9, + expectedImageSize); + } + } + + { + // applyTextFitVertical + // This set of tests against applyTextFit starts with a 20x100 image with a 5,5,15,95 content box + // that has been fitted to a 4*4 text with scale 4, resulting in a 16*16 image. + const auto verticalRectangle = mapbox::Bin(-1, 20, 100, -1, -1, 0, 0); + const style::ImageContent verticalContent = {5, 5, 15, 95}; + + { + // applyTextFit: stretchOnly, proportional + // Since textFitWidth is stretchOnly, it should be returned to + // the aspect ratio of the content rectangle (9:1) aspect ratio so 144x16. + testApplyTextFit(verticalRectangle, + verticalContent, + style::TextFit::proportional, + style::TextFit::stretchOnly, + shapedText, + fontScale, + expectedImageSize, + expectedImageSize * 9); + } + } +}