Прошлая статья нашла своих читателей, а поэтому продолжаем!
В этот раз поговорим о JPEG...

Что нам нужно знать про JPEG и почему этот парень заслуживает отдельной статьи?
А знать нам для начала нужно то, как JPEG устроен. Начнем с того, что в JPEG не используется RGB, вместо него мы имеем дело с YCbCr.
Из чего состоит YCbCr?
Y - Яркость (светлота). Вычисляется по формуле:
Cb - Разница между яркостью и синим
Сr - Разница между яркостью и красным
Продолжим. Второй факт о JPEG — он не хранит цвет каждого пикселя. Вместо этого JPEG использует DCT-сжатие:
Изображение разбивается на компоненты YCbCr и делится на блоки 8×8 пикселей.
Каждый блок преобразуется в частотное пространство с помощью DCT (дискретное косинусное преобразование).
Высокие частоты у каналов Cb и Cr, как правило, уменьшаются (зависит от степени сжатия)
Что такое DCT?
Это способ представить изображение (а в нашем случае каждый блок) в виде матрицы коэффициентов (или же по-другому частот), описывающих его
Низкие частоты: описывают плавные изменения (фон, крупные объекты). Изменение низких частот напрямую влияет на яркость изображения.
Средние частоты: описывают контуры объектов, крупные текстуры. Их изменения влияют на восприятие объектов и текстур на изображении, делая их более заметными или, наоборот, сглаженными.
Высокие частоты: описывают очень мелкие детали. Высокие частоты отвечают за резкость, чёткость и ощущение «детализации» в изображении.


Что мы будем делать?
Ясен ч... красен, мы будем менять последний бит в каждом элементе матрицы (на бит, нужный нам)! Подобную механику я описал в прошлой статье о PNG, только там мы меняли бит у цвета, а в этот раз нам предстоит менять бит у чисел в матрице.

Но сперва посмотрим, как изменения этой матрицы скажутся на изображении.




Примеры приведены с изменением частоты на 70% для пущей наглядности.
Но для нашей задачи требуется изменение всего на 1 bit. А следовательно, изменения будут совершенно незаметны человеческому глазу.
Итак! Чтобы спрятать наше сообщение внутри JPEG, нам остается лишь разбить его на биты и пройтись по матрице, меняя последний бит каждого элемента на нужный нам.

Техническую реализацию по получению DCT не прикладываю, так как она довольно объемная. Прекрасный пример - owencm/js-steg. Используя его, можно работать с матрицами DCT.
Простой пример сохранения и получения сообщения:
/**
* Переводит наше сообщение в байты
*/
function textToBytes(text){
let encoder = new TextEncoder();
return encoder.encode(text);
}
/**
* Производит изменения с матрифами
*/
function modifyCoefficients(coefficients){
//Наше сообщение
let message = "Message";
let data = textToBytes(message);
//coefficients[0] -> все блоки Y
//coefficients[1] -> все блоки Cb
//coefficients[2] -> все блоки Cr
//coefficients[0][0] //64 элемента матрицы DCT
//Меняем данные в матрице
//Для примера, работаем только с Y-каналом
let lumaCoefficients = coefficients[0];
for (let i = 0, bitIndex = 0; i < lumaCoefficients.length; i++) {
for (let j = 0; j < 64; j++) {
if(bitIndex < data.length * 8){
let bit = (data[Math.floor(bitIndex / 8)] >> (7 - (bitIndex % 8))) & 1;
//Меняем последний бит
lumaCoefficients[i][j] = (lumaCoefficients[i][j] & 0xFE) | bit;
bitIndex++;
}
}
}
}
jsSteg.reEncodeWithModifications(objectURL, modifyCoefficients, function (resultUri) {
//resultUri - наш результат (картинка base64)
});
/**
* Переврдит байты в строку
*/
function bytesToText(bytes){
let uint8Array = new Uint8Array(bytes);
let decoder = new TextDecoder();
return decoder.decode(uint8Array);
}
function readCoefficients(coefficients) {
//coefficients[1] - Y
//coefficients[2] - Cb
//coefficients[3] - Cr
let bytes = [];
let dataBitIndex = 0;
let currentByte = 0;
//Работаем так же только с Y
let lumaCoefficients = coefficients[1];
for (let i = 0; i < lumaCoefficients.length; i++) {
for (let j = 0; j < 64; j++) {
let bit = lumaCoefficients[i][j] & 1;
currentByte = (currentByte << 1) | bit;
dataBitIndex++;
if (dataBitIndex % 8 === 0) {
bytes.push(currentByte);
currentByte = 0;
}
}
}
return bytesToText(bytes);
}
//Чтение сообщения
jsSteg.getCoefficients(objectURL, function(coefficients){
console.log(readCoefficients(coefficients));
});

Код более развернутого решения можно найти на GitHub (использование AES, сокрытие файлов в картинке).
Кому интересно - можно поиграться с тем, какие частоты мы используем под хранение информации (от этого зависит визуальное изменение изображения).

З.ы: увидел ваши комментарии на счет "steganography", позже исправлю.