Getting font metrics through JavaScript for Automation or AppleScript

To automate choice of font-size (to exactly fill a box, for example) string length will suffice for monotyped fonts, but for most fonts we need font-specific metrics for particular strings (an i is always narrower than an M)

In an Execute JavaScript for Automation action, we can write:

    // helvetica12Width :: String -> Num
    function helvetica12Width(str) {
        return $.NSAttributedString.alloc.init.initWithString(
                str
            )
            .size.width;
    }

to get metrics for a particular string in the default Helvetica 12. What I haven’t yet managed to do is to pass in attributes for other fonts and font sizes, and get corresponding metrics for those.

Has anyone discovered an idiom/syntax that works here from JXA or AppleScript ?

https://developer.apple.com/reference/foundation/nsattributedstring?language=objc

Update: This is the kind of thing I have experimented with – clearly off the mark though, as variation in the font size/name values doesn’t affect the return value:

(() => {
    'use strict';

    ObjC.import('AppKit');

    return $.NSAttributedString.alloc.init.initWithStringAttributes(
            "Substantiation", {
                'NSFontAttributeName': $.NSFont.fontWithNameSize('Helvetica', 24)
            }
        )
        .size.width
})();
1 Like

Ah … this seems to to do it:

(function () {
    'use strict';

    ObjC.import('AppKit');

    // show :: a -> String
    function show(x) {
        return JSON.stringify(x, null, 2);
    }

    // stringSizeInFontAtPointSize :: String -> String -> Num
    //                                  -> {width:Num, height:Num}
    function stringSizeInFontAtPointSize(str, fontName, points) {
        return $.NSAttributedString.alloc.init.initWithStringAttributes(
            str, $({
                'NSFont': $.NSFont.fontWithNameSize(fontName, points)
            })
        )
        .size;
    }

    // TEST -------------------------------------------------------------------
    return show([
        stringSizeInFontAtPointSize("hello World", "Geneva", 32),
        stringSizeInFontAtPointSize("hello World", "Geneva", 64),
        stringSizeInFontAtPointSize("hello World", "Helvetica", 64),
    ]);
})();
[
  {
    "width": 171.015625,
    "height": 40
  },
  {
    "width": 342.03125,
    "height": 80
  },
  {
    "width": 319,
    "height": 78
  }
]
1 Like

And, as a footnote, for iterative box-fitting of an unwrapped line of text:

(ES6 version, Sierra only)

(() => {
    'use strict';

    ObjC.import('AppKit');

    // until :: (a -> Bool) -> (a -> a) -> a -> a
    const until = (p, f, x) => {
        let v = x;
        while (!p(v)) v = f(v);
        return v;
    };

    // pointSizeToFitSingleLineInBox :: Num -> Num -> String ->
    //                                  String -> Num -> Num -> Num
    function pointSizeToFitLineInBox(boxWidth, boxHeight, fontName, strText,
        minPoints, maxPoints) {
        const
            nMin = minPoints || 5,
            nMax = maxPoints || 100,
            mas = $.NSMutableAttributedString.alloc.init
            .initWithStringAttributes(
                strText, $({
                    'NSFont': $.NSFont.fontWithNameSize(fontName, nMin)
                })
            ),
            floor = Math.floor,
            dctRange = {
                location: 0,
                length: strText.length
            };
        let dctSize = mas.size;
        return until(
                x => x.highest - x.lowest <= 1,
                x => {
                    const
                        blnOver = (dctSize.width > boxWidth ||
                            dctSize.height > boxHeight),
                        upper = blnOver ? x.pointSize : x.highest,
                        lower = blnOver ? x.lowest : x.pointSize,
                        pSize = lower + floor((upper - lower) / 2);

                    // ITERATIVE MUTATION     ---------------------------------
                    mas.setAttributesRange($({
                        'NSFont': $.NSFont.fontWithNameSize(fontName, pSize)
                    }), dctRange);
                    dctSize = mas.size;
                    // --------------------------------------------------------

                    return {
                        highest: upper,
                        lowest: lower,
                        pointSize: pSize
                    };
                }, {
                    highest: nMax,
                    lowest: nMin,
                    pointSize: nMin
                }
            )
            .pointSize;
    }

    // Point size to fit Helvetica 'Hello World !' in  500pts * 50pts space
    return pointSizeToFitLineInBox(500, 50, "Helvetica", "Hello World !");

    // -> 41
})();
1 Like