Как поменять несколько бит в картинке bmp в с++

уважаемые знатоки. Возникла задача - записать в файл (картинку .bmp) некий текст. Текст нужно взять из файла и записать в существующую картинку. Я погуглил, и решил, что нужно открывать файл-картинку в двоичном виде, но проблема в том, что я не могу понять, как считывать эти самые биты. У меня получается всякая абра кадабра. Так вот вопрос, что нужно сделать, чтобы записать в конец каждого байта некоторый символ в виде его числового представления пока сообщение не кончится? Я написал такой код:

#include <iostream>
#include <fstream>
#include <string>

using namespace std;

int main() {
    string data = "I:\\NumMet\\bmp\\data.txt";
    string image = "I:\\NumMet\\bmp\\bmp.bmp";
    string message = "";
    ifstream fin(data);
    ifstream fin2(image, ios::binary);

    if (!fin.is_open() && !fin2.is_open()) {
        cout << "File not open";
        return 0;
    }

    // считываю текст для записи в картинку
    char ch;
    int count = 0;
    while (fin.get(ch)) {
        message += ch;
        count++;
    }
    cout << count << ' ';
    cout << message;
    fin.close();

    // скипаю байты на всякий случай
    int i = 0;
    fin2.seekg(256);

    // проверяю, что достаточно пикселей для записи
    i = 0;
    while (fin2.get(ch) && i < count) {
        i++;
    }
    if (i < count) {
        cout << "picture is small";
    }
    else {
        cout << "Good";
        // замена битов картинки на биты символов.
    }
    fin2.close();
} 

В нем я считываю сообщения для записи, пропускаю несколько байт, чтобы не задеть системные данные (но я не нашел, сколько именно надо пропускать, потому взял с запасом) и после проверки, что пикселей для записи хватит - должен записывать те самые данные.

Добавлено: Загрузил картинку с результатом работы программывведите сюда описание изображения


Ответы (1 шт):

Автор решения: Laukhin Andrey

Я рассмотрю вариант, когда алфавит кодируется пятью битами и в каждом байте пиксельных данных мы меняем 1 младший бит. Таким образом, для одного символа потребуется пять байт. Это вариант с наименьшим воздействием на изображение.

Имеем 26 букв латинского алфавита + пробел. Еще в запасе 5 кодов.

Если внимательно посмотреть на структуру, то мы увидим, что по адресу 0x0a можно прочитать смещение пиксельных данных от начала файла.

Записать данные не проблема, а вот как при чтении понять, какое количество символов нужно прочитать и есть ли этот текст вообще? Можно использовать пару из неиспользуемых кодов в алфавите в качестве терминальных символов (начало и конец строки). Но вероятность ложного захвата строки все же остается, хоть и сводится к минимуму при желании.

Я же обратил внимание на то, что есть неиспользуемые 4 байта по адресу 0x06, куда можно записать размер строки.

Дальнейшие комментарии в коде. Лишних проверок не делаю.

Запись данных

// Открываем файлы. BMP на чтение и запись
fstream fImg(image, ios::binary | ios::in | ios::out);
ifstream fTxt(text);

char ch;
unsigned char byte;
long offset;
long count = 0;

// Считываем смещение пиксельных данных
fImg.seekg(0x0a);
fImg.read((char*)&offset, 4);

while (fTxt.get(ch)) {

    // алфавит начинается с ASCII кода 97, нам нужно с 0
    ch -= 97;

    // все что не входит в 5 бит, будет под кодом 31 (в дальнейшем пробел)
    if (ch < 0 || ch > 31) ch = 31;

    // итерируем 5 бит символа
    for (int i = 0; i < 5; i++, offset++)
    {
        // значение младшего бита
        int firstBit = ch & 1;

        // читаем байт пиксельных данных
        fImg.seekg(offset);
        byte = fImg.peek();

        // Заменяем младший бит на firstBit символа
        byte ^= (-firstBit ^ byte) & 1U;

        // записываем байт обратно
        fImg.seekp(offset);
        fImg.put(byte);

        // побитовый сдвиг вправо, чтобы получить следующий младший бит
        ch >>= 1;
    }

    // кол-во записанных символов
    count++;
}

// Записываем в неиспользуемое поле заголовка длину строки
fImg.seekp(0x06);
fImg.write((const char*)&count, sizeof(count));

fTxt.close();
fImg.close();

Чтение

ifstream fImg(image);

char ch;
unsigned char byte;
long count;
long offset;

// Считываем длину строки
fImg.seekg(0x06);
fImg.read((char*)&count, 4);

// Считываем смещение пиксельных данных
fImg.seekg(0x0a);
fImg.read((char*)&offset, 4);

while (count--) {

    ch = 0;

    // нужно 5 байт, чтобы восстановить символ
    for (int i = 0; i < 5; i++, offset++)
    {
        // читаем байт
        fImg.seekg(offset);
        byte = fImg.peek();

        // получаем младший бит
        int firstBit = byte & 1;

        // заменяем i-тый бит на firstBit
        ch ^= (-firstBit ^ ch) & (1U << i);
    }

    // Все, что не буква, будет пробел
    if (ch > 25) ch = -65;
    ch += 97;

    cout << ch;
}

fImg.close();

Дополнение
Код работает для 24-битных изображений и 8-битных с равномерной grayscale-палитрой.

→ Ссылка