led-image-viewer.cc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. // -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
  2. // Copyright (C) 2015 Henner Zeller <h.zeller@acm.org>
  3. //
  4. // This program is free software; you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation version 2.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program. If not, see <http://gnu.org/licenses/gpl-2.0.txt>
  15. // To use this image viewer, first get image-magick development files
  16. // $ sudo apt-get install libgraphicsmagick++-dev libwebp-dev
  17. //
  18. // Then compile with
  19. // $ make led-image-viewer
  20. #include "led-matrix.h"
  21. #include "pixel-mapper.h"
  22. #include "content-streamer.h"
  23. #include <fcntl.h>
  24. #include <math.h>
  25. #include <signal.h>
  26. #include <stdio.h>
  27. #include <stdlib.h>
  28. #include <string.h>
  29. #include <sys/stat.h>
  30. #include <sys/time.h>
  31. #include <sys/types.h>
  32. #include <unistd.h>
  33. #include <algorithm>
  34. #include <map>
  35. #include <string>
  36. #include <vector>
  37. #include <Magick++.h>
  38. #include <magick/image.h>
  39. using rgb_matrix::Canvas;
  40. using rgb_matrix::FrameCanvas;
  41. using rgb_matrix::RGBMatrix;
  42. using rgb_matrix::StreamReader;
  43. typedef int64_t tmillis_t;
  44. static const tmillis_t distant_future = (1LL<<40); // that is a while.
  45. struct ImageParams {
  46. ImageParams() : anim_duration_ms(distant_future), wait_ms(1500),
  47. anim_delay_ms(-1), loops(-1), vsync_multiple(1) {}
  48. tmillis_t anim_duration_ms; // If this is an animation, duration to show.
  49. tmillis_t wait_ms; // Regular image: duration to show.
  50. tmillis_t anim_delay_ms; // Animation delay override.
  51. int loops;
  52. int vsync_multiple;
  53. };
  54. struct FileInfo {
  55. ImageParams params; // Each file might have specific timing settings
  56. bool is_multi_frame;
  57. rgb_matrix::StreamIO *content_stream;
  58. };
  59. volatile bool interrupt_received = false;
  60. static void InterruptHandler(int signo) {
  61. interrupt_received = true;
  62. }
  63. static tmillis_t GetTimeInMillis() {
  64. struct timeval tp;
  65. gettimeofday(&tp, NULL);
  66. return tp.tv_sec * 1000 + tp.tv_usec / 1000;
  67. }
  68. static void SleepMillis(tmillis_t milli_seconds) {
  69. if (milli_seconds <= 0) return;
  70. struct timespec ts;
  71. ts.tv_sec = milli_seconds / 1000;
  72. ts.tv_nsec = (milli_seconds % 1000) * 1000000;
  73. nanosleep(&ts, NULL);
  74. }
  75. static void StoreInStream(const Magick::Image &img, int delay_time_us,
  76. bool do_center,
  77. rgb_matrix::FrameCanvas *scratch,
  78. rgb_matrix::StreamWriter *output) {
  79. scratch->Clear();
  80. const int x_offset = do_center ? (scratch->width() - img.columns()) / 2 : 0;
  81. const int y_offset = do_center ? (scratch->height() - img.rows()) / 2 : 0;
  82. for (size_t y = 0; y < img.rows(); ++y) {
  83. for (size_t x = 0; x < img.columns(); ++x) {
  84. const Magick::Color &c = img.pixelColor(x, y);
  85. if (c.alphaQuantum() < 256) {
  86. scratch->SetPixel(x + x_offset, y + y_offset,
  87. ScaleQuantumToChar(c.redQuantum()),
  88. ScaleQuantumToChar(c.greenQuantum()),
  89. ScaleQuantumToChar(c.blueQuantum()));
  90. }
  91. }
  92. }
  93. output->Stream(*scratch, delay_time_us);
  94. }
  95. static void CopyStream(rgb_matrix::StreamReader *r,
  96. rgb_matrix::StreamWriter *w,
  97. rgb_matrix::FrameCanvas *scratch) {
  98. uint32_t delay_us;
  99. while (r->GetNext(scratch, &delay_us)) {
  100. w->Stream(*scratch, delay_us);
  101. }
  102. }
  103. // Load still image or animation.
  104. // Scale, so that it fits in "width" and "height" and store in "result".
  105. static bool LoadImageAndScale(const char *filename,
  106. int target_width, int target_height,
  107. bool fill_width, bool fill_height,
  108. std::vector<Magick::Image> *result,
  109. std::string *err_msg) {
  110. std::vector<Magick::Image> frames;
  111. try {
  112. readImages(&frames, filename);
  113. } catch (std::exception& e) {
  114. if (e.what()) *err_msg = e.what();
  115. return false;
  116. }
  117. if (frames.size() == 0) {
  118. fprintf(stderr, "No image found.");
  119. return false;
  120. }
  121. // Put together the animation from single frames. GIFs can have nasty
  122. // disposal modes, but they are handled nicely by coalesceImages()
  123. if (frames.size() > 1) {
  124. Magick::coalesceImages(result, frames.begin(), frames.end());
  125. } else {
  126. result->push_back(frames[0]); // just a single still image.
  127. }
  128. const int img_width = (*result)[0].columns();
  129. const int img_height = (*result)[0].rows();
  130. const float width_fraction = (float)target_width / img_width;
  131. const float height_fraction = (float)target_height / img_height;
  132. if (fill_width && fill_height) {
  133. // Scrolling diagonally. Fill as much as we can get in available space.
  134. // Largest scale fraction determines that.
  135. const float larger_fraction = (width_fraction > height_fraction)
  136. ? width_fraction
  137. : height_fraction;
  138. target_width = (int) roundf(larger_fraction * img_width);
  139. target_height = (int) roundf(larger_fraction * img_height);
  140. }
  141. else if (fill_height) {
  142. // Horizontal scrolling: Make things fit in vertical space.
  143. // While the height constraint stays the same, we can expand to full
  144. // width as we scroll along that axis.
  145. target_width = (int) roundf(height_fraction * img_width);
  146. }
  147. else if (fill_width) {
  148. // dito, vertical. Make things fit in horizontal space.
  149. target_height = (int) roundf(width_fraction * img_height);
  150. }
  151. for (size_t i = 0; i < result->size(); ++i) {
  152. (*result)[i].scale(Magick::Geometry(target_width, target_height));
  153. }
  154. return true;
  155. }
  156. void DisplayAnimation(const FileInfo *file,
  157. RGBMatrix *matrix, FrameCanvas *offscreen_canvas) {
  158. const tmillis_t duration_ms = (file->is_multi_frame
  159. ? file->params.anim_duration_ms
  160. : file->params.wait_ms);
  161. rgb_matrix::StreamReader reader(file->content_stream);
  162. int loops = file->params.loops;
  163. const tmillis_t end_time_ms = GetTimeInMillis() + duration_ms;
  164. const tmillis_t override_anim_delay = file->params.anim_delay_ms;
  165. for (int k = 0;
  166. (loops < 0 || k < loops)
  167. && !interrupt_received
  168. && GetTimeInMillis() < end_time_ms;
  169. ++k) {
  170. uint32_t delay_us = 0;
  171. while (!interrupt_received && GetTimeInMillis() <= end_time_ms
  172. && reader.GetNext(offscreen_canvas, &delay_us)) {
  173. const tmillis_t anim_delay_ms =
  174. override_anim_delay >= 0 ? override_anim_delay : delay_us / 1000;
  175. const tmillis_t start_wait_ms = GetTimeInMillis();
  176. offscreen_canvas = matrix->SwapOnVSync(offscreen_canvas,
  177. file->params.vsync_multiple);
  178. const tmillis_t time_already_spent = GetTimeInMillis() - start_wait_ms;
  179. SleepMillis(anim_delay_ms - time_already_spent);
  180. }
  181. reader.Rewind();
  182. }
  183. }
  184. static int usage(const char *progname) {
  185. fprintf(stderr, "usage: %s [options] <image> [option] [<image> ...]\n",
  186. progname);
  187. fprintf(stderr, "Options:\n"
  188. "\t-O<streamfile> : Output to stream-file instead of matrix (Don't need to be root).\n"
  189. "\t-C : Center images.\n"
  190. "\nThese options affect images FOLLOWING them on the command line,\n"
  191. "so it is possible to have different options for each image\n"
  192. "\t-w<seconds> : Regular image: "
  193. "Wait time in seconds before next image is shown (default: 1.5).\n"
  194. "\t-t<seconds> : "
  195. "For animations: stop after this time.\n"
  196. "\t-l<loop-count> : "
  197. "For animations: number of loops through a full cycle.\n"
  198. "\t-D<animation-delay-ms> : "
  199. "For animations: override the delay between frames given in the\n"
  200. "\t gif/stream animation with this value. Use -1 to use default value.\n"
  201. "\t-V<vsync-multiple> : For animation (expert): Only do frame vsync-swaps on multiples of refresh (default: 1)\n"
  202. "\t (Tip: use --led-limit-refresh for stable rate)\n"
  203. "\nOptions affecting display of multiple images:\n"
  204. "\t-f : "
  205. "Forever cycle through the list of files on the command line.\n"
  206. "\t-s : If multiple images are given: shuffle.\n"
  207. );
  208. fprintf(stderr, "\nGeneral LED matrix options:\n");
  209. rgb_matrix::PrintMatrixFlags(stderr);
  210. fprintf(stderr,
  211. "\nSwitch time between files: "
  212. "-w for static images; -t/-l for animations\n"
  213. "Animated gifs: If both -l and -t are given, "
  214. "whatever finishes first determines duration.\n");
  215. fprintf(stderr, "\nThe -w, -t and -l options apply to the following images "
  216. "until a new instance of one of these options is seen.\n"
  217. "So you can choose different durations for different images.\n");
  218. return 1;
  219. }
  220. int main(int argc, char *argv[]) {
  221. Magick::InitializeMagick(*argv);
  222. RGBMatrix::Options matrix_options;
  223. rgb_matrix::RuntimeOptions runtime_opt;
  224. if (!rgb_matrix::ParseOptionsFromFlags(&argc, &argv,
  225. &matrix_options, &runtime_opt)) {
  226. return usage(argv[0]);
  227. }
  228. bool do_forever = false;
  229. bool do_center = false;
  230. bool do_shuffle = false;
  231. // We remember ImageParams for each image, which will change whenever
  232. // there is a flag modifying them. This map keeps track of filenames
  233. // and their image params (also for unrelated elements of argv[], but doesn't
  234. // matter).
  235. // We map the pointer instad of the string of the argv parameter so that
  236. // we can have two times the same image on the commandline list with different
  237. // parameters.
  238. std::map<const void *, struct ImageParams> filename_params;
  239. // Set defaults.
  240. ImageParams img_param;
  241. for (int i = 0; i < argc; ++i) {
  242. filename_params[argv[i]] = img_param;
  243. }
  244. const char *stream_output = NULL;
  245. int opt;
  246. while ((opt = getopt(argc, argv, "w:t:l:fr:c:P:LhCR:sO:V:D:")) != -1) {
  247. switch (opt) {
  248. case 'w':
  249. img_param.wait_ms = roundf(atof(optarg) * 1000.0f);
  250. break;
  251. case 't':
  252. img_param.anim_duration_ms = roundf(atof(optarg) * 1000.0f);
  253. break;
  254. case 'l':
  255. img_param.loops = atoi(optarg);
  256. break;
  257. case 'D':
  258. img_param.anim_delay_ms = atoi(optarg);
  259. break;
  260. case 'f':
  261. do_forever = true;
  262. break;
  263. case 'C':
  264. do_center = true;
  265. break;
  266. case 's':
  267. do_shuffle = true;
  268. break;
  269. case 'r':
  270. fprintf(stderr, "Instead of deprecated -r, use --led-rows=%s instead.\n",
  271. optarg);
  272. matrix_options.rows = atoi(optarg);
  273. break;
  274. case 'c':
  275. fprintf(stderr, "Instead of deprecated -c, use --led-chain=%s instead.\n",
  276. optarg);
  277. matrix_options.chain_length = atoi(optarg);
  278. break;
  279. case 'P':
  280. matrix_options.parallel = atoi(optarg);
  281. break;
  282. case 'L':
  283. fprintf(stderr, "-L is deprecated. Use\n\t--led-pixel-mapper=\"U-mapper\" --led-chain=4\ninstead.\n");
  284. return 1;
  285. break;
  286. case 'R':
  287. fprintf(stderr, "-R is deprecated. "
  288. "Use --led-pixel-mapper=\"Rotate:%s\" instead.\n", optarg);
  289. return 1;
  290. break;
  291. case 'O':
  292. stream_output = strdup(optarg);
  293. break;
  294. case 'V':
  295. img_param.vsync_multiple = atoi(optarg);
  296. if (img_param.vsync_multiple < 1) img_param.vsync_multiple = 1;
  297. break;
  298. case 'h':
  299. default:
  300. return usage(argv[0]);
  301. }
  302. // Starting from the current file, set all the remaining files to
  303. // the latest change.
  304. for (int i = optind; i < argc; ++i) {
  305. filename_params[argv[i]] = img_param;
  306. }
  307. }
  308. const int filename_count = argc - optind;
  309. if (filename_count == 0) {
  310. fprintf(stderr, "Expected image filename.\n");
  311. return usage(argv[0]);
  312. }
  313. // Prepare matrix
  314. runtime_opt.do_gpio_init = (stream_output == NULL);
  315. RGBMatrix *matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt);
  316. if (matrix == NULL)
  317. return 1;
  318. FrameCanvas *offscreen_canvas = matrix->CreateFrameCanvas();
  319. printf("Size: %dx%d. Hardware gpio mapping: %s\n",
  320. matrix->width(), matrix->height(), matrix_options.hardware_mapping);
  321. // These parameters are needed once we do scrolling.
  322. const bool fill_width = false;
  323. const bool fill_height = false;
  324. // In case the output to stream is requested, set up the stream object.
  325. rgb_matrix::StreamIO *stream_io = NULL;
  326. rgb_matrix::StreamWriter *global_stream_writer = NULL;
  327. if (stream_output) {
  328. int fd = open(stream_output, O_CREAT|O_WRONLY, 0644);
  329. if (fd < 0) {
  330. perror("Couldn't open output stream");
  331. return 1;
  332. }
  333. stream_io = new rgb_matrix::FileStreamIO(fd);
  334. global_stream_writer = new rgb_matrix::StreamWriter(stream_io);
  335. }
  336. const tmillis_t start_load = GetTimeInMillis();
  337. fprintf(stderr, "Loading %d files...\n", argc - optind);
  338. // Preparing all the images beforehand as the Pi might be too slow to
  339. // be quickly switching between these. So preprocess.
  340. std::vector<FileInfo*> file_imgs;
  341. for (int imgarg = optind; imgarg < argc; ++imgarg) {
  342. const char *filename = argv[imgarg];
  343. FileInfo *file_info = NULL;
  344. std::string err_msg;
  345. std::vector<Magick::Image> image_sequence;
  346. if (LoadImageAndScale(filename, matrix->width(), matrix->height(),
  347. fill_width, fill_height, &image_sequence, &err_msg)) {
  348. file_info = new FileInfo();
  349. file_info->params = filename_params[filename];
  350. file_info->content_stream = new rgb_matrix::MemStreamIO();
  351. file_info->is_multi_frame = image_sequence.size() > 1;
  352. rgb_matrix::StreamWriter out(file_info->content_stream);
  353. for (size_t i = 0; i < image_sequence.size(); ++i) {
  354. const Magick::Image &img = image_sequence[i];
  355. int64_t delay_time_us;
  356. if (file_info->is_multi_frame) {
  357. delay_time_us = img.animationDelay() * 10000; // unit in 1/100s
  358. } else {
  359. delay_time_us = file_info->params.wait_ms * 1000; // single image.
  360. }
  361. if (delay_time_us <= 0) delay_time_us = 100 * 1000; // 1/10sec
  362. StoreInStream(img, delay_time_us, do_center, offscreen_canvas,
  363. global_stream_writer ? global_stream_writer : &out);
  364. }
  365. } else {
  366. // Ok, not an image. Let's see if it is one of our streams.
  367. int fd = open(filename, O_RDONLY);
  368. if (fd >= 0) {
  369. file_info = new FileInfo();
  370. file_info->params = filename_params[filename];
  371. file_info->content_stream = new rgb_matrix::FileStreamIO(fd);
  372. StreamReader reader(file_info->content_stream);
  373. if (reader.GetNext(offscreen_canvas, NULL)) { // header+size ok
  374. file_info->is_multi_frame = reader.GetNext(offscreen_canvas, NULL);
  375. reader.Rewind();
  376. if (global_stream_writer) {
  377. CopyStream(&reader, global_stream_writer, offscreen_canvas);
  378. }
  379. } else {
  380. err_msg = "Can't read as image or compatible stream";
  381. delete file_info->content_stream;
  382. delete file_info;
  383. file_info = NULL;
  384. }
  385. }
  386. else {
  387. perror("Opening file");
  388. }
  389. }
  390. if (file_info) {
  391. file_imgs.push_back(file_info);
  392. } else {
  393. fprintf(stderr, "%s skipped: Unable to open (%s)\n",
  394. filename, err_msg.c_str());
  395. }
  396. }
  397. if (stream_output) {
  398. delete global_stream_writer;
  399. delete stream_io;
  400. if (file_imgs.size()) {
  401. fprintf(stderr, "Done: Output to stream %s; "
  402. "this can now be opened with led-image-viewer with the exact same panel configuration settings such as rows, chain, parallel and hardware-mapping\n", stream_output);
  403. }
  404. if (do_shuffle)
  405. fprintf(stderr, "Note: -s (shuffle) does not have an effect when generating streams.\n");
  406. if (do_forever)
  407. fprintf(stderr, "Note: -f (forever) does not have an effect when generating streams.\n");
  408. // Done, no actual output to matrix.
  409. return 0;
  410. }
  411. // Some parameter sanity adjustments.
  412. if (file_imgs.empty()) {
  413. // e.g. if all files could not be interpreted as image.
  414. fprintf(stderr, "No image could be loaded.\n");
  415. return 1;
  416. } else if (file_imgs.size() == 1) {
  417. // Single image: show forever.
  418. file_imgs[0]->params.wait_ms = distant_future;
  419. } else {
  420. for (size_t i = 0; i < file_imgs.size(); ++i) {
  421. ImageParams &params = file_imgs[i]->params;
  422. // Forever animation ? Set to loop only once, otherwise that animation
  423. // would just run forever, stopping all the images after it.
  424. if (params.loops < 0 && params.anim_duration_ms == distant_future) {
  425. params.loops = 1;
  426. }
  427. }
  428. }
  429. fprintf(stderr, "Loading took %.3fs; now: Display.\n",
  430. (GetTimeInMillis() - start_load) / 1000.0);
  431. signal(SIGTERM, InterruptHandler);
  432. signal(SIGINT, InterruptHandler);
  433. do {
  434. if (do_shuffle) {
  435. std::random_shuffle(file_imgs.begin(), file_imgs.end());
  436. }
  437. for (size_t i = 0; i < file_imgs.size() && !interrupt_received; ++i) {
  438. DisplayAnimation(file_imgs[i], matrix, offscreen_canvas);
  439. }
  440. } while (do_forever && !interrupt_received);
  441. if (interrupt_received) {
  442. fprintf(stderr, "Caught signal. Exiting.\n");
  443. }
  444. // Animation finished. Shut down the RGB matrix.
  445. matrix->Clear();
  446. delete matrix;
  447. // Leaking the FileInfos, but don't care at program end.
  448. return 0;
  449. }