適用於可變像素密度的高 DPI 圖片

Boris Smus
Boris Smus

現今複雜的裝置生態中,有一個特徵是可用的螢幕像素密度範圍非常廣泛。某些裝置具有極高解析度的螢幕,有些則落後。應用程式開發人員需要支援多種像素密度,這可能相當困難。在行動版網站上,挑戰會因多項因素而加劇:

  • 多種板型規格的裝置。
  • 網路頻寬和電池續航力受限。

就圖片而言,網頁應用程式開發人員的目標是盡可能以最有效率的方式提供最高畫質的圖片。本文將介紹在今天和日後執行此項目的幾項實用技巧。

盡量避免使用圖片

因此在開啟這種方法之前,請記得,網路上有許多強大技術,但與解析度和 DPI 無關。具體來說,文字、SVG 和大部分 CSS 都會「正常運作」,因為網頁的自動像素縮放功能 (透過 devicePixelRatio)。

不過,您不一定能避免使用點陣圖。舉例來說,您可能會收到很難以純 SVG/CSS 複製的素材資源,或是要處理相片。雖然您可以自動將圖片轉換為 SVG,但將相片轉為向量圖並無意義,因為放大的版本通常看起來不太好。

背景

顯示密度的簡短發展史

早期電腦螢幕的像素密度為 72 或 96 dpi (每英寸點數)。

螢幕的像素密度逐漸提升,主要歸功於行動裝置的用途,因為使用者通常會將手機拿得更靠近臉部,因此更容易看見像素。到了 2008 年,150 dpi 的手機成為新標準。顯示密度持續提升,現今的新手機配備 300 dpi 螢幕 (Apple 的「Retina」品牌)。

當然,終極目標是打造完全看不見像素的螢幕。就手機板型規格而言,目前一代的 Retina/HiDPI 螢幕可能已接近理想狀態。不過,Project Glass 等新類型的硬體和穿戴式裝置,可能會持續提高像素密度。

實際上,低密度圖片在舊螢幕和新螢幕上看起來應該是相同的,但相較於使用者習慣看到的高密度圖片,低密度圖片看起來會顯得粗糙且有顆粒感。以下是 1x 圖片在 2x 螢幕上的粗略模擬結果。相較之下,2 倍的圖片看起來也很好。

狒狒 1x
Baboon 2x
Baboons! 在不同像素密度下。

網頁上的像素

網頁設計時,99% 的螢幕解析度為 96dpi (或假裝),而且幾乎沒有針對這方面的變化做出任何規定。由於螢幕大小和密度差異甚大,我們需要一種標準方式,讓圖片在各種螢幕密度和尺寸下都能呈現良好的效果。

HTML 規格最近解決了這個問題,定義了製造商用來判斷 CSS 像素大小的參考像素。

製造商可以使用參考像素,判斷裝置實體像素相對於標準或理想像素的大小。這個比率就稱為裝置像素比例。

計算裝置像素比例

假設智慧型手機的螢幕實體像素大小為每英寸 180 像素 (ppi)。計算裝置像素比例需經過三個步驟:

  1. 比較裝置與參考像素的實際保持距離。

    根據規格,我們知道在 28 英寸的情況下,每英寸的理想像素數為 96 像素。不過,由於這是智慧型手機,使用者會將裝置拿得比筆電更靠近臉部。我們假設這個距離為 18 英寸。

  2. 將距離比率乘以標準密度 (96ppi),即可取得指定距離的理想像素密度。

    idealPixelDensity = (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 會回報 1 的 DPR,而 Retina 對應裝置則會回報 2。CSS 規格建議:

像素單位是指最接近參考像素的整數裝置像素。

圓形比率較佳的其中一個原因,在於可能導致子像素成果較少。

不過,實際的裝置環境變化更多,Android 手機的 DPR 通常為 1.5。Nexus 7 平板電腦的 DPR 約為 1.33,這是透過與上述計算類似的計算方式得出。預計未來會有更多裝置 DPR 可變低。因此,請勿假設客戶會擁有整數 DPR。

高解析度圖片技術總覽

為了盡快顯示最佳品質的圖片,解決這類問題的方法有很多,大致可分為以下兩類:

  1. 最佳化單一圖片,以及
  2. 正在最佳化多張圖片的選取作業。

單一圖片策略:只使用一張圖片,但做一些巧妙。這些方法的缺點是,您必然會犧牲效能,因為即使在 DPI 較低的舊裝置上,您仍會下載 HiDPI 圖片。以下是適用於單一圖片案例的幾種方法:

  • 經過大量壓縮的 HiDPI 圖片
  • 超讚的圖片格式
  • 漸進式圖片格式

多張圖片方法:使用多張圖片,但以聰明的方式挑選要載入的圖片。這些方法會讓開發人員產生額外的負擔,因為他們必須為同一個素材資源建立多個版本,然後找出決策策略。可選擇的類型如下:

  • JavaScript
  • 伺服器端放送
  • CSS 媒體查詢
  • 內建瀏覽器功能 (image-set()<img srcset>)

大量壓縮的 HiDPI 圖片

下載一般網站時,圖片已佔用 60% 的頻寬。透過提供所有用戶端的 HiDPI 映像檔,可增加這個數字。會成長多少?

我執行過一些測試,產生 1 倍和 2 倍的圖片片段,並具有 90、50 和 20 的 JPEG 品質。以下是shell 指令碼 (使用 ImageMagick) 產生這些圖片:

資訊方塊範例 1。 資訊方塊範例 2。 資訊方塊範例 3。
不同壓縮率和像素密度的圖片範例。

從這項小型且非科學的抽樣調查來看,壓縮大型圖片似乎可提供良好的品質與大小權衡。在我看來,經過大量壓縮的 2x 圖像實際上比未壓縮的 1x 圖片更美觀。

當然,為 2x 裝置提供低品質、高度壓縮的 2x 圖像,比提供較高品質的圖像更糟糕,而且上述方法會導致圖像品質受罰。如果您比較品質: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 相片庫,其大小約比 JPEG 小 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 程式庫那樣。收集上述所有資訊後,您可以決定要載入的圖片。

大約有 一百萬個 JavaScript 程式庫可執行上述功能,但很遺憾,其中沒有任何一個程式庫特別出色。

這個方法的一大缺點是,使用 JavaScript 會延遲圖片載入作業,直到前瞻剖析器完成後才會執行。這基本上表示,除非 pageload 事件觸發,否則圖片不會開始下載。如要進一步瞭解這項功能,請參閱 Jason Grigsby 的文章

決定要載入哪些圖片

您可以針對提供的每張圖片編寫自訂要求處理常式,將決定實施在伺服器端。這類處理常式會根據使用者代理程式 (轉發至伺服器的唯一資訊) 檢查 Retina 支援情形。接著,根據伺服器端邏輯是否要提供高解析度素材資源,您可以載入適當的素材資源 (依據某些已知慣例命名)。

很抱歉,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); }
}

不過,所有供應商的前置字串都混合了這個情況,會比較複雜,因為其中不容易有「最小」和「最大」前置字串的刊登位置差異

@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>,且圖片必須全部是含有背景的元素。最後,如果您嚴格依賴裝置像素比率,在EDGE 連線下,高 DPI 智慧型手機可能會下載巨大的 2x 圖片素材資源。這並非最佳的使用者體驗。

使用新的瀏覽器功能

最近有許多討論,圍繞著高 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 螢幕最佳化。接著,瀏覽器會根據各種因素 (如果瀏覽器夠聰明的話,甚至可能包括網路速度) 選擇要載入哪一個 (據我所知,目前尚未實作)。

除了載入正確的圖片,瀏覽器也會相應縮放圖片。換句話說,瀏覽器會假設 2x 圖片的大小是 1x 圖片的兩倍,因此會將 2x 圖片縮小 2 倍,讓圖片在頁面上顯示為相同大小。

除了指定 1x、1.5x 或 Nx,您也可以使用 dpi 指定特定裝置的像素密度。

這項做法運作良好,但在某些不支援 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
);

上述指令會在支援圖片設定的瀏覽器中載入適當的素材資源,否則會改回使用 1x 素材資源。明顯的警告是,雖然 image-set() 瀏覽器支援率偏低,但大多數使用者代理程式都會取得 1x 素材資源。

這個示範使用 image-set() 載入正確圖片,如果系統不支援這個 CSS 函式,則會改回使用 1x 素材資源。

此時,您可能會想知道為何不只為 image-set() 進行 polyfill (也就是為其建構 JavaScript 墊片),然後就結束?事實上,要為 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 的裝置放送 click-phone.jpeg、在大螢幕高 DPI 裝置上,Banner-phone-HD.jpeg、banner-HD.jpeg 傳送至螢幕大於 640 像素的高 DPI 裝置,以及 banner.jpeg 用於所有其他裝置。

為圖片元素使用 image-set

由於大多數瀏覽器都未實作 img 元素的 srcset 屬性,因此您可能會想將 img 元素替換為帶有背景的 <div>,並使用圖片集合方法。這麼做是可行的,但有附帶條件。缺點是 <img> 標記具有長期語意值。實際上,這項做法對於網頁檢索器和無障礙功能來說特別重要。

如果您最終使用 -webkit-image-set,可能會想使用 background CSS 屬性。這種做法的缺點是,您需要指定圖片大小,但如果使用非 1x 圖片,則無法得知圖片大小。請改用以下 CSS 內容屬性:

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

這樣系統就會根據裝置 PixelRatio 自動調整圖像大小。請參閱這個範例,瞭解上述技術的實際運作方式,並為不支援 image-set 的瀏覽器額外提供 url() 備用方案。

聚填 srcset

srcset 的其中一項便利功能,就是隨附天然備用項目。如果未實作 srcset 屬性,所有瀏覽器都會處理 src 屬性。此外,由於這只是 HTML 屬性,因此可以使用 JavaScript 建立 polyfill

這個 polyfill 會附帶單元測試,確保盡可能符合規格。此外,如果已原生實作 srcset,系統會進行檢查,防止 polyfill 執行任何程式碼。

如要查看 polyfill 的實際運作方式,請按這裡

結論

沒有萬靈丹可以解決高 DPI 圖片的問題。

最簡單的解決方案是完全避免使用圖片,改用 SVG 和 CSS。不過,這不一定是實際可行的做法,尤其是當你在網站上使用高品質圖像時。

JS、CSS 和使用伺服器端的方法各有優缺點。不過,最有希望的方法是利用新的瀏覽器功能。雖然瀏覽器對 image-setsrcset 的支援仍不完整,但目前有合理的備用方案可供使用。

總結來說,我的建議如下:

  • 針對背景圖片,請使用 image-set 和適當的備用方案,以便在瀏覽器不支援時使用。
  • 如果是內容圖片,請使用 srcset polyfill,也可以採用「使用 image-set」的備用方案 (請參閱上文)。
  • 如果您願意犧牲圖片品質,建議使用經過大量壓縮的 2x 圖片。