summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--CMakeLists.txt28
-rw-r--r--designer.cpp244
-rw-r--r--designer.h40
-rw-r--r--director.cpp378
-rw-r--r--director.h31
-rw-r--r--main.cpp45
-rw-r--r--sap.cpp492
-rw-r--r--sap.h34
-rw-r--r--util.h60
m---------vendor/rawr-ebooks0
11 files changed, 926 insertions, 431 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2b3f04 --- /dev/null +++ b/.gitignore
@@ -0,0 +1,5 @@
1.DS_Store
2CMakeFiles
3CMakeCache.txt
4cmake_install.cmake
5Makefile
diff --git a/CMakeLists.txt b/CMakeLists.txt index 70a9572..efafdaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -1,22 +1,34 @@
1cmake_minimum_required (VERSION 3.1) 1cmake_minimum_required (VERSION 3.1)
2project (sap) 2project (sap)
3 3
4set(CMAKE_BUILD_TYPE Debug)
5
6set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") 4set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
7 5
8find_package(FFMPEG REQUIRED) 6find_package(FFMPEG REQUIRED)
9include_directories(${FFMPEG_INCLUDE_DIRS})
10 7
11find_package(PkgConfig) 8find_package(PkgConfig)
9pkg_check_modules(yaml-cpp yaml-cpp REQUIRED)
12pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED) 10pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED)
13include_directories(${GraphicsMagick_INCLUDE_DIRS})
14link_directories(${GraphicsMagick_LIBRARY_DIRS})
15 11
16add_subdirectory(vendor/rawr-ebooks EXCLUDE_FROM_ALL) 12add_subdirectory(vendor/rawr-ebooks EXCLUDE_FROM_ALL)
17include_directories(vendor/rawr-ebooks vendor/rawr-ebooks/vendor/libtwittercpp/src vendor/rawr-ebooks/vendor/yaml-cpp/include)
18 13
19add_executable(sap sap.cpp) 14include_directories(
15 ${FFMPEG_INCLUDE_DIRS}
16 vendor/rawr-ebooks
17 vendor/rawr-ebooks/vendor/libtwittercpp/src
18 ${yaml-cpp_INCLUDE_DIRS}
19 ${GraphicsMagick_INCLUDE_DIRS})
20
21link_directories(
22 ${yaml-cpp_LIBRARY_DIRS}
23 ${GraphicsMagick_LIBRARY_DIRS})
24
25add_executable(sap sap.cpp director.cpp designer.cpp main.cpp)
20set_property(TARGET sap PROPERTY CXX_STANDARD 11) 26set_property(TARGET sap PROPERTY CXX_STANDARD 11)
21set_property(TARGET sap PROPERTY CXX_STANDARD_REQUIRED ON) 27set_property(TARGET sap PROPERTY CXX_STANDARD_REQUIRED ON)
22target_link_libraries(sap rawr twitter++ ${FFMPEG_LIBRARIES} ${GraphicsMagick_LIBRARIES} yaml-cpp) 28
29target_link_libraries(sap
30 rawr
31 twitter++
32 ${FFMPEG_LIBRARIES}
33 ${GraphicsMagick_LIBRARIES}
34 ${yaml-cpp_LIBRARIES})
diff --git a/designer.cpp b/designer.cpp new file mode 100644 index 0000000..3bdcbeb --- /dev/null +++ b/designer.cpp
@@ -0,0 +1,244 @@
1#include "designer.h"
2#include <dirent.h>
3#include <stdexcept>
4#include <iostream>
5#include "util.h"
6
7designer::designer(std::string fontsDir)
8{
9 DIR* fontdir;
10 struct dirent* ent;
11 if ((fontdir = opendir(fontsDir.c_str())) == nullptr)
12 {
13 throw std::invalid_argument("Couldn't find fonts");
14 }
15
16 while ((ent = readdir(fontdir)) != nullptr)
17 {
18 std::string dname(ent->d_name);
19 if ((dname.find(".otf") != std::string::npos)
20 || (dname.find(".ttf") != std::string::npos))
21 {
22 fonts.push_back(fontsDir + "/" + dname);
23 }
24 }
25
26 closedir(fontdir);
27}
28
29Magick::Image designer::generate(
30 size_t width,
31 size_t height,
32 const std::string& text,
33 std::mt19937& rng) const
34{
35 // Initialize two layers: the text, and a shadow to increase contrast.
36 Magick::Image textimage(Magick::Geometry(width, height), "transparent");
37 Magick::Image shadowimage(Magick::Geometry(width, height), "transparent");
38
39 textimage.fillColor(Magick::Color(MaxRGB, MaxRGB, MaxRGB, MaxRGB * 0.0));
40 shadowimage.fillColor(Magick::Color(0, 0, 0, 0));
41 shadowimage.strokeColor("black");
42
43 bool hasFont = false;
44 std::uniform_int_distribution<size_t> fontDist(0, fonts.size() - 1);
45
46 std::vector<std::string> words = split<std::vector<std::string>>(text, " ");
47 std::vector<std::string>::const_iterator curWord = std::begin(words);
48
49 size_t top = V_PADDING;
50 size_t minWords = 1;
51 while (curWord != std::end(words))
52 {
53 // There is a 1 in 10 chance of randomly changing the font. We also have to
54 // set the font if this is the beginning of generation.
55 if (!hasFont || (std::bernoulli_distribution(0.1)(rng)))
56 {
57 std::string font = fonts[fontDist(rng)];
58
59 textimage.font(font);
60 shadowimage.font(font);
61
62 hasFont = true;
63 }
64
65 // Choose a font size for the current line.
66 std::uniform_int_distribution<size_t> sizeDist(
67 MIN_FONT_SIZE, MAX_FONT_SIZE);
68
69 size_t size = sizeDist(rng);
70 textimage.fontPointsize(size);
71
72 // Decide what words to put on this line.
73 size_t maxWords = maxWordsInLine(curWord, std::end(words), textimage);
74
75 size_t lineLen;
76 if (minWords > maxWords)
77 {
78 lineLen = maxWords;
79 } else {
80 std::uniform_int_distribution<size_t> lenDist(minWords, maxWords);
81 lineLen = lenDist(rng);
82 }
83
84 std::string prefixText = implode(curWord, curWord + lineLen, " ");
85
86 // Determine if the choice of font, font size, and number of words, would
87 // prevent the algorithm from being about to layout the entire string even
88 // if the rest of it were rendered in the smallest font size.
89 Magick::TypeMetric metric;
90 textimage.fontTypeMetrics(prefixText, &metric);
91
92 textimage.fontPointsize(MIN_FONT_SIZE);
93
94 // This is how much vertical space would be required to render the rest of
95 // the string at the minimum font size.
96 int lowpadding = minHeightRequired(
97 curWord + lineLen,
98 std::end(words),
99 textimage);
100
101 // This is the amount of space that would be left over if the rest of the
102 // string were rendered at the minimum font size.
103 int freespace =
104 static_cast<int>(height) - static_cast<int>(V_PADDING)
105 - static_cast<int>(top) - lowpadding - metric.textHeight();
106
107 // Some debug text.
108 std::cout << "top of " << top << " with lowpad of " << lowpadding
109 << " and textheight of " << metric.textHeight() << " with freespace="
110 << freespace << std::endl;
111
112 // If there wouldn't be enough room to render the rest of the string at the
113 // minimum font size, go back to the top of the loop and choose a different
114 // font, font size, and number of words.
115 if (freespace < 0)
116 {
117 minWords = lineLen;
118
119 continue;
120 }
121
122 minWords = 1;
123
124 // Determine how much space to leave between this line and the previous one.
125 size_t toppadding;
126 if (std::bernoulli_distribution(0.5)(rng))
127 {
128 // Exponential distribution, biased toward top
129 std::uniform_int_distribution<size_t> expDist(
130 1, static_cast<size_t>(exp(freespace + 1)));
131
132 toppadding = log(expDist(rng));
133 } else {
134 // Linear distribution, biased toward bottom
135 std::uniform_int_distribution<size_t> linDist(0, freespace);
136
137 toppadding = linDist(rng);
138 }
139
140 // Determine the x-coordinate of this line.
141 std::uniform_int_distribution<size_t> leftDist(
142 H_PADDING,
143 width - H_PADDING - static_cast<size_t>(metric.textWidth()) - 1);
144
145 size_t leftx = leftDist(rng);
146
147 // Render this line.
148 size_t ycor = top + toppadding + metric.ascent();
149
150 std::cout << "printing at " << leftx << "," << ycor << std::endl;
151
152 textimage.fontPointsize(size);
153 textimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, ycor));
154
155 shadowimage.fontPointsize(size);
156 shadowimage.strokeWidth(size / 10);
157 shadowimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, ycor));
158
159 top += toppadding + metric.textHeight();
160
161 std::advance(curWord, lineLen);
162 }
163
164 // Make the shadow layer semi-transparent.
165 Magick::PixelPacket* shadpixels = shadowimage.getPixels(0, 0, width, height);
166 Magick::PixelPacket* textpixels = textimage.getPixels(0, 0, width, height);
167 for (size_t j = 0; j < height; j++)
168 {
169 for (size_t i = 0; i < width; i++)
170 {
171 size_t ind = j * width + i;
172
173 if (shadpixels[ind].opacity != MaxRGB)
174 {
175 shadpixels[ind].opacity = MaxRGB * 0.25;
176 }
177 }
178 }
179
180 shadowimage.syncPixels();
181 textimage.syncPixels();
182
183 // Add some blur to the text and shadow.
184 shadowimage.blur(10.0, 20.0);
185 textimage.blur(0.0, 0.5);
186
187 // Put the text layer on top of the shadow.
188 shadowimage.composite(textimage, 0, 0, Magick::OverCompositeOp);
189
190 return shadowimage;
191}
192
193size_t designer::maxWordsInLine(
194 std::vector<std::string>::const_iterator first,
195 std::vector<std::string>::const_iterator last,
196 Magick::Image& textimage) const
197{
198 size_t result = 0;
199
200 std::string curline = "";
201 for (; first != last; first++)
202 {
203 curline += " " + *first;
204
205 Magick::TypeMetric metric;
206 textimage.fontTypeMetrics(curline, &metric);
207
208 if (metric.textWidth() > ((textimage.columns() / 10) * 9))
209 {
210 break;
211 } else {
212 result++;
213 }
214 }
215
216 return result;
217}
218
219size_t designer::minHeightRequired(
220 std::vector<std::string>::const_iterator first,
221 std::vector<std::string>::const_iterator last,
222 Magick::Image& textimage) const
223{
224 if (first == last)
225 {
226 return 0;
227 } else {
228 size_t result = 0;
229
230 while (first != last)
231 {
232 size_t prefixlen = maxWordsInLine(first, last, textimage);
233 std::string prefixText = implode(first, first + prefixlen, " ");
234
235 Magick::TypeMetric metric;
236 textimage.fontTypeMetrics(prefixText, &metric);
237 result += metric.textHeight() + V_PADDING;
238
239 std::advance(first, prefixlen);
240 }
241
242 return result - V_PADDING;
243 }
244}
diff --git a/designer.h b/designer.h new file mode 100644 index 0000000..6a348a0 --- /dev/null +++ b/designer.h
@@ -0,0 +1,40 @@
1#ifndef DESIGNER_H_CCE34BEB
2#define DESIGNER_H_CCE34BEB
3
4#include <random>
5#include <string>
6#include <Magick++.h>
7#include <vector>
8
9class designer {
10public:
11
12 designer(std::string fontsPath);
13
14 Magick::Image generate(
15 size_t width,
16 size_t height,
17 const std::string& text,
18 std::mt19937& rng) const;
19
20private:
21
22 const size_t MIN_FONT_SIZE = 48;
23 const size_t MAX_FONT_SIZE = 96;
24 const size_t V_PADDING = 5;
25 const size_t H_PADDING = 5;
26
27 size_t maxWordsInLine(
28 std::vector<std::string>::const_iterator first,
29 std::vector<std::string>::const_iterator last,
30 Magick::Image& textimage) const;
31
32 size_t minHeightRequired(
33 std::vector<std::string>::const_iterator first,
34 std::vector<std::string>::const_iterator last,
35 Magick::Image& textimage) const;
36
37 std::vector<std::string> fonts;
38};
39
40#endif /* end of include guard: DESIGNER_H_CCE34BEB */
diff --git a/director.cpp b/director.cpp new file mode 100644 index 0000000..24335da --- /dev/null +++ b/director.cpp
@@ -0,0 +1,378 @@
1#include "director.h"
2#include <dirent.h>
3#include <iostream>
4
5extern "C" {
6#include <libavformat/avformat.h>
7#include <libavcodec/avcodec.h>
8#include <libavutil/imgutils.h>
9#include <libswscale/swscale.h>
10}
11
12namespace ffmpeg {
13
14 class format {
15 public:
16
17 format(std::string videoPath)
18 {
19 if (avformat_open_input(&ptr_, videoPath.c_str(), nullptr, nullptr))
20 {
21 throw ffmpeg_error("Could not open video " + videoPath);
22 }
23
24 if (avformat_find_stream_info(ptr_, nullptr))
25 {
26 throw ffmpeg_error("Could not read stream");
27 }
28 }
29
30 ~format()
31 {
32 avformat_close_input(&ptr_);
33 }
34
35 format(const format& other) = delete;
36
37 AVFormatContext* ptr()
38 {
39 return ptr_;
40 }
41
42 private:
43
44 AVFormatContext* ptr_ = nullptr;
45 };
46
47 class codec {
48 public:
49
50 codec(
51 format& fmt,
52 AVStream* st)
53 {
54 AVCodec* dec = avcodec_find_decoder(st->codecpar->codec_id);
55 if (!dec)
56 {
57 throw ffmpeg_error("Failed to find codec");
58 }
59
60 ptr_ = avcodec_alloc_context3(dec);
61 if (!ptr_)
62 {
63 throw ffmpeg_error("Failed to allocate codec context");
64 }
65
66 if (avcodec_parameters_to_context(ptr_, st->codecpar) < 0)
67 {
68 throw ffmpeg_error("Failed to copy codec parameters to decoder");
69 }
70
71 // Init the decoders, with or without reference counting
72 AVDictionary* opts = nullptr;
73 av_dict_set(&opts, "refcounted_frames", "0", 0);
74
75 if (avcodec_open2(ptr_, dec, &opts) < 0)
76 {
77 throw ffmpeg_error("Failed to open codec");
78 }
79 }
80
81 codec(const codec& other) = delete;
82
83 ~codec()
84 {
85 avcodec_free_context(&ptr_);
86 }
87
88 int getWidth() const
89 {
90 return ptr_->width;
91 }
92
93 int getHeight() const
94 {
95 return ptr_->height;
96 }
97
98 enum AVPixelFormat getPixelFormat() const
99 {
100 return ptr_->pix_fmt;
101 }
102
103 AVCodecContext* ptr()
104 {
105 return ptr_;
106 }
107
108 private:
109
110 AVCodecContext* ptr_ = nullptr;
111 };
112
113 class packet {
114 public:
115
116 packet()
117 {
118 ptr_ = av_packet_alloc();
119 }
120
121 ~packet()
122 {
123 av_packet_free(&ptr_);
124 }
125
126 packet(const packet& other) = delete;
127
128 int getStreamIndex() const
129 {
130 return ptr_->stream_index;
131 }
132
133 AVPacket* ptr()
134 {
135 return ptr_;
136 }
137
138 int getFlags() const
139 {
140 return ptr_->flags;
141 }
142
143 private:
144
145 AVPacket* ptr_ = nullptr;
146 };
147
148 class frame {
149 public:
150
151 frame()
152 {
153 ptr_ = av_frame_alloc();
154 }
155
156 ~frame()
157 {
158 av_frame_free(&ptr_);
159 }
160
161 frame(const frame& other) = delete;
162
163 uint8_t** getData()
164 {
165 return ptr_->data;
166 }
167
168 int* getLinesize()
169 {
170 return ptr_->linesize;
171 }
172
173 AVFrame* ptr()
174 {
175 return ptr_;
176 }
177
178 private:
179
180 AVFrame* ptr_ = nullptr;
181 };
182
183 class sws {
184 public:
185
186 sws(
187 int srcW,
188 int srcH,
189 enum AVPixelFormat srcFormat,
190 int dstW,
191 int dstH,
192 enum AVPixelFormat dstFormat,
193 int flags,
194 SwsFilter* srcFilter,
195 SwsFilter* dstFilter,
196 const double* param)
197 {
198 ptr_ = sws_getContext(
199 srcW,
200 srcH,
201 srcFormat,
202 dstW,
203 dstH,
204 dstFormat,
205 flags,
206 srcFilter,
207 dstFilter,
208 param);
209
210 if (ptr_ == NULL)
211 {
212 throw ffmpeg_error("Could not allocate sws context");
213 }
214 }
215
216 ~sws()
217 {
218 sws_freeContext(ptr_);
219 }
220
221 sws(const sws& other) = delete;
222
223 void scale(
224 const uint8_t* const srcSlice[],
225 const int srcStride[],
226 int srcSliceY,
227 int srcSliceH,
228 uint8_t* const dst[],
229 const int dstStride[])
230 {
231 sws_scale(
232 ptr_,
233 srcSlice,
234 srcStride,
235 srcSliceY,
236 srcSliceH,
237 dst,
238 dstStride);
239 }
240
241 private:
242
243 SwsContext* ptr_;
244 };
245
246}
247
248director::director(std::string videosPath) : videosPath_(videosPath)
249{
250 DIR* videodir;
251 struct dirent* ent;
252 if ((videodir = opendir(videosPath.c_str())) == nullptr)
253 {
254 throw std::invalid_argument("Couldn't find videos");
255 }
256
257 while ((ent = readdir(videodir)) != nullptr)
258 {
259 std::string dname(ent->d_name);
260 if (dname.find(".mp4") != std::string::npos)
261 {
262 videos_.push_back(dname);
263 }
264 }
265
266 closedir(videodir);
267}
268
269Magick::Image director::generate(std::mt19937& rng) const
270{
271 std::uniform_int_distribution<size_t> videoDist(0, videos_.size() - 1);
272 std::string video = videosPath_ + "/" + videos_[videoDist(rng)];
273
274 ffmpeg::format fmt(video);
275
276 int streamIdx =
277 av_find_best_stream(fmt.ptr(), AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
278
279 if (streamIdx < 0)
280 {
281 throw ffmpeg_error("Could not find stream");
282 }
283
284 AVStream* stream = fmt.ptr()->streams[streamIdx];
285
286 ffmpeg::codec cdc(fmt, stream);
287
288 size_t codecw = cdc.getWidth();
289 size_t codech = cdc.getHeight();
290
291 std::uniform_int_distribution<int64_t> frameDist(0, stream->duration - 1);
292 int64_t seek = frameDist(rng);
293
294 std::cout << seek << std::endl;
295
296 if (av_seek_frame(fmt.ptr(), streamIdx, seek, 0))
297 {
298 throw ffmpeg_error("Could not seek");
299 }
300
301 ffmpeg::frame frame;
302 ffmpeg::frame converted;
303
304 av_image_alloc(
305 converted.getData(),
306 converted.getLinesize(),
307 codecw,
308 codech,
309 AV_PIX_FMT_RGB24,
310 1);
311
312 ffmpeg::packet pkt;
313
314 do
315 {
316 if (av_read_frame(fmt.ptr(), pkt.ptr()) < 0)
317 {
318 throw ffmpeg_error("Could not read frame");
319 }
320 } while ((pkt.getStreamIndex() != streamIdx)
321 || !(pkt.getFlags() & AV_PKT_FLAG_KEY));
322
323 int ret;
324 do {
325 if (avcodec_send_packet(cdc.ptr(), pkt.ptr()) < 0)
326 {
327 throw ffmpeg_error("Could not send packet");
328 }
329
330 ret = avcodec_receive_frame(cdc.ptr(), frame.ptr());
331 } while (ret == AVERROR(EAGAIN));
332
333 if (ret < 0)
334 {
335 throw ffmpeg_error("Could not decode frame");
336 }
337
338 ffmpeg::sws scaler(
339 cdc.getWidth(),
340 cdc.getHeight(),
341 cdc.getPixelFormat(),
342 cdc.getWidth(),
343 cdc.getHeight(),
344 AV_PIX_FMT_RGB24,
345 0, nullptr, nullptr, 0);
346
347 scaler.scale(
348 frame.getData(),
349 frame.getLinesize(),
350 0,
351 codech,
352 converted.getData(),
353 converted.getLinesize());
354
355 size_t buffer_size = av_image_get_buffer_size(
356 AV_PIX_FMT_RGB24, cdc.getWidth(), cdc.getHeight(), 1);
357
358 std::vector<uint8_t> buffer(buffer_size);
359
360 av_image_copy_to_buffer(
361 buffer.data(),
362 buffer_size,
363 converted.getData(),
364 converted.getLinesize(),
365 AV_PIX_FMT_RGB24,
366 cdc.getWidth(),
367 cdc.getHeight(),
368 1);
369
370 size_t width = 1024;
371 size_t height = codech * width / codecw;
372
373 Magick::Image image;
374 image.read(codecw, codech, "RGB", Magick::CharPixel, buffer.data());
375 image.zoom(Magick::Geometry(width, height));
376
377 return image;
378}
diff --git a/director.h b/director.h new file mode 100644 index 0000000..f00f2ee --- /dev/null +++ b/director.h
@@ -0,0 +1,31 @@
1#ifndef DIRECTOR_H_9DFD929C
2#define DIRECTOR_H_9DFD929C
3
4#include <stdexcept>
5#include <string>
6#include <vector>
7#include <Magick++.h>
8#include <random>
9
10class ffmpeg_error : public std::runtime_error {
11public:
12
13 ffmpeg_error(std::string msg) : std::runtime_error(msg)
14 {
15 }
16};
17
18class director {
19public:
20
21 director(std::string videosPath);
22
23 Magick::Image generate(std::mt19937& rng) const;
24
25private:
26
27 std::string videosPath_;
28 std::vector<std::string> videos_;
29};
30
31#endif /* end of include guard: DIRECTOR_H_9DFD929C */
diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..6ac8ee9 --- /dev/null +++ b/main.cpp
@@ -0,0 +1,45 @@
1#include "sap.h"
2#include <iostream>
3#include <ctime>
4#include <cstdlib>
5
6extern "C" {
7#include <libavformat/avformat.h>
8}
9
10int main(int argc, char** argv)
11{
12 Magick::InitializeMagick(nullptr);
13 av_register_all();
14
15 std::random_device randomDevice;
16 std::mt19937 rng(randomDevice());
17
18 // We also have to seed the C-style RNG because librawr uses it.
19 srand(time(NULL));
20 rand(); rand(); rand(); rand();
21
22 if (argc != 2)
23 {
24 std::cout << "usage: sap [configfile]" << std::endl;
25 return -1;
26 }
27
28 std::string configfile(argv[1]);
29
30 try
31 {
32 sap bot(configfile, rng);
33
34 try
35 {
36 bot.run();
37 } catch (const std::exception& ex)
38 {
39 std::cout << "Error running bot: " << ex.what() << std::endl;
40 }
41 } catch (const std::exception& ex)
42 {
43 std::cout << "Error initializing bot: " << ex.what() << std::endl;
44 }
45}
diff --git a/sap.cpp b/sap.cpp index 7e1412a..f0c3fd1 100644 --- a/sap.cpp +++ b/sap.cpp
@@ -1,22 +1,9 @@
1extern "C" { 1#include "sap.h"
2#include <libavformat/avformat.h>
3#include <libavcodec/avcodec.h>
4#include <libavutil/imgutils.h>
5#include <libswscale/swscale.h>
6}
7
8#include <Magick++.h>
9#include <iostream>
10#include <rawr.h>
11#include <vector>
12#include <list>
13#include <fstream>
14#include <dirent.h>
15#include <sstream>
16#include <twitter.h>
17#include <yaml-cpp/yaml.h> 2#include <yaml-cpp/yaml.h>
18#include <thread> 3#include <thread>
19#include <chrono> 4#include <chrono>
5#include <fstream>
6#include <iostream>
20 7
21/* - random frames from Spongebob (using ffmpeg) 8/* - random frames from Spongebob (using ffmpeg)
22 * with strange text overlaid, possibly rawr'd from 9 * with strange text overlaid, possibly rawr'd from
@@ -25,277 +12,24 @@ extern "C" {
25 * frames 12 * frames
26 */ 13 */
27 14
28template <class Container> 15sap::sap(
29Container split(std::string input, std::string delimiter) 16 std::string configFile,
30{ 17 std::mt19937& rng) :
31 Container result; 18 rng_(rng)
32
33 while (!input.empty())
34 {
35 int divider = input.find(delimiter);
36 if (divider == std::string::npos)
37 {
38 result.push_back(input);
39
40 input = "";
41 } else {
42 result.push_back(input.substr(0, divider));
43
44 input = input.substr(divider+delimiter.length());
45 }
46 }
47
48 return result;
49}
50
51template <class InputIterator>
52std::string implode(InputIterator first, InputIterator last, std::string delimiter)
53{
54 std::stringstream result;
55
56 for (InputIterator it = first; it != last; it++)
57 {
58 if (it != first)
59 {
60 result << delimiter;
61 }
62
63 result << *it;
64 }
65
66 return result.str();
67}
68
69int maxWordsInLine(std::vector<std::string> words, Magick::Image& textimage)
70{
71 int result = 0;
72
73 std::string curline = "";
74 Magick::TypeMetric metric;
75 for (auto word : words)
76 {
77 curline += " " + word;
78
79 textimage.fontTypeMetrics(curline, &metric);
80 if (metric.textWidth() > ((textimage.columns()/10)*9))
81 {
82 break;
83 } else {
84 result++;
85 }
86 }
87
88 return result;
89}
90
91int minHeightRequired(std::vector<std::string> words, Magick::Image& textimage)
92{
93 int result = 0;
94 while (!words.empty())
95 {
96 int prefixlen = maxWordsInLine(words, textimage);
97 std::string prefixText = implode(std::begin(words), std::begin(words) + prefixlen, " ");
98 std::vector<std::string> suffix(std::begin(words) + prefixlen, std::end(words));
99 Magick::TypeMetric metric;
100 textimage.fontTypeMetrics(prefixText, &metric);
101 result += metric.textHeight() + 5;
102
103 words = suffix;
104 }
105
106 return result - 5;
107}
108
109void layoutText(Magick::Image& textimage, Magick::Image& shadowimage, int width, int height, std::string text, const std::vector<std::string>& fonts)
110{
111 textimage.fillColor(Magick::Color(MaxRGB, MaxRGB, MaxRGB, MaxRGB * 0.0));
112 shadowimage.fillColor(Magick::Color(0, 0, 0, 0));
113 shadowimage.strokeColor("black");
114
115 int minSize = 48;
116 int realMaxSize = 96;
117 int maxSize = realMaxSize;
118 Magick::TypeMetric metric;
119 std::string font;
120 auto words = split<std::vector<std::string>>(text, " ");
121 int top = 5;
122 int minWords = 1;
123 while (!words.empty())
124 {
125 if (font.empty() || (rand() % 10 == 0))
126 {
127 font = fonts[rand() % fonts.size()];
128 textimage.font(font);
129 shadowimage.font(font);
130 }
131
132 int size = rand() % (maxSize - minSize + 1) + minSize;
133 textimage.fontPointsize(size);
134 int maxWords = maxWordsInLine(words, textimage);
135 int touse;
136 if (minWords > maxWords)
137 {
138 touse = maxWords;
139 } else {
140 touse = rand() % (maxWords - minWords + 1) + minWords;
141 }
142 std::string prefixText = implode(std::begin(words), std::begin(words) + touse, " ");
143 std::vector<std::string> suffix(std::begin(words) + touse, std::end(words));
144 textimage.fontTypeMetrics(prefixText, &metric);
145
146 textimage.fontPointsize(minSize);
147 int lowpadding = minHeightRequired(suffix, textimage);
148 int freespace = height - 5 - top - lowpadding - metric.textHeight();
149 std::cout << "top of " << top << " with lowpad of " << lowpadding << " and textheight of " << metric.textHeight() << " with freespace=" << freespace << std::endl;
150 if (freespace < 0)
151 {
152 minWords = touse;
153
154 continue;
155 }
156
157 maxSize = realMaxSize;
158 minWords = 1;
159
160 int toppadding;
161 if (rand() % 2 == 0)
162 {
163 // Exponential distribution, biased toward top
164 toppadding = log(rand() % (int)exp(freespace + 1) + 1);
165 } else {
166 // Linear distribution, biased toward bottom
167 toppadding = rand() % (freespace + 1);
168 }
169
170 int leftx = rand() % (width - 10 - (int)metric.textWidth()) + 5;
171 std::cout << "printing at " << leftx << "," << (top + toppadding + metric.ascent()) << std::endl;
172 textimage.fontPointsize(size);
173 textimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, top + toppadding + metric.ascent()));
174
175 shadowimage.fontPointsize(size);
176 shadowimage.strokeWidth(size / 10);
177 shadowimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, top + toppadding + metric.ascent()));
178 //shadowimage.draw(Magick::DrawableRectangle(leftx - 5, top + toppadding, leftx + metric.textWidth() + 5, top + toppadding + metric.textHeight() + 10 + metric.descent()));
179
180 words = suffix;
181 top += toppadding + metric.textHeight();
182 }
183
184 Magick::PixelPacket* shadowpixels = shadowimage.getPixels(0, 0, width, height);
185 Magick::PixelPacket* textpixels = textimage.getPixels(0, 0, width, height);
186 for (int j=0; j<height; j++)
187 {
188 for (int i=0; i<width; i++)
189 {
190 int ind = j*width+i;
191 if (shadowpixels[ind].opacity != MaxRGB)
192 {
193 shadowpixels[ind].opacity = MaxRGB * 0.25;
194 }
195
196 if (textpixels[ind].opacity != MaxRGB)
197 {
198 //textpixels[ind].opacity = MaxRGB * 0.05;
199 }
200 }
201 }
202
203 shadowimage.syncPixels();
204 textimage.syncPixels();
205
206 shadowimage.blur(10.0, 20.0);
207 textimage.blur(0.0, 0.5);
208}
209
210static int open_codec_context(int *stream_idx, AVFormatContext *fmt_ctx, enum AVMediaType type)
211{
212 int ret, stream_index;
213 AVStream *st;
214 AVCodecContext *dec_ctx = NULL;
215 AVCodec *dec = NULL;
216 AVDictionary *opts = NULL;
217 ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
218 if (ret < 0)
219 {
220 //fprintf(stderr, "Could not find %s stream in input file '%s'\n", av_get_media_type_string(type), src_filename);
221 return ret;
222 } else {
223 stream_index = ret;
224 st = fmt_ctx->streams[stream_index];
225
226 // find decoder for the stream
227 dec_ctx = st->codec;
228 dec = avcodec_find_decoder(dec_ctx->codec_id);
229 if (!dec)
230 {
231 fprintf(stderr, "Failed to find %s codec\n", av_get_media_type_string(type));
232 return AVERROR(EINVAL);
233 }
234
235 // Init the decoders, with or without reference counting
236 av_dict_set(&opts, "refcounted_frames", "0", 0);
237 if ((ret = avcodec_open2(dec_ctx, dec, &opts)) < 0)
238 {
239 fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type));
240 return ret;
241 }
242
243 *stream_idx = stream_index;
244 }
245
246 return 0;
247}
248
249int main(int argc, char** argv)
250{ 19{
251 srand(time(NULL)); 20 // Load the config file.
252 rand(); rand(); rand(); rand(); 21 YAML::Node config = YAML::LoadFile(configFile);
253
254 Magick::InitializeMagick(nullptr);
255 av_register_all();
256 22
257 if (argc != 2) 23 // Set up the Twitter client.
258 {
259 std::cout << "usage: sap [configfile]" << std::endl;
260 return -1;
261 }
262
263 std::string configfile(argv[1]);
264 YAML::Node config = YAML::LoadFile(configfile);
265
266 twitter::auth auth; 24 twitter::auth auth;
267 auth.setConsumerKey(config["consumer_key"].as<std::string>()); 25 auth.setConsumerKey(config["consumer_key"].as<std::string>());
268 auth.setConsumerSecret(config["consumer_secret"].as<std::string>()); 26 auth.setConsumerSecret(config["consumer_secret"].as<std::string>());
269 auth.setAccessKey(config["access_key"].as<std::string>()); 27 auth.setAccessKey(config["access_key"].as<std::string>());
270 auth.setAccessSecret(config["access_secret"].as<std::string>()); 28 auth.setAccessSecret(config["access_secret"].as<std::string>());
271
272 twitter::client client(auth);
273
274 // Fonts
275 std::vector<std::string> fonts;
276 {
277 std::string fontdirname = config["fonts"].as<std::string>();
278 DIR* fontdir;
279 struct dirent* ent;
280 if ((fontdir = opendir(fontdirname.c_str())) == nullptr)
281 {
282 std::cout << "Couldn't find fonts." << std::endl;
283 return -2;
284 }
285 29
286 while ((ent = readdir(fontdir)) != nullptr) 30 client_ = std::unique_ptr<twitter::client>(new twitter::client(auth));
287 {
288 std::string dname(ent->d_name);
289 if ((dname.find(".otf") != std::string::npos) || (dname.find(".ttf") != std::string::npos))
290 {
291 fonts.push_back(fontdirname + "/" + dname);
292 }
293 }
294 31
295 closedir(fontdir); 32 // Set up the text generator.
296 }
297
298 rawr kgramstats;
299 for (const YAML::Node& corpusname : config["corpuses"]) 33 for (const YAML::Node& corpusname : config["corpuses"])
300 { 34 {
301 std::ifstream infile(corpusname.as<std::string>()); 35 std::ifstream infile(corpusname.as<std::string>());
@@ -307,164 +41,76 @@ int main(int argc, char** argv)
307 { 41 {
308 line.pop_back(); 42 line.pop_back();
309 } 43 }
310 44
311 corpus += line + " "; 45 corpus += line + " ";
312 } 46 }
313
314 kgramstats.addCorpus(corpus);
315 }
316 47
317 kgramstats.compile(5); 48 kgramstats_.addCorpus(corpus);
318 kgramstats.setMinCorpora(config["corpuses"].size());
319
320 std::string videodirname = config["videos"].as<std::string>();
321 DIR* videodir;
322 struct dirent* ent;
323 if ((videodir = opendir(videodirname.c_str())) == nullptr)
324 {
325 std::cout << "Couldn't find videos." << std::endl;
326 return -1;
327 } 49 }
328 50
329 std::vector<std::string> videos; 51 kgramstats_.compile(5);
330 while ((ent = readdir(videodir)) != nullptr) 52 kgramstats_.setMinCorpora(config["corpuses"].size());
331 {
332 std::string dname(ent->d_name);
333 if (dname.find(".mp4") != std::string::npos)
334 {
335 videos.push_back(dname);
336 }
337 }
338 53
339 closedir(videodir); 54 // Set up the layout designer.
340 55 layout_ = std::unique_ptr<designer>(
56 new designer(config["fonts"].as<std::string>()));
57
58 // Set up the frame picker.
59 director_ = std::unique_ptr<director>(
60 new director(config["videos"].as<std::string>()));
61}
62
63void sap::run() const
64{
341 for (;;) 65 for (;;)
342 { 66 {
343 std::string video = videodirname + "/" + videos[rand() % videos.size()]; 67 std::cout << "Generating tweet..." << std::endl;
344 std::cout << "Opening " << video << std::endl;
345
346 AVFormatContext* format = nullptr;
347 if (avformat_open_input(&format, video.c_str(), nullptr, nullptr))
348 {
349 std::cout << "could not open file" << std::endl;
350 return 1;
351 }
352
353 if (avformat_find_stream_info(format, nullptr))
354 {
355 std::cout << "could not read stream" << std::endl;
356 return 5;
357 }
358
359 int video_stream_idx = -1;
360 if (open_codec_context(&video_stream_idx, format, AVMEDIA_TYPE_VIDEO))
361 {
362 std::cout << "could not open codec" << std::endl;
363 return 6;
364 }
365
366 AVStream* stream = format->streams[video_stream_idx];
367 AVCodecContext* codec = stream->codec;
368 int codecw = codec->width;
369 int codech = codec->height;
370 68
371 int64_t seek = (rand() % format->duration) * codec->time_base.num / codec->time_base.den; 69 try
372 std::cout << seek << std::endl;
373 if (av_seek_frame(format, video_stream_idx, seek, 0))
374 { 70 {
375 std::cout << "could not seek" << std::endl; 71 // Pick the video frame.
376 return 4; 72 Magick::Image image = director_->generate(rng_);
377 } 73
378 74 // Generate the text.
379 AVPacket packet; 75 std::uniform_int_distribution<size_t> lenDist(5, 19);
380 av_init_packet(&packet); 76 std::string action = kgramstats_.randomSentence(lenDist(rng_));
381
382 AVFrame* frame = av_frame_alloc();
383 AVFrame* converted = av_frame_alloc();
384
385 int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecw, codech, 1);
386 uint8_t* buffer = new uint8_t[buffer_size];
387 77
388 av_image_alloc(converted->data, converted->linesize, codecw, codech, AV_PIX_FMT_RGB24, 1); 78 // Lay the text on the video frame.
79 Magick::Image textimage =
80 layout_->generate(image.columns(), image.rows(), action, rng_);
81 image.composite(textimage, 0, 0, Magick::OverCompositeOp);
389 82
390 for (;;) 83 // Send the tweet.
84 std::cout << "Sending tweet..." << std::endl;
85
86 sendTweet(std::move(image));
87
88 std::cout << "Tweeted!" << std::endl;
89
90 // Wait.
91 std::this_thread::sleep_for(std::chrono::hours(1));
92 } catch (const Magick::ErrorImage& ex)
391 { 93 {
392 if (av_read_frame(format, &packet)) 94 std::cout << "Image error: " << ex.what() << std::endl;
393 { 95 } catch (const twitter::twitter_error& ex)
394 std::cout << "could not read frame" << std::endl; 96 {
395 return 2; 97 std::cout << "Twitter error: " << ex.what() << std::endl;
396 } 98
397 99 std::this_thread::sleep_for(std::chrono::hours(1));
398 if (packet.stream_index != video_stream_idx)
399 {
400 continue;
401 }
402
403 int got_pic;
404 if (avcodec_decode_video2(codec, frame, &got_pic, &packet) < 0)
405 {
406 std::cout << "could not decode frame" << std::endl;
407 return 7;
408 }
409
410 if (!got_pic)
411 {
412 continue;
413 }
414
415 if (packet.flags && AV_PKT_FLAG_KEY)
416 {
417 SwsContext* sws = sws_getContext(codecw, codech, codec->pix_fmt, codecw, codech, AV_PIX_FMT_RGB24, 0, nullptr, nullptr, 0);
418 sws_scale(sws, frame->data, frame->linesize, 0, codech, converted->data, converted->linesize);
419 sws_freeContext(sws);
420
421 av_image_copy_to_buffer(buffer, buffer_size, converted->data, converted->linesize, AV_PIX_FMT_RGB24, codecw, codech, 1);
422 av_frame_free(&frame);
423 av_frame_free(&converted);
424 av_packet_unref(&packet);
425 avcodec_close(codec);
426 avformat_close_input(&format);
427
428 int width = 1024;
429 int height = codech * width / codecw;
430
431 Magick::Image image;
432 image.read(codecw, codech, "RGB", Magick::CharPixel, buffer);
433 image.zoom(Magick::Geometry(width, height));
434
435 std::string action = kgramstats.randomSentence(rand() % 15 + 5);
436 Magick::Image textimage(Magick::Geometry(width, height), "transparent");
437 Magick::Image shadowimage(Magick::Geometry(width, height), "transparent");
438 layoutText(textimage, shadowimage, width, height, action, fonts);
439 image.composite(shadowimage, 0, 0, Magick::OverCompositeOp);
440 image.composite(textimage, 0, 0, Magick::OverCompositeOp);
441
442 image.magick("jpeg");
443
444 Magick::Blob outputimg;
445 image.write(&outputimg);
446
447 delete[] buffer;
448
449 std::cout << "Generated image." << std::endl << "Tweeting..." << std::endl;
450
451 try
452 {
453 long media_id = client.uploadMedia("image/jpeg", (const char*) outputimg.data(), outputimg.length());
454 client.updateStatus("", {media_id});
455
456 std::cout << "Done!" << std::endl << "Waiting..." << std::endl << std::endl;
457 } catch (const twitter::twitter_error& error)
458 {
459 std::cout << "Twitter error: " << error.what() << std::endl;
460 }
461
462 break;
463 }
464 } 100 }
465 101
466 std::this_thread::sleep_for(std::chrono::hours(1)); 102 std::cout << std::endl;
467 } 103 }
468 104}
469 return 0; 105
106void sap::sendTweet(Magick::Image image) const
107{
108 Magick::Blob outputimg;
109 image.magick("jpeg");
110 image.write(&outputimg);
111
112 long media_id = client_->uploadMedia("image/jpeg",
113 static_cast<const char*>(outputimg.data()), outputimg.length());
114
115 client_->updateStatus("", {media_id});
470} 116}
diff --git a/sap.h b/sap.h new file mode 100644 index 0000000..5288e57 --- /dev/null +++ b/sap.h
@@ -0,0 +1,34 @@
1#ifndef SAP_H_11D8D668
2#define SAP_H_11D8D668
3
4#include <random>
5#include <string>
6#include <memory>
7#include <Magick++.h>
8#include <twitter.h>
9#include <rawr.h>
10#include "designer.h"
11#include "director.h"
12
13class sap {
14public:
15
16 sap(
17 std::string configFile,
18 std::mt19937& rng);
19
20 void run() const;
21
22private:
23
24 void sendTweet(Magick::Image image) const;
25
26 std::mt19937& rng_;
27 std::unique_ptr<twitter::client> client_;
28 rawr kgramstats_;
29 std::unique_ptr<designer> layout_;
30 std::unique_ptr<director> director_;
31
32};
33
34#endif /* end of include guard: SAP_H_11D8D668 */
diff --git a/util.h b/util.h new file mode 100644 index 0000000..c38f624 --- /dev/null +++ b/util.h
@@ -0,0 +1,60 @@
1#ifndef UTIL_H_1F6A84F6
2#define UTIL_H_1F6A84F6
3
4#include <string>
5#include <sstream>
6#include <iterator>
7
8template <class InputIterator>
9std::string implode(
10 InputIterator first,
11 InputIterator last,
12 std::string delimiter)
13{
14 std::stringstream result;
15
16 for (InputIterator it = first; it != last; it++)
17 {
18 if (it != first)
19 {
20 result << delimiter;
21 }
22
23 result << *it;
24 }
25
26 return result.str();
27}
28
29template <class OutputIterator>
30void split(std::string input, std::string delimiter, OutputIterator out)
31{
32 while (!input.empty())
33 {
34 int divider = input.find(delimiter);
35 if (divider == std::string::npos)
36 {
37 *out = input;
38 out++;
39
40 input = "";
41 } else {
42 *out = input.substr(0, divider);
43 out++;
44
45 input = input.substr(divider+delimiter.length());
46 }
47 }
48}
49
50template <class Container>
51Container split(std::string input, std::string delimiter)
52{
53 Container result;
54
55 split(input, delimiter, std::back_inserter(result));
56
57 return result;
58}
59
60#endif /* end of include guard: UTIL_H_1F6A84F6 */
diff --git a/vendor/rawr-ebooks b/vendor/rawr-ebooks
Subproject ba25493b55b4e4e35de3fca69afd15ddcbaa545 Subproject 247ee4de24eab5ecd030542724db9f69aaa1ed1