Я не пишу на Rust каждый день, но какое-то время мне это очень нравилось. Rust - чрезвычайно производительный, мощный и довольно элегантный язык.

И хотя я новичок в Rust, я хотел бы поделиться процессом создания игрушечного приложения, которое я недавно разработал. Это очень простой инструмент командной строки, который печатает файл изображения, используя символы ASCII, прямо на ваш терминал. (Вы можете найти его здесь https://github.com/mightykho/rascii)

Мы начнем с самого простого варианта использования - статического ввода изображения и вывода ASCII - и перейдем к цветному выводу и анимации gif, добавляя по ходу более совершенные абстракции.

Итак, давайте поговорим о том, чего мы пытаемся достичь. Что будет входом и выходом нашего приложения? Я ожидаю, что пользователь укажет путь к изображению и получит вывод, состоящий из символов ASCII и похожий на предоставленное изображение.

Создание нового проекта

Итак, без лишних слов, давайте сразу перейдем к делу. Для начала убедитесь, что у вас установлен Cargo. Затем нам нужно создать новый проект на Rust:

$ cargo new ascii_renderer; cd ascii_renderer

и откройте Cargo.toml файл. Этот файл содержит информацию о нашем пакете и его зависимостях. На данный момент нам нужно всего два:

  1. Изящный пакет под названием Clap, который помогает вам обрабатывать аргументы командной строки. Нам это нужно, так как мы создаем инструмент командной строки.
  2. И очень мощный пакет под названием Image, который - сюрприз, сюрприз - обрабатывает изображения за вас.

В результате наш [dependencies] раздел в Cargo.toml должен выглядеть примерно так:

Обратите внимание, что мы указали yamlfeature для clap. Это позволит нам создать cli.yml файл, который будет содержать всю информацию о приложении и аргументах, которую мы предоставим нашему пользователю.

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

$ cargo build

После этого мы создадим наш src/cli.yml.

По сути, это означает, что нашему приложению требуется один аргумент, и это будет «изображение».

Затем откройте src/main.rs (это основная точка входа для любого приложения Rust) и давайте скажем Rust, чтобы он действительно использовал наш cli.yml.

Давайте посмотрим, что здесь происходит ...

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

В строках 5 и 6 мы используем макрос Clap для загрузки нашего yaml-файла, а затем получаем аргументы, переданные нашему приложению. Далее мы получаем image_path аргумент. Обратите внимание на unwrap в конце; мы используем его, потому что, согласно документации Клапа, value_of метод возвращает Option<&str>. Тип Option - это перечисление, которое содержит либо Some(value), либо None. Дело в том, что в Rust нет концепции null (nil), поэтому каждый метод, который потенциально может ничего не возвращать, является необязательным значением.

Есть много интересных приемов, как безопасно получить исходное значение из Option. Метод unwrap не входит в их число :) Если значение image_path равно None, наше приложение выйдет из строя, но мы можем безопасно использовать его здесь, поскольку библиотека Clap гарантирует, что image_path был передан (поскольку он отмечен как требуемый) еще до того, как мы попытаться получить его ценность.

Затем мы просто используем макрос println! для вывода значения image_path на консоль.

Давай запустим ...

$ cargo run test.png
 Finished dev [unoptimized + debuginfo] target(s) in 0.07s
 Running `target/debug/ascii_renderer test.png`
Passed image path is test.png

Все кажется правильным, но не очень захватывающим.

Изменение размера изображения до размера вывода терминала

Давайте подумаем, что мы хотим делать дальше. На данный момент я могу представить, что мы хотим получить вывод определенной ширины (скажем, 100 символов) и высоты, которая рассчитывается на основе соотношения сторон изображения. У нас есть размеры изображения и ширина вывода. Нам нужно рассчитать высоту вывода. Для расчета выходной высоты мы используем формулу, которая выглядит так:

output_height = (img_height / img_width) * output_width

Затем мы хотим изменить размер нашего изображения до output_width × output_height, чтобы для каждого пикселя у нас был один символ. Затем мы хотим перебрать пиксели и распечатать их на терминале. На данный момент мы выведем любой символ для каждого пикселя, чтобы убедиться, что все работает должным образом.

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

Затем мы определяем OUTPUT_WIDTH константу. (вы должны четко указать тип константы)

После этого мы открываем изображение (снова None значение было обработано image), затем мы получаем ширину и высоту изображения для расчета соотношения сторон. Обратите внимание, что мы конвертируем размеры в f64, чтобы получить значение соотношения сторон с плавающей запятой. Затем мы используем это значение для получения выходной высоты, которая сохраняется как u32. Мы используем output_height и output_width для создания версии изображения с измененным размером.

Затем мы определяем изменяемую переменную cur_y, которая представляет текущую строку пикселей, над которой мы работаем. Обратите внимание, что мы должны использовать ключевое слово mut. По умолчанию все переменные в ржавчине неизменяемы, а это означает, что вы не можете изменить их, если они не отмечены таким образом.

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

Если значение текущей строки пикселя отличается от координаты y пикселя, мы печатаем новую строку и переназначаем значение cur_y. В качестве отправной точки мы выведем звездочку для каждого пикселя.

Давай попробуем ...

$ cargo run 1.png

И снова наш результат не выглядит очень захватывающим.

********************************************************************
********************************************************************
********************************************************************
********************************************************************
********************************************************************
********************************************************************
********************************************************************
********************************************************************
...

Кроме того, он выглядит слишком высоким. Не волнуйтесь, мы решим это позже.

Превращение пикселей в символы

Затем давайте определим «палитру» символов, которую мы будем использовать для нашего цветового представления. Я рекомендую использовать эти символы .:-=+*#%@, поскольку они очень хорошо работают в монохромном спектре (вы можете перевернуть этот список, если используете яркую тему в своем терминале). Затем нам нужно выбрать соответствующий символ для каждого пикселя в зависимости от его яркости. Кроме того, мы хотим сначала обесцветить наше изображение до четных значений R, G и B (которые представлены как значение u8 (0..255)).

Затем мы используем простую формулу, чтобы получить индекс символа в нашем списке:

color_value = any_color / 255.0;
char_index = color_value * (char_list_length - 1);

Затем мы берем этого персонажа и выводим его на консоль.

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

Причина этого в том, что пиксели в обычных изображениях представляют собой квадраты, но символы в нашем рисунке ASCII занимают больше места по вертикали в зависимости от высоты строки, которую вы используете. У наших персонажей есть собственное соотношение сторон, поэтому нам нужно использовать его при вычислении соотношения сторон изображения. Это значение будет отличаться в зависимости от настроек шрифтов, но для меня это 2,75.

Применяя эти изменения, мы получаем:

и мой окончательный результат выглядит так:

Убедитесь, что вы запустили cargo build --release, чтобы собрать двоичный файл без функции отладки. Это сделает его в несколько раз быстрее;)

PS

В этом крошечном руководстве мы написали самую простую реализацию средства визуализации ASCII. Мы можем многое сделать, чтобы улучшить его, например:

  • Сделайте его более гибким, позволяя пользователю передавать дополнительные аргументы
  • Добавить поддержку анимации gif
  • Используйте xterm-256color для цветного вывода

.. и более. Пожалуйста, дайте мне знать, что вы хотели бы увидеть во второй части!