samizdatco / skia-canvas

A GPU-accelerated 2D graphics environment for Node.js
MIT License
1.71k stars 67 forks source link

Font Measurement is inaccurate #4

Open octref opened 4 years ago

octref commented 4 years ago

Hello! I'm trying to use skia-canvas to do font measurement. I'd like to replace puppeteer with it in my SVG renderer for shiki:

Running with node:

const { Canvas } = require('skia-canvas')

const canvas = new Canvas(600, 600)
const ctx = canvas.getContext('2d')

ctx.font = `16px 'Courier'`
console.log(ctx.measureText('M'))

What I get:

TextMetrics {
  width: 9.600000381469727,
  actualBoundingBoxLeft: 0,
  actualBoundingBoxRight: 9.600000381469727,
  actualBoundingBoxAscent: 12.0625,
  actualBoundingBoxDescent: 3.9375,
  fontBoundingBoxAscent: 14.784375190734863,
  fontBoundingBoxDescent: 4.415625095367432,
  emHeightAscent: 14.784375190734863,
  emHeightDescent: 4.415625095367432,
  hangingBaseline: 11.4765625,
  alphabeticBaseline: 0,
  ideographicBaseline: -4.415625095367432,
  lines: [
    {
      x: 0,
      y: -12.0625,
      width: 9.600000381469727,
      height: 16,
      baseline: 0,
      startIndex: 0,
      endIndex: 0
    }
  ]
}

Running this script in Chrome:

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

ctx.font = `16px 'Courier'`
console.log(ctx.measureText('M'))

What I get:

TextMetrics {width: 9.6015625, actualBoundingBoxLeft: -0.09375, actualBoundingBoxRight: 9.5078125, actualBoundingBoxAscent: 9.265625, actualBoundingBoxDescent: 0}
actualBoundingBoxAscent: 9.265625
actualBoundingBoxDescent: 0
actualBoundingBoxLeft: -0.09375
actualBoundingBoxRight: 9.5078125
width: 9.6015625
__proto__: TextMetrics

I'm wondering why would skia-canvas generate a different actualBoundingBoxDescent than Chrome? M should have 0 descent as far as I can tell.

samizdatco commented 3 years ago

Unfortunately there's basically zero agreement between different browsers on how the same glyphs should be measured. For instance, Safari's output to your sample script is:

actualBoundingBoxAscent: 10
actualBoundingBoxDescent: 1
actualBoundingBoxLeft: 0
actualBoundingBoxRight: 9.6015625
alphabeticBaseline: -0
emHeightAscent: 14
emHeightDescent: 4
fontBoundingBoxAscent: 14
fontBoundingBoxDescent: 4
hangingBaseline: 14
ideographicBaseline: -4
width: 9.6015625

Which is not to say that ‘correct’ and ‘incorrect’ have no meaning; just that it's kind of the Wild West out there.

Here's the how the metrics are currently calculated. If anyone has a better handle on how to make some of these judgment calls in terms of accuracy, conformance to the CSS spec, etc. I'd love to look over a pull request...

As with all questions involving font metrics, the answer is surely somewhere in here: https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align

octref commented 3 years ago

@samizdatco I'm getting this in Firefox:

actualBoundingBoxAscent: 9.265625
actualBoundingBoxDescent: 0
actualBoundingBoxLeft: 0.90625
actualBoundingBoxRight: 10.5078125
width: 9.600000381469727

Unfortuantely I'm not familiar with font measurement so I can't help much there.

PikaDude commented 2 years ago

+1 to this issue, the metrics are incorrect and trying to calculate an accurate height for text appears to be impossible using skia-canvas. Are there any possible workarounds?

Skida12138 commented 2 years ago

@PikaDude Perhaps you can use RenderingContext2D.outlineText('Your text here').bounds.top, it is more accurate and work for me.

Skida12138 commented 2 years ago

@samizdatco Do you think use skia_safe::font::Font::get_bounds to calculate actualBoundingBoxAscent would be a better idea? If so I would try spare some time work for this.

Skida12138 commented 2 years ago

@samizdatco Do you think use skia_safe::font::Font::get_bounds to calculate actualBoundingBoxAscent would be a better idea? If so I would try spare some time work for this.

That's what actually chromium did.