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

У PDF-файлов есть полный контроль над отображением текста и графическими изображениями на странице. К сожалению, библиотеки для генерации динамически заполняющихся PDF-файлов не входят в стандартный инструментарий PHP, JS (Web), Java или Swift (Android и iOS соответственно). В этой статье хочу вам рассказать об опенсорс-решении для генерации PDF-файлов.



JasperReports — это открытая Java-библиотека для генерации динамически заполняющихся файлов. В ней есть много инструментов для создания сложных отчетных форм, в том числе в формате PDF, но также доступны и другие форматы: RTF, DOCX, HTML, XLS, XLS, CSV и XML. Иными словами, достаточно разработать одну форму, сделать одну верстку — и можно будет экспортировать его в любой из вышеперечисленных форматов.

Есть еще хорошие библиотеки, например PDFLib (коммерческая версия) для PHP и ее версия с открытыми исходными кодами PDFLib-Lite. Правда, библиотека довольно дорогая, а облегченная версия распространяется только в исходных кодах, и при ее установке в среде разработки это ограничение может стать проблемой.

PDFbox — еще одна библиотека Java с открытым исходным кодом для работы с документами PDF. Она позволяет создавать новые PDF-документы, управлять существующими документами с возможностью извлекать контент из них. Но у нее нет UI (User Interface), в отличие от JasperReports.

Думаю, JasperReports особо полезен в больших проектах, связанных с отчетностью и не только в PDF-формате. Здесь есть все необходимое, чтобы внедрить ее в ваш проект: простое создание сложных отчетных форм, UI для удобной верстки, серверное приложение и простая интеграция с фронтом.

В статье я раскрою следующие темы:

  • Установка среды разработки и серверного приложения.
  • Создание PDF-файла автоматически заполняющегося из базы данных.
  • Интеграция серверного приложения с фронтендом для получения созданного PDF.

Чтобы начать использовать JasperReports в вашем проекте, необходимо скачать два приложения: JaspersoftStudio — далее будем называть его рабочей средой — и JasperServer — будем называть серверным приложением.

JaspersoftStudio — среда разработки на основе Eclipse со встроенной Java-библиотекой JasperReports, где разрабатываются динамические или статичные PDF-файлы: например, билеты, квитанции, договора, аналитические чарты и другие.

JasperServer — это серверное приложение, куда из JaspersoftStudio деплоятся и хранятся файлы. К ним можно получить доступ из мобильного или веб-приложения. В JasperServer есть UI, с его помощью можно просматривать отчеты, создавать учетные записи для разных пользователей и давать им соответствующие доступы. Также можно настроить рассылку на электронную почту (Scheduler).

Настраиваем рабочую среду и серверное приложение


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

Servers → Create JasperReports Server Connection → Указать предпочитаемое имя сервера, URL, логин и пароль. Нажмите Test Connection, чтобы проверить, что соединение с сервером установлено. Если видите надпись Successful — едем дальше.



Создаем динамически заполняющийся PDF и публикуем его


Мы установили среду разработки, сервер и наладили соединение между ними. Теперь давайте создадим примитивный динамически заполняющийся PDF-файл, который при его запуске (генерации) будет брать данные из PostgreSQL, и задеплоим его на серверное приложение.

В первую очередь следует прикрутить к рабочей среде источник данных (в нашем случае — PostgreSQL), откуда PDF будет брать данные. Затем приступим к разработке нашего первого PDF-файла.

Data Adapters → Create Data Adapter → Database JDBS Connection и указываем данные соединения:

  1. JDBC Driver — PostgreSQL (org.postgresql.Driver). Если нет драйвера вашего СУБД, можно установить нужный драйвер во вкладке Driver Classpath.
  2. JDBC URL — он состоит их хоста, порта и названия БД.
  3. Username и password — логпасс от вашей учетной записи СУБД.



Нажимаем на уже знакомую нам кнопку Test и при успешном соединении (Successful) с БД — Finish.

Из этой БД PDF будет заполняться в рабочей среде. Так как мы планируем задеплоить наш первый PDF-файл еще и на сервер, то давайте прикрутим тот же источник данных к серверному приложению:

Data Sources → Add Resource → Data Source и повторяем все из пункта выше.

Теперь можно приступать к созданию PDF. Исходники в JasperReports хранятся в формате JRXML — это XML с зашитыми тегами и атрибутами, с которыми работает API JasperReports.

Нажимаем File → New → Jasper Report → Blank A4 → Указываем имя JRXML-файла → Finish.



После создания нового проекта вы увидите следующую картину:



Семь разных блоков — для каждого блока прописано свое поведение, отличающееся от других. Более подробно об этом можно почитать в документации. Нажав на Source можно увидеть структуру этих блоков:

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport
	xmlns="http://jasperreports.sourceforge.net/jasperreports"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Example" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="ae9517f6-ff0b-41bb-a8dc-82196190e940">
	<queryString>
		<![CDATA[]]>
	</queryString>
	<background>
		<band splitType="Stretch"/>
	</background>
	<title>
		<band height="79" splitType="Stretch"/>
	</title>
	<pageHeader>
		<band height="35" splitType="Stretch"/>
	</pageHeader>
	<columnHeader>
		<band height="61" splitType="Stretch"/>
	</columnHeader>
	<detail>
		<band height="125" splitType="Stretch"/>
	</detail>
	<columnFooter>
		<band height="45" splitType="Stretch"/>
	</columnFooter>
	<pageFooter>
		<band height="54" splitType="Stretch"/>
	</pageFooter>
	<summary>
		<band height="42" splitType="Stretch"/>
	</summary>
</jasperReport>

Итак, давайте удалим пять лишних блоков и оставим только два: Title и Detail. В этом нам поможет кнопка Delete (Windows) или Backspace (OS X).

Теперь добавим два элемента. Добавить новый элемент можно двумя способами: прописать контейнер в XML-структуре (кнопка Source) либо перетащить нужный элемент из правого верхнего окна Pallette — Static Text, где будут названия полей и Text Field, внутрь которого зашьем переменные поля, вытащенные из БД:

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport
	xmlns="http://jasperreports.sourceforge.net/jasperreports"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Example" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="ae9517f6-ff0b-41bb-a8dc-82196190e940">
	<queryString>
		<![CDATA[]]>
	</queryString>
	<background>
		<band splitType="Stretch"/>
	</background>
	<title>
		<band height="30" splitType="Stretch">
			<staticText>
				<reportElement x="0" y="0" width="100" height="30" uuid="7b697ed9-f52a-483c-965e-f0b2dc6130c1"/>
				<text>
					<![CDATA[Static Text]]>
				</text>
			</staticText>
		</band>
	</title>
	<detail>
		<band height="169" splitType="Stretch">
			<textField>
				<reportElement x="0" y="0" width="100" height="20" uuid="41002e0b-ddb2-4e4b-a049-10810ab51208"/>
				<textFieldExpression>
					<![CDATA["Text Field"]]>
				</textFieldExpression>
			</textField>
		</band>
	</detail>
</jasperReport>

Запрос в БД можно написать в теге queryString либо нажать на кнопку DataSet and Query editor dialog. После этого откроется новое окно, где необходимо выбрать источник данных (1), написать запрос (2) и объявить переменные поля. Кнопка Read Fields (3) прочитает все поля автоматически при валидном запросе. Чтобы просмотреть данные, нужно нажать на Data preview (4).



Отлично! Мы получили четыре поля с типом String, теперь можем совершать с ними практически любые манипуляции. Для примера мы просто выведем их списком и пропишем небольшую логику.

Напечатаем названия нужных полей в Static Text элементах и поместим их в контейнер Title. Переменные поля укажем в Text Field элементах в контейнере Detail, так как они будут множиться. Наш PDF будет выводить имя, город и адрес электронной почты. Чтобы не было совсем скучно, давайте в Text Field элементе пропишем простую логику, используя четвертое поле — пол клиента, Sex.

Сделаем следующее: если клиент — женщина, то перед именем будем добавлять Mrs., если мужчина — Mr. Для этого используем тернарный оператор Java:

<textFieldExpression>
      <![CDATA[$F{sex}.equals( "male" )?"Mr. "+$F{name}:"Mrs. "+$F{name}]]>
</textFieldExpression> 

Нажав на Preview рядом с кнопкой Source, можно увидеть результат:



Как видно на скриншоте, PDF успешно собрался: забрал все значения и применил логику, правильно проставив Mrs. и Mr.

Еще заведем входной параметр City, чтобы была возможность фильтровать данные по городам. Это можно сделать либо нажав Parameters → Create Parameter в окне Outline, либо добавив новый тег parameter с атрибутами name и class:

<parameter name="City" class="java.lang.String"/>

Осталось только добавить параметр в SQL-запрос:

SELECT 
Id, name, sex, city, email
FROM users WHERE city = $P{City}

Передадим в параметр City значение San Francisco (о том, как это сделать, расскажу в следующем пункте) и нажмем Data Preview для просмотра результата.



PDF собрался, успешно отфильтровав данные. Едем дальше

Так как у нас уже есть динамически заполняющийся PDF-файл, можем задеплоить его на сервер для дальнейшей интеграции с нашими фронтенд-приложениями. Для этого нажимаем кнопку Publish Report to JasperReports Server → двойным кликом открываем сервер → Выбираем серверную папку, куда нужно загрузить PDF (в нашем случае — reports) → Next → Data Source from Repository → выбираем источник данных, который создали ранее на серверном приложении → Finish.

Интеграция с фронтендом


В JasperReports API входит своя RESTful-реализация для взаимодействия клиента с сервером — REST v2. Если вам она не подходит, можете использовать простой протокол доступа к объектам — SOAP.

Мы рассмотрим REST v2.

Доступны основные четыре метода для действий CRUD (Create-Read-Update-Delete): GET (получить), POST (добавить, изменить, удалить), PUT (добавить, заменить), DELETE (удалить). Вся подробная информация имеется в документации по ссылкам выше.

Мы рассмотрим более распространенный и актуальный для этой статьи метод GET.

http://<host>:<port>/jasperserver[pro]/rest_v2/reports/path/to/report.<format>?<arguments> 

Выше — синхронный запрос, с помощью которого можно получить выходные данные файла (готовый PDF) в одном запросе-ответе (с асинхронным вызовом можно ознакомиться здесь).

Думаю, с хостом и портом все понятно, а /reports/path/to/report — это URI того файла, который вызывается. Так как мы задеплоили исходник PDF-файла (Example.jrxml) в серверную папку reports, заполненный вариант URI будет таким: /reports/reports/Example.

format — это формат (в нашем случае PDF).
arguments — параметры.

Выше мы добавили параметр City, его и передадим в запросе со значением San Francisco для фильтрации данных по этому городу.

Если вызов делается не с авторизованной зоны, нужно добавить еще два параметра/атрибута: j_username и j_password (логпасс для авторизации). По умолчанию логин и пароль на сервере — jasperadmin.

Таким образом, получаем следующий URL:

http://localhost:8080/jasperserver/rest_v2/reports/reports/Example.PDF?city=San Francisco&j_username=jasperadmin&j_password=jasperadmin

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

Может возникнуть необходимость от��бразить PDF картинкой. Например, если клиент хочет просто просмотреть файл, можно показать документ в PNG-формате, если же хочет скачать его, то в PDF.

На примере Java с использованием библиотеки PDFbox рассмотрим, как можно из внешнего приложения сгенерировать и забрать PDF-файл, а затем сконвертировать его в PNG.

Ниже — класс PullPDF с одним методом, принимающим в качестве аргумента URL-адрес.

import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.tools.imageio.ImageIOUtil;

public class PullPDF {
 public String convertPDF2PNG(String valuefromParam) throws IOException {
  BufferedInputStream input_file = new BufferedInputStream(new URL(valuefromParam).openStream());
  ByteArrayOutputStream baos = new ByteArrayOutputStream();

  try {
   PDDocument document = PDDocument.load(input_file);
   PDFRenderer pdfRenderer = new PDFRenderer(document);
   for (int page = 0; page < document.getNumberOfPages(); ++page) {
    BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
    ImageIOUtil.writeImage(bim, "png", baos);
    baos.flush();
    byte[] encodedBytes = Base64.getEncoder().encode(baos.toByteArray());
    valuefromParam = new String(encodedBytes);
   }
  } catch (Exception e) {

  }
  return valuefromParam;
 }
}

Можно получить тот же результат, используя, например, Spring Framework. Но я постарался показать универсальный способ, который можно применить и в Android, и в вебе при работе с Java.

Заключение


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