Привет, я бы хотел вам рассказать о том, как я учился программировать на языке Rust с нуля, для этого я выбрал цель — сделать QR-код-энкодер с помощью ментора из свободной школы «Пионир».
Пионир — это свободная, самоуправляемая и бесплатная школа в которой мы, ученики, изучаем и создаём проекты, которые решают какие-то социальные проблемы.
Для начала, почему я выбрал Rust? Во-первых, я хотел попробовать что-то новое, раньше я писал на простом Python, не имея опыта с чем-то более низкоуровневым и сложным, и во-вторых, этот язык выглядит перспективным и интересным.
QR-код-энкодер я выбрал из-за того, что этот проект не настолько сложный, и я смогу хорошо ознакомиться с основами Rust-а.
Также из-за того, что у нас есть планы на будущее, связанные с 2D-кодами, я хотел из этого проекта научиться, как работать с битами, с матрицей, с Rust-ом и его библиотеками, алгоритмом Рида-Соломона и с алгоритмами для исправления ошибок.
Что же такое QR-код, и как его генерировать? QR-код(Quick Response code) — это двухмерный штрихкод, который хранит в себе какую-то информацию и с помощью поисковых узоров легко считывается сканерами.
QR-коды хороши тем, что они быстро считываются и сохраняют большое количество информации, также с помощью алгоритма Рида-Соломона QR-коды могут скорректировать ошибки до 30%.
Для генерации QR кода нужно пройти вот эти этапы:
Кодирование данных.
Добавление служебной информации и заполнения.
Разделение информации на блоки.
Создание байтов коррекции.
Объединение блоков.
Размещение информации на QR-коде.
Моё решение
У меня были разные идеи по поводу архитектуры моего генератора, но я выбрал ту, которая, по моему мнению, самая простая и понятная.
Моя идея заключается в том, что существует четыре основные структуры:
QrMatrix ⏤ основная матрица, которая будет состоять из модулей, и её можно изменять с помощью координат.
DataBitvec ⏤ конвертирует строку в битовый вектор с помощью кодировки и добавляет коррекцию ошибок, которая дальше будет посылаться в DataEncoder.
DataEncoder ⏤ эта структура будет энкодировать данные в QrMatrix c помощью итератора ZigZagIt.
QrBuilder ⏤ получая как поле QrMatrix, он добавляет в него все базовые элементы, энкодирует информацию и создаёт маску.
Эти основные структуры взаимодействуют друг с другом в структуре QrBuilder, и в итоге получается готовая QR-код-матрица, которая в конце просто изобразит QR-код.
QrMatrix
В начале для QrMatrix я хотел создать свою структуру данных, которая бы просто состояла из Vec<Vec<Module>>
,и я мог делать все изменения с помощью функции поиска модулей по координатам, но я подумал, что не стоит изобретать велосипед, поэтому использовал библиотеку generic-matrix.
#[derive(Debug)]
pub struct QrMatrix {
size: usize,
modules: Matrix<Module>,
}
impl QrMatrix {
pub fn new(size: usize) -> Self {
let to_matrix_vec: Vec<Module> = vec![Module::Unknown; size * size];
let modules: Matrix<Module> = Matrix::from_vec(size, size, to_matrix_vec);
QrMatrix { size, modules }
}
С помощью size создаётся матрица размером size x size и заполняется модулями. Модуль — это enum, который сделан, чтобы отделить функциональные блоки от обычной информации, так гораздо удобнее и нет возможности изменить функциональные блоки.
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Module {
Unknown,
//Reserved,
Data(bool),
Function(bool),
}
impl Module {
pub fn set_module(&mut self, new_module: Module) {
*self = new_module;
}
pub fn is_fun(&self) -> bool {
match self {
Module::Function(_) => return true,
_ => return false,
}
}
pub fn flip_module(&mut self) {
match self {
Self::Data(true) => *self = Self::Data(false),
Self::Data(false) => *self = Self::Data(true),
_ => (),
}
}
}
Reserved
в модуле не используется в моей структуре: он нужен, для того чтобы информационные блоки были не заполнены до тех пор, пока не найдена лучшая маска для QR-кода, в моём случае я пропустил это, и всегда использую одну маску.
Для изменения модулей в матрице есть функции:
pub fn set_square(&mut self, size: usize, cordinate: (usize, usize), matrix_module: &mut Matrix<Module>) {
let start_width: usize = cordinate.1;
let start_height: usize = cordinate.0;
for i in 0..size {
for j in 0..size {
let new_module = matrix_module.index_mut((i, j));
self.set_module((i + start_height, j + start_width), *new_module);
}
}
}
pub fn set_module(&mut self, cordinate: (usize, usize), new_module: Module) {
let mut _module: &mut Module = self.get_mut_module(cordinate);
_module.set_module(new_module);
}
А для отображения QR-кода я использовал терминал:
FinderBuilder and TimingBuilder
Для генерации поисковых узоров в QR коде я создал константу в виде массива:
pub const FINDER_BLOCK: [[i32; FINDER_SIZE]; FINDER_SIZE] = [[1, 1, 1, 1, 1, 1, 1, 0],
[1, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 0, 0, 0, 0, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0]];
Так как поисковых узоров всегда три, и их начальные координаты легко вычислить, я подумал, что нет смысла генерировать напрямую в QrMatrix
, и просто константу FINDER_BLOCК
ротировал и вставлял целиком в определённые координаты.
Создание синхронных полос же намного легче, их координаты всегда известны, и длину можно легко узнать:
fn get_size_timing(&self) -> usize {
return self.matrix.get_size() - 2 * FINDER_SIZE;
}
Так как полоса всегда начинается с белого модуля, каждый ход меняя значение на противоположное, можно получить вектор с правильными модулями для синхронных полос.
DataEncoder
Для того чтобы что-то энкодировать в QR-код, нужно сперва конвертировать заданную строку в битовый вектор, для этого используются кодировки, которые могут принять в себя разные символы:
Цифровое кодирование ⏤ кодирует только цифры, используется 10 битов на 3 символа.
Буквенно-цифровое кодирование ⏤ кодирует прописные буквы латинского алфавита, цифры символы $%*+-./: и пробел.
Побайтовое кодирование ⏤ хоть и имеет маленькую плотность информации, может кодировать любой символ(например, в UTF-8).
Существует еще кодирование кандзи для иероглифов и других символов, но его я пропустил.
Для хранения битов и для их удобного использования я использовал библиотеку bit_vec. Также для удобной работы с информацией я создал структуру QrCodeBitvec
, которая имеет три поля: для хранения служебной информации (тип кодировки и количество символов), для хранения данных и для хранения байтов коррекции ошибок.
Для примера, чтобы показать, как работает эта система, я энкодирую число 1234 в QR-код.
Сперва, то что нужно сделать, — это узнать, какой тип кодировки стоит использовать и какое количество символов в строке. В нашем случае это цифровой тип кодировки и длина символов 4, и это преобразуется в BitVec
:0001 0000000100
Для длины символов в цифровом кодировании используется 10 битов.
Теперь надо конвертировать число 1234 в цифровую кодировку. Это делается довольно просто: делится на группы по 3 числа ⏤ 123 и 4 — потом эти числа конвертируются в BitVec
, для чисел больше 99 используется 10 битов, для чисел больше 9 — 7 битов и 4 битов для меньших, чем 9.
Теперь мы получаем вот это:
0001 0000000100 0001111011 0100
fn generate_bitvec(integer: u32) -> BitVec {
let bit_len = |num: u32| {
if num > 99 {
return 10;
}
else if num > 9 {
return 7;
}
else {
return 4;
}
};
let mut bitvector: BitVec = BitVec::new();
bitvector.reserve(bit_len(integer));
append_to_bitvec(&mut bitvector, &integer, bit_len(integer));
return bitvector;
}
Вот сейчас мы получили BitVec
длиной 28 битов, но спецификация QR-кодов заставляет заполнять данные полностью, в нашем случае нужно заполнить этотBitVec
до 152 битов. Чтобы это сделать, нам нужны три действия:
Добавить терминатор ⏤ заполняет 4 бита нулями.
Заполнить данные нулями, чтобы они могли делиться на 8.
Дальше циклично и поочерёдно добавлять 2 байта ⏤ 11101100 и 00010001.
Эти действия нужно выполнять, пока количество данных не будет равно 152.
Вот так будут выглядеть данные в конце:
0001 0000000100 0001111011 0100 0000<11101100 и 00010001 до конца>
Эти данные готовы для отправления в алгоритм Рида-Соломона.
Алгоритм Рида-Соломона
Для этого алгоритма я использовал библиотеку reed-solomon.
Я не особо хорошо знаю, как этот алгоритм работает с математической стороны, но я пытался хотя бы понять его логику, а также её правильно использовать для этого генератора. Человек, который также создавал генератор QR-кода написал свою имплементацию этого алгоритма, но этот код мне не понравился, плохие наименования и странная структура мешали понимать логику его алгоритма, а также в этом не было много смысла, поэтому я отбросил эту идею и просто взял готовую библиотеку.
Наши данные, все 152 бита, мы отправляем в Encoder-структуру этой библиотеки. Эта структура принимает в себя одно значение ⏤ количество байтов коррекции, для первой версии QR-кода с уровнем коррекции L используется 7 байтов коррекции:
fn create_reed_solomon_encoder(num_error_correction_blocks: usize) -> Encoder {
let reed_encoder: Encoder = Encoder::new(num_error_correction_blocks);
return reed_encoder;
}
И потом, конвертируя байты из BitVec
в десятичные числа, мы шлём эти кодовые слова в структуру Encoder
:
pub fn create_error_corrections_blocks(data: Vec<u8>, num_error_correction_blocks: usize) -> Vec<u8> {
let reed_encoder: Encoder = Self::create_reed_solomon_encoder(num_error_correction_blocks);
let data_copy = reed_encoder.encode(&data).to_vec();
return data_copy;
}
Вставляем эти байты коррекции в конецBitVec-а
:
0001 0000000100 0001111011 0100 0000<11101100 и 00010001 до конца данных><7 байтов коррекции>
Эти готовые данные мы энкодируем в QR-код.
ZigZagIt и DataEncoder
Для энкодирования информации в QR код я создал итератор, который будет давать координаты, начиная с нижней правой стороны. Двигаясь зигзагообразно и пропуская функциональные блоки, чтобы их не изменить, мы получим правильные координаты для вставки информации:
impl Iterator for ZigZagIt {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
if !self.valid { return None; }
let cordinates: (usize, usize) = (self.row_cordinate, self.column_cordinate);
self.move_cordinate();
return Some(cordinates);
}
}
fn move_horizontaly(&mut self) {
match self.column_cordinate {
0 => self.valid = false,
6 => self.column_cordinate -= 2,
_ => self.column_cordinate -= 1,
}
self.horizontaly_next = false;
}
fn move_vertical(&mut self) {
if (self.upward && self.row_cordinate == 0) || (!self.upward && self.row_cordinate == self.matrix_size - 1) {
self.upward = !self.upward;
self.move_horizontaly();
}
else {
if self.upward { self.row_cordinate -= 1 }
else if !self.upward { self.row_cordinate += 1 }
self.column_cordinate += 1;
}
self.horizontaly_next = true;
}
И теперь, идя по BitVec-у
и вставляя в координаты биты, мы получаем QR-код с правильными данными, теперь мы должны просто маскировать их, и получаем готовый, рабочий QR-код.
Вот так будет выглядеть число 1234 в первой версии, с уровнем коррекции L и с первой маской.
Заключение
С помощью этого проекта я выучил много полезного, как минимум, я начал лучше понимать программирование и научился писать на Rust, но мне есть, куда расти, и я бы очень хотел дальше развиваться в программировании.
В дальнейшем у нас есть планы создавать и другие интересные проекты, например — полярную 2D-код-матрицу, которая будет похожа на ShotCode, но более эффективную.
Если у вас есть советы или проблемы, которые стоит решить в моём коде, свободно рассказывайте о них. Если захотите увидеть целый код, вот мой репозиторий на GitHub.
Комментарии (10)
Karbofoss
23.08.2023 14:22+1А можно ссылку на пионир?
AbrielTartar Автор
23.08.2023 14:22Да, конечно: https://pionir.org. Это Сербская школа которая обучает нас, в основном с помощью практики. Мы много чем занимаемся, и пытаемся улучшить окружение око нас.
snaiper04ek
23.08.2023 14:22+1Сайт чуть-чуть не информативен, поэтому разрешите задать вопрос: со скольки и до скольки лет принимаются заявки? Сайт на сербском без возможности выбора языка, значит ли это то, что нужно знать сербский? На каком языке говорят в вашем чате?
И, как я понял из обрывков информации, там кроме программирования ещё проектируют устройства на печатных платах?AbrielTartar Автор
23.08.2023 14:22+2Да, сайт и правда не информативен, так-как староват, мы сейчас создаём новый.
Пока что здесь принимаются дети которые знают Сербский и Английский, но если что, здесь есть люди которые говорят на нескольких языках, например наш директор знает Русский.
Заявки же принимаются с 13 до 17 лет, но с идеей что ученики останутся и дальше в школе.
Кроме программирования мы много чем занимаемся, например: электроника, дизайн, этика, право, социология, экономия, и даже садоводство в каком-то смысле, всё это учится вместе, интегрировано в остальные моменты. Но ученик сам выбирает свою сферу обучения, по своему желанию и возможностям.
Надеюсь это немного прояснило про нашу школу.
Helltraitor
Советую всем к ознакомлению с серией статей от Энтони Фу
https://antfu.me/posts/ai-qrcode
Касаемо статьи: много
return
, которые можно опустить.Если необходимо было освоиться в Rust, то считаю, что лучшим решением было бы написать крейт с нуля
AbrielTartar Автор
Спасибо за комментарий! В моём коде я хотел правильно делить функции, но я кажется перестарался. Return же я просто привык писать, но кажется стоить себя переучивать. В следующий раз я исправлю эти недочёты.
Про создание своего крейта, Вы это про алгоритм Рида Соломона?
Helltraitor
В том числе. Это был бы очень хороший опыт
AbrielTartar Автор
Да, конечно, у нас много планов, и алгоритмы для коррекции ошибок одни из них, этот проект это только начало.
domix32
Ещё б неплохо почитать про
match
и про DRY.Пелёнка превращается
В элегентную функцию
AbrielTartar Автор
И правда, выглядит очень хорошо, спасибо за пример, я обязательно почитаю про это.