Генерация документов с помощью ONLYOFFICE DocumentBuilder
Привет, Хабр.
Я хочу рассказать об утилите под названием DocumentBuilder, которая позволяет генерировать документы, таблицы и презентации, а также показать, как можно его использовать для решения ваших задач, на примере создания резюме из шаблонов.
Работает билдер следующим образом: вы пишете код на js, используя специальные методы из документации, отдаете его утилите, а она собирает документ. Или не собирает, если есть ошибки.
Использовать билдер целесообразно, если вы хотите решить задачи вида:
- Нужно сделать много документов с небольшими вариациями, или документы на основе большого количества данных.
- Нужно встроить генерирование документов в какой-либо сервис.
Существует сервисы, позволяющие создавать резюме: пользователь заполняет необходимые поля, а система генерирует документ и отдает пользователю. Это и будет примером того, как можно использовать билдер.
В качестве инструментов буду использовать Node.js (express).
План:
- Пользователь вводит данные в форму на странице в браузере, форма отправляется на сервер.
- На сервере Node.js создает на основе пользовательских данных скрипт для билдера.
- Node.js отдает скрипт билдеру.
- Билдер делает с помощью скрипта документ.
- Node.js возвращает пользователю ссылку на документ.
Создание формы
Для начала создадим форму, в которую пользователь будет вводить свои данные. В форме будет 8 полей: «Full name», «Phone number», «email», «profile», «degree», «university», «location», «year», «skill». Поле skill можно клонировать.
Создаем файл index.html, и добавляем в него код шаблона.
Здесь я использую две функции add_skill_by_enter (event) и add_skill (). Они нужны, чтобы добавлять несколько полей по нажатию кнопки + или Enter. Сами эти функции опишу чуть позже.
Добавляем кнопку для отправки формы на сервер:
Теперь напишем функции для работы с формой.
Первая функция — add_skill ()
add_skill = () => {
const newSkill = document.getElementById("new-skill");
if (newSkill.value === '') {return; } // ничего не делаем, если в поле не введены данные
const div = document.createElement("div"); .//внешний div
const span = document.createElement("span"); // название навыка
const button = document.createElement("button"); // кнопка удаления навыка
span.innerText += newSkill.value; // добавляем в span введенный текст
newSkill.value = ''; // обнуляет поле с названием способности
newSkill.focus(); // возвращаем фокус в поле с названием навыка
button.innerText += "-";
button.onclick = () => { // добавляем действие на кнопку удаления
div.remove();
};
div.appendChild(span); // добавляем span в div
div.appendChild(button); //добавляем кнопку удаления
document.getElementsByClassName('skills')[0].appendChild(div); // добавляем объект на страницу
};
add_skill_by_enter()
add_skill_by_enter = (event) => {
if (event.code === "Enter") { // добавляем элемент, только если нажали на кнопку enter
add_skill();
}
};
Добавляем простую функцию для сбора данных из полей и отправки их на сервер.
get_skill_values = () => {
const skills = [];
if (document.getElementById('new-skill').value !== '') {
skills.push(document.getElementById('new-skill').value);
}
Array.from(document.getElementsByClassName('skillfield')).forEach(current_element => {
skills.push(current_element.innerHTML);
});
return skills;
};
sendForm()
sendForm = () => {
fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userData: {
fillName: document.getElementById('fill-name').value,
phoneNumber: document.getElementById('phone-number').value,
email: document.getElementById('email').value,
profile: document.getElementById('profile').value,
education: {
degree: document.getElementById('degree').value,
university: document.getElementById('university').value,
location: document.getElementById('location').value,
year: document.getElementById('year').value,
},
skills: get_skill_values()
}
})
}).then(res => res.json())
.then(response => {
location.replace('/' + response.filename); // скачиваем файл, который будет доступен по этой ссылке
})
.catch(error => console.error('Error:', error));
};
Серверная часть для работы с формой
Серверную часть пишу на express. Подключение всех библиотек, конфигурация сервера и описание get и post методов выглядит так:
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname + '/index.html'));
});
app.post('/', (req, res) => {
// сюда допишем потом функцию для сборки файла
});
app.listen(3000, () => console.log(`Example app listening on port ${3000}!`));
Запускаем express: node main.js
Открываем в браузере адреc: http:localhost:3000
Видим созданную форму. Заполняем ее произвольными данными:
Получаем следующий json:
{"userData":{"fillName":"Rotatyy Dmitryi","phoneNumber":"89879879898","email":"flamine@list.ru","profile":"Hi, my name is Joe\nAnd I work in a button factory\nI got a wife and two kids\nOne day, my boss says, "Joe, are you busy?”\nI said, "no”\n"Then push this button with your right hand”","country":"Russia","city":"Nizhniy Novgorod","education":{"degree":"Master of Pupets","university":"Nizhny novgorod state technical university","location":"Nizhniy Novgorod","year":"2015-05-02"},"skills":["apple.js","vintage.js","zerg.js","css","html","linux"]}};
Теперь нужно написать скрипт для билдера. Я взял за основу шаблон, который предлагает Google Docs (шаблон резюме).
Выглядит этот шаблон так:
Используя эти данные и шаблон, нужно сделать скрипт, с помощью которого билдер создаст документ.
Есть несколько вариантов как это сделать, и самый простой — это скачать desktop-версию редакторов ONLYOFFICE и написать макрос, который создаст документ, используя данные. А потом дописать к макросу создание и сохранение файла — получится скрипт для билдера. Это сработает, потому что макросы и билдер используют один и тот же API.
Создание скрипта для билдера
Начинаем с инициализации объекта страницы и добавления данных, которые пришли от пользователя:
const Document = Api.GetDocument();
const data =
{"userData":{"fillName":"Rotatyy Dmitryi","phoneNumber":"89879879898","email":"flamine@list.ru","profile":"Hi, my name is Joe\nAnd I work in a button factory\nI got a wife and two kids\nOne day, my boss says, "Joe, are you busy?”\nI said, "no”\n"Then push this button with your right hand”","country":"Russia","city":"Nizhniy Novgorod","education":{"degree":"Master of Pupets","university":"Nizhny novgorod state technical university","location":"Nizhniy Novgorod","year":"2015-05-02"},"skills":["apple.js","vintage.js","zerg.js","css","html","linux"]}};
Теперь нужно добавить параграф с полным именем пользователя. Он написан 14 шрифтом, жирным, и у этого параграфа line spacing равен 1.15.
let paragraph = document.GetElement(0); // в документе всегда есть первый параграф
FullName_style = Document.CreateStyle("FullName"); // создаем новый стиль
FullName_style.GetTextPr().SetFontSize(28); // меняем размер шрифта
FullName_style.GetTextPr().SetBold(true); // добавляем bold
paragraph.SetStyle(FullName_style); // применяем созданный стиль к параграфу
paragraph.SetSpacingLine(1.15 * 240, "auto"); // меняем межстрочный интервал
paragraph.AddText(data.userData.fillName); // добавляем текст в параграф
// Country and city
const CountryCity_style = Document.CreateStyle("CountryCity");
CountryCity_style.GetTextPr().SetFontSize(20);
CountryCity_style.GetTextPr().SetCaps(true);
CountryCity_style.GetTextPr().SetBold(true);
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.country + ', ' + data.userData.city);
paragraph.SetStyle(CountryCity_style);
paragraph.SetSpacingAfter(0);
Document.Push(paragraph);
// phone number
const PhoneNumber_style = Document.CreateStyle("PhoneNumber");
PhoneNumber_style.GetTextPr().SetFontSize(20);
PhoneNumber_style.GetParaPr().SetSpacingAfter(0);
PhoneNumber_style.GetTextPr().SetBold(true);
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.phoneNumber);
paragraph.SetStyle(PhoneNumber_style);
Document.Push(paragraph);
// email
const Email_style = Document.CreateStyle("Email");
Email_style.GetTextPr().SetFontSize(18);
Email_style.GetParaPr().SetSpacingAfter(0);
Email_style.GetTextPr().SetBold(true);
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.email);
paragraph.SetStyle(Email_style);
Document.Push(paragraph);
// SectionHeader style
const SectionHeader = Document.CreateStyle("SectionHeader");
SectionHeader.GetTextPr().SetBold(true);
SectionHeader.GetTextPr().SetColor(247, 93, 93, false);
SectionHeader.GetTextPr().SetFontSize(28);
SectionHeader.GetParaPr().SetSpacingBefore(1.33 * 240);
SectionHeader.GetParaPr().SetSpacingLine(1 * 240, "auto");
// add header Profile:
paragraph = Api.CreateParagraph();
paragraph.AddText("Profile:")
paragraph.SetStyle(SectionHeader);
Document.Push(paragraph);
// add profile text:
paragraph = Api.CreateParagraph();
paragraph.AddText(data.userData.profile)
Document.Push(paragraph);
// add header Education:
paragraph = Api.CreateParagraph();
paragraph.AddText("Education:")
paragraph.SetStyle(SectionHeader);
Document.Push(paragraph);
// add education year:
const EducationYear_style = Document.CreateStyle("EducationYear");
EducationYear_style.GetTextPr().SetColor(102, 102, 102);
EducationYear_style.GetTextPr().SetFontSize(18);
EducationYear_style.GetParaPr().SetSpacingAfter(0);
paragraph = Api.CreateParagraph();
paragraph.SetStyle(EducationYear_style);
paragraph.AddText(data.userData.education.year)
Document.Push(paragraph);
// add education university:
paragraph = Api.CreateParagraph();
run = Api.CreateRun();
run.AddText(data.userData.education.university)
run.AddText(', ')
run.AddText(data.userData.education.location)
run.SetBold(true);
paragraph.AddElement(run);
run = Api.CreateRun();
run.AddText(' – ' + data.userData.education.degree)
paragraph.AddElement(run);
Document.Push(paragraph);
// add header Skills:
paragraph = Api.CreateParagraph();
paragraph.AddText("Skills:")
paragraph.SetStyle(SectionHeader);
Document.Push(paragraph);
// add skills text:
paragraph = Api.CreateParagraph();
const skills = data.userData.skills.map(x => ' ' + x).toString();
paragraph.AddText(skills)
Document.Push(paragraph);
Исполнив этот скрипт, мы получим такой документ:
Теперь пришло время добавить функции для записи кода скрипта в файл и генерации документа.
Генерируем скрипт → записываем в файл → отдаем файл билдеру → возвращаем пользователю ссылку на файл.
Добавляем подключение дополнений для работы с файлами и запуска команд с помощью Node.js, а также создаем папку «public» и делаем ее публичной:
const {exec} = require('child_process');
const fs = require('fs');
app.use(express.static('public'));
Функция для генерации текста со скриптом будет очень простой — она просто будет возвращать строку со всем кодом для билдера, добавляя при этом пользовательские данные. Важно добавить символ переноса строки в конце каждой строки, иначе ничего не заработает.
generate_script = (data) => {
let first_template = 'builder.CreateFile("docx");\n' +
'const Document = Api.GetDocument();\n';
first_template += 'const data = ' + JSON.stringify(data) + ';\n';
first_template += 'let paragraph = Document.GetElement(0);\n' +
'FullName_style = Document.CreateStyle("FullName");\n' + .... остальной код
~~~~~~~~~~~
return first_template;
};
Теперь нужно записать скрипт в файл и отдать его билдеру. По сути, вся работа с билдером будет сводиться к тому, что нам нужно исполнить команду documentbuilder path/script.js с помощью Node.js
Напишем функцию build, которая будет это делать:
build = (data, res) => {
const filename = Math.random().toString(36).substring(7) + '.docx'; // случайное имя файла
let script = generate_script(data);
script += 'builder.SaveFile("docx", "' + __dirname + '/public/' + filename + '");\n' + 'builder.CloseFile();';
fs.writeFile('public/' + filename + 'js', script, () => {
exec('documentbuilder ' + 'public/' + filename + 'js', () => { res.send({'filename': filename }); });
});
};
Добавляем вызов метода build (req.body, res); при post запросе
app.post('/', (req, res) => {
build(req.body, res);
});
И все готово. На всякий случай полный код примера я выложил сюда.
Так можно встроить ONLYOFFICE DocumentBuilder в web-приложение.
Надеюсь, что несмотря на большое количество упрощений, все понятно. Хотел написать только необходимый минимум кода, чтобы показать, как все работает.
В данный момент есть мысли допилить утилиту и расширить спектр решаемых проблем. Буду признателен, если вы поделитесь реальными кейсами генерации документов (ну и таблиц с презентациями, конечно, тоже) в комментариях или в личке.