mapnik / mapnik

Mapnik is an open source toolkit for developing mapping applications

Home Page:http://mapnik.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TextSymbolizer not rendering lower parts of lowercase letters near canvas boundary

ShapovalovKL opened this issue · comments

Hi, we noticed that some labels (TextSymbolizer) on our map missing lower parts of lowercase letters near metatile boundary.

There seems to be a bug somewhere in text_layout::bounds_ computation:

  1. characters glyph_info are loaded from font with ymin and ymax values
  2. text_line::height for first line (max_char_height()) computed as maximum glyph_info::height() = ymax() - ymin()
    https://github.com/mapnik/mapnik/blob/master/include/mapnik/text/harfbuzz_shaper.hpp#L385
    https://github.com/mapnik/mapnik/blob/master/include/mapnik/text/harfbuzz_shaper.hpp#L397
    https://github.com/mapnik/mapnik/blob/master/src/text/text_line.cpp#L90
  3. text_layout::height_ computed as sum of text_line::height contained in a layout
    https://github.com/mapnik/mapnik/blob/master/src/text/text_layout.cpp#L438
  4. text_layout::layout() computed text_layout::bounds_ as rectangle with height equal to text_layout::height_
    https://github.com/mapnik/mapnik/blob/master/src/text/text_layout.cpp#L214
  5. placement_finder::find_point_placement does not render labels with text_layout::bounds() not intersecting map canvas
    https://github.com/mapnik/mapnik/blob/master/src/text/placement_finder.cpp#L244

This algorithm produces incorrect bounds when characters with different ymax and ymin present in a single line.
Incorrect layout.bounds() can cause placement_finder skip painting part of TextSymbolizer if it is placed on the edge of canvas, as if it is complytly outside canvas (or in map buffer)
This could also cause incorrect collision detection, since it uses the same bounds.

For example: for some font and font-size characters X (uppercase) and y (lowercase) could have:
X - ymax=11,ymin=0, height=11
y - ymax=7, ymin=-3,height=10
Then for simple string 'Xy' text_layout::height_ will be 11, with will produce incorrect layout.bounds() with height=11 instead of 14

Here is small program to reproduce this bug.
mapnik_text_tile_edge_bug.zip
It renders single label 'XyXyXyXy' at (0,0) to two adjacent images (top.png and bottom.png).
If images border is on equator (offset=0) string is displayed correctly (part of label is in top.png and another part in bottom.png)
If images border is moved to south (offset=-2.2) lower part of the string not rendered in bottom.png

top.png and bottom.png with offset=0:
top_0
bottom_0

top.png and bottom.png with offset=-2.2:
top_22
bottom_22

#include <mapnik/map.hpp>
#include <mapnik/load_map.hpp>
#include <mapnik/agg_renderer.hpp>
#include <mapnik/image_util.hpp>
#include <mapnik/datasource_cache.hpp>
#include <mapnik/font_engine_freetype.hpp>

int main()
{
    try
    {
        mapnik::datasource_cache::instance().register_datasources("./mapnik/input/");
        mapnik::freetype_engine::register_fonts("./mapnik/fonts",true);
        mapnik::Map map(256,256);
        mapnik::load_map(map,"./data/mapnik.xml",true);
        map.set_buffer_size(256);
        double offset=-2.2;
        {
            map.zoom_to_box(mapnik::box2d<double>(-50,0+offset,50,100+offset));
            mapnik::image_rgba8 im(map.width(),map.height());
            mapnik::agg_renderer<mapnik::image_rgba8> ren(map,im);
            ren.apply();
            mapnik::save_to_file(im,"./data/top.png");
        }
        {
            map.zoom_to_box(mapnik::box2d<double>(-50,-100+offset,50,0+offset));
            mapnik::image_rgba8 im(map.width(),map.height());
            mapnik::agg_renderer<mapnik::image_rgba8> ren(map,im);
            ren.apply();
            mapnik::save_to_file(im,"./data/bottom.png");
        }
    } catch ( const std::exception & ex )
    {
        std::cout<<ex.what();
        return -1;
    }
    return 0;
}

Tested on mapnik-v3.0.22

@ShapovalovKL thanks for highlighting! It does look like a bug in text bounds calculations in v3.0.x. Have you tried running the same test with master?

I just ran the same test with the latest version from master (1ba1278) and it gave exactly the same results as with 3.0.22.

I just ran the same test with the latest version from master (1ba1278) and it gave exactly the same results as with 3.0.22.

@ShapovalovKL ok, thanks!

You can also find this bug on osm if you follow tile boundaries
https://www.openstreetmap.org/#map=16/51.5081/-0.1279
Capture

@ShapovalovKL - So it looks like we should be keeping track of both ymin and ymax in order to calculate correct max_glyph_height. 👍 for the comprehensive use case demonstrating this issue!

Related issues

@ShapovalovKL - The bounds are not including descender (part of glyph below baseline). It was left like that for some reason and I don't remember details now. I've tried fixing max_char_height_ calculation to track both ymin and ymax - it kind of works for your case e.g bottom parts are rendered as expected but it breaks text positioning in many other places.. Adding adjustment to the vertical displacement is not helping or at least I didn't find a way to do it correctly, yet.
I'll need to dig deeper, If you have some suggestions, please, let me know.

FYI - adding <DebugSymbilizer/> is a useful for seeing actual bounding boxes per glyph.

From FreeType2 docs:

image

@ShapovalovKL https://github.com/mapnik/mapnik/tree/label-bbox - my attempt so far to fix label bounding box calculation. This is WIP as lots of visual tests are failing

top
bottom

Same issue as #4233?