Drawing text with non Latin alphabet

Hi,
Trying to do some typography work using Polish alphabet. All characters with diacritics, (like acute accent, overdot, stroke, etc) are missing from drawn text. I am using otf or ttf fonts that support all set of characters. Is this implemented in openrndr?
thanks

Hi and welcome sidec! Currently we support a pretty limited set of characters in OPENRNDR. I can add the missing entries for the Polish alphabet if desired. Our current approach for rendering text uses pre-rendered glyphs from a texture, which has pros but also cons (such as limited alphabest).

Would it be possible somehow to specify the character range by the user?

thanks @edwin! that would be very nice, if this is not too much work for you. does this go into https://github.com/openrndr/openrndr/blob/master/openrndr-gl3/src/main/kotlin/org/openrndr/internal/gl3/FontImageMapManagerGL3.kt right? I can create pr if you don’t mind.

Exactly! Would save me some time if you create a PR for it, which is much appreciated!

I think that is a good stop-gap solution until a better text renderer is implemented

sure, I will prepare small p with adding all diacritics (~8) to standard, as this is extension to Latin.

1 Like

@edwin would it be feasible for FontMapManager to display CJK characters (tens of thousands of different characters) if such an alphabet is provided, or for each font, specify a different alphabet?

I don’t think this won’t be feasible with current implementation, but I created some helper class that basically convert font glyph into OPENRNDR shape. I’m planning to wrap it into a small library, or think about how it might contribute to main codebase, but for now I can share kotlin file in this tread if you like

I can’t upload the file, but here the content. Should be self explanatory. I use it for poster type typography and it is not as performant as existing text implementation, but it handles any alphabet. I used it for Greek with no issues.

package font

import org.openrndr.math.Vector2
import org.openrndr.shape.Segment
import org.openrndr.shape.Shape
import org.openrndr.shape.ShapeContour
import org.slf4j.LoggerFactory
import java.awt.Font
import java.awt.font.FontRenderContext
import java.awt.font.TextLayout
import java.awt.geom.AffineTransform
import java.awt.geom.GeneralPath
import java.awt.geom.PathIterator
import java.io.File
import java.net.URL

typealias JShape = java.awt.Shape

object FontShape {
    val logger = LoggerFactory.getLogger(FontShape::class.java)

    fun String.getShape(fontName: String, from: Vector2, fontSize: Double = 128.0): Shape {
        val stream = try {
            fontName.asResource()?.openStream()
        } catch (e: NullPointerException) {
            File(fontName).inputStream()
        }
        val font = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(fontSize.toFloat())

        val context = FontRenderContext(null, true, true)
        val shape = GeneralPath()
        val layout = TextLayout(this, font, context)
        val transform = AffineTransform.getTranslateInstance(from.x, from.y)
        val outline = layout.getOutline(transform)
        shape.append(outline, true)
        return getPoints(shape)
    }

    private fun getPoints(shape: JShape): Shape {
        val iterator: PathIterator = shape.getPathIterator(null)
        val coordinates = DoubleArray(6)
        var x = 0.0
        var y = 0.0
        var cursor = Vector2(x, y)
        val contours = mutableListOf<ShapeContour>()
        val segments = mutableListOf<Segment>()

        while (!iterator.isDone) {

            var segment: Segment? = null
            when (iterator.currentSegment(coordinates)) {
                PathIterator.SEG_CLOSE -> {
                    logger.info("SEG_CLOSE")
                    contours.add(ShapeContour(segments.toList(), true))
                    segments.clear()
                }
                PathIterator.SEG_QUADTO -> {
                    logger.info("SEG_QUADTO")
                    val x1 = coordinates[0]
                    val y1 = coordinates[1]
                    val x2 = coordinates[2]
                    val y2 = coordinates[3]
                    x = x2
                    y = y2
                    segment = Segment(start = cursor, c0 = Vector2(x1, y1), end = Vector2(x2, y2))
                }
                PathIterator.SEG_CUBICTO -> {
                    logger.info("SEG_CUBICTO")
                    val x1 = coordinates[0]
                    val y1 = coordinates[1]
                    val x2 = coordinates[2]
                    val y2 = coordinates[3]
                    val x3 = coordinates[4]
                    val y3 = coordinates[5]
                    x = x3
                    y = y3
                    segment = Segment(start = cursor, c0 = Vector2(x1, y1), c1 = Vector2(x2, y2), end = Vector2(x3, y3))
                }
                PathIterator.SEG_LINETO -> {
                    logger.info("SEG_LINETO")
                    val x1 = coordinates[0]
                    val y1 = coordinates[1]
                    x = x1
                    y = y1
                    segment = Segment(start = cursor, end = Vector2(x1, y1))
                }
                PathIterator.SEG_MOVETO -> {
                    logger.info("SEG_MOVETO")
                    val x1 = coordinates[0]
                    val y1 = coordinates[1]
                    x = x1
                    y = y1
                }
            }
            cursor = Vector2(x, y)
            if (segment != null) {
                segments.add(segment)
            }
            iterator.next()
        }
        return Shape(contours)
    }
}

fun String.asResource(): URL? {
    return object {}.javaClass.classLoader.getResource(this)
}
2 Likes

Thank you @sidec. This is so helpful! I’m able to display Chinese using your code with no effort.

1 Like

For me the code never completes. But I also figured that
Font(“Dialog”, Font.PLAIN, 14)
will never return anything. Is there some configuration, I am not aware of?

Any call to any awt related classes will freeze the application.

Like GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts or Canvas() etc…

@andremichelle what java/kotlin versions, OS?
Also, how you used the snippet? something like
drawer.shape("some text".getShape("Dialog", Vector(width/4.0,height/2.0)))?

Yes, I am using it like that. But the application freezes whenever anything tries to access the java.awt package. There is no other code in the program

macOS Catalina 10.15.5

java version "1.8.0_172" Java(TM) SE Runtime Environment (build 1.8.0_172-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.172-b11, mixed mode)

kotlinVersion = "1.3.72"

Can you try enabling AWT’s headless mode?

This can be done by adding the following before using any AWT code
System.setProperty("java.awt.headless", "true")

Update: seems solved with the above System.setProperty("java.awt.headless", "true") - as another awt call (getting the font list) was working earlier in the code, I thought this would not be my problem … but I guess that call doesn’t require a UI yet and therefore works, while the latter requires it and then fails… Thanks!

Hi - I was trying this today - I got the part to get a shape working when using something like
val font = Font("Arial", Font.PLAIN, 128)
inside the String.getShape() call, but then later when I try to draw that eg with drawer.contours(shape.contours) I get an error
could not compile vertex shader (Exception)
Any idea?

Another question: could it be that in the CLOSE section something is missing like a

if ((anchor - cursor).length > 0.001) {
    segments.add(Segment(cursor, anchor))
}

? Without that, if I draw an “I” with fill, it looks like:

image

so there seems to be Segment missing.
With that addition (having set the anchor in the SEG_MOVETO) it looks correct like

image

@axel Did you manage to figure out the segments issue? Do you have some code you can share as an example?

The code with the mentioned change (the addition to the CLOSE part) is working for me currently, but then, I did not yet try the latest OpenRNDR version. My question was meant to ask whether I should expect the code as given by @sidec originally to work, or whether my observation is a required fix.

1 Like