← 返回 Siami 首頁

一張 JPEG 竟能播放 90 格影片:開發者把漸進式掃描玩成瀏覽器動畫

▲ 500 💬 47
一張 JPEG 竟能播放 90 格影片:開發者把漸進式掃描玩成瀏覽器動畫

編按:本文綜合整理自 Maurycy 的原始技術實驗、MDN 的漸進式媒體說明、美國國會圖書館 JPEG Progressive 格式說明 與 Hacker News 討論串,並加入 Siami 編輯部觀點與分析。

JPEG 明明是一種靜態圖片格式,波蘭開發者 Maurycy 卻讓一張 .jpg 在瀏覽器裡接連換畫面,甚至塞進約 90 格黑貓影像。這不是把 GIF 偷改副檔名,也沒有用 JavaScript 輪播,而是刻意利用漸進式 JPEG 原本「先顯示模糊預覽、再補上細節」的掃描結構,把後來的資料變成全新的畫面。

這篇名為「Regressive JPEGs」的實驗在 Hacker News 迅速累積 500 分與 47 則留言。它的實用性接近零,作者也坦言比較適合拿來做非常規 Rickroll 或技術惡作劇;但從影像格式、瀏覽器相容性與內容安全來看,這正是那種看似胡鬧、卻能把系統邊界照得很清楚的作品。

漸進式 JPEG 原本怎麼工作?

一般的 baseline JPEG 會從畫面上方一路解碼到下方,檔案只下載一部分時,常見結果是上半截清楚、下半截空白。Progressive JPEG 則把資料拆成多個 scan:第一輪先交出大致輪廓,後面幾輪再補充更高頻的細節與更精確的係數。

JPEG 壓縮的核心是離散餘弦轉換(DCT)。影像會被切成區塊,再把像素變成不同空間頻率的係數:低頻描述大面積的亮暗與輪廓,高頻描述邊緣、紋理與細節。Maurycy 拆解的一張代表性圖片共有 10 個 scan,第一個 scan 只帶三個色彩通道的最低 DC 頻率,之後才逐步補齊其餘資料。

  • Y 通道:負責亮度,人眼對它最敏感,因此保留較多資訊。
  • Cb、Cr 通道:負責藍色差與紅色差,可用較低解析度儲存。
  • DC 係數:提供每個區塊最基本的平均色彩與亮度。
  • AC 係數:補上區塊內更細的紋理與邊緣。

MDN 對 progressive image 的描述很直白:瀏覽器可以先渲染低品質版本,資料繼續抵達後再更新成完整品質。正常情況下,使用者只會看見同一張圖從模糊變清楚;Maurycy 的問題是,既然每個 scan 都能指定自己處理的頻率範圍,後來的 scan 能不能乾脆把已顯示的內容改掉?

Computerphile 的影片從 DCT 開始拆解 JPEG 壓縮,可搭配本文理解「低頻先到、高頻後補」為何能成立。


把「補細節」改造成「換畫面」

第一版做法非常直接:把多張同尺寸 JPEG 的壓縮資料串在一起,並移除重複的檔案開頭、frame 開頭與檔案結尾標記。當伺服器慢慢傳送這個檔案,瀏覽器就會在收到新 scan 時重算畫面,於是同一個 <img> 元素看起來像在切換不同照片。

問題也立刻出現。標準 progressive scan 不能在同一輪同時包含 DC 與 AC 資料;baseline decoder 又會讀完第一個 scan 就停。若每一格都保留完整低頻、高頻與多階精度,scan 數量很快超標,解碼器也可能為防止類似壓縮炸彈的資源消耗而停止處理。

作者最後採用更極端也更聰明的方案:每一格只留下 DC scan,不再傳送真正的高頻細節。因為 JPEG 的 DCT 區塊在這個設定下相當於 16×16 像素,每格只剩原圖約 1/16 線性解析度的粗糙輪廓,但對連續動作而言已足以讓人辨認。

生成單一影格不需要自製編碼器,jpegtran 就能用掃描腳本完成:

0,1,2:0-0,0,0;

這一行的意思是,只為 Y、Cb、Cr 三個通道產生 DC 頻率資料。接著再把大量影格的掃描資料合併,一個「靜態 JPEG 影片」便成形。作者實測 Chrome 約可處理 90 格才停止,Firefox 對更多 scan 顯得更有耐心。

數據解讀:90 格不是 JPEG 的官方上限

標題裡最吸睛的「90 格」需要加上重要限定:這是作者在特定 Chrome 版本、檔案結構與傳輸方式下觀察到的近似結果,不是 JPEG 標準承諾的動畫容量,也不是所有瀏覽器共同遵守的固定上限。

Hacker News 留言呈現出很不一致的實測:有人回報 Safari 會凍結到整張圖下載完,另有人在 WebKit 看到載入被取消,Firefox 則可能長時間維持未完成狀態。換句話說,這個效果同時取決於:

  1. 解碼器最多願意處理多少 scan。
  2. 瀏覽器是否把部分解碼結果立即畫到畫面上。
  3. 伺服器與網路如何分段送出資料。
  4. 快取、代理與圖片最佳化服務是否重寫原始 JPEG。

它也沒有真正影片格式必備的時間資訊。每一格何時出現,完全由下一段位元組何時抵達決定;網路快時可能一閃而過,網路抖動時則會忽快忽慢。留言者提出由伺服器固定間隔送 chunk、或以 Service Worker 模擬慢速連線,確實能近似控制速度,但那已把播放邏輯移到傳輸層,不是 JPEG 檔案本身學會了計時。


為什麼這件事重要?

這項實驗不會取代 GIF、WebP、AVIF 或 MP4,真正價值在於揭露「格式規格允許什麼」與「軟體通常假設什麼」之間的落差。開發者平常看到 .jpg,自然會假定它最後只對應一張穩定畫面;但在部分渲染期間,使用者實際看見的內容可能與最終解碼結果完全不同。

第一個影響是內容安全與審核。若上傳平台只擷取第一個預覽,惡意檔案可以把敏感內容藏在後續 scan;若掃描器只看最終畫面,早期短暫出現的內容又可能被忽略。Hacker News 也有人聯想到隱寫與校園內容過濾。這不代表現有系統一定能被直接繞過,但足以提醒服務端:驗證圖片不能只看副檔名、第一格或單一解碼器結果。

第二個影響是效能與資源上限。Progressive JPEG 不是免費功能,解碼器可能多次更新畫面,過多 scan 也會增加 CPU、記憶體與重繪成本。瀏覽器選擇在某個數量後停止,可能正是避免惡意或畸形檔案無限消耗資源。網站若依賴大量漸進圖,也應在真實裝置上測試,而不是只看檔案大小變小了多少。

第三個影響是跨瀏覽器相容性。同一份符合部分規則的資料流,在 Chrome、Firefox、Safari 與 WebKit 上可能分別動畫、等待、卡住或失敗。這類差異對日常照片無關緊要,但對數位鑑識、內容預覽、爬蟲、CDN 轉檔與自動化測試來說都很重要。格式邊界不只是一個好玩的 demo,也是一份免費的壓力測試案例。

社群反應:很酷,但別真的拿來當播放器

討論串最受歡迎的延伸點,是把伺服器變成節拍器:即時產生 JPEG,並以固定間隔逐段送出,甚至串接網路攝影機,讓「單張圖片」持續增加新畫面。這能把網路延遲造成的不穩定降到較低,卻也意味著連線不會自然結束,代理伺服器、CDN 與瀏覽器資源管理都可能介入。

另一派則提醒 progressive decoding 本身可能比 baseline 慢。對現代高速網路而言,使用者已很少明顯看見圖片從糊變清楚;若產品只沿用「漸進式永遠比較好」的舊建議,卻沒有量測解碼時間與重繪成本,可能反而拖慢體感。

比較精準的結論不是「JPEG 原來能播影片」,而是「JPEG 的 progressive scan 可以被安排成連續覆寫畫面,而某些瀏覽器願意把中間狀態畫出來」。它是巧妙的格式駭客實驗,不是可靠、可控制或可攜的媒體方案。


接下來值得追蹤什麼?

JPEG 委員會介紹的 JPEG XL 本來就支援 progressive coding,也正式支援 animation;新格式不需要靠網路延遲與解碼器副作用模擬影片。對照之下,Maurycy 的作品更像是對 1990 年代格式設計的考古:同一套低頻先行概念,在標準功能裡是提升感知載入速度,在邊界玩法裡卻能變成一部沒有時間軸的短片。

後續可以觀察三個方向:

  • Chrome、Firefox 與 WebKit 是否會因資源管理或安全考量,調整 scan 數量與部分渲染策略。
  • 圖片掃描器、社群平台縮圖服務與 CDN 轉檔,是否會把這種檔案正規化成單一最終畫面。
  • 研究者能否把範例整理成跨瀏覽器測試集,釐清「合法但極端」與「損毀檔案」的界線。

這張會動的 JPEG 最有價值的地方,或許正是它沒有實用價值。當工程師可以放心把格式推到荒謬邊界,瀏覽器、解碼器與安全工具平常看不見的假設,才會一個個浮上來。

延伸閱讀:原始實驗與 merge.c|MDN:漸進式媒體渲染|Library of Congress:Progressive JPEG|libjpeg-turbo:MozJPEG|JPEG.org:JPEG XL

網友熱門留言 (5)

#1 cousin_it ▲ 4
可以讓伺服器即時產生這個 JPEG,並用固定間隔把資料分段送給客戶端,近似控制播放時間;來源甚至可以是一支網路攝影機,讓圖片持續延伸。
#2 schobi ▲ 1
原本以為必須艱難計算高頻係數,作者卻直接把不同圖片的低頻與高頻資料串起來;做法很簡單,效果卻意外地好。
#3 Yokohiii ▲ 5
在 OpenGL 與 jpeg-turbo 的使用經驗裡,開啟 progressive 會明顯拖慢解碼;漸進式 JPEG 未必永遠是免費的使用者體驗改進。
#4 LoganDark ▲ 2
Safari 會停在原地,直到整張圖片完全下載完才顯示,這也說明效果高度依賴瀏覽器解碼策略。
#5 tda ▲ 3
這種預覽畫面與最終畫面不同的特性,可能值得從隱寫與自動內容掃描的角度研究,因為某些分析器也許只檢查最終影格。