Формуємо зображення з тексту в PhantomJS

Добрий вечір, Хабровчане! Новорічні свята відгриміли і всі потихеньку входять в робочий ритм після вихідний тижні, а це значить, що саме час описати свою новорічну забаву.
Якщо вам цікаво дізнатися, як генерувати зображення з простих картинок з допомогою PhantomJS і невеликий магії, то ласкаво просимо під кат!
Трохи передісторії
Цей новий рік ми з друзями вирішили провести незвично, додати деякий інтерактив, який би не залежав ні від кого. Так як більшість у моєму колі спілкування так чи інакше пов'язані з комп'ютерними іграми, то мною і моїм другом (далі Микита) було вирішено придумати список новорічних досягнень (або ачивік). Список був складений за кілька днів і було вирішено як-небудь їх оформити і видавати так, щоб ачівкі не забувалися через п'ять хвилин після отримання. На підсумок прийняли рішення надрукувати, наклеїти на акварельний папір, зробити дві дірки у верхньому правому та лівому кутах і вішати картки з ачивками на шию. Повністю розібравшись з техпроцесом, Микита намалював нехитрий дизайн, який чудово б роздрукувався на чорно-білому принтері, і ми приступили до заповнення ачів.
Я відразу ж вирішив, що руками додавати текст в PSD файл більше п'ятдесяти разів мені не хочеться, і як будь-який адекватний програміст витратив на автоматизацію часовий завдання трохи більше двох годин.
Приклад дизайну ачівкі:
image
Реалізація
Пул технологій був обраний моментально. Node.js для генерації тексту і html сторінок, PhantomJS для відтворення та збереження.

Парсим файл

Формат для завдання ачівкі було поставлено таким:
назва -- цитата -- опис
(Цитата опціональна)
Потрібно перевести файл, що цілком складається з таких рядків, в JS об'єкт.
module.exports = (contents) => {
return new Promise((resolve, reject) => {
return resolve(contents.toString().split('\n').filter(e => e).map(e => {
const contents = e.split(' -- '); // Розбиваємо контент
const achieve = {};
const achieveName = capitalizeFirstLetter(contents[0].trim());

let quote = capitalizeFirstLetter(contents[1].trim());
let achieveDescr = capitalizeFirstLetter((contents[2] || ").trim());

if (!achieveDescr) { // якщо немає опису, то не ввели цитату.
achieveDescr = quote;
quote = null;
}

achieve.name = achieveName;
achieve.description = achieveDescr;

if (quote) {
achieve.quote = quote;
}

return achieve;
}));
});
}

подається На вхід функції
Buffer
, який повертає
fs.readFile
, а на виході ми маємо масив:
[{
"name": "Пейсатель",
"quote": "Клац-клац і відправив",
"description": "Написати статтю на Хабрахабр."
}]

Відмінно, працюємо далі.

Створюємо html сторінки

Для того, щоб змусити PhantomJS відкривати сторінки, для початку потрібні самі сторінки.
Я створив простий template.html
template.html
<html>
<head>
<link rel="stylesheet" href="/index.css">
<meta charset="utf-8">
</head>
<body>
<div class="achieve">
<div class="achieve__wrapper">
<div class="achieve__text">
<div class="achieve__heading-text{{extraHtmlClass}}">
{{name}}
</div>
<div class="achieve__main-text">
<div class="achieve__artistic">
{{quote}}
</div>
<div class="achieve__description">
{{description}}
</div>
</div>
</div>
<div class="achieve__image"></div>
<div class="clearfix"></div>
</div>
</div>
</body>
</html>

і невеликий файл стилів до нього, який в точності повторює дизайн.
index.css
body {
margin: 0;
padding: 0;
}

.achieve {
width: 917px;
background: #b3b4b3;
position: relative;
}

.achieve__wrapper {
padding-top: 35px;
}

.achieve__text, .achieve__image {
float: left;
}

.achieve__text {
padding-top: 15px;
padding-left: 50px;
width: 550px;
color: #353534;
min-height: 293px;
}

.achieve__heading-text {
font-size: 70pt;
margin-bottom: 40px;
font-weight: bold;
font-family: 'Impact', sans-serif;
font-style: italic;
}
.achieve__heading-text--small {
font-size: 50pt;
line-height: 50pt;
margin-bottom: 50px;
}
.achieve__heading-text-super-small {
font-size: 45pt;
line-height: 50pt;
margin-bottom: 50px;
}
.achieve__main-text {
padding-bottom: 30px;
}
.achieve__artistic, .achieve__description {
font-family: 'Verdana', sans-serif;
}
.achieve__artistic {
font-style: italic;
font-size: 20pt;
}
.achieve__image {
background: url('/image.png') no-repeat;
width: 300px;
height: 309px;
background-size: contain;
position: absolute;
bottom: 0;
right: 20px;
}
.achieve__description {
font-size: 25pt;
}
.clearfix {
clear: both;
}

В тексті template.html є якийсь текст, обрамлений {{}}. Це зачатки нашого майбутнього шаблонизатора, який буде змінювати створювати html файл для ачівкі виходячи з даних від парсера.
Сам шаблонизатор вмістився у 8 рядків:
function template (шаблон, data) {
for (const key in data) {
const templateKey = '{{' + key + '}}';
template = template.replace(templateKey, data[key]);
}

return template;
}

Так само я додав кілька правил, яких повинні додаватися класи до назви ачівкі. (Ачивка з назвою Експериментатор не хотіла влазити і весь час залазила на кубок)
if (element.name.split(' ').length > 1 || element.name.length >= 9) {
data.extraHtmlClass = 'achieve__heading-text--small';
}

if (element.name.split(' ').length === 1 && element.name.length >= 14) {
data.extraHtmlClass = 'achieve__heading-text-super-small';
}

Це вирізка з файлу, який генерує html, повна версія файлу лежить тут.
На підсумок нам генерується потрібно кількість html файлів, які підставлені назва, цитата і опис ачівкі.
Поспішаю додати що у мене не вийшло змусити PhantomJS читати директорію, хоча це повинно бути просто. Тому разом з html файлами я генерую файл
names.json
, який містить всі імена створених сторінок.

Створюємо картинки

найпростіша частина, нам потрібно скопіювати example код доки за PhantomJS, трохи його модифікувати і запустити.
const fs = require('fs');
const data = require('../data/names.json');
const config = require('../config/config.json');
var pageCount = 0;

data.forEach(function (e) { // Перебираємо усі сторінки
const page = require('webpage').create(); // Створюємо phantomjs сторінку

page.open('http://127.0.0.1:' + config.port + '/pages/' + e + '.html', function(status) { // завантажуємо html
setTimeout(function() {
if(status === "success") {
page.render('achievements/' + e + '.png'); // перетворюємо html в зображення
pageCount++;

if (pageCount === data.length) {
phantom.exit();
}
}
}, 2000);
});
page.onResourceError = function(resourceError) {
console.log('Unable to load resource (#' + resourceError.id + 'URL:' + resourceError.url + ')');
console.log('Error code:' + resourceError.errorCode + '. Description: '+ resourceError.errorString);
};
});

Запускаємо це
phatnom lib/phantom.js
і генеруємо зображення з html файлів.

Що щось тут не так...

Справді щось не так! PhantomJS завантажує дані з HTTP, а HTTP сервера зі статичними файлами у нас немає, і картинок ми отримати не зможемо. Це значить, що потрібно зробити невеликий static сервер, який ми убъем під кінець виконання програми.
Слава богу, що за нас вже написали static сервер, і ми просто зобов'язані його використовувати.
static const = require('node-static');
const file = new static.Server('./public');

module.exports.spawnServer = (port) => {
return new Promise(resolve => { // Як тільки створиться сервер, промис зарезолвится
const server = require('http').createServer(function (request, response) {
request.addListener('end', function () {
file.serve(request, response);
}).resume();
}).слухати(port, () => {
resolve(server);
});
});
};

module.exports.killServer = (server) => {
server.close(); // Вбити інстанси сервера. Швидко і безболісно.
}

Тепер у нас є парсер, який з тексту робить JS масив, є генератор сторінок, статичний сервер і phantomjs скрипт, який створює сторінки. Залишилося все скомпонувати і новорічна розвага готово!
Так як весь код написаний на Promises, а над усіма використовуваними функціями є Promise обгортки, компонування методів не займе багато часу:
staticServer.spawnServer(config.port).then((serverInstance) => {
staticServerInstance = serverInstance;
return folderManager.create();
})
.then(() => promiseFuncs.readFile(listFile))
.then(buffer => parser(buffer))
.then(data => Promise.all(data.map(e => pageGenerator(e))))
.then(names => promiseFuncs.writeFile('./data/names.json', JSON.stringify(names)))
.then(() => promiseFuncs.execAndOnClose('./node_modules/.bin/phantomjs', ['lib/phantom.js']))
.then(() => {
staticServer.killServer(staticServerInstance);
console.log('Achievements generated!');

if (config.removeFolders) {
return folderManager.remove();
}

return;
}).catch(e => {
console.log(e);
});

Ось і все. Залишилося проявити крапельку креативу і придумати оригінальні назви для досягнень (бажано з використанням локальних мемів), і веселощі гарантовані. Достатньо лише святкувати отримання кожного досягнення святковим "УРА!" і урочисто його видавати.
На жаль, навіть половину ачивік не вийшло роздати, хоч ми й дуже старалися, коли втілювали картинки в реальне життя. (Більше 50 досягнень було наклеєно на папір, продырявлено дироколом і акуратно обв'язане ниточкою).
Сподіваюся, що ця стаття допомогла вам розібратися, як можна з простого тексту генерувати зображення з мінімальними витратами.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.