С написания предыдущей статьи про генерацию Excel документов по шаблону прошло много времени и задача несколько изменилась. Новая задача была поставлена следующим образом: из готового документа excel или word сделать шаблон через веб-интерфейс. В процессе формирования подставлять в шаблон нужные значения, убирать и/или «клонировать» куски шаблона. После формирования, документ должен быть доступен пользователю для визуального редактирования в браузере. Готовый документ должен сохраниться на сервере, быть доступным для скачивания пользователем как в своём расширении (*.doc/*.xls), так и в pdf. При этом верстка скачиваемого файла должна быть идентична шаблону, который был загружен в самом начале (без всяких искажений полей и областей печати).
Что же, задача есть — будем решать!
1. Испробованные инструменты
Сначала нужно решить, чем перегонять загружаемые файлы из doc, docx, xls, xlsx в html и обратно, при этом не испортив вёрстку.
Apache POI: Отличный инструмент, который мы успешно использовали, но он не умеет генерировать HTML разметку из существующего документа.
DocX4J: С этой либой была долгая история. Она умеет много всяких приятных вещей, о которых неоднократно писалось. И изначально мы именно этой библиотекой и хотели воспользоваться.
Недостатки DocX4J: работать можно только с docx и xlsx. Но это не так страшно. Проблемы начинаются, когда пытаешься HTML обратно сконвертировать в docx или xlsx. Едут все стили документа, шрифты прописываются вообще произвольные и т.д. Обратились к разработчику. Он сказал, что есть такая проблема и решена она частично в платной версии — docx4j-web-editor. Но и платная версия тоже со своими багами оказалась. В конце коцов от этой библиотеки тоже пришлось отказаться.
Решение — использовать LibreOffice. Пусть он сам на сервере конвертирует файлы в HTML и обратно. Осталось только связать его с нашим web-приложением.
Для работы с LibreOffice используется маленькая библиотека — jodconverter которая, к сожалению, давно не обновляется, но работает при этом отлично. Она подключается к LibreOffice через TCP сокет и отдает ему файл на конвертирование, в ответ приходит отконвертированный файл. Все это работает гораздо быстрее и правильнее чем все вышеперечисленные Java библиоткеи. Кроме того, LibreOffice работает в своем процессе, освобождая Java приложение от такой грамоздкой задачи, как разбор и хранение документа в куче web-приложения.
2. Загружаем файл на сервер и делаем из него шаблон
Но jodconverter умеет работать с файловой системой на сервере. Поэтому нужно передать в него из веб-приложения загружаемый файл и решить обратную задачу — сконвертировать HTML в нужного формата файл и отдать пользователю.
package ru.cpro.uchteno.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.artofsolving.jodconverter.OfficeDocumentConverter;
import org.artofsolving.jodconverter.office.ExternalOfficeManagerConfiguration;
import org.artofsolving.jodconverter.office.OfficeConnectionProtocol;
import org.artofsolving.jodconverter.office.OfficeManager;
public class Libre {//Класс для конвертирования документов
public static void doc2html(InputStream is, OutputStream os) {//конвертит doc в html
try {
File inf = File.createTempFile("doc", ".doc"); //создаем временный файл
FileOutputStream infos = new FileOutputStream(inf); //делаем из него поток
//переписываем все из входого потока в этот файл
int n = 0;
byte buff[] = new byte[1024];
while (n >= 0) {
n = is.read(buff);
if (n > 0) {
infos.write(buff, 0, n);
}
}
//закрываем все
is.close();
infos.close();
//Создаем выходной файл
File onf = File.createTempFile("doc", ".html");
//создаем конфигурацию jodconverter'а
ExternalOfficeManagerConfiguration officeConfiguration = new ExternalOfficeManagerConfiguration();
//через tcp сокет
officeConfiguration
.setConnectionProtocol(OfficeConnectionProtocol.SOCKET);
//порт
officeConfiguration.setPortNumber(2002);
//стрим по конфигу officeManager
OfficeManager officeManager = officeConfiguration
.buildOfficeManager();
//стартуем ьенеджера с имеющейся конфигурацией
officeManager.start();
//Делаем конвертор
OfficeDocumentConverter converter = new OfficeDocumentConverter(
officeManager);
//конвертируем документ через либреофис
converter.convert(inf, onf);
//останавливаем менеджера
officeManager.stop();
//Теперь перегоняем из созданного офисом файла в выходной поток
FileInputStream outfis = new FileInputStream(onf);
n = 0;
while (n >= 0) {
n = outfis.read(buff);
if (n > 0) {
os.write(buff, 0, n);
}
}
//закрываем все
outfis.close();
os.close();
//Глушим временные файлы
inf.delete();
onf.delete();
} catch (IOException ex) {
Logger.getLogger(Libre.class.getName()).log(Level.SEVERE, null, ex);
}
}
public static void doc2pdf(InputStream is, OutputStream os) {
try {
File inf = File.createTempFile("doc", ".doc");
FileOutputStream infos = new FileOutputStream(inf);
int n = 0;
byte buff[] = new byte[1024];
while (n >= 0) {
n = is.read(buff);
if (n > 0) {
infos.write(buff, 0, n);
}
}
is.close();
infos.close();
File onf = File.createTempFile("doc", ".pdf");
ExternalOfficeManagerConfiguration officeConfiguration = new ExternalOfficeManagerConfiguration();
officeConfiguration
.setConnectionProtocol(OfficeConnectionProtocol.SOCKET);
officeConfiguration.setPortNumber(2002);
OfficeManager officeManager = officeConfiguration
.buildOfficeManager();
officeManager.start();
OfficeDocumentConverter converter = new OfficeDocumentConverter(
officeManager);
converter.convert(inf, onf);
officeManager.stop();
FileInputStream outfis = new FileInputStream(onf);
n = 0;
while (n >= 0) {
n = outfis.read(buff);
if (n > 0) {
os.write(buff, 0, n);
}
}
outfis.close();
os.close();
inf.delete();
onf.delete();
} catch (IOException ex) {
Logger.getLogger(Libre.class.getName()).log(Level.SEVERE, null, ex);
}
}
public static void html2doc(InputStream is, OutputStream os) {
try {
File inf = File.createTempFile("doc", ".html");
FileOutputStream infos = new FileOutputStream(inf);
int n = 0;
byte buff[] = new byte[1024];
while (n >= 0) {
n = is.read(buff);
if (n > 0) {
infos.write(buff, 0, n);
}
}
is.close();
infos.close();
File onf = File.createTempFile("doc", ".doc");
ExternalOfficeManagerConfiguration officeConfiguration = new ExternalOfficeManagerConfiguration();
officeConfiguration
.setConnectionProtocol(OfficeConnectionProtocol.SOCKET);
officeConfiguration.setPortNumber(2002);
OfficeManager officeManager = officeConfiguration
.buildOfficeManager();
officeManager.start();
OfficeDocumentConverter converter = new OfficeDocumentConverter(
officeManager);
converter.convert(inf, onf);
officeManager.stop();
FileInputStream outfis = new FileInputStream(onf);
n = 0;
while (n >= 0) {
n = outfis.read(buff);
if (n > 0) {
os.write(buff, 0, n);
}
}
outfis.close();
os.close();
inf.delete();
onf.delete();
} catch (IOException ex) {
Logger.getLogger(Libre.class.getName()).log(Level.SEVERE, null, ex);
}
}
public static void html2docx(InputStream is, OutputStream os) {
try {
File inf = File.createTempFile("doc", ".html");
FileOutputStream infos = new FileOutputStream(inf);
int n = 0;
byte buff[] = new byte[1024];
while (n >= 0) {
n = is.read(buff);
if (n > 0) {
infos.write(buff, 0, n);
}
}
is.close();
infos.close();
File onf = File.createTempFile("doc", ".docx");
ExternalOfficeManagerConfiguration officeConfiguration = new ExternalOfficeManagerConfiguration();
officeConfiguration
.setConnectionProtocol(OfficeConnectionProtocol.SOCKET);
officeConfiguration.setPortNumber(2002);
OfficeManager officeManager = officeConfiguration
.buildOfficeManager();
officeManager.start();
OfficeDocumentConverter converter = new OfficeDocumentConverter(
officeManager);
converter.convert(inf, onf);
officeManager.stop();
FileInputStream outfis = new FileInputStream(onf);
n = 0;
while (n >= 0) {
n = outfis.read(buff);
if (n > 0) {
os.write(buff, 0, n);
}
}
outfis.close();
os.close();
inf.delete();
onf.delete();
} catch (IOException ex) {
Logger.getLogger(Libre.class.getName()).log(Level.SEVERE, null, ex);
}
}
public static void html2pdf(InputStream is, OutputStream os) {
try {
File inf = File.createTempFile("doc", ".html");
FileOutputStream infos = new FileOutputStream(inf);
int n = 0;
byte buff[] = new byte[1024];
while (n >= 0) {
n = is.read(buff);
if (n > 0) {
infos.write(buff, 0, n);
}
}
is.close();
infos.close();
File onf = File.createTempFile("doc", ".pdf");
ExternalOfficeManagerConfiguration officeConfiguration = new ExternalOfficeManagerConfiguration();
officeConfiguration
.setConnectionProtocol(OfficeConnectionProtocol.SOCKET);
officeConfiguration.setPortNumber(2002);
OfficeManager officeManager = officeConfiguration
.buildOfficeManager();
officeManager.start();
OfficeDocumentConverter converter = new OfficeDocumentConverter(
officeManager);
converter.convert(inf, onf);
officeManager.stop();
FileInputStream outfis = new FileInputStream(onf);
n = 0;
while (n >= 0) {
n = outfis.read(buff);
if (n > 0) {
os.write(buff, 0, n);
}
}
outfis.close();
os.close();
inf.delete();
onf.delete();
} catch (IOException ex) {
Logger.getLogger(Libre.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
3. Работаем с шаблоном
Когда у нас есть HTML, нужные операции над ним достаточно легко производятся с помощью velocity. Всё легко делается по описанию.
4. Визуальное редактирование документа
С визуальными редакторами есть своя особенность – визуальные редакторы портят HTML-код и при обратном конвертировании вся вёрстка нашего документа будет исковеркана до неузнаваемости. В ходе экспериментов с разными редакторами пришли к тому, что TinyMCE меньше всего умничает коверкает разметку и на конечном результате при обратном конвертировании практически не сказывается.
В итоге методом тыка проб и ошибок подобрали оптимальную конфигурацию редактора:
tinymce.init({
selector: "textarea",
theme: "modern",
fullpage_default_doctype: "<!DOCTYPE xhtml>",
plugins: [
"advlist autolink lists link image charmap print preview hr anchor pagebreak",
"searchreplace wordcount visualblocks visualchars code fullscreen",
"insertdatetime media nonbreaking save table contextmenu directionality",
"emoticons template paste textcolor fullpage"
],
toolbar1: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image",
toolbar2: "print preview media | forecolor backcolor emoticons",
image_advtab: true
});
Каждый раз чтобы сбросить содержимое редактора в DOM не забываем делать tinyMCE.triggerSave();
5. Скачиваем готовый документ
Для этих целей опять воспользуемся библиотечкой Libre.java
:
Конертит hmtl в doc — html2doc()
Конертит hmtl в docx — html2docx()
Конертит hmtl в pdf — html2pdf()
Вот, собственно, и всё. Будем рады, если эта статья кому-то поможет и уменьшит время, потраченное на пляски с бубном!
Материал подготовили: akaiser, boiler5.
Автор: akaiser