铬画布 2d 上下文测量文本给我奇怪的结果

Chrome canvas 2d context measureText giving me weird results

本文关键字:结果 文本 2d 上下文 测量      更新时间:2023-09-26

这是我问题的精简版本

let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.font = '11pt Calibri'
ctx.fillStyle = '#000000'
let temp = ctx.font
console.log(ctx.font)
console.log(ctx.measureText('M').width)
ctx.font = 'bold ' + ctx.font
console.log(ctx.font)
console.log(ctx.measureText('M').width)
ctx.font = 'italic ' + ctx.font
console.log(ctx.font)
console.log(ctx.measureText('M').width)
ctx.font = temp
console.log(ctx.font)
console.log(ctx.measureText('M').width)

在 chrome 中运行此代码会产生不正确的数字,至少在最后是这样。首先,我将字体设置为"11pt Calibri",但由于某种原因,chrome立即将其更改为"15px Calibri",因此它产生的文本略大于正确值。我读到画布以 96dpi 运行,所以正确的 px 应该是 14.6。

之后,我正在测量文本 M 的宽度,对我来说为 12.53401184,这个数字很重要。

之后,我修改了字体以添加粗体和斜体,然后将其回滚到原来的字体。现在当我测量它时,它给了我 12.824707,这是一个巨大的 0.3px。我正在画布上绘制宽度从 600px 到 800px 的文本,我需要它正确换行,所以我需要它精确到 1px 的行,所以单个字母需要至少具有 0.02px 的精度,这在我开始使用粗体和斜体之前一直很不错。

以上问题在 Firefox 上都不存在,在 chrome 上禁用画布硬件加速似乎没有任何效果。我正在使用chrome 52.0,这是当前的最新版本。

编辑:我发现你甚至不需要做任何事情来获得不正确的数字,只需这样做就足够了。

let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.font = '11pt Calibri'
ctx.fillStyle = '#000000'
console.log(ctx.font)
console.log(ctx.measureText('M').width)
let temp = ctx.font
ctx.font = temp
console.log(ctx.font)
console.log(ctx.measureText('M').width)

不要在画布上使用"pt"来调整字体大小。

CSS 绝对单位和魔术单位

不建议使用 pt 来调整字体大小,因为它对于以像素(离散不可分割的图像单位(表示视觉信息的媒体没有实际意义,并且显示在没有固定像素密度的屏幕上。

pt是绝对测量单位,与cm相同,而px是"魔术单位",只有在介质类型为印刷时才具有绝对意义。

OP:"我读到画布以 96dpi 运行,所以正确的像素应该是 14.6。

这是不正确的,因为画布没有绝对测量单位。作为 CSS 单位的像素仅在打印媒体类型时具有绝对尺寸,在这种情况下,1px = 1/96 英寸。画布不被视为印刷媒体。


为什么宽度会改变?

明显的问题

ctx.font = '11pt Calibri'
console.log(ctx.font);                   // 15px Calibri
console.log(ctx.measureText('M').width); // 12.534011840820312
ctx.font = ctx.font
console.log(ctx.font);                   // 15px Calibri
console.log(ctx.measureText('M').width); // 12.82470703125

尽管ctx.font值相同,但测量的字体宽度不同

简单的解决方案

ctx.font = ctx.font = '11pt Calibri';

将避免测量的大小差异,但我相信没有人会认为这只是解决"明显"浏览器特定错误的丑陋工作。

解决方案

设置画布字体时不要使用pt单位。

这是怎么回事。

问题是对ctx.font属性实际是什么的误解。它不代表当前字体的实际内部表示,而是一种抽象的人类可读形式。

W3C 2D Canvas:"获取时,font 属性必须返回上下文的当前字体的序列化形式

序列化过程将失去精度。序列化 CSS 值。W3C 标准规定font-sizepx 为单位,在这种情况下,这进一步放大了明显的"错误">

font属性集函数获取 CSS 字体字符串,对其进行解析。如果有效,则设置画布内部字体并将序列化的 CSS 字体值写入context.font 两者不必匹配,标准也没有指定它们应该匹配。


总结

问题中描述的行为不是"错误"。浏览器之间的不一致(一如既往(是一个问题。如果我们要遵循这些标准,人们可以认为没有显示测量不一致的浏览器错误地解释了标准,并用他们自己的解释填补了歧义(尽管这是我的推测(。

该问题的简单解决方案是遵循标准的准则,并且在为除印刷介质以外的任何内容设置font-size值时不使用pt

与所有计算机媒体一样,"dpi"仅在打印时具有意义,直到那时才被定义。打印时像素也不一定等同于点。在引用像素而不是dp1时始终使用分辨率(我的宠物讨厌(

我意识到它为什么坏了。Chrome 在内部做了一些事情来补偿 pt 值,即使字体被劫持到 15px。因此,当我从ctx.font获取字体值进行修改时,我得到的是修改后的px值,而不是原始的pt,所以我实际上给了它一个原始的15px值,所以当这种情况发生时,chrome不会补偿。解决方法是将原始字体保留在其他地方,例如ctx.originalFont,然后将其用于修改而不是ctx.font

例如,这有效

let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.font = '11pt Calibri'
ctx.originalFont = '11pt Calibri'
ctx.fillStyle = '#000000'
console.log(ctx.font)
console.log(ctx.measureText('M').width)
let temp = ctx.originalFont
ctx.font = temp
console.log(ctx.font)
console.log(ctx.measureText('M').width)