locationtech / locationtech/jts

Unexpected output from FontGlyphReader for truetype fonts

Open
#1,075 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
2.2k
Forks
475
Avg merge
14d 10h
Merged PRs (30d)
1

Description

This test illustrates the issue:


import static org.junit.jupiter.api.Assertions.assertEquals;

import java.awt.Font;
import java.awt.FontFormatException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.locationtech.jts.awt.FontGlyphReader;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;

/**
 * Illustrating unexpected output from {@link FontGlyphReader}
 */
@SuppressWarnings("static-method")
class FontGlyphReaderTest {

	private static final GeometryFactory GF = new GeometryFactory();
	private static final Font SERIF = Font.decode( FontGlyphReader.FONT_SERIF );
	private static final Font KRYPTON;
	static {
		try( InputStream is = new URI(
				"https://github.com/githubnext/monaspace/raw/main/fonts/otf/MonaspaceKrypton-Regular.otf" )
						.toURL().openStream() ) {
			KRYPTON = Font.createFont( Font.TRUETYPE_FONT, is );
		}
		catch( FontFormatException | IOException | URISyntaxException e ) {
			throw new IllegalStateException( e );
		}
	}

	/**
	 * For simple characters (completely connected, no holes), both fonts produce
	 * {@link Polygon} geometry with no interior rings, as we'd expect.
	 */
	@Test
	void simple() {
		assertEquals( "Polygon with 0 interior rings",
				details( FontGlyphReader.read( "z", SERIF, GF ) ) );
		assertEquals( "Polygon with 0 interior rings",
				details( FontGlyphReader.read( "z", KRYPTON, GF ) ) );
	}

	/**
	 * For characters with two disconnected elements the built-in font produces the
	 * multipolygon you'd expect, while the truetype font produces a single polygon
	 * with a hole
	 */
	@Test
	void disconnected() {
		assertEquals( "MultiPolygon with 2 constituents",
				details( FontGlyphReader.read( "=", SERIF, GF ) ) );
		assertEquals( "Polygon with 1 interior rings",
				details( FontGlyphReader.read( "=", KRYPTON, GF ) ) );
	}

	/**
	 * For characters with holes the truetype font now swings the <i>other</i> way
	 */
	@Test
	void holes() {
		assertEquals( "Polygon with 1 interior rings",
				details( FontGlyphReader.read( "o", SERIF, GF ) ) );
		assertEquals( "MultiPolygon with 2 constituents",
				details( FontGlyphReader.read( "o", KRYPTON, GF ) ) );
	}

	private static String details( Geometry g ) {
		StringBuilder sb = new StringBuilder();
		sb.append( g.getClass().getSimpleName() );
		if( g instanceof Polygon p ) {
			sb.append( " with " ).append( p.getNumInteriorRing() ).append( " interior rings" );
		}
		else if( g instanceof MultiPolygon mp ) {
			sb.append( " with " ).append( mp.getNumGeometries() ).append( " constituents" );
		}
		return sb.toString();
	}

}

I suspect that the winding order of the font is messing things up - drawing the geometries reveals that the vertices are in the opposite order in the truetype font when compared against the standard font.
For this font the holes are at least specified in the opposite winding order to the shell, but this SA question suggests that there is no standard for the winding order of the vertices in a font, or even any assurance that the polygon shell is specified before the holes.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with FontGlyphReader and reproduce the supplied FontGlyphReaderTest cases using the standard and Monaspace Krypton fonts. Inspect how glyph contours and winding order are converted into Geometry, then verify that disconnected components and holes retain the expected Polygon or MultiPolygon structure. Done means the three assertions pass for both fonts.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
computer-graphics
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.