Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CTFontDrawGlyphs draws at incorrect position

EDITED: I replaced my original code stub with a self-contained version

I am using Quartz2D to draw musical symbols to a CGContext. I am doing this in a Playground using Swift 4.

Musical Notes are composed of a single CGGlyph for the notehead and a line primitive for the stem. The glyph is taken from the music font "bravura". The x-position of the notehead glyph is exactly positioned at its left edge. The stem must be aligned perfectly to this origin, therefore its x-position must be the notehead's x-position + the stem's thickness / 2.

I have a function that takes a CGFloat as the x-position and draws the glyph and the stem into the CGContext. This works perfectly fine when the value for x is a rounded value like 30.0. However, when the value is a fractional number like 30.9, the stem gets misaligned:

picture of 1 good and 1 bad note

It is obvious that the line is drawing correctly but the glyph is ignoring the fractional part of the CGFloat. When I raise the x-value further to 31.0 both parts are in sync again.

Here is the simplified Code, which will work in your XCode9 Playground:

import PlaygroundSupport
import Cocoa

let fontSize = CGFloat(64)

// uncomment the next paragraph if you have added the "Bravura" Font to the ressources folder
/*
let fontURL = Bundle.main.url(forResource: "Bravura", withExtension: "otf")
CTFontManagerRegisterFontsForURL(fontURL! as CFURL, CTFontManagerScope.process, nil)
var font = CTFontCreateWithName("Bravura" as CFString, fontSize, nil)
*/

// comment out the next line if you use Bravura
var font = CTFontCreateWithName("Helvetica" as CFString, fontSize, nil)

func drawAtXPosition(_ posX: CGFloat, inContext context: CGContext) {
    // draw font glyph (notehead)
    var glyph = CTFontGetGlyphWithName(font, "uniE0A3" as CFString)
    var notePosition = CGPoint(x: posX, y: 100)
    CTFontDrawGlyphs(font, &glyph, &notePosition, 1, context)

    // draw line (note stem)
    let stemThickness = CGFloat(fontSize/4*0.12)
    let x: CGFloat = posX + stemThickness / 2
    let y: CGFloat = 100 - (0.168 * fontSize / 4)
    let stemBegin = CGPoint(x:x, y:y)
    let stemEnd = CGPoint(x:x, y: y - (3.5 * fontSize / 4))

    context.setLineWidth(stemThickness)
    context.strokeLineSegments(between: [stemBegin, stemEnd])
}

class MyView: NSView {
    override func draw(_ rect: CGRect) {
        let context = NSGraphicsContext.current!.cgContext
        context.setStrokeColor(CGColor.black)
        context.setFillColor(CGColor.black)

        drawAtXPosition(100, inContext: context)    // this draws as expected

        drawAtXPosition(200.99, inContext: context) // here the glyph is drawn at x=200, while the line is correctly drawn at 200.99
    }
}

var myView = MyView(frame: CGRect(x: 0, y: 0, width: 500, height: 200))
let myViewController = NSViewController()
myViewController.view = myView
PlaygroundPage.current.liveView = myViewController

You can download the music font "Bravura" from...

https://www.smufl.org/fonts/

... but I rewrote the code so it uses Helvetica per default, which also works for demonstration.

As a workaround in my original code I have implemented the .rounded() method of Foundation before passing the x-values to the drawing function. This works, but I'm afraid that at small font sizes (the whole code is written so that everything scales around the font size) the inaccuracy will be noticable.

I came across the methods setShouldSubpixelPositionFonts and setAllowsFontSubpixelPositioning of CGContext, but setting them to true does not do any change.

So, is there a way to draw the font objects just as precisely as the primitives (there must be one)? Or is my concern about rounding the CGFloats to Ints unjustified?

like image 768
MassMover Avatar asked Oct 29 '22 22:10

MassMover


1 Answers

After endless digging I found the solution:

It is not sufficient to set

context.setShouldSubpixelPositionFonts(true)

You also have to set

context.setShouldSubpixelQuantizeFonts(false)

as the latter seems to override the former.

like image 82
MassMover Avatar answered Nov 15 '22 07:11

MassMover