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

от автора

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

ссылка на оригинал статьи http://habrahabr.ru/post/224795/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *