Введение: почему выбор формата цвета - это не только эстетика
Формат хранения цвета задаёт четыре измеримых параметра: точность передачи оттенка, объём записи на диске, скорость сравнения и индексации значений, стоимость конвертации при отдаче данных клиенту. Ошибка при выборе проявляется не сразу, а на масштабе: когда в таблице миллионы строк, а API пересчитывает цвет на каждом запросе, переделывать приходится схему, код и миграции.
Как это выглядит в железе. В проекте Aqara T2 цвет передаётся как RGB с целочисленными компонентами в диапазоне 0-255, а входные значения проверяются на выход за границы: при ошибке возвращается сообщение "RGB values must be between 0-255. Got r:". Для динамических эффектов устройство принимает массив из 1-8 RGB-объектов и параметр скорости в диапазоне 1-100%. Функции конвертации описаны как общие и идентичные для моделей T1M, T1 Strip и T2: один и тот же код обслуживает три устройства. Прошивка работает с целочисленным RGB, а остальная цветовая арифметика ложится на серверный слой.
В 2026 году к классическим форматам добавились расширения: CSS Color 4 поддерживает hsl(), oklch() и color(display-p3 ...), дисплеи всё чаще отдают охват P3, который шире sRGB примерно на 25%. Основа при этом не изменилась: шестнадцатеричная запись, целочисленный RGB и HSL закрывают большинство задач хранения и обмена, а CMYK остаётся языком печати.
Ниже: структура каждого формата, сравнение по точности, размеру и читаемости, конвертация с потерями и без, готовые схемы колонок для PostgreSQL и MySQL и итоговые рекомендации для DevOps-инженеров и системных администраторов.
Краткий обзор форматов: HEX, RGB, HSL, CMYK
Четыре формата описывают цвет в разных базисах, и путаница между ними - источник половины ошибок при хранении.
HEX - шестнадцатеричная запись RGB-каналов: #RRGGBB, где каждая пара знаков кодирует один канал (00-FF, то есть 0-255). Сокращённая форма #RGB разворачивается в полную удвоением знаков: #F00 превращается в #FF0000. Расширение #RRGGBBAA добавляет альфа-канал на 256 уровней прозрачности. Полный диапазон без альфы: 16 777 216 оттенков. HEX компактен в тексте и привычен фронтенду, но неудобен для арифметики: чтобы смешать два цвета, строку нужно распаковать в числа.
RGB - аддитивная модель: три канала складываются в свет, нулевые значения дают чёрный, максимальные - белый. На практике компоненты целые 0-255, в CSS Color 4 допускаются доли 0.0-1.0 и проценты. RGB явно указывает каналы, поэтому легко сравнивается, сортируется, интерполируется и передаётся в API структурированным объектом.
HSL - цилиндрическая модель: тон H в градусах 0-360, насыщенность S 0-100%, светлота L 0-100%. Изменение L даёт готовые оттенки одного цвета, поэтому HSL удобен для генерации палитр и светлых или тёмных тем. Охват тот же sRGB, но хранить приходится дробные числа.
CMYK - субтрактивная модель печати: голубой, пурпурный, жёлтый и чёрный (K) в процентах 0-100%. Краски вычитают свет из белой бумаги, поэтому охват уже sRGB: часть насыщенных экранных цветов напечатать физически нельзя.
| Формат | Структура | Диапазоны | Число значений | Типовая область |
|---|---|---|---|---|
| HEX | #RRGGBB, #RGB, #RRGGBBAA | 00-FF на канал | 16 777 216 | CSS, API, конфиги |
| RGB | R, G, B | 0-255 или 0.0-1.0 | 16 777 216 | Прошивки, графика, API |
| HSL | H, S, L | 0-360, 0-100%, 0-100% | Тот же охват sRGB | Темы, генерация палитр |
| CMYK | C, M, Y, K | 0-100% каждый | Уже sRGB по охвату | Полиграфия, макеты |
Важная деталь для инфраструктуры: HEX и RGB - это одно значение в двух записях, а HSL и CMYK требуют пересчёта. Железо и встраиваемые протоколы обычно принимают RGB: проверка границ 0-255 и массив из 1-8 цветов в Aqara T2 подтверждают, что RGB остаётся рабочим форматом для устройств.
Сравнение форматов по ключевым критериям
Точность и цветовой охват
HEX и RGB кодируют один и тот же 24-битный sRGB: 16 777 216 оттенков, переход между записями обратим и не теряет ни одного значения. Точность здесь ограничена не форматом, а 8 битами на канал: градиенты на тёмных участках дают видимые ступени, и для них в 2026 году применяют 10-битные каналы и охват P3.
HSL описывает тот же охват, но в другом базисе и с дробными компонентами. При пересчёте целых RGB в HSL и обратно возможны отклонения в 1-2 единицы на канал из-за округления при приведении float к int. Для интерфейсов это незаметно, для точных сравнений цветов в тестах уже нет: два цвета, равные в RGB, после двойного прохода могут разойтись.
CMYK охватом уже sRGB. Неоново-зелёный, насыщенный голубой и часть оттенков оранжевого не воспроизводятся офсетной печатью. Перевод CMYK в RGB зависит от метода: простая формула даёт для (0, 100, 100, 0) чистый красный (255, 0, 0), а конвертация через профиль печати вроде US Web Coated SWOP - примерно (237, 28, 36). Разница в 18 единиц по красному каналу возникает потому, что реальные краски не идеальны. Для хранения это означает: CMYK-значение без указания профиля неоднозначно.
Размер хранения и производительность
Разница в байтах кажется мелочью до момента, когда таблица переваливает за десяток миллионов строк.
| Представление | Тип в PostgreSQL | Тип в MySQL | Байт на значение | Объём на 10 млн строк |
|---|---|---|---|---|
| HEX #RRGGBB | char(7) или varchar(7) | CHAR(7) или VARCHAR(7) | 7-8 | 70-80 МБ |
| Упакованный RGB | integer (0xRRGGBB) | INT UNSIGNED или MEDIUMINT UNSIGNED | 3-4 | 30-40 МБ |
| RGB тремя колонками | smallint x 3 | TINYINT UNSIGNED x 3 | 3-6 | 30-60 МБ |
| HSL | real x 3 или numeric x 3 | FLOAT x 3 | 12 и больше | от 120 МБ |
| CMYK | smallint x 4 | TINYINT UNSIGNED x 4 | 4-8 | 40-80 МБ |
Упакованный RGB в один integer - самый экономный вариант: 24 бита значения помещаются в 32-битный int4 без потерь, диапазон 0-16 777 215 укладывается в допустимые значения типа. В MySQL аналог - MEDIUMINT UNSIGNED, ровно 3 байта на значение. В PostgreSQL однобайтового целого нет, поэтому три RGB-канала логично держать одним integer, а не тремя smallint.
Упаковка ускоряет сравнение и индексацию: B-tree сравнивает 4 байта вместо строки с коллацией, а фильтр по диапазону целочисленного RGB отрабатывает по индексу. Целочисленный RGB применяют и в устройствах: Aqara T2 принимает компоненты 0-255 и массив из 1-8 цветов, что делает такой формат единым для API, базы и прошивки.
Читаемость и удобство для разработчиков
HEX привычен тем, кто пишет CSS и конфиги: его видно глазами, он копируется без кавычек и не раздувает JSON. Цена - невозможность арифметики: сложить два HEX или вычислить средний цвет строкой не получится, нужна распаковка в числа.
RGB читается как явные каналы и не требует пояснений: (255, 0, 0) однозначен, из него напрямую считаются яркость, градиент и средний цвет. В коде это либо три числа, либо один int, что снижает риск перепутать порядок каналов при передаче через слои приложения.
HSL удобен для дизайн-систем: правка одного числа L даёт согласованный набор светлых и тёмных вариантов, тогда как в RGB придётся пересчитывать все три канала. Минус - дополнительный слой конвертации, если железо и API работают с RGB.
CMYK понятен специалистам по печати и требует профиля для корректного перевода в экранные цвета. В веб-хранилище он почти никогда не нужен как основной формат.
Конвертация между форматами: как избежать потерь
Из четырёх форматов обратимая пара только одна. Остальные переходы нужно либо контролировать тестами, либо хранить исходное значение рядом с производным.
HEX ↔ RGB: простая и точная конвертация
Каждая пара знаков HEX - это один канал в шестнадцатеричной системе. Запись #FF0000 разбирается как R = 0xFF = 255, G = 0x00 = 0, B = 0x00 = 0. Обратный переход - форматирование с ведущими нулями, иначе #0A0A0A потеряет нули и превратится в #AAA.
def hex_to_rgb(value):
h = value.lstrip("#")
if len(h) == 3:
h = "".join(ch * 2 for ch in h)
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
def rgb_to_hex(r, g, b):
return "#{:02X}{:02X}{:02X}".format(r, g, b)
Операция без потерь: 16 777 216 значений переводятся туда и обратно однозначно. Типичная ошибка - хранить HEX без ведущего нуля или без нормализации регистра, из-за чего сравнение строк даёт ложные расхождения.
RGB ↔ HSL: нюансы округления
Пересчёт идёт через максимум и минимум каналов: светлота - среднее между ними, насыщенность зависит от размаха, тон определяется тем, какой канал максимален.
def rgb_to_hsl(r, g, b):
r1, g1, b1 = r / 255.0, g / 255.0, b / 255.0
mx, mn = max(r1, g1, b1), min(r1, g1, b1)
l = (mx + mn) / 2.0
if mx == mn:
return 0.0, 0.0, l * 100
d = mx - mn
s = d / (2.0 - mx - mn) if l > 0.5 else d / (mx + mn)
if mx == r1:
h = ((g1 - b1) / d + (6 if g1 < b1 else 0)) / 6.0
elif mx == g1:
h = ((b1 - r1) / d + 2.0) / 6.0
else:
h = ((r1 - g1) / d + 4.0) / 6.0
return h * 360.0, s * 100.0, l * 100.0
Чистые цвета проходят цикл точно: RGB (255, 0, 0) даёт HSL (0, 100%, 50%) и возвращается в (255, 0, 0). Для составных цветов появляются отклонения: значение RGB (128, 64, 32) при округлении тона до целых градусов и насыщенности до процентов возвращается как (128, 65, 32) или (129, 64, 32). На одном цвете это незаметно, но если HSL служит источником истины для тысяч значений, расхождения накапливаются при каждом проходе. Держите каноничным целочисленный RGB, а HSL считайте производным представлением.
CMYK → RGB: конвертация с потерями
Простая формула вычитания краски: R = 255 x (1 - C) x (1 - K), G = 255 x (1 - M) x (1 - K), B = 255 x (1 - Y) x (1 - K), где C, M, Y, K заданы долями 0-1. Обратный переход: K = 1 - max(R, G, B) / 255, затем C = (1 - R/255 - K) / (1 - K) и аналогично для M и Y.
Эта модель игнорирует реальные свойства красок и бумаги, поэтому годится только для приблизительных оценок. Точный перевод делают через ICC-профиль печатного процесса. Даже с профилем часть цветов выпадает из охвата и заменяется ближайшими доступными, то есть потери при CMYK → RGB необратимы. Печатный файл нельзя использовать как источник цвета для экранного каталога: правильнее хранить RGB-эталон и отдельно описывать, как он ложится в CMYK для конкретной типографии.
Единообразие конвертации стоит фиксировать в коде. В Aqara T2 функции перевода описаны как общие и идентичные для T1M, T1 Strip и T2: один набор формул обслуживает парк устройств, поэтому цвет ведёт себя одинаково на всех моделях. Тот же принцип работает на серверной стороне: одна библиотека перевода на весь проект, иначе разные сервисы посчитают один цвет по-разному.
Хранение нескольких представлений цвета: зачем и как
Одновременное хранение HEX и RGB в одной строке - это денормализация: чтение становится проще, запись и поддержка сложнее.
Плюсы и минусы денормализации
Плюсы: чтение без конвертации на каждом запросе, разные клиенты получают привычный формат (фронтенд HEX, устройство RGB), отладка SQL и выгрузок упрощается, API-контракт меньше меняется со временем. Дополнительная колонка HEX обходится примерно в 70 МБ на 10 млн строк, а индексы по ней стоят дороже самих данных.
Минусы: растёт объём, появляется риск рассинхронизации, каждое изменение цвета нужно проводить через единую точку записи. Если UPDATE прошёл только по одной колонке, в базе окажутся HEX и RGB, описывающие разные оттенки, и найти такую строку можно лишь сверкой всех представлений.
Безопаснее держать один каноничный формат и получать остальные вычислением. Хранить дубли оправдано, когда конвертация попадает в горячий путь: например, при отдаче миллионов запросов в сутки или при фильтрации по цвету в тяжёлых аналитических выборках.
Практические схемы хранения
Вариант 1, каноничный integer с вычисляемой колонкой для HEX. В PostgreSQL 12+ и MySQL 8 поддерживаются генерируемые колонки, которые считаются из базового значения и могут индексироваться.
CREATE TABLE palette (
id bigint PRIMARY KEY,
rgb_int int NOT NULL CHECK (rgb_int BETWEEN 0 AND 16777215),
hex_val char(7) GENERATED ALWAYS AS (
'#' || lpad(to_hex(rgb_int), 6, '0')
) STORED
);
CREATE INDEX palette_rgb_idx ON palette (rgb_int);
Здесь HEX не нужно обновлять руками: значение пересчитывается при записи rgb_int, рассинхронизация невозможна в принципе.
Вариант 2, три числовых колонки с ограничениями диапазона: r, g, b по 0-255. Схема читается глазами, но занимает больше места, а составной индекс (r, g, b) даёт слабую селективность: значений красного канала всего 256, и планировщик почти всегда выберет перебор по такому префиксу.
Вариант 3, только HEX в виде char(7) с проверкой формата. Подходит для справочников палитр, которые читают и правят руками, но плохо ложится в фильтры и сортировки по каналам.
Вариант 4, jsonb или JSON-массив, когда хранится последовательность цветов. Динамические эффекты Aqara T2 принимают массив из 1-8 RGB-объектов, и такая структура естественно ложится в JSON. Плата - размер и отсутствие строгой типизации: проверять диапазоны 0-255 придётся на уровне приложения или через CHECK по jsonb-выражению.
Если палитра пока небольшая и живёт в git, начинать с базы смысла нет: разбор критериев выбора между JSON, базой данных и сервисом токенов есть в материале о выборе системы хранения цветовых палитр. Когда представлений несколько и они меняются, добавьте версионирование: снимки и дельта-хранение описаны в руководстве по версионированию цветов и палитр.
Выбор формата для API и веб-разработки
Для API каноничным стоит сделать одно представление и описать правило конвертации в документации. Практичный набор:
- HEX-строка в JSON - основной формат: компактно, совместимо с CSS, читаемо в логах и в ответах REST.
- Объект с каналами {r, g, b} - когда клиент сразу работает с числами: градиенты, смешивание, расчёт контраста.
- Массив цветов - для палитр, тем и эффектов, как в массиве 1-8 RGB-объектов у Aqara T2.
Веб-платформа в 2026 году поддерживает hsl(), oklch() и color(display-p3 ...) из CSS Color 4, и эти функции удобно применять для тем, но хранить значения лучше в sRGB HEX или целочисленном RGB: они однозначны и не зависят от интерпретации браузера. Цвета, выходящие за sRGB в P3, потребуют отдельного поля с расширенным диапазоном, иначе при сохранении в 8-битный HEX они обрежутся до ближайшего sRGB-значения.
Отдельная зона - изображения. Текстовые форматы цвета описывают один оттенок, а в картинках цвет хранится попиксельно в своих контейнерах, и там работают другие правила сжатия и выдачи. Практика настройки такой раздачи с автоматической конвертацией в WebP и AVIF разобрана в статье об отдаче изображений и конвертации форматов.
Влияние формата на производительность выборок
Скорость запросов определяется не столько типом данных, сколько тем, попадает ли условие в индекс. Целочисленный RGB индексируется напрямую: сравнение 4 байт дешевле сравнения строки с учётом коллации и длины. Разница становится заметной на десятках миллионов строк и в диапазонных запросах.
Главная ошибка - конвертация в условии на лету. Запрос, который сравнивает диапазон значений, полученных функцией перевода из HEX-строки, заставляет базу вычислять эту функцию для каждой строки и полностью отключает индекс. Два выхода:
- Хранить значение в целевом виде (целочисленный RGB) и фильтровать по нему.
- Создать генерируемую колонку или индекс на выражении, тогда планировщик сможет их использовать.
Для выборок по оттенку полезно заранее посчитать канал тона: если приложение часто ищет все синие оттенки, колонка hue с индексом даст ответ быстрее, чем перебор трёх RGB-колонок. Сортировка по HEX даёт порядок, не совпадающий ни с яркостью, ни с тоном, поэтому для сортировки от тёмного к светлому нужна отдельная вычисляемая колонка яркости.
Замеры стоит проводить на стенде, близком к продакшену: облачный сервер или managed-база с нужной версией поднимается за минуты, и разница между схемами видна на реальном объёме данных. Подходящий вариант для тестового контура - облачные серверы и базы данных Timeweb Cloud. Более широкий разбор типов и способов организации хранения под нагрузку есть в материале о хранении данных в 2026 году.
Рекомендации по выбору формата в 2026 году
Итоговые решения по сценариям:
| Сценарий | Формат хранения | Комментарий |
|---|---|---|
| Основное хранилище цвета в базе | integer 0xRRGGBB или три TINYINT UNSIGNED | 24 бита без потерь, дешёвый индекс, компактно |
| Справочник палитр для ручной правки | HEX char(7) | Читаемо, при необходимости вычисляемая RGB-колонка |
| Темы и генерация оттенков | HSL как производное | Считается из RGB на лету, канон остаётся в RGB |
| Обмен с API и фронтендом | HEX-строка | Совместимо с CSS и REST, компактно в JSON |
| Передача в устройства и прошивки | целочисленный RGB 0-255 | Проверка границ и массив цветов для эффектов |
| Печать и полиграфия | CMYK с указанием ICC-профиля | Хранить вместе с профилем, иначе значение неоднозначно |
| Расширенный охват P3 | float или 10-битные каналы | 8 бит sRGB обрежет такие цвета |
Проверяйте конвертацию тестом на полном диапазоне: прогон всех 16 777 216 значений RGB через HSL и обратно занимает минуты на одном ядре и сразу показывает, есть ли расхождения. Если нужен быстрый контроль в CI, ограничьтесь ключевыми цветами: чистый красный, зелёный, синий, белый, чёрный и несколько серых.
Автоматизация тоже уместна: если палитры приходят из внешних каталогов и нужны нормализация значений, перевод в другой охват или генерация имён оттенков, эту работу удобно отдать моделям через API. Единый доступ к GPT, Gemini и Claude с оплатой в рублях даёт агрегатор AiTunnel, что снимает вопрос с ключами и VPN для нескольких сервисов.
Заключение
Формат цвета - инженерное решение с измеримыми последствиями: 4 байта против 8 на значение, индекс по числу против сканирования строк, обратимая конвертация против потери охвата.
- Определите основной сценарий: хранение в базе, обмен по API, передача в устройство или печать.
- Выберите каноничный формат: целочисленный RGB для базы, HEX для API, CMYK с профилем для полиграфии.
- Остальные представления считайте генерируемыми колонками или на лету, чтобы исключить рассинхронизацию.
- Проверьте конвертацию тестом, включая крайние значения 0 и 255 и переходы через серый.
- Задокументируйте формат и правило перевода: это избавит от разночтений между сервисами.
- Пересмотрите решение при переходе на P3 или 10-битные каналы: 8-битный HEX такие цвета обрежет.
Целочисленный RGB с проверкой диапазона остаётся рабочей основой и в серверных хранилищах, и в устройствах вроде Aqara T2, а HEX - самым удобным форматом обмена. Такой набор закрывает задачи 2026 года без миграций и потери точности.