Иконка выглядит иначе на компьютере коллеги
Разглядывая экран коллеги во время разговора, разработчик заметил, что логотип на его компьютере отображается не так, как на своём. На чужом экране изображение выглядело тоньше и точнее соответствовало оригиналу. Картинка отрисовывалась размером 15px; ниже — увеличенная версия для наглядности.
Оригинальное изображение из того случая не сохранилось, поэтому для демонстрации проблемы было создано новое.

Слева — Firefox, справа — Chrome.
Если немного прищуриться или отойти от экрана, версия из Chrome выглядит заметно толще. Странно, но замена изображения на SVG решила проблему. При этом оставался вопрос: почему рендеринг вёл себя именно так?
Изучение вопроса привело к любопытной оптимизации, которую Chrome применяет при отображении JPEG в маленьком масштабе.
Уменьшение изображений может быть неэффективным
Интуитивно понятный способ отрисовать маленькое изображение из JPEG — полностью распаковать его в память, а затем уменьшить.
Но это не всегда эффективно.
Представим JPEG размером 2000 × 2000, который нужно отобразить как 20 × 20. После распаковки изображение занимает намного больше памяти, чем итоговый результат: полноразмерный битмап требует около 12 МБ, а конечное изображение 20 × 20 — всего около 1,2 КБ. Большая часть информации из крупной версии теряется при уменьшении.
Какая информация теряется при уменьшении
Интересный момент: теряющаяся информация не случайна.
При сильном уменьшении изображения пропадают в основном высокочастотные детали. Это легко представить интуитивно: возьмём дерево с множеством листьев и грубой корой — такие мелкие детали быстро меняются от пикселя к пикселю, и именно это считается высокочастотной информацией.
Если уменьшить это дерево до крошечного размера, скажем 20 × 10, останется просто зелёное пятно сверху — крона — и коричневая палочка снизу — ствол. Уменьшенная версия отбросила мелкие детали, то есть высокочастотную информацию.

Иллюстрация уменьшения дерева
Часть высокочастотной информации всё же сохраняется — в той мере, в какой детали смешиваются друг с другом.
Как JPEG хранит данные изображения
Объяснение будет по возможности без сложной терминологии и математики, но несколько технических терминов всё же упомянуты — как отправная точка для тех, кто захочет разобраться глубже. Часть полного процесса преобразования JPEG здесь пропущена, поскольку она не нужна для понимания сути.
При сжатии JPEG изображение разбивается на блоки 8 × 8, которые переводятся в частотную область. Эта операция называется ДКП (дискретное косинусное преобразование, DCT).
В блоке 8 × 8 самая низкая возможная «частота» — это сплошной цвет. Строго говоря, это не частота в полном смысле, поскольку ничего не меняется — это постоянная составляющая. На другом конце спектра самая высокая частота выглядит как шахматная доска, где значение меняется максимально резко. Всё, что между этими крайностями, представляет остальную часть частотной области. Эти паттерны называются базисными функциями.

Базисные функции: слева сверху — сплошной цвет, справа снизу — шахматная доска.
Перевод блока 8 × 8 в частотную область — это, по сути, вопрос: сколько каждого паттерна присутствует в этом блоке? Эти величины называются коэффициентами.
После этого сжатие JPEG проходит ещё несколько шагов для эффективного хранения коэффициентов — именно там происходит сжатие с потерями. Но эта часть не важна для темы статьи.
Собираем всё вместе: отрисовка JPEG в масштабе 1/8
Предположим, изображение нужно уменьшить в 8 раз.
Упомянутые ранее блоки 8 × 8 теперь можно представить одним пикселем в уменьшенном изображении. При таком масштабе изображению в основном нужна только низкочастотная информация, потому что, как и в примере с деревом, высокочастотные детали в основном пропадают при уменьшении.
Поэтому вместо полной распаковки JPEG можно пропустить коэффициенты, отвечающие за высокочастотные части, и использовать только те, что нужны для грубой версии изображения. Так получается уменьшенный результат без предварительной полной распаковки оригинала.
Декодированное изображение занимает меньше места и распаковывается быстрее, поскольку значительная часть коэффициентов просто пропускается.
Этот подход можно применять и для других соотношений — при условии, что они выражаются дробью со знаменателем 8. Технически это называется частичным IDCT-масштабированием*. Подробнее — на jpegclub.org (там же можно увидеть, что эту технику можно использовать и для увеличения изображений).
* Обратное дискретное косинусное преобразование (IDCT): перевод из частотной области обратно в область изображения.
Где здесь Chrome
Chrome делегирует декодирование и рендеринг изображений библиотеке Skia. Для JPEG Skia использует libjpeg-turbo, которая реализует частичное IDCT-масштабирование. Это позволяет декодировать только низкочастотные данные, когда целевой размер достаточно мал.
Иными словами, Chrome/Skia не всегда полностью распаковывает изображение, а затем уменьшает его. Библиотека вычисляет ближайшую дробь со знаменателем 8 и декодирует изображение сразу в этом масштабе. Затем изображение дополнительно уменьшается более традиционным алгоритмом даунсемплинга до нужного размера.
Именно поэтому изображение выглядело толще на одном из компьютеров. Поскольку картинка отрисовывалась настолько мелко, она декодировалась в масштабе 1/8 с помощью частичного IDCT-масштабирования. В итоге из частотного представления осталась только постоянная составляющая — всё сглаживание краёв и градиенты просто не использовались.
Мораль здесь простая: не стоит использовать JPEG для иконок и подобных элементов интерфейса. Формат и его оптимизации созданы с учётом особенностей восприятия фотографий человеком.
Это, впрочем, заложено прямо в названии: Joint Photographic Experts Group.