采用可变像素密度的高 DPI 图片

鲍里斯·萨姆斯
Boris Smus

如今,设备环境非常复杂,其中一个特点就是提供非常广泛的屏幕像素密度。有些设备具有超高分辨率显示屏,而另一些设备则落后于其他设备。应用开发者需要支持一定范围的像素密度,这可能相当具有挑战性。在移动网络上,这些挑战更加复杂化:

  • 各种各样的设备,外形规格不一。
  • 网络带宽和电池续航时间受限。

就图片而言,Web 应用开发者的目标是尽可能高效地提供最优质的图片。本文介绍了一些用于当前和近期的实用方法。

尽可能避免使用图片

在打开这种蠕虫之前,请记住,网络拥有许多强大的技术,这些技术在很大程度上独立于分辨率和 DPI。具体来说,由于网页的自动像素缩放功能(通过 devicePixelRatio 实现),因此文本、SVG 和大部分 CSS 将“正常运作”。

也就是说,您无法始终避免使用光栅图片。例如,您可能会获得难以在纯 SVG/CSS 中复制的资源,或者您要处理照片。虽然您可以将图片自动转换为 SVG,但将照片矢量化没有什么意义,因为放大后的版本通常看起来不好看。

背景

显示密度的非常短的历史

早期,计算机显示屏的像素密度为 72 或 96dpi(每英寸的点数)。

显示屏的像素密度逐渐提高,这在很大程度上受移动设备用例的推动。在这种用例中,用户通常会将手机靠近自己的脸部,使像素更清晰。到 2008 年,150dpi 手机已成为新标配。显示密度上升的趋势不断增长,如今的新手机采用 300dpi 显示屏(Apple 的“Retina”品牌)。

毫无疑问,这种显示屏中的像素完全不可见。对于手机的外形规格,当前一代 Retina/HiDPI 显示屏可能接近该理想水平。但是,Project Glass 等新型硬件和穿戴式设备可能会继续推动像素密度的提升。

实际上,低密度图片在新屏幕上应该与在旧屏幕上一样,但与高密度用户习惯看到的清晰图像相比,低密度图像看起来不清晰且像素化。以下是 1x 图像在 2x 显示屏上的粗略模拟。相比之下,2 倍图像看起来相当不错。

蝙蝠 1 只
猿猴 2 倍
不同像素密度的蝙蝠!

网页版 Pixel

在设计网络时,99% 的显示屏为 96dpi(或假冒为 96dpi),在这方面几乎没有对变化做出规定。由于屏幕尺寸和密度有较大差异,因此我们需要一种标准方法,让图片在各种屏幕密度和尺寸下都能呈现良好的显示效果。

HTML 规范最近通过定义参考像素(制造商用于确定 CSS 像素尺寸)解决了此问题。

使用参考像素,制造商可以确定设备物理像素相对于标准或理想像素的大小。该比率称为设备像素比。

计算设备像素比

假设智能手机的屏幕物理像素尺寸为 180 像素/英寸 (ppi)。计算设备像素比分为三个步骤:

  1. 将设备的实际保持距离与参考像素的距离进行比较。

    根据规格规范,我们知道对于 28 英寸,理想尺寸为每英寸 96 像素。不过,由于这是智能手机,因此用户把设备靠近自己的脸部就会比拿着笔记本电脑要靠近一些。我们估算一下 距离为 18 英寸

  2. 将距离比与标准密度 (96ppi) 相乘,即可得出指定距离的理想像素密度。

    bestPixelDensity = (28/18) * 96 = 150 像素/英寸(近似值)

  3. 计算物理像素密度与理想像素密度的比率,即可得出设备像素比。

    devicePixelRatio = 180/150 = 1.2

如何计算 devicePixelRatio。
展示一个参考角度像素的示意图,有助于说明 devicePixelRatio 的计算方式。

因此,现在,当浏览器需要了解如何根据理想或标准分辨率调整图片大小以适应屏幕时,浏览器会将设备像素比设为 1.2,也就是说,对于每个理想的像素,此设备具有 1.2 个物理像素。理想像素(如网页规范的定义)和物理像素(设备屏幕上的圆点)之间的计算公式如下:

physicalPixels = window.devicePixelRatio * idealPixels

过去,设备供应商倾向于对 devicePixelRatios (DPR) 进行舍入。Apple 的 iPhone 和 iPad 报告的 DPR 为 1,而其 Retina 等效项报告的 DPR 为 2。CSS 规范建议:

像素单位是指最接近参考像素的设备像素总数。

舍入比之所以更好的原因之一是,因为它们可能会减少子像素伪影

然而,实际设备环境更加多样,Android 手机通常具有 1.5 的 DPR。Nexus 7 平板电脑的 DPR 约为 1.33,是通过与上述示例类似的计算结果计算得出的。我们预计未来会有更多 DPR 可变的设备。因此,切勿假设客户端具有整数 DPR。

HiDPI 图像技术概览

有许多技术可以解决尽快显示最优质图像的问题,大体可分为两类:

  1. 优化单张图片
  2. 优化多张图片之间的选择。

单图方法:使用一张图片,但要巧妙地运用它。这些方法的缺点是您不可避免地会降低性能,因为即使是在 DPI 较低的旧款设备上,您也会下载 HiDPI 图片。以下是一些适用于单张图片的方法:

  • 过度压缩的 HiDPI 映像
  • 超赞的图片格式
  • 渐进式图片格式

多图片方法:使用多张图片,但要巧妙地选择要加载哪张图片。开发者先为同一资源创建多个版本,然后制定决策策略,这些方法就会产生固有开销。您有以下几种选择:

  • JavaScript
  • 服务器端传送
  • CSS 媒体查询
  • 内置浏览器功能(image-set()<img srcset>

过度压缩的 HiDPI 映像

图片已经占用了下载普通网站高达 60% 的带宽。通过向所有客户端提供 HiDPI 图片,我们会增加此数量。它能长多大?

我运行了一些测试,这些测试生成了 1x 和 2x 图片片段,JPEG 画质分别为 90、50 和 20。下面是我用来(采用 ImageMagick)生成的 Shell 脚本

功能块示例 1. 功能块示例 2. 功能块示例 3.
采用不同压缩率和像素密度的图片示例。

从这种不科学的小型采样来看,压缩大型图像似乎能够在质量与大小之间取得良好的平衡。在我看来,高压缩的 2 倍图像实际上比未压缩的 1 倍图片看起来更棒。

当然,与为 2 倍设备提供质量较高的设备相比,向 2 倍设备提供低质量、高度压缩的 2 倍图像更糟糕,上述方法会导致图像质量下降。如果您比较 90 张图片的质量与 20 张图片的质量,会发现清晰度下降了,而颗粒感却更明显。高质量的图片至关重要(例如照片查看器应用),或者对于不愿意牺牲的应用开发者来说,这些伪影可能无法接受。

以上比较完全使用压缩的 JPEG 进行。值得注意的是,在广泛实现的图片格式(JPEG、PNG、GIF)之间,存在许多权衡取舍,这使得我们能够...

超赞的图片格式

WebP 是一种非常富有吸引力的图片格式,能够在保持高图片保真度的同时进行很好的压缩。当然,这种方式尚未在所有地方实现

一种方式是通过 JavaScript 检查是否支持 WebP。您可以通过 data-uri 加载 1px 图片,等待已加载或错误事件触发,然后验证大小是否正确。Modernizr 附带了这样一个功能检测脚本,该脚本可通过 Modernizr.webp 获取。

不过,较好的一种方法是直接在 CSS 中使用 image() 函数。因此,如果您有 WebP 图片和 JPEG 回退,可以编写以下内容:

#pic {
  background: image("foo.webp", "foo.jpg");
}

这种方法存在一些问题。首先,image() 的实现并不广泛。其次,虽然 WebP 压缩将 JPEG 从水中清除,但这仍然是相对增量的改进 - 根据此 WebP 图库,大约缩小了 30%。因此,仅 WebP 不足以解决高 DPI 问题。

渐进式图片格式

JPEG 2000、渐进式 JPEG、渐进式 PNG 和 GIF 等渐进式图片格式的优势在于,它们可在图片完全加载之前看到图片已就位(存在一些争议)。它们可能会产生一定大小的开销,不过对此有冲突的证据。Jeff Atwood 声称,渐进式模式“会将 PNG 图片的大小增加约 20%,将 JPEG 和 GIF 图片的大小增加约 10%”。不过,Stoyan Stefanov 声称,对于大文件,渐进式模式更高效(在大多数情况下)。

乍一看,渐进式图片在尽可能快地提供质量上是非常有前景的。其理念是,一旦浏览器知道额外数据不会提高图片质量(即所有保真度改进都是子像素),浏览器就可以停止下载和解码图片。

虽然连接易于终止,但重新启动的成本通常很高。对于具有许多图片的网站,最高效的方法是使单个 HTTP 连接保持活跃状态,并尽可能长时间地重复使用该连接。如果由于一张图像已下载完毕而提前终止连接,则浏览器随后需要创建新连接,这样在低延迟环境中会非常慢。

一种解决方法是使用 HTTP Range 请求,该请求可让浏览器指定要提取的字节范围。智能浏览器可以发出 HEAD 请求以获取标头、对其进行处理、确定实际需要多少图片,然后提取图片。遗憾的是,Web 服务器对 HTTP Range 的支持很差,因此这种方法不切实际。

最后,这种方法有一个明显的局限性,那就是您无法选择要加载哪个图像,只能选择同一图像的不同保真度。因此,这并未涉及“艺术指导”用例。

使用 JavaScript 决定要加载哪张图片

确定要加载哪个图片的第一个也是最明显的方法是在客户端中使用 JavaScript。通过这种方法,您可以找出关于用户代理的所有信息,然后采取正确的措施。您可以通过 window.devicePixelRatio 确定设备像素比,获取屏幕宽度和高度,甚至可以通过 navigator.connection 或发出虚假请求进行网络连接嗅探,就像 foresight.js 库一样。收集了所有这些信息后,您就可以决定加载哪张图片。

目前,大约有 100 万个 JavaScript 库可执行此类操作,但遗憾的是,它们都不是特别突出。

这种方法的一大缺点是,使用 JavaScript 意味着您要延迟图片加载,直到先行解析器完成。这实际上意味着,图片要等到 pageload 事件触发后才会开始下载。如需了解详情,请参阅 Jason Grigsby 的文章

确定要在服务器上加载的图片

您可以为提供的每张图片编写自定义请求处理程序,将此决策推迟到服务器端。此类处理程序会根据用户代理(中继到服务器的唯一信息)检查 Retina 是否支持。然后,根据服务器端逻辑是否想要投放 HiDPI 资源,加载适当的资源(根据一些已知惯例命名)。

遗憾的是,用户代理不一定提供足够的信息来决定设备是接收高质量图片还是低质量图片。此外,显然,与用户代理相关的任何内容都是黑客手段,应尽可能避免。

使用 CSS 媒体查询

作为声明式的,CSS 媒体查询可让您表达自己的意图,并让浏览器代表您执行正确的操作。除了最常见的媒体查询(匹配设备尺寸)之外,您还可以匹配 devicePixelRatio。关联的媒体查询是设备像素比率,并且具有关联的最小和最大变体,如您所料。如果您想加载高 DPI 图片,而设备像素比超过阈值,则可执行以下操作:

#my-image { background: (low.png); }

@media only screen and (min-device-pixel-ratio: 1.5) {
  #my-image { background: (high.png); }
}

混入所有供应商前缀后,代码会变得有点复杂,尤其是因为“min”和“max”前缀的放置位置差异大不相同:

@media only screen and (min--moz-device-pixel-ratio: 1.5),
    (-o-min-device-pixel-ratio: 3/2),
    (-webkit-min-device-pixel-ratio: 1.5),
    (min-device-pixel-ratio: 1.5) {

  #my-image {
    background:url(high.png);
  }
}

通过这种方法,您可以重新获得先行解析的优势,而 JS 解决方案已不具备这种优势。此外,您还可以灵活选择响应断点(例如,可以包含低 DPI 图像、中 DPI 图像和高 DPI 图像),这是服务器端方法无法做到的。

遗憾的是,这仍然有点麻烦,并且会导致 CSS 外观奇怪(或需要进行预处理)。此外,此方法仅适用于 CSS 属性,因此您无法设置 <img src>,并且图片必须全部为具有背景的元素。最后,如果严格依赖设备像素比,那么高 DPI 智能手机可能会在使用 EDGE 连接时下载大量的 2 倍图片资源。这并非最佳用户体验。

使用新的浏览器功能

最近,围绕高 DPI 图像问题的网络平台支持展开了大量讨论。Apple 最近入侵了这一领域,将 image-set() CSS 函数引入了 WebKit。因此,Safari 和 Chrome 都支持此功能。由于这是一个 CSS 函数,因此 image-set() 无法解决 <img> 标记的问题。输入 @srcset,它可以解决这个问题,但(在撰写本文时)还没有参考实现。下一部分将深入介绍 image-setsrcset

支持高 DPI 的浏览器功能

归根结底,您要采取哪种方法的决定取决于您的特定要求。不过请注意,上述所有方法都有缺点。不过,展望未来,image-set 和 srcset 获得广泛支持后,它们将成为解决该问题的适当解决方案。目前,我们来谈谈一些最佳实践,这些最佳实践可帮助我们尽可能接近这个理想的未来。

首先,这两种格式有何不同?image-set() 是一个 CSS 函数,适合用作背景 CSS 属性的值。srcset 是 <img> 元素的专用属性,具有类似语法。这两个标记都可让您指定图片声明,但 srcset 属性可让您根据视口大小配置要加载哪张图片。

图片集最佳做法

image-set() CSS 函数可以使用前缀为 -webkit-image-set()。语法非常简单,采用一个或多个逗号分隔的图片声明,其中包含网址字符串或 url() 函数,后跟相关分辨率。例如:

background-image:  -webkit-image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

这会告知浏览器有两张图片可供选择。 其中一个针对 1x 显示屏进行了优化,另一个针对 2x 显示屏进行了优化。然后,浏览器会根据各种因素选择要加载哪个网页,如果浏览器足够智能,甚至可能包括网速(目前据我所知尚未实现)。

除了加载正确的图片之外,浏览器还会相应地缩放图片。换言之,浏览器会假设 2 张图片是 1 倍大小的两倍大小,因此会将这 2 倍大小的图片缩小 2 倍,以使这张图片在页面上看起来是相同的。

您还可以指定特定的设备像素密度(以 dpi),而不是指定 1x、1.5x 或 Nx。

这非常有效,除非在不支持 image-set 属性的浏览器中,该属性不会显示任何图片!这显然是糟糕的,因此您必须使用回退(或一系列回退)来解决此问题:

background-image: url(icon1x.jpg);
background-image: -webkit-image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);
/* This will be useful if image-set gets into the platform, unprefixed.
    Also include other prefixed versions of this */
background-image: image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

以上代码将在支持 image-set 的浏览器中加载适当的素材资源,否则会回退到 1x 素材资源。需要特别注意的是,虽然 image-set() 浏览器支持很低,但大多数用户代理都会获取 1 倍素材资源。

此演示使用 image-set() 加载正确的图片,如果此 CSS 函数不受支持,则回退到 1x 素材资源。

此时,您可能会好奇,为什么不只使用 polyfill(即为它构建一个 JavaScript shim)image-set() 并将其命名为“day”?事实证明,为 CSS 函数实现高效的 polyfill 并非易事。(如需详细了解原因,请参阅此 www-style 讨论)。

图片 srcset

下面是一个 srcset 示例:

<img alt="my awesome image"
  src="banner.jpeg"
  srcset="banner-HD.jpeg 2x, banner-phone.jpeg 640w, banner-phone-HD.jpeg 640w 2x">

如您所见,除了 image-set 提供的 x 声明外,srcset 元素还采用与视口大小相对应的 w 值和 h 值,以尝试提供最相关的版本。以上代码会向视口宽度低于 640px 的设备提供 banner-phone.jpeg,向屏幕宽度大于 640px 的高 DPI 设备提供 banner-phone-HD.jpeg 向小屏幕高 DPI 设备提供 banner-HD.jpeg ,向屏幕大于 640px 的高 DPI 设备提供 banner.jpeg 格式。

对图片元素使用 image-set

由于大多数浏览器中都不会实现 img 元素的 srcset 属性,因此您可能会想要将 img 元素替换为带有背景的 <div> 并使用图片集方法。此方法可以正常运行,但需要注意。这样做的缺点是,<img> 标记具有很长时间的语义值。在实践中,这对于网页抓取工具和无障碍原因最为重要。

如果您最终使用了 -webkit-image-set,可能会想要使用背景 CSS 属性。这种方法的缺点是,您需要指定图片大小,如果您使用的是非 1x 图片,则无法确定大小。除此之外,您也可以使用内容 CSS 属性,如下所示:

<div id="my-content-image"
  style="content: -webkit-image-set(
    url(icon1x.jpg) 1x,
    url(icon2x.jpg) 2x);">
</div>

这将根据 devicePixelRatio 自动缩放图片。请参阅上述方法的这个示例的实际运用。另外,对于不支持 image-set 的浏览器,还可以额外回退到 url()

执行 Polyfill 处理 srcset

srcset 的一个便捷功能是它带有自然回退。如果未实现 srcset 属性,所有浏览器都知道要处理 src 属性。此外,由于它只是一个 HTML 属性,因此您可以使用 JavaScript 创建 polyfill

此 polyfill 附带单元测试,以确保其尽可能接近规范。此外,如果以原生方式实现 srcset,系统会采取一些检查措施,以防止 polyfill 执行任何代码。

这里展示了 polyfill 的实际应用。

总结

解决高 DPI 图像的问题没有什么灵丹妙药。

最简单的解决方案是完全避免使用图片,而选择 SVG 和 CSS。但是,这并不总是现实的,特别是当您的网站上有高品质图像时。

JS、CSS 和使用服务器端的方法各有优缺点。不过,最具潜力的方法就是利用新的浏览器功能。虽然浏览器对 image-setsrcset 的支持尚不完善,但目前有合理的回退方案可供使用。

总的来说,我的建议如下: