1 // -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*-
2 // (c) 2016 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
16 #include "image-display.h"
17
18 #include "terminal-canvas.h"
19 #include "timg-time.h"
20
21 #include <Magick++.h>
22 #include <assert.h>
23 #include <fcntl.h>
24 #include <math.h>
25 #include <string.h>
26 #include <sys/stat.h>
27 #include <sys/types.h>
28 #include <unistd.h>
29
30 #include <algorithm>
31
32 static constexpr bool kDebug = false;
33
34 namespace timg {
CopyToFramebuffer(const Magick::Image & img,timg::Framebuffer * result)35 static void CopyToFramebuffer(const Magick::Image &img,
36 timg::Framebuffer *result) {
37 assert(result->width() >= (int) img.columns()
38 && result->height() >= (int) img.rows());
39 for (size_t y = 0; y < img.rows(); ++y) {
40 for (size_t x = 0; x < img.columns(); ++x) {
41 const Magick::Color &c = img.pixelColor(x, y);
42 result->SetPixel(x, y,
43 {
44 ScaleQuantumToChar(c.redQuantum()),
45 ScaleQuantumToChar(c.greenQuantum()),
46 ScaleQuantumToChar(c.blueQuantum()),
47 (uint8_t)(0xff - ScaleQuantumToChar(
48 c.alphaQuantum()))
49 });
50 }
51 }
52 }
53
54 // Frame already prepared as the buffer to be sent, so copy to terminal-buffer
55 // does not have to be done online. Also knows about the animation delay.
56 class ImageLoader::PreprocessedFrame {
57 public:
PreprocessedFrame(const Magick::Image & img,const DisplayOptions & opt,bool is_part_of_animation)58 PreprocessedFrame(const Magick::Image &img, const DisplayOptions &opt,
59 bool is_part_of_animation)
60 : delay_(DurationFromImgDelay(img, is_part_of_animation)),
61 framebuffer_(img.columns(), img.rows()) {
62 CopyToFramebuffer(img, &framebuffer_);
63 framebuffer_.AlphaComposeBackground(opt.bgcolor_getter,
64 opt.bg_pattern_color,
65 opt.pattern_size * opt.cell_x_px,
66 opt.pattern_size * opt.cell_y_px/2);
67 }
delay() const68 Duration delay() const { return delay_; }
framebuffer() const69 const timg::Framebuffer &framebuffer() const { return framebuffer_; }
70
71 private:
DurationFromImgDelay(const Magick::Image & img,bool is_part_of_animation)72 static Duration DurationFromImgDelay(const Magick::Image &img,
73 bool is_part_of_animation) {
74 if (!is_part_of_animation) return Duration::Millis(0);
75 int delay_time = img.animationDelay(); // in 1/100s of a second.
76 if (delay_time < 1) delay_time = 10;
77 return Duration::Millis(delay_time * 10);
78 }
79 const Duration delay_;
80 timg::Framebuffer framebuffer_;
81 };
82
~ImageLoader()83 ImageLoader::~ImageLoader() {
84 for (PreprocessedFrame *f : frames_) delete f;
85 }
86
VersionInfo()87 const char *ImageLoader::VersionInfo() {
88 return "GraphicsMagick " MagickLibVersionText " (" MagickReleaseDate ")";
89 }
90
EndsWith(const std::string & filename,const char * suffix)91 static bool EndsWith(const std::string &filename, const char *suffix) {
92 const size_t flen = filename.length();
93 const size_t slen = strlen(suffix);
94 if (flen < slen) return false;
95 return strcasecmp(filename.c_str() + flen - slen, suffix) == 0;
96 }
97
98 struct ExifImageOp { int angle = 0; bool flip = false; };
GetExifOp(Magick::Image & img)99 static ExifImageOp GetExifOp(Magick::Image &img) {
100 const std::string rotation_tag = img.attribute("EXIF:Orientation");
101 if (rotation_tag.empty() || rotation_tag.size() != 1)
102 return {}; // Nothing to do or broken tag.
103 switch (rotation_tag[0]) {
104 case '2': return { 180, true };
105 case '3': return { 180, false };
106 case '4': return { 0, true };
107 case '5': return { 90, true };
108 case '6': return { 90, false };
109 case '7': return { -90, true };
110 case '8': return { -90, false };
111 }
112 return {};
113 }
114
LooksLikeApng(const std::string & filename)115 static bool LooksLikeApng(const std::string &filename) {
116 // Somewhat handwavy: the "acTL" chunk could of course be at other places as well,
117 // let's assume it is just after IHDR.
118 int fd = open(filename.c_str(), O_RDONLY);
119 if (fd < 0) return false;
120 char actl_sig[4] = {};
121 static constexpr int kPngHeaderLen = 8;
122 static constexpr int kPngIHDRLen = 8 + 13 + 4;
123 const ssize_t len = pread(fd, actl_sig, 4, kPngHeaderLen + kPngIHDRLen + 4);
124 close(fd);
125 return len == 4 && memcmp(actl_sig, "acTL", 4) == 0;
126 }
127
LoadAndScale(const DisplayOptions & opts,int frame_offset,int frame_count)128 bool ImageLoader::LoadAndScale(const DisplayOptions &opts,
129 int frame_offset, int frame_count) {
130 options_ = opts;
131
132 const char *const file = filename().c_str();
133 for (const char *ending : { ".png", ".apng" }) {
134 if (strcasecmp(file + strlen(file) - strlen(ending), ending) == 0 &&
135 LooksLikeApng(filename())) {
136 return false; // ImageMagick does not deal with apng. Let Video deal with it
137 }
138 }
139
140 std::vector<Magick::Image> frames;
141 try {
142 readImages(&frames, filename()); // ideally, we could set max_frames
143 }
144 catch(Magick::Warning &warning) {
145 if (kDebug) fprintf(stderr, "Meh: %s (%s)\n",
146 filename().c_str(), warning.what());
147 }
148 catch (std::exception& e) {
149 // No message, let that file be handled by the next handler.
150 if (kDebug) fprintf(stderr, "Exception: %s (%s)\n",
151 filename().c_str(), e.what());
152 return false;
153 }
154
155 if (frames.size() == 0) {
156 if (kDebug) fprintf(stderr, "No image found.");
157 return false;
158 }
159
160 // We don't really know if something is an animation from the frames we
161 // got back (or is there ?), so we use a blacklist approach here: filenames
162 // that are known to be containers for multiple independent images are
163 // considered not an animation.
164 const bool could_be_animation =
165 !EndsWith(filename(), "ico") && !EndsWith(filename(), "pdf");
166
167 // We can't remove the offset yet as the coalesceImages() might need images
168 // prior to our desired set.
169 if (frame_count > 0 && frame_offset + frame_count < (int)frames.size()) {
170 frames.resize(frame_offset + frame_count);
171 }
172
173 std::vector<Magick::Image> result;
174 // Put together the animation from single frames. GIFs can have nasty
175 // disposal modes, but they are handled nicely by coalesceImages()
176 if (frames.size() > 1 && could_be_animation) {
177 Magick::coalesceImages(&result, frames.begin(), frames.end());
178 is_animation_ = true;
179 } else {
180 result.insert(result.end(), frames.begin(), frames.end());
181 is_animation_ = false;
182 }
183
184 if (frame_offset > 0) {
185 frame_offset = std::min(frame_offset, (int)result.size() - 1);
186 result.erase(result.begin(), result.begin() + frame_offset);
187 }
188
189 for (Magick::Image &img : result) {
190 ExifImageOp exif_op;
191 if (opts.exif_rotate) exif_op = GetExifOp(img);
192
193 // We do trimming only if this is not an animation, which will likely
194 // not create a pleasent result.
195 if (!is_animation_) {
196 if (opts.crop_border > 0) {
197 const int c = opts.crop_border;
198 const int w = std::max(1, (int)img.columns() - 2*c);
199 const int h = std::max(1, (int)img.rows() - 2*c);
200 img.crop(Magick::Geometry(w, h, c, c));
201 }
202 if (opts.auto_crop) {
203 img.trim();
204 }
205 }
206
207 // Figure out scaling for the image.
208 int target_width = 0, target_height = 0;
209 if (CalcScaleToFitDisplay(img.columns(), img.rows(),
210 opts, abs(exif_op.angle) == 90,
211 &target_width, &target_height)) {
212 try {
213 auto geometry = Magick::Geometry(target_width, target_height);
214 geometry.aspect(true); // Force to scale to given size.
215 if (opts.antialias)
216 img.scale(geometry);
217 else
218 img.sample(geometry);
219 }
220 catch (const std::exception& e) {
221 if (kDebug) fprintf(stderr, "%s: %s\n",
222 filename().c_str(), e.what());
223 return false;
224 }
225 }
226
227 // Now that the image is nice and small, the following ops are cheap
228 if (exif_op.flip) img.flip();
229 img.rotate(exif_op.angle);
230
231 frames_.push_back(new PreprocessedFrame(img, opts, result.size() > 1));
232 }
233
234 max_frames_ = (frame_count < 0)
235 ? (int)frames_.size()
236 : std::min(frame_count, (int)frames_.size());
237
238 return true;
239 }
240
IndentationIfCentered(const PreprocessedFrame * frame) const241 int ImageLoader::IndentationIfCentered(const PreprocessedFrame *frame) const {
242 return options_.center_horizontally
243 ? (options_.width - frame->framebuffer().width()) / 2
244 : 0;
245 }
246
SendFrames(Duration duration,int loops,const volatile sig_atomic_t & interrupt_received,const Renderer::WriteFramebufferFun & sink)247 void ImageLoader::SendFrames(Duration duration, int loops,
248 const volatile sig_atomic_t &interrupt_received,
249 const Renderer::WriteFramebufferFun &sink) {
250 if (options_.scroll_animation) {
251 Scroll(duration, loops, interrupt_received,
252 options_.scroll_dx, options_.scroll_dy, options_.scroll_delay,
253 sink);
254 return;
255 }
256
257 int last_height = -1; // First image emit will not have a height.
258 if (frames_.size() == 1 || !is_animation_)
259 loops = 1; // If there is no animation, nothing to repeat.
260
261 // Not initialized or negative value wants us to loop forever.
262 // (note, kNotInitialized is actually negative, but here for clarity
263 const bool loop_forever = (loops < 0) || (loops == timg::kNotInitialized);
264
265 timg::Duration time_from_first_frame;
266 bool is_first = true;
267 for (int k = 0;
268 (loop_forever || k < loops)
269 && !interrupt_received
270 && time_from_first_frame < duration;
271 ++k) {
272 for (int f = 0; f < max_frames_ && !interrupt_received; ++f) {
273 const auto &frame = frames_[f];
274 time_from_first_frame.Add(frame->delay());
275 const int dx = IndentationIfCentered(frame);
276 const int dy = is_animation_ && last_height > 0 ? -last_height : 0;
277 SeqType seq_type = SeqType::FrameImmediate;
278 if (is_animation_) {
279 seq_type = is_first
280 ? SeqType::StartOfAnimation
281 : SeqType::AnimationFrame;
282 }
283 sink(dx, dy, frame->framebuffer(), seq_type,
284 std::min(time_from_first_frame, duration));
285 last_height = frame->framebuffer().height();
286 if (time_from_first_frame > duration) break;
287 is_first = false;
288 }
289 }
290 }
291
gcd(int a,int b)292 static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
293
Scroll(Duration duration,int loops,const volatile sig_atomic_t & interrupt_received,int dx,int dy,Duration scroll_delay,const Renderer::WriteFramebufferFun & write_fb)294 void ImageLoader::Scroll(Duration duration, int loops,
295 const volatile sig_atomic_t &interrupt_received,
296 int dx, int dy, Duration scroll_delay,
297 const Renderer::WriteFramebufferFun &write_fb) {
298 if (frames_.size() > 1) {
299 if (kDebug) fprintf(stderr, "This is an %simage format, "
300 "scrolling on top of that is not supported. "
301 "Just doing the scrolling of the first frame.\n",
302 is_animation_ ? "animated " : "multi-");
303 // TODO: do both.
304 }
305
306 const Framebuffer &img = frames_[0]->framebuffer();
307 const int img_width = img.width();
308 const int img_height = img.height();
309
310 const int display_w = std::min(options_.width, img_width);
311 const int display_h = std::min(options_.height, img_height);
312
313 // Since the user can choose the number of cycles we go through it,
314 // we need to calculate what the minimum number of steps is we need
315 // to do the scroll. If this is just in one direction, that is simple: the
316 // number of pixel in that direction. If we go diagonal, then it is
317 // essentially the least common multiple of steps.
318 const int x_steps = (dx == 0)
319 ? 1
320 : ((img_width % abs(dx) == 0) ? img_width / abs(dx) : img_width);
321 const int y_steps = (dy == 0)
322 ? 1
323 : ((img_height % abs(dy) == 0) ? img_height / abs(dy) : img_height);
324 const int64_t cycle_steps = x_steps * y_steps / gcd(x_steps, y_steps);
325
326 // Depending if we go forward or backward, we want to start out aligned
327 // right or left.
328 // For negative direction, guarantee that we never run into negative numbers.
329 const int64_t x_init = (dx < 0)
330 ? (img_width - display_w - dx*cycle_steps) : 0;
331 const int64_t y_init = (dy < 0)
332 ? (img_height - display_h - dy*cycle_steps) : 0;
333 bool is_first = true;
334
335 timg::Framebuffer display_fb(display_w, display_h);
336 timg::Duration time_from_first_frame;
337 for (int k = 0;
338 (loops < 0 || k < loops)
339 && !interrupt_received
340 && time_from_first_frame < duration;
341 ++k) {
342 for (int64_t cycle_pos = 0; cycle_pos <= cycle_steps; ++cycle_pos) {
343 if (interrupt_received || time_from_first_frame > duration)
344 break;
345 const int64_t x_cycle_pos = dx*cycle_pos;
346 const int64_t y_cycle_pos = dy*cycle_pos;
347 for (int y = 0; y < display_h; ++y) {
348 for (int x = 0; x < display_w; ++x) {
349 const int x_src = (x_init + x_cycle_pos + x) % img_width;
350 const int y_src = (y_init + y_cycle_pos + y) % img_height;
351 display_fb.SetPixel(x, y, img.at(x_src, y_src));
352 }
353 }
354 time_from_first_frame.Add(scroll_delay);
355 write_fb(0, is_first ? 0 : -display_fb.height(), display_fb,
356 is_first
357 ? SeqType::StartOfAnimation
358 : SeqType::AnimationFrame,
359 time_from_first_frame);
360 is_first = false;
361 }
362 }
363 }
364
365 } // namespace timg
366