Файлы как они есть. Работа с типизированными массивами

от автора

Всем привет! Меня зовут Егор, я фронтенд-разработчик в Райффайзенбанке. В этой статье я хочу показать, как благодаря типизированным массивам мы можем взаимодействовать с бинарными данными в браузере. В качестве примера мы напишем приложение для шифрования текста внутрь изображения и посмотрим, как работают типизированные массивы.

Введение

Ни для кого не секрет, что компьютер обрабатывает данные в бинарном формате, где каждый бит указывает на наличие или отсутствие электрического сигнала. Для того, чтобы отобразить текстовые данные, мы передаем последовательность битов компьютеру, а он с помощью специальных утилит переводит ее в понятный человеку символ. 

При обработке графических данных компьютеру тоже поступают сигналы, где последовательность из трех байт (или четырех, при наличии альфа-канала) является значением цвета RGB (RGBA). При разработке приложения мы будем взаимодействовать с BMP-форматом, но аналогичное возможно и с любым другим форматом файлов.

Для начала определим, что должно уметь наше приложение:

  • Кодировать текстовое сообщение в файл. При этом вес, структура и визуальное отображение файла не должны измениться.

  • Расшифровывать текстовое сообщение.

Демонстрация работы приложения

Работа с изображением

Для реализации приложения нам понадобится описание BMP-формата, которое достаточно подробно описано в «Википедии».

Описание заголовка BITMAPCOREHEADER

Смещение

Размер (байты)

Описание

0

2

Отметка для отличия формата от других (сигнатура формата).

Возможные значения: BM, BA, CI, CP, IC, PT

2

4

Размер файла в байтах

6

2

Зарезервированы. Должны содержать ноль

8

2

10

4

Начальный адрес байта, в котором могут быть найдены данные растрового изображения (bitmap data)

Что мы узнаем из описания этого формата?

По соображениям совместимости большинство приложений используют старые заголовки DIB для сохранения файлов. Поскольку OS / 2 больше не поддерживается после Windows 2000, на данный момент распространенным форматом Windows является заголовок BITMAPINFOHEADER

Поэтому во внимание берем только заголовок BITMAPINFOHEADER и сигнатуру BM.

Описание заголовка BITMAPINFOHEADER

Смещение

Размер (байты)

Описание

14

4

Размер заголовка

18

4

Ширина растрового изображения в пикселях (целое число со знаком)

22

4

Высота растрового изображения в пикселях (целое число со знаком)

26

2

Количество цветовых плоскостей. В BMP допустимо только значение 1

28

2

Количество бит на пиксель

30

4

Используемый метод сжатия

34

4

Размер пиксельных данных в байтах

38

4

Количество пикселей на метр по горизонтали

42

4

Количество пикселей на метр по вертикали

46

4

Количество цветов в цветовой палитре

50

4

Количество ячеек от начала цветовой палитры до последней используемой (включая её саму).

Для начала создаем класс, отвечающий за работу с ArrayBuffer, полученным из изображения. После этого проверяем соответствие BMP-формату:

class BmpParser {   #BMP_HEADER_FIELD = 'BM'   #view   #decoder    constructor(buffer) {     this.#view = new DataView(buffer)     this.#decoder = new TextDecoder()     this.#checkHeaderField()   }       #checkHeaderField() {     if (this.#decoder.decode(new Uint8Array(this.#view.buffer, 0, 2)) !== this.#BMP_HEADER_FIELD) {               throw new Error('Ожидается .bmp файл!')     }   }  }

Теперь нам необходимо узнать смещение, где может быть найден массив пикселей (bitmap data). В таблице указан размер в 4 байта, поэтому мы используем маску Uint32Array, так как 1 байт равен 8 бит:

class BmpParser {   // ...   get offsetBits() {       return this.#view.getUint32(10, true)    } // ... }

Чтобы проверить, что текстовое сообщение не превышает размер пиксельных данных, мы будем использовать размер bitmap data. Его можно получить из заголовка BITMAPINFOHEADER:

class BmpParser {   // ...   get bitmapDataSize() {     return this.#view.getUint32(34, true)   }   // ... }

Это все данные, которые необходимо получить из изображения. 

Шифрование

Алгоритм для шифрования сообщения:

  1. Конверуем текстовое сообщение в бинарный код. Возможные значения: «1», «0», «,».

  2. Поочередно рассматриваем каждый символ:

    1. Если этот символ имеет значение 0 или 1, оставляем без изменений.

    2. Если этот символ имеет значение «,» — устанавливаем ему значение 2. Это будет свидетельствовать, что предыдущую последовательность нулей и единиц можно собрать в символ из зашифрованного сообщения.

    3. Записать полученное число в bitmap data.

  3. Добавить точку выхода, в нашем случае — значение «3».

Приступим к реализации.

Создаем класс, отвечающий за шифрование текстового сообщения внутрь изображения, добавив метод конвертации текста в бинарный код:

class Encryptor {   // Текущее смещение битов в ArrayBuffer   #offset = 0   #view   #encryptionText   #bmpParser    constructor(buffer, encryptionText) {     this.#view = new DataView(buffer)     this.#encryptionText = encryptionText     this.#bmpParser = new BmpParser(buffer)   }    // Конвертирует текст в бинарный код   // Тест => ["10000100010", "10000110101", "10001000001", "10001000010"]   encode(value) {     return value.split('').map(char => char.charCodeAt(0).toString(2))   } }

Добавим проверку, что длина сообщения не превышает размер изображения в байтах:

class Encryptor {   // ...   #checkPhraseLength(binaryLength) {     if (binaryLength >= this.#bmpParser.bitmapDataSize) {       throw new Error('Фраза слишком велика для данного файла!')     }   } // ... }

Конфигурируем константы, которые потребуются для расшифровки:

export const MAX_HEXADECIMAL_VALUE = 0xFF  export const POSSIBLE_DIFFERENCE = {   EXIT_POINT: 3,   SEPARATOR: 2,   BINARY_ONE: 1,   BINARY_ZERO: 0, }

Реализуем сам механизм шифрования. При обходе bitmap data мы будем использовать маску Uint8Array, так как каждый символ здесь равен одному байту:

class Encryptor {   // ...   #updateUint8(char) {     // Текущий элемент bitmap data     const currentValue = +this.#view.getUint8(this.#offset)     // Установка значения в зависимости от символа const binaryChar = char === ',' ? POSSIBLE_DIFFERENCE.SEPARATOR : +char      // Проверка на возможность добавления     // Если текущее значение bitmap data после увеличения на 3      // больше верхней границы (255) - выполняем вычитание     if (currentValue >= MAX_HEXADECIMAL_VALUE - POSSIBLE_DIFFERENCE.EXIT_POINT) {       return currentValue - binaryChar     }  return currentValue + binaryChar   }    encrypt() {     // Получение начального положения bitmap data     this.#offset = this.#bmpParser.offsetBits // Конвертация текстового сообщения в бинарный код  // и приведение полученного результата к виду ['1', '0', '1', '0', ',', ...] const binaryChars = this.encode(this.#encryptionText).join().split('')      // Проверка длины сообщения this.#checkPhraseLength(binaryChars.length)      binaryChars.forEach(char => {       this.#view.setUint8(this.#offset, this.#updateUint8(char))       this.#offset++     })      // Добавляем точку выхода     this.#view.setUint8(this.#offset, this.#updateUint8(POSSIBLE_DIFFERENCE.EXIT_POINT))      return this.#view    }   // ... }

Зашифрованное изображение получится визуально неотличимо от оригинала. Это произойдет из-за того, что изменение значений в bitmap data происходит максимум на 3 пункта, а вес и структура файла при этом остаются неизменными.

Расшифровка

Для расшифровки потребуется оригинальное изображение — оно будет являться ключом, а также изображение с закодированным сообщением.

Алгоритм для расшифровки сообщения:

Нам потребуются две переменные для хранения бинарной последовательности символов и результата.

Запускаем цикл и, начиная с первого байта bitmap data, находим разность по модулю между значением ключа и закодированного изображения

  1. Если разность равна 0 или 1 — добавляем значение в строку с бинарной последовательность.

  2. Если разность равна 2 — очищаем строку с бинарной последовательностью, а ее значение конвертируем в текст и добавляем в результирующую строку.

  3. Если разность равна 3 — выполняем действия из п. 2 и останавливаем цикл.

Рассмотрим алгоритм на примере следующей разности: [1, 0, 1, 2, 1, 0, 1, 0, 3]

Разность

Значение бинарной последовательности

Значение результирующей строки

1

1

0

10

1

101

2

e

1

1

e

0

10

e

1

101

e

0

1010

e

3

Приступим к реализации:

export class Decipher {   #offset = 0   #encryptedView   #viewKey   #bmpParserEncrypted   #bmpParserKey    constructor(encryptedBuffer, bufferKey) {     this.#encryptedView = new DataView(encryptedBuffer)     this.#viewKey = new DataView(bufferKey)     this.#bmpParserEncrypted = new BmpParser(encryptedBuffer)     this.#bmpParserKey = new BmpParser(bufferKey)   }    // Конвертация бинарного кода в текст   decode(value) {     return String.fromCharCode(parseInt(value, 2))   }    decrypt() {     this.#offset = this.#bmpParserKey.offsetBits      let binaryChar = ''     let string = ''      while (true) {       // Получение разности       const encryptedByte = Math.abs(this.#encryptedView.getUint8(this.#offset) - this.#viewKey.getUint8(this.#offset))               switch (encryptedByte) {         case POSSIBLE_DIFFERENCE.EXIT_POINT:           string += this.decode(binaryChar)           return string         case POSSIBLE_DIFFERENCE.SEPARATOR:           string += this.decode(binaryChar)           binaryChar = ''           break         case POSSIBLE_DIFFERENCE.BINARY_ONE:         case POSSIBLE_DIFFERENCE.BINARY_ZERO:           binaryChar += encryptedByte           break         default:           throw new Error('Недопустимое значение!')       }        this.#offset++     }   } }

Код приложения на GitHub

Заключение

Это небольшое приложение позволяет показать, как работать с бинарными данными. При этом возможностей у типизированных массивов при работе с файлами куда больше: можно добавить водяной знак в изображение или видео, рассчитать размер изображения перед выводом на экран или прочитать файлы из zip-архива. 


ссылка на оригинал статьи https://habr.com/ru/articles/578284/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *