Good evening, Habrovchane! New Year's holidays died down and everyone slowly enters the working rhythm after the weekend, which means that it's time to describe your New Year's fun.
If you are interested in learning how to generate images from simple pictures using PhantomJS and a little magic, then welcome under the cat!
This new year my friends and I decided to spend an unusual, add some interactive, which would not depend on anyone. Since the majority in my social circle are somehow connected with computer games, it was decided by me and my friend (hereinafter Nikita) to come up with a list of New Year's achievements (or achivok). The list was compiled within a few days and it was decided to somehow arrange and issue them so that the achievements would not be forgotten five minutes after they were received. For the result, they decided to print, stick on watercolor paper, make two holes in the upper right and left corners and hang cards with eyelets on the neck. Having completely understood the technical process, Nikita drew a simple design that would be perfectly printed on a black-and-white printer, and we started to fill in the achiv.
I immediately decided that I didn’t want to add text to the PSD file more than fifty times more than any other, and like any adequate programmer, I spent a little more than two hours to automate the hourly task.
Example of achivka design:
The technology pool was selected instantly. Node.js for generating text and html pages, PhantomJS for drawing and saving.
The format for specifying the progress was set as follows:-- --
(Quote is optional)
It is necessary to translate a file entirely consisting of such strings into a JS object.
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; })); }); }
The input of the function is Buffer
, which is returned by fs.readFile
, and the output is an array:
[{ "name": "", "quote": "- ", "description": " ." }]
Great, working on.
In order to get PhantomJS to open the pages, we first need the pages themselves.
I created a simple 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>
and a small style file to it, which is exactly the same design.
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; }
In the text of template.html there is some text, framed {{and}}. These are the beginnings of our future template engine, which will change to create an html file for the animation based on data from the parser.
The template engine itself fit in 8 lines:
function template (template, data) { for (const key in data) { const templateKey = '{{' + key + '}}'; template = template.replace(templateKey, data[key]); } return template; }
I also added a few rules by which classes should be added to the name of the class. (Achivka with the name The experimenter did not want to fit in and climbed onto the cup all the time)
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'; }
This is a clipping from a file that generates html, the full version of the file is here .
As a result, we need to generate the number of html files, in which the title, quote and description of the file are substituted.
I hasten to add that I could not get PhantomJS to read the directory, although it should be easy. Therefore, together with the html files, I generate the names.json
file, which contains all the names of the generated pages.
The simplest part, we need to copy the example code from the docks by PhantomJS, modify it a bit and run it.
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); }; });
Run this phatnom lib/phantom.js
and generate images from html files.
Truly something is wrong! PhantomJS downloads data via HTTP, but we don’t have an HTTP server with static files, and we won’t be able to get pictures. This means that you need to make a small static server, which we kill at the end of the program.
Thank God that a static server has already been written for us, and we just have to use it.
const static = 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(); }).listen(port, () => { resolve(server); }); }); }; module.exports.killServer = (server) => { server.close(); // . . }
Now we have a parser that makes a JS array from the text, there is a page generator, a static server and a phantomjs script that creates the pages. It remains to put together and Christmas entertainment is ready!
Since all the code is written in Promises, and there is a Promise wrapper above all the functions used, the layout of the methods will not take us much time:
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); });
That's all. It remains to show a bit of creativity and come up with original names for achievements (preferably using local memes), and fun is guaranteed. Suffice it to celebrate the receipt of each achievement festive "Hurray!" and solemnly issue it.
Unfortunately, even half of the achivok could not be distributed, even though we tried very hard when we embodied the pictures in real life. (More than 50 achievements were pasted on paper, holed with a hole punch and neatly tied with a thread).
I hope that this article has helped you figure out how to generate images from simple text with minimal cost.
Source: https://habr.com/ru/post/319674/
All Articles