Chrome canvas 2d context measureText给了我奇怪的结果

Posted

技术标签:

【中文标题】Chrome canvas 2d context measureText给了我奇怪的结果【英文标题】:Chrome canvas 2d context measureText giving me weird results 【发布时间】:2016-12-13 21:11:48 【问题描述】:

这是我的问题的精简版

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.3 像素。我在画布上绘制文本,宽度从 600 像素到 800 像素不等,我需要它正确换行,所以我需要它在线条上精确到 1 像素,所以单个字母需要至少有 0.02 像素的精度,这在我开始使用粗体和斜体之前工作得很好。

上述问题在firefox上都不存在,在chrome上禁用canvas硬件加速似乎也没有任何效果。我使用的是当前最新版本的 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)

【问题讨论】:

好吧,我认为您已经准备好向 chrome 提交错误报告。他们正在将字体大小单位即时转换为 px 而他们不应该这样做,并且由于他们不为此 px 单位使用浮动值,因此会产生此问题,但您已经知道了。跨度> 好的,我会这样做,只是想确保我没有遗漏一些明显的东西。 不,我认为这是一个糟糕的实现,在 FF 中,它们也允许 px 单元的浮动值。 以防万一——一个可能的解决方法是尝试使用普通的 html 跨度来测量文本。只需将其绝对定位,根据需要设置visibility: hiddenfont样式属性,将contentText属性设置为文本值进行测量,然后调用span.getBoundingClientRect().width,它似乎在Chrome中给出了准确的浮点像素值。 好的,我提交了错误报告。还有安德鲁,我是否需要将跨度添加到 DOM 才能正常工作? bugs.chromium.org/p/chromium/issues/detail?id=635427 【参考方案1】:

我知道它为什么坏了。即使字体被劫持到 15px,Chrome 也会在内部做一些事情来补偿 pt 值。所以当我从 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)

【讨论】:

【参考方案2】:

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

CSS 绝对单位和魔法单位

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

pt 是一个绝对测量单位,与cm 相同,而px 是一个“魔术单位”,只有在媒体类型为打印时才具有绝对意义。

OP:“我读到画布以 96dpi 运行,因此正确的 px 应该是 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: "在获取时,字体属性必须返回上下文当前字体的序列化形式。"

序列化的过程会丢失精度。 Serialising CSS values.W3C 标准规定 font-sizepx 为单位,在这种情况下,这进一步放大了明显的“错误”

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


总结

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

该问题的简单解决方案是遵循标准的指导方针,并且在为除印刷媒体之外的任何内容设置 font-size 值时不要使用 pt

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

【讨论】:

问题中描述的行为是一个错误。 Chrome 确实将字体大小设置为一定长度,但返回的字符串与设置的不同。如果他们想转换为 px 我会很好,但是 getter 应该反映它设置的确切值。他们要么在设置值时直接进行舍入,要么在 getter 上输出真正的浮点值,要么这是一个错误。 OP 在 pt 上也发现了这种行为,但同样适用于 em 和浮动 px。 Pps:根据您的链接序列化 css、部分长度和编号,它们应该返回一个以 10 为底的数字,最大 6 位小数精度。 @Kaiido Chrome 或 Firefox 都不会将 11pt Calibri 转换为正确的 px 值。 11pt = 0.15278"15px = 0.15625"11pt = 14.667px15px = 11.25pt Firefox 具有非标准值,因为它将 ctx.font 值设置为 11pt Calibri 尽管标准明确指出它应该以 px 为单位 不,它谈论 css 像素,以区分画布像素,但它没有说它应该默认为 px 单位。我并不是说 FF 实现是正确的,我们都知道文本样式实现在画布中的一致性就像 UA 所做的一样有价值,但是您在答案中提供的所有链接都与 chrome 背道而驰。如果他们真的必须像 css 一样解析字体,那么他们应该允许浮点值,精度为 6 位小数。他们没有,这是一个错误。

以上是关于Chrome canvas 2d context measureText给了我奇怪的结果的主要内容,如果未能解决你的问题,请参考以下文章

鼠标左键向下并在 Chrome 上移动时 Html Canvas 滞后

JavaScript入门之Canvas: 2D Context

HTML5 Canvas ( 扩展context('2d') ) CanvasRenderingContext2D.prototype.你的方法名

canvas教学RenderingContext2D对象的属性

context扩展及canvas的浏览器兼容

Canvas 使用指南