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

Boris Smus
Boris Smus

当今复杂的设备形态的一个特点是,屏幕像素密度非常多样。有些设备的显示屏分辨率非常高,而有些则落后于后。应用开发者需要支持一系列像素密度,这可能非常具有挑战性。在移动网络上,由于以下几种因素,这些挑战会更加复杂:

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

在图片方面,Web 应用开发者的目标是尽可能高效地提供最优质的图片。本文将介绍目前和近期可用于实现此目的的一些实用技巧。

尽可能避免使用图片

在打开这个潘多拉魔盒之前,请记住,Web 上有许多强大的技术,它们在很大程度上不依赖于分辨率和 DPI。具体而言,由于 Web 的自动像素放大功能(通过 devicePixelRatio),文本、SVG 和大部分 CSS 将“正常运行”。

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

背景

显示密度简史

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

显示屏的像素密度逐渐提高,这在很大程度上是受移动设备用例的影响,在这种用例中,用户通常会将手机靠近脸部,因此像素更容易被看到。到 2008 年,150dpi 手机已成为新常态。显示密度上升的趋势持续存在,当今的新手机采用 300dpi 显示屏(Apple 标有“Retina”)。

当然,理想状态是像素完全不可见。对于手机外形规格,当前一代 Retina/HiDPI 显示屏可能接近理想状态。但是,Project Glass 等新类别的硬件和穿戴式设备可能会继续推动增加像素密度。

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

浣熊 1 号
Baboon 2x
不同像素密度的蝙蝠!

网页上的像素

在设计 Web 时,99% 的显示屏具有 96dpi(或“假装”)为 96dpi,而对于这方面的变化,则很少进行预配。由于屏幕尺寸和密度存在很大差异,因此我们需要一种标准方法,使图片在各种屏幕密度和尺寸下都能呈现良好的显示效果。

HTML 规范最近通过定义制造商用于确定 CSS 像素大小的参考像素来解决此问题。

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

计算设备像素比

假设一部智能手机的屏幕物理像素尺寸为每英寸 180 像素 (ppi)。计算设备像素比需要完成以下三个步骤:

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

    根据规范,我们知道理想尺寸为 28 英寸时,理想尺寸为每英寸 96 像素。不过,由于它是智能手机,因此用户会将设备握得比笔记本电脑更靠近脸部。让我们估计距离为 18 英寸

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

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

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

    devicePixelRatio = 180/150 = 1.2

devicePixelRatio 的计算方式。
显示一个参考角度像素的示意图,以帮助说明 devicePixelRatio 的计算方式。

因此,现在当浏览器需要知道如何根据理想分辨率或标准分辨率调整图片大小以适应屏幕时,会参考设备像素比 1.2,即对于每个理想像素,此设备具有 1.2 个物理像素。用于在理想像素(由 Web 规范定义)和物理像素(设备屏幕上的点)之间转换的公式如下所示:

physicalPixels = window.devicePixelRatio * idealPixels

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

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

舍入比更高的原因之一是,它们可以减少子像素伪影

不过,实际的设备情况要多样得多,Android 手机的 DPR 通常为 1.5。Nexus 7 平板电脑的 DPR 约为 1.33,这是通过与上述计算类似的方法得出的。预计未来会看到更多具有可变 DPR 的设备。因此,您绝不能假设客户端将具有整数 DPR。

高 DPI 图片技术概览

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

  1. 优化单张图片,以及
  2. 优化了在多张图片之间进行选择的功能。

单图方法:使用一张图片,但要对其进行巧妙的处理。 这些方法的缺点在于,您不可避免地会牺牲性能,因为即使在 DPI 较低的旧版设备上,您也需要下载高 DPI 图片。以下是针对单张图片的一些方法:

  • 高度压缩的 HiDPI 图片
  • 非常棒的图片格式
  • 渐进式图片格式

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

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

高度压缩的 HiDPI 图片

在下载一个普通网站时,图像已经占据了 60% 的带宽。通过向所有客户端提供高 DPI 图片,我们将提高这一比例。它还会变大多少?

我进行了一些测试,生成了 1x 和 2x 的图片碎片,JPEG 质量分别为 90、50 和 20。下面是我用来生成它们的 Shell 脚本(采用 ImageMagick):

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

从这小部分非科学的样本来看,压缩大型图片似乎可以实现良好的质量与大小权衡。在我看来,经过严格压缩的 2x 图像实际上比未压缩的 1x 图像效果更好。

当然,向双倍分辨率设备提供低质量、高度压缩的双倍分辨率图片,比提供更高质量的图片更糟糕,而且上述方法会导致图片质量下降。如果将质量(90 张图片)与质量(20 张图片)进行比较,您会看到清晰度下降,颗粒度增加。如果高质量图片至关重要(例如照片查看器应用),或者应用开发者不愿意妥协,则可能无法接受这些工件。

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

非常棒的图片格式

WebP 是一种非常出色的图片格式,不仅压缩效果出色,还能保持较高的图片保真度。当然,此功能尚未在所有地方实现

一种方法是通过 JavaScript 检查是否支持 WebP。您可以通过 data-uri 加载 1 像素的图片,等待“已加载”或“错误”事件触发,然后验证大小是否正确。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 请求来获取标头、对其进行处理、确定实际需要的图片大小,然后进行提取。遗憾的是,网络服务器对 HTTP Range 的支持不佳,因此这种方法不切实际。

最后,这种方法的一个明显局限性是,您无法选择要加载的图片,只能选择同一张图片的保真度有所不同。因此,这无法解决“艺术指导”用例。

使用 JavaScript 决定要加载的图片

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

大约有 100 万个 JavaScript 库可执行上述操作,但遗憾的是,其中没有一个特别出色。

此方法的一大缺陷是,使用 JavaScript 意味着您将延迟加载图像,至少要等到先行解析器结束。这实际上意味着,图像要等到 pageload 事件触发后才能开始下载。如需了解详情,请参阅 Jason Grigsby 的文章

决定在服务器上加载什么图片

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

遗憾的是,User-Agent 不一定会提供足够的信息来决定设备应接收高质量还是低质量图片。此外,不用说,与 User-Agent 相关的任何内容都是黑客行为,应尽可能避免。

使用 CSS 媒体查询

通过声明式 CSS 媒体查询,您可以表达自己的意图,并让浏览器代表您执行正确的操作。除了媒体查询最常见的用法(匹配设备尺寸)之外,您还可以匹配 devicePixelRatio。关联的媒体查询是 device-pixel-ratio,并且具有关联的最小值和最大值变体,这可能符合您的预期。如果您想加载高 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 图片),而这是使用服务器端方法时无法实现的。

遗憾的是,它仍然有点难以操作,并且会导致 CSS 看起来很奇怪(或需要预处理)。此外,此方法仅适用于 CSS 属性,因此无法设置 <img src>,并且您的图片都必须是具有背景的元素。最后,如果仅依赖设备像素比,在某些情况下,高 DPI 智能手机在连接到 EDGE 网络时,可能会下载巨大的 2x 图片素材资源。这并非最佳用户体验。

使用新的浏览器功能

近期,我们就 Web 平台对高 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 张图片是 1x 图片的两倍大,因此会将 2x 图片的 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() 的浏览器非常少,大部分用户代理会加载 1x 资产。

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

此时,您可能想知道为什么不只是使用 polyfill(即为 image-set() 构建 JavaScript shim)并将其命名为“一天”呢?事实证明,为 CSS 函数实现高效的 polyfill 非常困难。(要了解原因,请参阅此 www 样式讨论)。

图片 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 的设备提供 banner-phone-HD.jpeg,为适用于小屏幕高 DPI 的设备提供 banner-HD.jpeg ,为适用于屏幕大于 640px 的高 DPI 设备提供 banner-HD.jpeg,并为其他任何设备提供 banner.jpeg 。

为图片元素使用 image-set

由于大多数浏览器都未实现 img 元素上的 srcset 属性,因此您可能很想将 img 元素替换为带有背景的 <div>,并使用图片集方法。这种方法可以使用,但有一定限制。这里的缺点是,<img> 标记具有长期语义值。在实践中,这对于网页抓取工具和无障碍功能非常重要。

如果您最终使用 -webkit-image-set,可能会想使用 background CSS 属性。此方法的缺点是,您需要指定图片大小,如果您使用的是非 1x 图片,则大小未知。您可以改用 content CSS 属性,如下所示:

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

这会根据 devicePixelRatio 自动缩放图片。如需查看上述技术的实际运作方式,请参阅此示例,其中针对不支持 image-set 的浏览器额外回退到 url()

为 srcset 填充多边形

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

此 polyfill 附带单元测试,以确保其尽可能贴近规范。此外,我们还设置了一些检查,以防止在 srcset 以原生方式实现时,polyfill 执行任何代码。

以下是 polyfill 演示的实际应用。

总结

没有任何灵丹妙药可以解决高 DPI 图片问题。

最简单的解决方案是完全避免使用图片,而选用 SVG 和 CSS。不过,这并不总是现实情况,尤其是当您的网站上有高质量图片时。

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

总而言之,我的建议如下: