How can I get the optical bounds of an NSAttributedString?










1















I need the optical bounds of an attributed string. I know I can call the .size() method and read its width but this obviously gives me typographic bounds with additional space to the right.



My strings would all be very short and consist only of 1-3 characters, so every string would contain exactly one glyphrun.



I found the function CTRunGetImageBounds, and after following the hints in the link from the comment I was able to extract the run and get the bounds, but obviously this does not give me the desired result.



The following swift 4 code works in an XCode9 Playground:



import Cocoa
import PlaygroundSupport

public func getGlyphWidth(glyph: CGGlyph, font: CTFont) -> CGFloat
var glyph = glyph
var bBox = CGRect()
CTFontGetBoundingRectsForGlyphs(font, .default, &glyph, &bBox, 1)
return bBox.width



class MyView: NSView

init(inFrame: CGRect)
super.init(frame: inFrame)


required init?(coder decoder: NSCoder)
fatalError("init(coder:) has not been implemented")


override func draw(_ rect: CGRect)

// setup context properties

let context: CGContext = NSGraphicsContext.current!.cgContext

context.setStrokeColor(CGColor.black)
context.setTextDrawingMode(.fill)


// prepare variables and constants

let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L"]
let font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
var glyphX: CGFloat = 10

// draw alphabet as single glyphs

for letter in alphabet
var glyph = CTFontGetGlyphWithName(font, letter as CFString)
var glyphPosition = CGPoint(x: glyphX, y: 80)
CTFontDrawGlyphs(font, &glyph, &glyphPosition, 1, context)
glyphX+=getGlyphWidth(glyph: glyph, font: font)


let textStringAttributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font : font,
]
glyphX = 10

// draw alphabet as attributed strings

for letter in alphabet

let textPosition = NSPoint(x: glyphX, y: 20)
let text = NSAttributedString(string: letter, attributes: textStringAttributes)
let line = CTLineCreateWithAttributedString(text)
let runs = CTLineGetGlyphRuns(line) as! [CTRun]

let width = (CTRunGetImageBounds(runs[0], nil, CFRange(location: 0,length: 0))).maxX
text.draw(at: textPosition)
glyphX += width




var frameRect = CGRect(x: 0, y: 0, width: 400, height: 150)

PlaygroundPage.current.liveView = MyView(inFrame: frameRect)


The code draws the single letters from A - L as single Glyphs in the upper row of the playground's live view. The horizontal position will be advanced after each letter by the letter's width which is retrieved via the getGlyphWidth function.



Then it uses the same letters to create attributed strings from it which will then be used to create first a CTLine, extract the (only) CTRun from it and finally measure its width. The result is seen in the second line in the live view.



The first line is the desired result: The width function returns exactly the width of every single letter, resulting in them touching each other.



I want the same result with the attributed string version, but here the ImageBounds seem to add an additional padding which I want to avoid.



How can I measure the exact width from the leftmost to the rightmost pixel of a given text?



And is there a less clumsy way to achieve this without having to cast four times (NSAtt.Str->CTLine->CTRun->CGRect->maxX) ?










share|improve this question
























  • Check this stackoverflow.com/a/33425181/771231

    – Desdenova
    Nov 13 '18 at 13:29















1















I need the optical bounds of an attributed string. I know I can call the .size() method and read its width but this obviously gives me typographic bounds with additional space to the right.



My strings would all be very short and consist only of 1-3 characters, so every string would contain exactly one glyphrun.



I found the function CTRunGetImageBounds, and after following the hints in the link from the comment I was able to extract the run and get the bounds, but obviously this does not give me the desired result.



The following swift 4 code works in an XCode9 Playground:



import Cocoa
import PlaygroundSupport

public func getGlyphWidth(glyph: CGGlyph, font: CTFont) -> CGFloat
var glyph = glyph
var bBox = CGRect()
CTFontGetBoundingRectsForGlyphs(font, .default, &glyph, &bBox, 1)
return bBox.width



class MyView: NSView

init(inFrame: CGRect)
super.init(frame: inFrame)


required init?(coder decoder: NSCoder)
fatalError("init(coder:) has not been implemented")


override func draw(_ rect: CGRect)

// setup context properties

let context: CGContext = NSGraphicsContext.current!.cgContext

context.setStrokeColor(CGColor.black)
context.setTextDrawingMode(.fill)


// prepare variables and constants

let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L"]
let font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
var glyphX: CGFloat = 10

// draw alphabet as single glyphs

for letter in alphabet
var glyph = CTFontGetGlyphWithName(font, letter as CFString)
var glyphPosition = CGPoint(x: glyphX, y: 80)
CTFontDrawGlyphs(font, &glyph, &glyphPosition, 1, context)
glyphX+=getGlyphWidth(glyph: glyph, font: font)


let textStringAttributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font : font,
]
glyphX = 10

// draw alphabet as attributed strings

for letter in alphabet

let textPosition = NSPoint(x: glyphX, y: 20)
let text = NSAttributedString(string: letter, attributes: textStringAttributes)
let line = CTLineCreateWithAttributedString(text)
let runs = CTLineGetGlyphRuns(line) as! [CTRun]

let width = (CTRunGetImageBounds(runs[0], nil, CFRange(location: 0,length: 0))).maxX
text.draw(at: textPosition)
glyphX += width




var frameRect = CGRect(x: 0, y: 0, width: 400, height: 150)

PlaygroundPage.current.liveView = MyView(inFrame: frameRect)


The code draws the single letters from A - L as single Glyphs in the upper row of the playground's live view. The horizontal position will be advanced after each letter by the letter's width which is retrieved via the getGlyphWidth function.



Then it uses the same letters to create attributed strings from it which will then be used to create first a CTLine, extract the (only) CTRun from it and finally measure its width. The result is seen in the second line in the live view.



The first line is the desired result: The width function returns exactly the width of every single letter, resulting in them touching each other.



I want the same result with the attributed string version, but here the ImageBounds seem to add an additional padding which I want to avoid.



How can I measure the exact width from the leftmost to the rightmost pixel of a given text?



And is there a less clumsy way to achieve this without having to cast four times (NSAtt.Str->CTLine->CTRun->CGRect->maxX) ?










share|improve this question
























  • Check this stackoverflow.com/a/33425181/771231

    – Desdenova
    Nov 13 '18 at 13:29













1












1








1








I need the optical bounds of an attributed string. I know I can call the .size() method and read its width but this obviously gives me typographic bounds with additional space to the right.



My strings would all be very short and consist only of 1-3 characters, so every string would contain exactly one glyphrun.



I found the function CTRunGetImageBounds, and after following the hints in the link from the comment I was able to extract the run and get the bounds, but obviously this does not give me the desired result.



The following swift 4 code works in an XCode9 Playground:



import Cocoa
import PlaygroundSupport

public func getGlyphWidth(glyph: CGGlyph, font: CTFont) -> CGFloat
var glyph = glyph
var bBox = CGRect()
CTFontGetBoundingRectsForGlyphs(font, .default, &glyph, &bBox, 1)
return bBox.width



class MyView: NSView

init(inFrame: CGRect)
super.init(frame: inFrame)


required init?(coder decoder: NSCoder)
fatalError("init(coder:) has not been implemented")


override func draw(_ rect: CGRect)

// setup context properties

let context: CGContext = NSGraphicsContext.current!.cgContext

context.setStrokeColor(CGColor.black)
context.setTextDrawingMode(.fill)


// prepare variables and constants

let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L"]
let font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
var glyphX: CGFloat = 10

// draw alphabet as single glyphs

for letter in alphabet
var glyph = CTFontGetGlyphWithName(font, letter as CFString)
var glyphPosition = CGPoint(x: glyphX, y: 80)
CTFontDrawGlyphs(font, &glyph, &glyphPosition, 1, context)
glyphX+=getGlyphWidth(glyph: glyph, font: font)


let textStringAttributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font : font,
]
glyphX = 10

// draw alphabet as attributed strings

for letter in alphabet

let textPosition = NSPoint(x: glyphX, y: 20)
let text = NSAttributedString(string: letter, attributes: textStringAttributes)
let line = CTLineCreateWithAttributedString(text)
let runs = CTLineGetGlyphRuns(line) as! [CTRun]

let width = (CTRunGetImageBounds(runs[0], nil, CFRange(location: 0,length: 0))).maxX
text.draw(at: textPosition)
glyphX += width




var frameRect = CGRect(x: 0, y: 0, width: 400, height: 150)

PlaygroundPage.current.liveView = MyView(inFrame: frameRect)


The code draws the single letters from A - L as single Glyphs in the upper row of the playground's live view. The horizontal position will be advanced after each letter by the letter's width which is retrieved via the getGlyphWidth function.



Then it uses the same letters to create attributed strings from it which will then be used to create first a CTLine, extract the (only) CTRun from it and finally measure its width. The result is seen in the second line in the live view.



The first line is the desired result: The width function returns exactly the width of every single letter, resulting in them touching each other.



I want the same result with the attributed string version, but here the ImageBounds seem to add an additional padding which I want to avoid.



How can I measure the exact width from the leftmost to the rightmost pixel of a given text?



And is there a less clumsy way to achieve this without having to cast four times (NSAtt.Str->CTLine->CTRun->CGRect->maxX) ?










share|improve this question
















I need the optical bounds of an attributed string. I know I can call the .size() method and read its width but this obviously gives me typographic bounds with additional space to the right.



My strings would all be very short and consist only of 1-3 characters, so every string would contain exactly one glyphrun.



I found the function CTRunGetImageBounds, and after following the hints in the link from the comment I was able to extract the run and get the bounds, but obviously this does not give me the desired result.



The following swift 4 code works in an XCode9 Playground:



import Cocoa
import PlaygroundSupport

public func getGlyphWidth(glyph: CGGlyph, font: CTFont) -> CGFloat
var glyph = glyph
var bBox = CGRect()
CTFontGetBoundingRectsForGlyphs(font, .default, &glyph, &bBox, 1)
return bBox.width



class MyView: NSView

init(inFrame: CGRect)
super.init(frame: inFrame)


required init?(coder decoder: NSCoder)
fatalError("init(coder:) has not been implemented")


override func draw(_ rect: CGRect)

// setup context properties

let context: CGContext = NSGraphicsContext.current!.cgContext

context.setStrokeColor(CGColor.black)
context.setTextDrawingMode(.fill)


// prepare variables and constants

let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L"]
let font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
var glyphX: CGFloat = 10

// draw alphabet as single glyphs

for letter in alphabet
var glyph = CTFontGetGlyphWithName(font, letter as CFString)
var glyphPosition = CGPoint(x: glyphX, y: 80)
CTFontDrawGlyphs(font, &glyph, &glyphPosition, 1, context)
glyphX+=getGlyphWidth(glyph: glyph, font: font)


let textStringAttributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font : font,
]
glyphX = 10

// draw alphabet as attributed strings

for letter in alphabet

let textPosition = NSPoint(x: glyphX, y: 20)
let text = NSAttributedString(string: letter, attributes: textStringAttributes)
let line = CTLineCreateWithAttributedString(text)
let runs = CTLineGetGlyphRuns(line) as! [CTRun]

let width = (CTRunGetImageBounds(runs[0], nil, CFRange(location: 0,length: 0))).maxX
text.draw(at: textPosition)
glyphX += width




var frameRect = CGRect(x: 0, y: 0, width: 400, height: 150)

PlaygroundPage.current.liveView = MyView(inFrame: frameRect)


The code draws the single letters from A - L as single Glyphs in the upper row of the playground's live view. The horizontal position will be advanced after each letter by the letter's width which is retrieved via the getGlyphWidth function.



Then it uses the same letters to create attributed strings from it which will then be used to create first a CTLine, extract the (only) CTRun from it and finally measure its width. The result is seen in the second line in the live view.



The first line is the desired result: The width function returns exactly the width of every single letter, resulting in them touching each other.



I want the same result with the attributed string version, but here the ImageBounds seem to add an additional padding which I want to avoid.



How can I measure the exact width from the leftmost to the rightmost pixel of a given text?



And is there a less clumsy way to achieve this without having to cast four times (NSAtt.Str->CTLine->CTRun->CGRect->maxX) ?







swift nsattributedstring bounds






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Nov 13 '18 at 19:42







MassMover

















asked Nov 13 '18 at 13:17









MassMoverMassMover

538




538












  • Check this stackoverflow.com/a/33425181/771231

    – Desdenova
    Nov 13 '18 at 13:29

















  • Check this stackoverflow.com/a/33425181/771231

    – Desdenova
    Nov 13 '18 at 13:29
















Check this stackoverflow.com/a/33425181/771231

– Desdenova
Nov 13 '18 at 13:29





Check this stackoverflow.com/a/33425181/771231

– Desdenova
Nov 13 '18 at 13:29












1 Answer
1






active

oldest

votes


















0














Ok, I found the answer myself:



  1. Using the .width parameter of the CTRunGetImageBounds instead of .maxX brings the right result


  2. The same function also does exist for the CTLine: CTLineGetImageBounds






share|improve this answer






















    Your Answer






    StackExchange.ifUsing("editor", function ()
    StackExchange.using("externalEditor", function ()
    StackExchange.using("snippets", function ()
    StackExchange.snippets.init();
    );
    );
    , "code-snippets");

    StackExchange.ready(function()
    var channelOptions =
    tags: "".split(" "),
    id: "1"
    ;
    initTagRenderer("".split(" "), "".split(" "), channelOptions);

    StackExchange.using("externalEditor", function()
    // Have to fire editor after snippets, if snippets enabled
    if (StackExchange.settings.snippets.snippetsEnabled)
    StackExchange.using("snippets", function()
    createEditor();
    );

    else
    createEditor();

    );

    function createEditor()
    StackExchange.prepareEditor(
    heartbeatType: 'answer',
    autoActivateHeartbeat: false,
    convertImagesToLinks: true,
    noModals: true,
    showLowRepImageUploadWarning: true,
    reputationToPostImages: 10,
    bindNavPrevention: true,
    postfix: "",
    imageUploader:
    brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
    contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
    allowUrls: true
    ,
    onDemand: true,
    discardSelector: ".discard-answer"
    ,immediatelyShowMarkdownHelp:true
    );



    );













    draft saved

    draft discarded


















    StackExchange.ready(
    function ()
    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53281882%2fhow-can-i-get-the-optical-bounds-of-an-nsattributedstring%23new-answer', 'question_page');

    );

    Post as a guest















    Required, but never shown

























    1 Answer
    1






    active

    oldest

    votes








    1 Answer
    1






    active

    oldest

    votes









    active

    oldest

    votes






    active

    oldest

    votes









    0














    Ok, I found the answer myself:



    1. Using the .width parameter of the CTRunGetImageBounds instead of .maxX brings the right result


    2. The same function also does exist for the CTLine: CTLineGetImageBounds






    share|improve this answer



























      0














      Ok, I found the answer myself:



      1. Using the .width parameter of the CTRunGetImageBounds instead of .maxX brings the right result


      2. The same function also does exist for the CTLine: CTLineGetImageBounds






      share|improve this answer

























        0












        0








        0







        Ok, I found the answer myself:



        1. Using the .width parameter of the CTRunGetImageBounds instead of .maxX brings the right result


        2. The same function also does exist for the CTLine: CTLineGetImageBounds






        share|improve this answer













        Ok, I found the answer myself:



        1. Using the .width parameter of the CTRunGetImageBounds instead of .maxX brings the right result


        2. The same function also does exist for the CTLine: CTLineGetImageBounds







        share|improve this answer












        share|improve this answer



        share|improve this answer










        answered Nov 13 '18 at 20:58









        MassMoverMassMover

        538




        538



























            draft saved

            draft discarded
















































            Thanks for contributing an answer to Stack Overflow!


            • Please be sure to answer the question. Provide details and share your research!

            But avoid


            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.

            To learn more, see our tips on writing great answers.




            draft saved


            draft discarded














            StackExchange.ready(
            function ()
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53281882%2fhow-can-i-get-the-optical-bounds-of-an-nsattributedstring%23new-answer', 'question_page');

            );

            Post as a guest















            Required, but never shown





















































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown

































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown







            Popular posts from this blog

            Top Tejano songwriter Luis Silva dead of heart attack at 64

            ReactJS Fetched API data displays live - need Data displayed static

            政党