Skip to content

Формат хеша ​

Как устроен HazeHash, для тех, кто хочет разобраться в хеше или написать другой декодер. Это формат версии 1 в реализации hazehash 0.1. Соответствие определяют зафиксированные тестовые векторы в репозитории (packages/core/test/vectors/v1.json): декодер должен выдавать тот же RGBA с точностью ±1 на канал.

Обзор ​

Изображение превращается в короткую строку байт: фиксированный заголовок, за которым следуют коэффициенты DCT изображения в OKLab, закодированные по Голомбу–Райсу. Бюджет по умолчанию 28 байт (38 символов base64url), поддерживаемый диапазон 16–48 байт. Один заголовок занимает 7 байт, или 9 с альфой, а хеш не может быть длиннее 1024 байт.

Биты записываются старшим вперёд, а многобитные поля идут в big-endian. Хвост последнего байта дополняется нулями. Бит за концом данных читается как 0, поэтому обрезанная строка допустима и декодируется с меньшей детализацией.

Заголовок ​

Заголовок занимает 56 бит:

ПолеБитСмещениеЗначение
ver200 для версии 1; 1–3 зарезервированы и должны отклоняться
alpha121, если после заголовка идёт блок альфа-канала
aspect63r = 2^((code − 32) / 8), код 32 это 1:1
Lx − 139ширина сетки яркости, 1–8
Ly − 1312высота сетки яркости, 1–8
Cx − 1215ширина сетки цвета, 1–4
Cy − 1217высота сетки цвета, 1–4
DC L619L = q / 63
DC a, DC b6+625, 31v = q · 0.64 / 63 − 0.32
scale L/a/b4×337код предельной амплитуды AC для каждого канала
k L/a/b2×349параметр Райса для каждого канала
reserved155записывается как 0, при чтении игнорируется

Блок альфа-канала занимает 16 бит и присутствует только при alpha = 1: DC A (5 бит, A = q / 31), Ax − 1 (2), Ay − 1 (2), scale A (4), k A (2) и зарезервированный бит (1).

Читатель должен отклонять вход короче заголовка (InvalidLength), длиннее 1024 байт и неизвестную версию (UnsupportedVersion).

Соотношение сторон и размер вывода ​

Пропорции хранятся как code = clamp(round(8 · log2(W / H)) + 32, 0, 63) с округлением половин от нуля, так что один шаг это около 9%, а диапазон примерно от 1:16 до 15:1. Декодер, которого просят превью с длинной стороной size, выдаёт size × max(1, round(size / r)) при r ≥ 1 и max(1, round(size · r)) × size в остальных случаях. Соотношение нужно для рисования плейсхолдера; для вёрстки страницы берите настоящие размеры изображения.

Коэффициенты ​

Изображение раскладывается в базисе DCT-II с выборкой в центрах пикселей на единичном квадрате:

text
f(u, v) = Σ c[i][j] · cos(π i u) · cos(π j v)

c[0][0] это среднее значение канала, оно лежит в заголовке. Остальные коэффициенты сетки nx × ny хранятся, только когда 2 · (i · ny + j · nx) < 3 · nx · ny, то есть i / nx + j / ny < 1.5, что отрезает только дальний угол сетки. Число хранимых AC-коэффициентов сетки n × n равно 0, 3, 8, 14, 23, 32, 45 и 57 для n = 1…8.

Порядок в потоке ​

Все AC-коэффициенты всех каналов образуют одну последовательность, отсортированную по:

  1. ρ² = (i² · ny² + j² · nx²) / (nx² · ny²), сравнивается перекрёстным умножением в целых числах,
  2. затем по каналу в порядке L, a, b, A,
  3. затем по меньшему j, затем по меньшему i.

Поэтому отрезание хвоста убирает самые высокие частоты всех каналов сразу.

Квантование ​

У каждого канала есть масштаб s = smax · 2^(−(15 − code) / 3), где smax = 0.64 для L и A и 0.32 для a и b. Коэффициент кодируется целым числом q из [−Qmax, Qmax]:

text
q = round(sign(c) · sqrt(min(|c| / s, 1)) · Qmax)
ĉ = sign(q) · s · (|q| / Qmax)²

Qmax равно 7 для яркости и 3 для a, b и альфы на любой частоте. Декодер обязан восстанавливать ровно по этой формуле, без смещения восстановления, и ограничивать декодированное q значениями ±Qmax.

Энтропийное кодирование ​

Каждое q отображается в n = 2q (при q ≥ 0) или −2q − 1 (при q < 0) и записывается как n >> k единичных бит, нулевой бит, затем k младших бит n. k (0–3) хранится для каждого канала. Нулевой коэффициент стоит k + 1 нулевых бит, поэтому энкодер может отбросить хвостовые нулевые байты данных, из-за чего хеш простого изображения короче бюджета.

Декодирование в пиксели ​

  1. Восстановить каждый канал разделимым синтезом в размере вывода.
  2. Ограничить L диапазоном [0, 1] и перевести OKLab в линейный RGB матрицами Оттоссона.
  3. Отображение в гамму: если какая-либо компонента выходит за [−0.0005, 1.0005], масштабировать цветность на наибольшее t ∈ [0, 1], найденное 8 шагами деления пополам, затем ограничить значения диапазоном [0, 1].
  4. Перевести в sRGB и применить дизеринг interleaved-gradient-noise: u = fract(52.9829189 · fract(0.06711056 x + 0.00583715 y)) и out = floor(v · 255 + u).
  5. Альфа, если есть, это clamp(A, 0, 1) · 255 с округлением. Цвета не умножаются на альфу заранее.

Строковая форма ​

Строковая форма это base64url (RFC 4648 §5) без выравнивания, так что 28 байт дают 38 символов. Длина, дающая остаток 1 по модулю 4, или любой символ вне алфавита недопустимы.