Конвертер в текстовую графику

Вас пригласили поучаствовать в разработке приложения, умеющего скачивать картинки по URL и конвертировать изображения в текстовую графику (т. е. в текст из разных символов, которые в совокупности выглядят как изображение). Вот пример его работы. Картинка на нём — это текст из мелких символов:

Скачиваем проект

Часть приложения уже написана. Ваша задача — доработать его основную логическую часть в соответствии с требованиями. Поэтому для начала работы нужно скачать заготовку проекта. Для этого откройте идею, выберите в меню File -> New -> Project from Version Control. В открывшемся окне в поле URL введите https://github.com/netology-code/java-diplom, как показано на картинке, и нажмите Clone.

Если после нажатия выскочила ошибка об отсутствии гита на вашем компьютере, установите гит по этой инструкции и попробуйте снова.

После того как идея скачает проект, убедитесь, что для него выбрана версия Java. Откройте File -> Project structure и убедитесь, что это так. Версия должна быть 11-й.

После откройте класс Main и запустите метод main. Запуск должен завершиться ошибкой:

Exception in thread "main" java.lang.IllegalArgumentException: Серверу нужно передать в конструктор объект-конвертер, а было передано null.
	at ru.netology.graphics.server.GServer.<init>(GServer.java:24)
	at ru.netology.graphics.Main.main(Main.java:13)

Если вы видите эту ошибку, проект настроен верно. Если нет, напишите своему руководителю по курсовой, он подскажет с настройкой.

Структура проекта

Перед вами java-проект с несколькими незнакомыми вам папками (например, assets), которые вам не нужно будет трогать. Нас будут интересовать .java-файлы, которые располагаются по пакетам:

Класс / Интерфейс Для чего?
BadImageSizeException Класс исключения, которое вы будете выбрасывать
TextColorSchema Интерфейс цветовой схемы, который вы будете реализовывать
TextGraphicsConverter Интерфейс конвертера картинок, который вы будете реализовывать
GServer Готовый класс сервера, который будет использовать ваш конвертер; вам туда не нужно
Main Запуск приложения. В нём запускается сервер, также в нём можно будет конвертировать картинки в текстовые файлы без сервера

Требования к конвертеру

В проекте уже написан класс сервера, который будет использовать ваш конвертер. Т. к. конвертер ещё не написан, был создан специальный интерфейс TextGraphicsConverter, объект которого сервер ждёт параметром в Main:

        TextGraphicsConverter converter = null; // Создайте тут объект вашего класса конвертера

        GServer server = new GServer(converter); // Создаём объект сервера
        server.start(); // Запускаем

Все требования к конвертеру описаны в интерфейсе. Чуть ниже мы их подробнее разберём. Самый главный метод — это метод convert, который принимает параметром URL в виде текста, например, «https://raw.githubusercontent.com/netology-code/java-diplom/main/pics/simple-test.png», внутри метода качает и анализирует картинку, после чего отдаёт значение типа String, в котором содержится это изображение в виде текстовой графики.

Помните, что в Java String — это текст и не обязательно всего одна строчка текста. Т. е. в один объект типа String можно занести многострочный текст, а разделителем строк (по сути Enter) будет специальный символ, который пишется в коде как \n. В итоге у вас в конце каждой строчки текстового изображения будет символ переноса строки (запись в коде — \n).

Пример работы конвертера, результат которого выводим в консоль:

        String url = "https://raw.githubusercontent.com/netology-code/java-diplom/main/pics/simple-test.png";
        String imgTxt = converter.convert(url);
        System.out.println(imgTxt);

В итоге мы видим такой результат, где более тёмные участки заменяются на более жирные символы, а светлые на более незаметные символы:

Также интерфейс конвертера требует от него возможность выставлять ему настройки перед конвертацией:

  • максимально допустимое соотношение сторон (ширины и высоты). Если метод не вызывали, то любое соотношение допустимо;
  • максимально допустимую высоту итогового изображения. Если метод не вызывали, то любая высота допустима;
  • максимально допустимую ширину итогового изображения. Если метод не вызывали, то любая ширина допустима;
  • текстовую цветовую схему — объект специального интерфейса, который будет отвечать за превращение степени белого (числа от 0 до 255) в символ. Если метод не вызывали, должен использоваться объект написанного вами класса как значение по умолчанию.

Например, следующий конвертер не должен конвертировать, если ширина больше длины в три раза, т. к. максимальное соотношение сторон ему выставлено в 2:

        TextGraphicsConverter converter = ...; // Создайте тут объект вашего класса конвертера
	converter.setMaxRatio(2);  // выставляет максимально допустимое соотрношение сторон картинки
	String imgTxt = converter.convert(...); // для слишком широкой картинки должно выброситься исключение BadImageSizeException.

Будьте внимательны: когда вы передадите конвертер серверу, он выставит ему свои желаемые параметры, которые будут влиять на конвертацию:

image

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

Общая схема работы метода convert будет соответствовать последовательности действий (подробнее описаны ниже):

  1. Скачиваем картинку по URL.
  2. Менеджеру могли выставить максимально допустимое соотношение сторон (ширины и высоты); если оно слишком большое, то конвертация не делается и выбрасывается исключение.
  3. При конвертации мы будем менять каждый пиксель на символ: чем пиксель темнее, тем жирнее символ, который мы подставим. Менеджеру могли выставить максимальные ширину и высоту итоговой картинки, при этом если исходная картинка больше, то нам надо уменьшить её размер, соблюдая пропорции.
  4. Превращаем цветное изображение в чёрно-белое, чтобы мы смотрели только на интенсивность цвета, а не подбирали для красного одни символы, для зелёного другие и т. п.
  5. Перебираем все пиксели изображения, спрашивая у них степень белого (число от 0 до 255, где 0 — это чёрный, а 255 — это светлый). В зависимости от этого числа выбираем символ из заранее подготовленного набора.
  6. Собираем все полученные символы в единую строку, отдаём как результат конвертации.

Костяк метода convert:

    @Override
    public String convert(String url) throws IOException, BadImageSizeException {
        // Вот так просто мы скачаем картинку из интернета :)
        BufferedImage img = ImageIO.read(new URL(url));

        // Если конвертер попросили проверять на максимально допустимое
        // соотношение сторон изображения, то вам здесь нужно сделать эту проверку,
        // и, если картинка не подходит, выбросить исключение BadImageSizeException.
        // Чтобы получить ширину картинки, вызовите img.getWidth(), высоту - img.getHeight()

        // Если конвертеру выставили максимально допустимые ширину и/или высоту,
        // вам нужно по ним и по текущим высоте и ширине вычислить новые высоту
        // и ширину.
        // Соблюдение пропорций означает, что вы должны уменьшать ширину и высоту
        // в одинаковое количество раз.
        // Пример 1: макс. допустимые 100x100, а картинка 500x200. Новый размер
        // будет 100x40 (в 5 раз меньше).
        // Пример 2: макс. допустимые 100x30, а картинка 150x15. Новый размер
        // будет 100x10 (в 1.5 раза меньше).
        // Подумайте, какими действиями можно вычислить новые размеры.
        // Не получается? Спросите вашего руководителя по курсовой, поможем.
        int newWidth = ???;
        int newHeight = ???;

        // Теперь нам нужно попросить картинку изменить свои размеры на новые.
        // Последний параметр означает, что мы просим картинку плавно сузиться
        // на новые размеры. В результате мы получаем ссылку на новую картинку, которая
        // представляет собой суженную старую.
        Image scaledImage = img.getScaledInstance(newWidth, newHeight, BufferedImage.SCALE_SMOOTH);

        // Теперь сделаем её чёрно-белой. Для этого поступим так:
        // Создадим новую пустую картинку нужных размеров, заранее указав последним
        // параметром чёрно-белую цветовую палитру:
        BufferedImage bwImg = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_BYTE_GRAY);
        // Попросим у этой картинки инструмент для рисования на ней:
        Graphics2D graphics = bwImg.createGraphics();
        // А этому инструменту скажем, чтобы он скопировал содержимое из нашей суженной картинки:
        graphics.drawImage(scaledImage, 0, 0, null);

        // Теперь в bwImg у нас лежит чёрно-белая картинка нужных нам размеров.
        // Вы можете отслеживать каждый из этапов, в любом удобном для
        // вас моменте сохранив промежуточную картинку в файл через:
        // ImageIO.write(imageObject, "png", new File("out.png"));
        // После вызова этой инструкции у вас в проекте появится файл картинки out.png

        // Теперь давайте пройдёмся по пикселям нашего изображения.
        // Если для рисования мы просили у картинки .createGraphics(),
        // то для прохода по пикселям нам нужен будет этот инструмент:
        WritableRaster bwRaster = bwImg.getRaster();

        // Он хорош тем, что у него мы можем спросить пиксель на нужных
        // нам координатах, указав номер столбца (w) и строки (h)
        // int color = bwRaster.getPixel(w, h, new int[3])[0];
        // Выглядит странно? Согласен. Сам возвращаемый методом пиксель — это 
        // массив из трёх интов, обычно это интенсивность красного, зелёного и синего.
        // Но у нашей чёрно-белой картинки цветов нет, и нас интересует
        // только первое значение в массиве. Ещё мы параметром передаём интовый массив на три ячейки.
        // Дело в том, что этот метод не хочет создавать его сам и просит
        // вас сделать это, а сам метод лишь заполнит его и вернёт.
        // Потому что создавать массивы каждый раз слишком медленно. Вы можете создать
        // массив один раз, сохранить в переменную и передавать один
        // и тот же массив в метод, ускорив тем самым программу.

        // Вам осталось пробежаться двойным циклом по всем столбцам (ширина)
        // и строкам (высота) изображения, на каждой внутренней итерации
        // получить степень белого пикселя (int color выше) и по ней
        // получить соответствующий символ c. Логикой превращения цвета
        // в символ будет заниматься другой объект, который мы рассмотрим ниже
        for ??? {
          for ??? {
            int color = bwRaster.getPixel(w, h, new int[3])[0];
            char c = schema.convert(color);
            ??? //запоминаем символ c, например, в двумерном массиве или как-то ещё на ваше усмотрение
          }
        }

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

        return ???; // Возвращаем собранный текст.
    }

Требования к цветовой схеме

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

public interface TextColorSchema {
    char convert(int color);
}

Предлагается следующая логика его работы. Вот список символов от самых тёмных к самым светлым: '▇', '●', '◉', '◍', '◎', '○', '☉', '◌', '-'. Если вы программируете на Windows, рекомендуем другой список из более стандартных символов, иначе может отрисовываться криво: '#', '$', '@', '%', '*', '+', '-', '''. В зависимости от переданного значения интенсивности белого должен выбираться соответствующий символ. Например, если значение близко к 0, то выбрать надо '▇'; если к 255, то '-'. Если где-то посередине, то и выбирать надо тоже где-то посередине.

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

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

Внимание: Все ваши новые классы должны быть в пакете ru.netology.graphics.image, никакие другие классы, кроме класса Main, менять нельзя.

Тестирование, отладка и решение проблем

После того как вы реализуете классы, откройте класс Main и заполните переменную для конвертера объектом вашего класса, чтобы он был передан серверу. Теперь после старта метода main будет запущен сервер на строке server.start(). Сервер будет писать о происходящем в консоль. Если ему удалось успешно стартовать, страница приложения будет доступна по локальному для вашего компьютера адресу http://localhost:8888/ .

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

Для тестирования можете использовать URL следующих изображений:

  • Простое маленькое изображение для отладки: https://raw.githubusercontent.com/netology-code/java-diplom/main/pics/simple-test.png
  • Другое изображение для демонстрации: https://i.ibb.co/6DYM05G/edu0.jpg

Во время поиска проблем вы также можете в Main закомментировать секцию, запускающую сервер, и раскомментировать логику, выводящую сконвертированное изображение в консоль. Также вы можете воспользоваться отладчиком:

С чего начать

Вы сами выбираете, с чего вам легче начать. Ниже представлен один из таких вариантов:

  1. Начните реализацию с класса цветовой схемы. Создайте в пакете ru.netology.graphics.image класс, имплементирующий интерфейс TextColorSchema.
  2. Реализуйте в нём метод конвертации цвета в символ, согласно требованиям к цветовой схеме, которые изложены выше.
  3. Проверьте вашу цветовую схему. Создайте в Main её объект и попробуйте вызвать её метод. Правда ли, что на цветах ближе к белому будут более светлые символы и наоборот? Если это не так, проверьте работу отладчиком.
  4. Приступите к реализации класса конвертера. Создайте в пакете ru.netology.graphics.image класс, имплементирующий интерфейс TextGraphicsConverter.
  5. Сначала реализуйте просто метод convert, не обращая внимания на дополнительные требования: проверка картинки на макс. размеры и т. д.
  6. Создайте объект конвертера и передайте его серверу в main или протестируйте в текстовом режиме, как было описано выше. Если вы сразу не реализовывали функции масштабирования картинки, то тестируйте только на небольших картинках.
  7. Если конвертация работает, приступайте к реализации настроек конвертера — логики с проверкой на максимальное соотношение сторон, масштабирование картинки и т. д. Реализуйте функциональность по одной за раз, добиваясь работспособности вашего конвертера после каждой волны изменений. При возникновении трудностей пользуйтесь отладчиком.
  8. Перечитайте условие — все ли требования выполнены? Протестируйте конвертер на разных картинках — работает ли? Если всё хорошо, ознакомьтесь с разделом «Как сдавать» и приступайте к отправке работы на проверку.

Как сдавать

Перед отправкой работы верните класс Main в исходное состояние, добавив только создание конвертера, т. е. оставьте режим с запуском сервера. Все ваши новые классы должны быть в пакете ru.netology.graphics.image, никакие другие классы, кроме класса Main, менять нельзя.

При отправке прикрепите файл Main.java и все созданные вами .java-файлы. Другие файлы прикреплять не нужно, также не стоит объединять все файлы в один архив, скидывать ссылку на репозиторий, реплит и прочее.