Делаем визуальный web-редактор документов на основе LibreOffice, jodconverter и TinyMCE

в 2:23, , рубрики: Без рубрики

Как же я люблю спецификацию офиса!С написания предыдущей статьи про генерацию 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 в нужного формата файл и отдать пользователю.

Под катом небольшой класс-обёртка для jodconverter с комментариями:

Libre.java

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js