Динамічна графіка в Java сервлетах

Кен Маккрері
Переклад на російську © Віктор Смирнов, 2000
Оригінал статті опубліковано на сайті Javable.com
www.citforum.ru

Огляд

Графіки та діаграми дозволяють полегшити сприйняття аналітичної інформації. Уявіть, наприклад, таблицю вимірювань параметрів завантаженості каналу, що проводяться кожні п'ять хвилин. Важко повірити, що у вас вистачить терпіння переглядати її хоча б раз на тиждень. Тоді як, відповідна діаграма видається цілком легкотравної і вичерпною. У багатьох випадках зображення повинні створюватися в реальному часі по запиту користувача. Потреба в цьому може бути зумовлена природою даних, що відображаються, у разі, коли дані надходять безперервно або змінюються з часом, або ж можливістю значно зменшити використовується дисковий простір. Звичайно ж, не завжди доречно (технічно можливе) конструювати зображення динамічно, але, там, де це дійсно необхідно, використання вдалих технологічних рішень може призвести до значного виграшу в продуктивності і якості сервісу.

Читання і запис зображень, вимоги до JDK

Для того щоб сервлет – додаток, що виконується на сервері, що обробляє запити користувачів, – міг динамічно створювати зображення, перш за все, необхідний механізм читання і запису графічних файлів. Точніше, сервлет повинен вміти відправити отриману картинку у відповідь на запит користувача. Основні бібліотеки (Core API) для Java не надають коштів, використовуючи які можна було б зберегти отримане в пам'яті зображення в одному з графічних форматів. Є бібліотеки Sun для Java 1.1 і ліцензовані Sun бібліотеки для Java 1.2. Оскільки, вони розташовані в пакеті com.sun вони не відносяться ні до основних бібліотеках, ні до стандартних розширень (standard extension), тому використовують їх застосування не можна вважати стерпними (portable). Іншими словами, вони можуть і не працювати на віртуальній машині, випущеної іншою компанією. Варто зазначити, що затверджений запит Java Specification Request JSR-000015 на стандартне розширення Java 2 містить опис механізму для читання і запису графічних файлів; коли він буде реалізований, можна буде писати стерпні програми, що здійснюють введення / вивід зображень.

Для цієї статті я підготував приклади, що працюють на платформах Java 1.1 і Java 1.2.

Формати зображень

Формат GIF – найпоширеніший формат графічних файлів у Веб. Він широко підтримується броузерами, в тому числі і найпершими. На жаль, написання програм, що генерують зображення в цьому форматі, потенційно утруднено патентом на алгоритм стиснення даних. Програм, наведені в цій статті, створюють зображення у форматі JPEG і PNG. Формат JPEG обраний, перш за все, з тих міркувань, що реалізація Sun Java 1.2 дозволяє формувати зображення цього типу без застосування додаткових бібліотек. (У прикладі програми для JDK 1.1 можна отримати файл не тільки у форматі PNG, але і в багатьох інших, в тому числі – JPEG і GIF – прим. перекладача)

Одне з відмінностей між форматами JPEG і GIF полягає в тому, що алгоритм стиснення даних, який використовується в GIF, на відміну від алгоритму, що використовується в JPEG, не спотворює зображення. Зазвичай, артефакти в JPEG картинках не сильно помітні. (Слід зазначити, що формат JPEG краще справляється з поданням фотографій, ніж тексту, діаграм або зображень, що містять тонкі лінії і чіткі межі колірних переходів – прим. перекладача) Другий приклад застосовує формат PNG з алгоритмом стиснення без втрати даних і, до того ж, вільний від правових труднощів.

Архітектура сервлета

Розіб'ємо нашу програму на дві частини. Перша частина – сервлет – відповідає за обробку HTTP запиту і повертає клієнту потрібне зображення, якщо це можливо. Друга частина, клас, що формує зображення. Для простоти реалізації, як параметр при зверненні до сервлету буде передаватися ім'я використовуваного класу. Відповідний Java клас повинен реалізовувати певний інтерфейс для спілкування з сервлетом. Наведемо опис цього інтерфейсу:

public interface ImageProducer {
        /**
* MIME тип створюваного зображення.
         *
* @ Return MIME тип зображення.
         */
        public String getMIMEType();
        /**
* Створює зображення і записує його у вказаний потік.
         *
* @ Param stream Куди писати картинку.
         */
        public void createImage(OutputStream stream)
                 throws IOException;
}

Інтерфейс ImageProducer містить метод для визначення типу зображення і метод для формування зображення. Отримана картинка відправляється клієнту.

Наступний код демонструє, як сервлет працює з класами, що реалізують інтерфейс ImageProducer:

ImageProducer imageProducer =
        (ImageProducer)
     Class.forName(request.getQueryString()).newInstance();
response.setContentType(imageProducer.getMIMEType());
imageProducer.createImage(response.getOutputStream());

Сервлет створює (завантажується) клас з ім'ям, вказаним в параметрах запиту – частини URL справа після "?". Отриманий клас приводиться до типу ImageProducer. Потім, звертаючись до відповідних методів, сервлет отримує тип і формує зображення. У разі якщо немає помилок, картинка пересилається клієнту.

Наведений код може викликати кілька винятків, найбільш поширені з них: ClassNotFoundException і ClassCastException. Перше викликано тим, що клас, ім'я якого передано в якості параметра запиту, не доступний завантажувачу (ClassLoader), друге ж тим, що вказаний клас не реалізує інтерфейс ImageProducer. У випадку помилки клієнт, звичайно ж, не отримує картинку, і броузер виводить зображення, показує, що сервер не відповів на запит. Програма тестувалася з використанням Java Server Web Development Kit (JSWDK 1.0.1), але ви повинні отримати аналогічні результати на більшості інших Веб серверах, які підтримують Java.

Створення зображень у Java 1.1

Для свого першого прикладу я підготував код, який буде працювати на платформі Java 1.1. Хоча віртуальні машини JVM 1.2 досить поширені, не всі можуть розміщувати програми на цій платформі. На жаль, провайдери не сильно поспішають оновлювати віртуальні машини на своїх серверах. Як наслідок, доводиться розробляти сервлети, що працюють на платформі Java 1.1. Це не сильно ускладнять завдання; тим не менш, є деякі складнощі, пов'язані з формуванням зображень, які ми обговоримо нижче.

Будемо будувати кругову діаграму (pie chart), яку ми, для повноти картини, розфарбуємо і підпишемо. На діаграмі будуть зображені напої, уживані розробниками програмного забезпечення. Подивимося, як можна це зробити.

Для операцій вводу / виводу зображень в Java 1.1 Sun пропонує бібліотеку Jimi. Sun придбав її у невеликої компанії. Перед тим, як почати поширювати цю бібліотеку, компанія Sun перенесла класи в пакет com.sun, а в іншому нічого не змінила.

Наступні кроки необхідно виконати для формування PNG зображення з використанням Jimi.

  1. Створити вікно програми (Frame для отримання зображення.
  2. Використовую AWT зображення, створити реалізацію класу Graphics.
  3. Намалювати картинку за допомогою об'єкту Graphics.
  4. Створити об'єкт JimiWriter на основі потоку даних, переданих клієнтові.
  5. Перетворити зображення у відповідний формат і відправити його клієнту.

Для формування і модифікації зображень на платформі Java 1.1 необхідно використовувати активну AWT компоненту. Як правило, сервлет не повинен створювати AWT компоненти, які зазвичай використовуються в додатках з графічним інтерфейсом користувача. У цьому випадку, вам потрібен графічний (AWT) об'єкт для того, щоб отримати об'єкт класу Graphics, за допомогою якого можна малювати нові картинки. Доведеться змиритися з маленьким віконцем на консолі Веб сервера. Якщо вам відомі способи створення зображення в Java 1.1 без використання AWT компонент, будь ласка, повідомте мені. Один з можливих варіантів, завантажити з диска сервера заздалегідь підготовлений шаблон, наприклад методом java.awt.Toolkit.createImage (). Після того, як ви створили AWT зображення, ви можете отримати реалізацію класу Graphics і використовувати її для малювання. Маючи у своєму розпорядженні об'єкт Graphics, ви можете закрити непотрібне вікно на консолі сервера. Я залишу цей експеримент зацікавленим читачам.

Нагадаю, що для зв'язку з класом, який малює кругову діаграму, сервлет використовує інтерфейс ImageProducer. Тому наш клас JIMIProducer повинен реалізовувати цей інтерфейс. Перш за все, створимо AWT вікно, AWT зображення і, нарешті, об'єкт Graphics:

        Frame f = new Frame();
        f.setVisible(true;
        image = f.createImage(ImageWidth, ImageHeight);
        graphics = image.getGraphics();
        f.setVisible(false);

Ключовий метод класу JIMIProducer – drawSlice. Отримуючи на вході мітку і величину сегменту в градусах, цей метод малює сегмент діаграми, розфарбовує його і підписує. Далі, я розповім детальніше, як це відбувається.

Будемо заповнювати "нутрощі" діаграми за годинниковою стрілкою, починаючи з положення "три години", тобто першу лінію проведемо з центру до кола обмежує діаграму в положенні годинникової стрілки в три години дня (чи ночі, як вам більше подобається). Відступимо за годинниковою стрілкою необхідну кількість градусів і намалюємо другу лінію, що обмежує сегмент діаграми. І так далі. Наведений нижче код отримує на вході величину сегменту в градусах і малює кордон цього сегмента. Він викликається послідовно для всіх сегментів, при цьому передбачається, що в сумі кутові величини складуть 360 градусів.

        //*************************************************
/ / Перетворимо кут в радіани
/ / 1 градус = pi/180 радіан
        //*************************************************
        doubletheta = degrees * (3.14/180);
        currentTheta += theta;
        //*************************************************
/ / Переводимо в декартові координати
        // x = r cos @
        // y = r sin @
        //*************************************************
        double x = Radius * Math.cos(currentTheta);
        double y = Radius * Math.sin(currentTheta);
        Point mark2 = newPoint(center);
        mark2.translate((int)x,(int)y);
        graphics.drawLine
                 (center.x, center.y, mark2.x, mark2.y);

Потім, потрібно розфарбувати сегменти діаграми. Для цього можна скористатися дуже корисним методом fillArc ():

        graphics.setColor(colors[colorIndex++]);
        graphics.fillArc(Inset,
                        Inset,
                        PieWidth,
                        PieHeight,
                        -1 * lastAngle,
                        -1 * degrees);

Останнє завдання методу drawSlice () намалювати мітки сегментів. Розташування написи розраховується, грунтуючись на метриці використовуваного шрифту. Остаточний варіант діаграми можна подивитися на малюнку.

Рис. 1. Остаточний варіант кругової діаграми. (Не всі броузери показують файли формату PNG, тому тут ми поставили GIF файл.)

Створення зображень у Java 1.2

Практично всі цікавляться зараз курсами акцій на біржі. Намалюємо (всім на радість) на нашій діаграмі курс акцій компанії Sun за одну з тижнів березня.

Для створення зображень у форматі JPEG використовується клас com.sun.image.codec.jpeg.JPEGImageEncoder. Повторюся, що приналежність до пакету com.sun означає, що цей клас не є частиною основного API і до неї належать усі відповідні попередження.

Клас JPEGImageEncoder "розуміє", як перекодувати зображення java.awt.image.BufferedImage, так що його і будемо використовувати. Для створення класу BufferedImage потрібно вказати необхідні розміри зображення. Після створення, клас BufferedImage надає клас ava.awt.Graphics2D, який можна використовувати для малювання на відповідній класу BufferedImage картинці. Все просто? Отже, основні кроки, необхідні для формування зображення у форматі JPEG:

  1. Створюємо клас JPEGImageEncoder, надаючи йому потік OutputStream даних передаються клієнтові.
  2. Створюємо клас BufferedImage необхідних розмірів.
  3. Використовуємо реалізацію класу Graphics2D, що надається класом BufferedImage, для малювання діаграми.
  4. За допомогою отриманого раніше класу JPEGImageEncoder перекодіруем намальоване зображення у формат JPEG і пишемо його в потік, що відправляється клієнтові.

Обговоримо деякі деталі малювання з використанням Graphics2D.

Для створення графіка курсу акцій потрібно, власне, намалювати кілька ліній. Пакет Java 2D, java.awt.geom, містить всі необхідні для цього класи. Абстрактний клас Line2D визначає відрізок прямої. Він має дві реалізації, що відрізняються типом використовуваних координат. Клас Line2D.Double застосовує примітивний тип double, а клас Line2D.Float – дійсні числа з плаваючою крапкою. Висока точність абсолютно зайва для нашого графіка, але я не буду економити і створю відрізки прямої класом Line2D.Double.

Наведемо, як приклад, ділянку програми, який малює горизонтальну вісь графіка:

        horAxis = new Line2D.Double(HorzInset,
                        ImageHeight - VertInset,
                        ImageWidth -  HorzInset,
                        ImageHeight - VertInset);
        graphics.draw(horAxis);

Горизонтальна вісь будується з невеликим відступом від межі екрану, надаючи місце для розмітки. Після створення ви малюєте ліню, звертаючись до методу draw () об'єкта Graphics2D. Нагадаю, що ви отримали його від реалізації класу BufferedImage. Клас Graphics2D містить методи для малювання графічних примітивів, для наших цілей більш ніж достатньо. На малюнку 2 наведено остаточний варіант графіка.

Рис. 2. Остаточний варіант графіка курсу акцій.

Продуктивність

Погана продуктивність – одне з основних звинувачень супротивників Java. Я не поділяю цієї позиції. Я переконаний в тому, що від можна домогтися гарної продуктивності для програми, написаної на Java, динамічно створює зображення. Як розробник програмного забезпечення, я можу дати вам пораду: якщо вас не влаштовує продуктивність, купуйте більш досконалі комп'ютери.

Висновок

У цій статті я показав, як можна створювати в програмах на мові Java прості зображення, і скористався цією технікою для написання сервлета, що формує картинку по запиту користувача. Ця технологія має величезну кількість додатків, обмежене, хіба що, рамками уяви. Два питання варто мати на увазі, розробляючи програми на основі викладених ідей: продуктивність алгоритму, що створює зображення, і обмеження пропускної здатності каналу зв'язку сервера. Для вирішення останньої проблеми, очевидно, потрібно максимально зменшити розмір картинки, в першому випадку застосовувати стандартні методи оптимізації додатків.

Про автора

KenMcCrary, сертифікований Sun Java developer, живе в Нью-Йорку, Research Triangle Park. Він працював над великою кількістю Java проектів. Ви можете відвідати його сайт http://www.KenMcCrary.com

Ресурси