Function returning (approx) true height for build in number fonts

As covered in many discussions, the functions dc.getFontHeight() and dc.getTextDimensions() do not return the actual height of the build in Garmin fonts.

See eg. https://forums.garmin.com/developer/connect-iq/f/discussion/223609/values-returned-by-dc-gettextdimensions-text-font-do-not-fit-the-text

This has annoyed me, since I’d like to be able to use biggest number font possible for a given screen area in my data fields. 

So I tried to see if I could find some “systematic” way of approximating the actual height for number fonts, knowing that Garmin reuse the same type of number fonts across multiple watches. What I learned is that there is indeed a “system” across most watches. That is:

- For watches where dc.getFontDescent() return 0, height is equal to dc.getFontAscent()

- Otherwise, height is approximate equal to a scalar multiplied by dc.getFontDescent()+dc.getFontAscent() – where the scalar depends on type of number font

I then furthermore leverage that the dot '.' and percentage '%' characters have a height/width ratio that varies depending on type of number font. And use this to select an appropriate scalar. This approach results in an accuracy of +/- 1 pixel.

I thought others might find this code useful, hence this post to share.

Limitations: Works for build in number fonts only (no custom, not text fonts). And designed and verified for newer SDK 3.0+ watches only. Not Edge, nor outdoor/handhelds devices. And I have not bothered with descentg1 and instinct watches. See code for the exact devices verified. And, obviously, there are no guarantees this code will work for future new watches.

For the few watches this does not work for (Venu series, Vivoactive 2 series and Forerunner 55), code has been added to manage these special cases. 

Height definition: Height I have defined as how much of the screen is drawn upon from the baseline to the top of text. See the green rectangle below (Vivoactive 3). Note some number fonts though go “a bit” below the baseline for some digits. For those I have defined the height to exclude the “similar” extra bit at the top (the two to the right). Due to this, and due to the +/-1 pixel accuracy, make sure to have extra space above and below numbers which is always a good idea for readability. 

This code is used to print the above example:

    H = getApproxNumberFontHeightInPixels(dc, NumberFont);
    dc.drawRectangle(x, dc.getHeight()/2 – H + 1, dc.getTextDimensions("8", NumberFont)[0], H);
    dc.drawText(x, dc.getHeight()/2 - dc.getFontAscent(NumberFont), NumberFont, ".", Graphics.TEXT_JUSTIFY_LEFT);

Input much appreciated! And, whether there is a desire for the code to be maintained as new watches are launched.

The code:

function getApproxNumberFontHeightInPixels(dc, NumberFont) {

  // Returns the appoximate height for a number font with an accuracy of +/- 1 pixel
  // NumberFont must be either 5, 6, 7 or 8 (between Graphics.FONT_NUMBER_MILD and Graphics.FONT_NUMBER_THAI_HOT)
  // Works for all SDK 3.0+ Garmin watches, except for: descentg1, instinct2 & instinct2s, instinctcrossover
  // Does not work for Edge, nor outdoor/handhelds devices. All devices supported are listed in the comments below

  var Height; var Ascent; var Descent; var DotRatio; var PercRatio;

  Ascent = dc.getFontAscent(NumberFont);
  Descent = dc.getFontDescent(NumberFont);
  DotRatio = dc.getTextDimensions(".", Graphics.FONT_NUMBER_THAI_HOT);
  DotRatio = DotRatio[1].toFloat() / DotRatio[0]; // Dot '.' character height/width ratio
  PercRatio = dc.getTextDimensions("%", Graphics.FONT_NUMBER_THAI_HOT);
  PercRatio = PercRatio[1].toFloat() / PercRatio[0]; // Percentage '%' character height/width ratio

  if (Descent==0) {
    // For fonts with no Descent, then Height=dc.getFontAscent(NumberFont);
    // Applies for: fr935, fr645, fr645m, fenix5, fenix5plus, fenix5splus, fenix5xplus, fenix5x, fenixchronos,
    // d2charlie, d2delta, d2deltapx, d2deltas, descentmk1
    Height = Ascent;
  } else {
    if (DotRatio < 5.0) {
      // For fonts with Dot ratio below 5.0
      // Applies (with PercRatio>1.7) for: fr245, fr245m, fr255, fr255m, fr255s, fr255sm, fr745, fr945, fr945lte, fr955, approachs62
      // vivoactive4, legacyherofirstavenger, legacysagadarthvader, vivoactive4s, legacyherocaptainmarvel, legacysagarey
      // Applies (with PercRatio<1.7) for: fr265, fr265s, fr965
      Height = (Ascent + Descent) * (NumberFont<=6 ? 0.600 : (PercRatio>1.7 ? 0.550 : 0.595));
    } else {
      // For fonts with Dot ratio above 5.0
      // Applies for: fenix7, fenix7s, fenix7x, epix2, d2mach1, marq2, marq2aviator,
      // fenix6, fenix6s, fenix6pro, fenix6spro, fenix6xpro, descentmk2, descentmk2s, enduro,
      // marqadventurer, marqathlete, marqaviator, marqcaptain, marqcommander, marqdriver, marqexpedition, marqgolfer,
      // venu2, venu2plus, d2airx10, venu2s, venusq, venusqm, venusq2, venusq2m
      Height = (Ascent + Descent) * 0.50;
    }

    // Special case handling:
    var i = NumberFont-5;
    var partNumber = System.getDeviceSettings().partNumber;
    if (partNumber.equals("006-B2700-00") or partNumber.equals("006-B2976-00") or partNumber.equals("006-B3446-00") or // vivoactive3
        partNumber.equals("006-B2988-00") or partNumber.equals("006-B3163-00") or partNumber.equals("006-B3066-00") or // vivoactive3m, vivoactive3mlte
        partNumber.equals("006-B3473-00") or partNumber.equals("006-B3477-00")) { // vivoactive3d
      Height = [21, 25, 40, 49][i];
    }

    if (partNumber.equals("006-B3226-00") or partNumber.equals("006-B3389-00") or // venu
        partNumber.equals("006-B3740-00") or partNumber.equals("006-B3737-00") or partNumber.equals("006-B2187-00")) { // venud, d2air
      Height = [43, 51, 78, 92][i];
    }

    if (partNumber.equals("006-B3869-00") or partNumber.equals("006-B4033-00")) { // fr55
      Height = [20, 43, 49, 49][i];
    }
  }

  return Math.round(Height).toNumber();

}

 

 

  • EDIT:

    I update the original post and code to support the new type of font used in new forerunners AMOLED devices: fr965, fr265, fr265s. It required an extra metric, the '%' char height/width ratio.

  • Thank you for sharing this.  I am struggling with this same issue right now.  I came up with a more primitive attempt for the text fonts, and have been considering just doing a "brute force" rendering of all the text fonts on each device, counting the pixels manually, and creating a table.

    For the Graphics.FONT_LARGE fonts, multiplying dc.getFontHeight() by 0.75 is pretty close.  (Close enough for my datafield, anyway.)

    For Graphics.FONT_TINY / FONT_XTINY, multiplying by 0.75 and then subtracting 1 is pretty close.

    Aligning different-sized fonts vertically is still a problem, though, and has required a lot of trial-and-error.

    It's too bad that Dc or BufferedBitmap don't have methods to get the value of a specific pixel.  If they did, it would be easy to automate rendering every font, then iterate through the pixels to determine the top/bottom spacing and the actual height of the rendered text.

  • Thanks for the feedback - and I agree, methods for getting the value of a specific pixel would really help here!