GCC Code Coverage Report


Directory: ./
File: overlays/PixelOverlayEffects.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 974 1122 86.8%
Functions: 79 88 89.8%
Branches: 1052 1978 53.2%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16
17 #include <Magick++/Blob.h>
18 #include <Magick++/Color.h>
19 #include <Magick++/Geometry.h>
20 #include <Magick++/Image.h>
21 #include <Magick++/Include.h>
22 #include <Magick++/TypeMetric.h>
23 #include <magick/image.h>
24 #include <algorithm>
25 #include <filesystem>
26 #include <memory>
27 #include <climits> // INT_MAX bounds the Bars effect args
28 #include <atomic>
29 #include <cmath> // std::ceil/std::lround in the image scaling helpers
30 #include <cstdint>
31 #include <cstdlib>
32 #include <list>
33 #include <map>
34 #include <string.h>
35 #include <string>
36 #include <utility>
37 #include <vector>
38
39 #include "../Warnings.h"
40 #include "../commands/Commands.h"
41 #include "../common.h"
42 #include "../log.h"
43 #include "../settings.h" // FPP_DIR_IMAGE for the Image effect
44
45 #include "PixelOverlay.h"
46 #include "PixelOverlayModel.h"
47 #include "TextColorizer.h"
48 #include "WLEDEffects.h"
49
50 #include "PixelOverlayEffects.h"
51
52 // Map the long-form scroll directions used in the UI onto the short codes
53 // ImageMovementEffect understands. Shared by the Text and Image effects.
54 259 static const std::string MapOverlayPosition(const std::string& p) {
55
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 210 times.
259 if (p == "Center") {
56 49 return p;
57
5/6
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 203 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 203 times.
✓ Branch 5 taken 7 times.
210 } else if (p == "Right to Left" || p == "R2L") {
58
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
406 return "R2L";
59
5/6
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 5 times.
7 } else if (p == "Left to Right" || p == "L2R") {
60
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 return "L2R";
61
5/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 4 times.
5 } else if (p == "Bottom to Top" || p == "B2T") {
62
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return "B2T";
63
5/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 1 times.
4 } else if (p == "Top to Bottom" || p == "T2B") {
64
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
6 return "T2B";
65 }
66
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return "Center";
67 }
68
69 43 bool IsOverlayImageFile(const std::string& path) {
70 static const char* const IMAGE_EXTENSIONS[] = {
71 ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".ppm", ".tga", ".tif", ".tiff", ".webp"
72 };
73 43 std::error_code ec;
74
3/4
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 24 times.
43 if (!std::filesystem::is_regular_file(path, ec)) {
75 19 return false;
76 }
77
3/6
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
24 std::string ext = std::filesystem::path(path).extension().string();
78 24 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
79
2/2
✓ Branch 0 taken 176 times.
✓ Branch 1 taken 8 times.
184 for (const char* known : IMAGE_EXTENSIONS) {
80
3/4
✓ Branch 0 taken 176 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 160 times.
176 if (ext == known) {
81 16 return true;
82 }
83 }
84 8 return false;
85 24 }
86
87 69 void ScaleOverlayImage(Magick::Image& image, int w, int h, const std::string& mode) {
88
5/10
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 69 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 69 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 69 times.
69 if (w <= 0 || h <= 0 || image.columns() == 0 || image.rows() == 0) {
89 ✗ return;
90 }
91 69 image.modifyImage();
92
93
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 69 times.
69 if (mode == "Stretch") {
94 // aspect(true) is Magick's '!' flag: resize to exactly this geometry
95 // rather than fitting inside it.
96 ✗ Magick::Geometry g(w, h);
97 ✗ g.aspect(true);
98 ✗ image.resize(g);
99 ✗ return;
100 ✗ }
101
102
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 69 times.
69 if (mode == "Crop to Fill") {
103 // Cover the box: the larger of the two scale factors is the one that
104 // leaves no gap on either axis. The overflow is then cropped centred.
105 ✗ double sx = (double)w / (double)image.columns();
106 ✗ double sy = (double)h / (double)image.rows();
107 ✗ double s = std::max(sx, sy);
108 ✗ Magick::Geometry g((size_t)std::ceil(image.columns() * s),
109 ✗ (size_t)std::ceil(image.rows() * s));
110 ✗ g.aspect(true);
111 ✗ image.resize(g);
112
113 ✗ int xoff = ((int)image.columns() - w) / 2;
114 ✗ int yoff = ((int)image.rows() - h) / 2;
115 ✗ image.crop(Magick::Geometry(w, h, xoff < 0 ? 0 : xoff, yoff < 0 ? 0 : yoff));
116 ✗ return;
117 ✗ }
118
119
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 53 times.
69 if (mode != "Scale to Fit") {
120 16 return; // "None" or unrecognized - leave the image alone
121 }
122
123 // Resize to slightly larger since trying to get exact can leave us off by
124 // one pixel. Going slightly larger lets us crop to exact size below.
125
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 image.resize(Magick::Geometry(w + 4, h + 4, 0, 0));
126
127 53 int cols = image.columns();
128 53 int rows = image.rows();
129
130
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 3 times.
53 if (cols < w) // center horizontally
131 {
132 50 int diff = w - cols;
133
134
2/4
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✗ Branch 3 not taken.
50 image.borderColor(Magick::Color("black"));
135
2/4
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✗ Branch 3 not taken.
50 image.border(Magick::Geometry(diff / 2 + 1, 1, 0, 0));
136
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 } else if (rows < h) // center vertically
137 {
138 ✗ int diff = h - rows;
139
140 ✗ image.borderColor(Magick::Color("black"));
141 ✗ image.border(Magick::Geometry(1, diff / 2 + 1, 0, 0));
142 }
143
144
2/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 53 times.
✗ Branch 3 not taken.
53 image.crop(Magick::Geometry(w, h, 1, 1));
145 }
146
147 // Preserve aspect while matching the axis the image is NOT scrolling along, so
148 // a scrolling banner fills the model's height (or width) and runs off the ends.
149 3 static void scaleToCrossAxis(Magick::Image& image, int w, int h, bool horizontal) {
150
5/10
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 3 times.
3 if (image.columns() == 0 || image.rows() == 0) {
151 ✗ return;
152 }
153 int nw, nh;
154
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (horizontal) {
155 2 nh = h;
156
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 nw = (int)std::lround((double)image.columns() * h / (double)image.rows());
157 } else {
158 1 nw = w;
159
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 nh = (int)std::lround((double)image.rows() * w / (double)image.columns());
160 }
161 3 nw = std::max(nw, 1);
162 3 nh = std::max(nh, 1);
163
164
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 image.modifyImage();
165
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 Magick::Geometry g(nw, nh);
166
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 g.aspect(true);
167
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 image.resize(g);
168 3 }
169
170 // Extract an image into a freshly malloc'd RGB (3 bytes/pixel) buffer sized
171 // columns*rows*3. The caller owns the buffer; ImageMovementEffect frees the
172 // one it is handed. Returns nullptr on allocation/pixel-cache failure.
173 263 static uint8_t* imageToRGBBuffer(Magick::Image& image) {
174 263 image.modifyImage();
175
176 263 int cols = image.columns();
177 263 int rows = image.rows();
178
2/4
✓ Branch 0 taken 263 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 263 times.
263 if (cols <= 0 || rows <= 0) {
179 ✗ return nullptr;
180 }
181
182 263 const MagickLib::PixelPacket* pixel_cache = image.getConstPixels(0, 0, cols, rows);
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 263 times.
263 if (!pixel_cache) {
184 ✗ return nullptr;
185 }
186
187 263 uint8_t* newData = (uint8_t*)malloc((size_t)cols * rows * 3);
188
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 263 times.
263 if (!newData) {
189 ✗ return nullptr;
190 }
191
192
2/2
✓ Branch 0 taken 8930 times.
✓ Branch 1 taken 263 times.
9193 for (int yi = 0; yi < rows; yi++) {
193 8930 int idx = yi * cols;
194 8930 int nidx = yi * cols * 3;
195
196
2/2
✓ Branch 0 taken 487895383 times.
✓ Branch 1 taken 8930 times.
487904313 for (int xi = 0; xi < cols; xi++) {
197 487895383 const MagickLib::PixelPacket* ptr2 = &pixel_cache[idx + xi];
198 487895383 uint8_t* np = &newData[nidx + (xi * 3)];
199
200 487895383 float r = Magick::Color::scaleQuantumToDouble(ptr2->red);
201 487895383 float g = Magick::Color::scaleQuantumToDouble(ptr2->green);
202 487895383 float b = Magick::Color::scaleQuantumToDouble(ptr2->blue);
203 487895383 r *= 255;
204 487895383 g *= 255;
205 487895383 b *= 255;
206 487895383 np[0] = r;
207 487895383 np[1] = g;
208 487895383 np[2] = b;
209 }
210 }
211 263 return newData;
212 }
213
214 // Push a full model-sized RGB frame at the model, expanding to RGBW first if
215 // the model wants more than 3 bytes per pixel.
216 263 static void setModelDataFromRGB(PixelOverlayModel* m, const uint8_t* rgb) {
217 263 int bpp = m->getBytesPerPixel();
218
2/2
✓ Branch 0 taken 262 times.
✓ Branch 1 taken 1 times.
263 if (bpp == 3) {
219
1/2
✓ Branch 0 taken 262 times.
✗ Branch 1 not taken.
262 m->setData(rgb);
220 262 return;
221 }
222 1 int pixels = m->getWidth() * m->getHeight();
223
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::vector<uint8_t> expanded((size_t)pixels * bpp, 0);
224
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 1 times.
51 for (int p = 0; p < pixels; p++) {
225 50 expanded[p * bpp] = rgb[p * 3];
226 50 expanded[p * bpp + 1] = rgb[p * 3 + 1];
227 50 expanded[p * bpp + 2] = rgb[p * 3 + 2];
228 }
229
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 m->setData(expanded.data());
230 1 }
231
232 52 bool DrawEncodedImageOnModel(PixelOverlayModel* m, const void* data, size_t len,
233 const std::string& scaling, int boxW, int boxH,
234 int xOffset, int yOffset, bool center,
235 const PixelOverlayState& st, bool toOverlayBuffer,
236 std::string& err) {
237
3/6
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 52 times.
52 if (!m || !data || len == 0) {
238 ✗ err = "empty image body";
239 ✗ return false;
240 }
241
242
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 Magick::Image image;
243 try {
244
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 image.quiet(true); // squelch warning exceptions
245
1/2
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
52 Magick::Blob blob(data, len);
246
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 2 times.
52 image.read(blob);
247
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 image.autoOrient();
248
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 image.type(Magick::TrueColorType);
249
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 image.depth(8);
250
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 ScaleOverlayImage(image, boxW, boxH, scaling);
251
1/3
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
54 } catch (Magick::Exception& error_) {
252
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 err = std::string("could not decode image: ") + error_.what();
253 2 return false;
254 2 } catch (const std::exception& ex) {
255 // Not everything thrown here is a Magick::Exception (std::bad_alloc,
256 // for one), and an escape would unwind through the HTTP handler while
257 // it holds the models lock.
258 ✗ err = std::string("could not decode image: ") + ex.what();
259 ✗ return false;
260 ✗ }
261
262
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 int cols = image.columns();
263
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 int rows = image.rows();
264
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 uint8_t* rgb = imageToRGBBuffer(image);
265
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 50 times.
50 if (!rgb) {
266 ✗ err = "could not read pixels from the decoded image";
267 ✗ return false;
268 }
269
270 50 int x = xOffset;
271 50 int y = yOffset;
272
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 if (center) {
273 50 x += (boxW - cols) / 2;
274 50 y += (boxH - rows) / 2;
275 }
276
277
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 50 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
50 bool ok = toOverlayBuffer ? m->blitOverlayBuffer(rgb, cols, rows, 3, x, y)
278
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 : m->blitData(rgb, cols, rows, 3, x, y, st);
279 50 free(rgb);
280
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 50 times.
50 if (!ok) {
281 ✗ err = "the image landed entirely outside the model";
282 }
283 50 return ok;
284 52 }
285
286 1252 static uint32_t applyColorPct(uint32_t c, float pct) {
287 1252 uint32_t r = (c >> 16) & 0xFF;
288 1252 uint32_t g = (c >> 8) & 0xFF;
289 1252 uint32_t b = c & 0xFF;
290
291 1252 float t = r;
292 1252 t *= pct;
293 1252 r = (int)t;
294 1252 t = g;
295 1252 t *= pct;
296 1252 g = (int)t;
297 1252 t = b;
298 1252 t *= pct;
299 1252 b = (int)t;
300 1252 return (r << 16) + (g << 8) + b;
301 }
302
303 class StopRunningEffect : public RunningEffect {
304 public:
305 6 StopRunningEffect(PixelOverlayModel* m, const std::string& n, bool ad) :
306 RunningEffect(m),
307 6 effectName(n),
308
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 autoDisable(ad) {
309 6 }
310 34824 StopRunningEffect(PixelOverlayModel* m, const std::string& n, const std::string& ad) :
311 RunningEffect(m),
312 34824 effectName(n),
313
1/2
✓ Branch 0 taken 34824 times.
✗ Branch 1 not taken.
34824 autoDisable(false) {
314
1/2
✓ Branch 0 taken 34824 times.
✗ Branch 1 not taken.
34824 PixelOverlayState st(ad);
315
2/2
✓ Branch 0 taken 34823 times.
✓ Branch 1 taken 1 times.
34824 if (st.getState() != PixelOverlayState::PixelState::Disabled) {
316 34823 autoDisable = true;
317 }
318 34824 }
319 1236 const std::string& name() const override {
320 1236 return effectName;
321 }
322 240 virtual int32_t update() override {
323 240 model->clearOverlayBuffer();
324 240 model->flushOverlayBuffer();
325
2/2
✓ Branch 0 taken 100 times.
✓ Branch 1 taken 140 times.
240 if (stopped) {
326
2/2
✓ Branch 0 taken 99 times.
✓ Branch 1 taken 1 times.
100 if (autoDisable) {
327
1/2
✓ Branch 0 taken 99 times.
✗ Branch 1 not taken.
99 model->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
328 }
329 100 return EFFECT_DONE;
330 }
331 140 stopped = true;
332 140 return EFFECT_AFTER_NEXT_OUTPUT;
333 }
334 bool stopped = false;
335 std::string effectName;
336 bool autoDisable;
337 };
338
339 class ColorFadeEffect : public PixelOverlayEffect {
340 public:
341 2346 ColorFadeEffect() :
342
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 PixelOverlayEffect("Color Fade") {
343
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Color", "color", "Color").setDefaultValue("#FF0000"));
344
5/10
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
14076 args.push_back(CommandArg("fadeIn", "int", "Fade In MS").setRange(0, 10000));
345
5/10
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
14076 args.push_back(CommandArg("fadeOut", "int", "Fade Out MS").setRange(0, 10000));
346 2346 }
347
348 class CFRunningEffect : public RunningEffect {
349 public:
350 1424 CFRunningEffect(PixelOverlayModel* m, const std::string& ae, const std::vector<std::string>& args) :
351 RunningEffect(m),
352 1424 autoEnable(false) {
353
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 color = PixelOverlayManager::mapColor(args[0]);
354 // Neither declares a default, so a caller that doesn't set them
355 // (the WLED API, a preset saved without them) sends "", which
356 // std::stol threw on. Unset or unreadable is no fade.
357
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 fadeIn = (int)std::clamp(std::strtol(args[1].c_str(), nullptr, 10), 0L, 3600000L);
358
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 fadeOut = (int)std::clamp(std::strtol(args[2].c_str(), nullptr, 10), 0L, 3600000L);
359
360
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 PixelOverlayState st(ae);
361
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 if (st.getState() != PixelOverlayState::PixelState::Disabled) {
362 1424 autoEnable = true;
363
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 model->setState(st);
364 }
365
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 startTime = GetMonotonicTimeMS();
366 1424 done = false;
367
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 if (fadeIn == 0) {
368
1/2
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
1424 fill(color);
369 } else {
370 ✗ fill(0);
371 }
372 1424 }
373 3476 const std::string& name() const override {
374
3/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3471 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
3476 static std::string NAME = "Color Fade";
375 3476 return NAME;
376 }
377 432 virtual void toJson(Json::Value& v) const override {
378
3/6
✓ Branch 0 taken 432 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 432 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 432 times.
✗ Branch 5 not taken.
432 v["name"] = name();
379
2/4
✓ Branch 0 taken 432 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 432 times.
✗ Branch 3 not taken.
432 v["color"] = color;
380
2/4
✓ Branch 0 taken 432 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 432 times.
✗ Branch 3 not taken.
432 v["fadeIn"] = fadeIn;
381
2/4
✓ Branch 0 taken 432 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 432 times.
✗ Branch 3 not taken.
432 v["fadeOut"] = fadeOut;
382 432 };
383
384 2950 void fill(uint32_t c) {
385 2950 model->fill((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
386 2950 }
387 1796 virtual int32_t update() override {
388 1796 long long nowTime = GetMonotonicTimeMS();
389 1796 nowTime -= startTime;
390
391
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1796 times.
1796 if (nowTime < fadeIn) {
392 // fading in
393 ✗ float f = nowTime;
394 ✗ f /= fadeIn;
395 ✗ uint32_t c = applyColorPct(color, f);
396 ✗ fill(c);
397
3/4
✓ Branch 0 taken 1796 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1252 times.
✓ Branch 3 taken 544 times.
1796 } else if ((nowTime >= fadeIn) && (nowTime <= (fadeIn + fadeOut))) {
398 // fading out
399 1252 nowTime -= fadeIn;
400 1252 float f = nowTime;
401 1252 f /= fadeOut;
402 1252 f = 1.0f - f;
403 1252 uint32_t c = applyColorPct(color, f);
404 1252 fill(c);
405
3/4
✓ Branch 0 taken 274 times.
✓ Branch 1 taken 270 times.
✓ Branch 2 taken 274 times.
✗ Branch 3 not taken.
1796 } else if (!done && autoEnable) {
406 // end
407
1/2
✓ Branch 0 taken 274 times.
✗ Branch 1 not taken.
274 if (!fadeOut) {
408 274 fill(color);
409 } else {
410 ✗ fill(0x000000);
411 }
412 274 done = true;
413 274 return EFFECT_AFTER_NEXT_OUTPUT;
414 } else {
415
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
270 if (autoEnable) {
416
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
270 model->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
417 ✗ } else if (!fadeOut) {
418 ✗ fill(color);
419 } else {
420 ✗ fill(0x000000);
421 }
422 270 return 0;
423 }
424 1252 return 25;
425 }
426 uint32_t color = 0xFFFFFF;
427 long long startTime = 0;
428 int fadeIn = 0;
429 int fadeOut = 0;
430 bool autoEnable;
431 bool done;
432 };
433
434 1424 virtual bool apply(PixelOverlayModel* model, const std::string& autoEnable, const std::vector<std::string>& args) override {
435
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1424 times.
1424 if (args.size() != 3) {
436 ✗ LogInfo(VB_CHANNELOUT, "ColorFadeEffect: not enough args\n");
437 ✗ return false;
438 }
439
2/6
✓ Branch 0 taken 1424 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1424 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
1424 CFRunningEffect* re = new CFRunningEffect(model, autoEnable, args);
440 1424 model->setRunningEffect(re, 1);
441 1424 return true;
442 }
443 };
444
445 class BarsEffect : public PixelOverlayEffect {
446 public:
447 2346 BarsEffect() :
448
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 PixelOverlayEffect("Bars") {
449
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
18768 args.push_back(CommandArg("Direction", "string", "Direction").setContentList({ "Up", "Down", "Left", "Right" }));
450
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("time", "int", "Time (ms)").setRange(1, 60000).setDefaultValue("2000"));
451
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("iterations", "range", "Iterations").setRange(1, 30).setDefaultValue("1"));
452
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("colorRep", "range", "Color Rep.").setRange(1, 5).setDefaultValue("1"));
453
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("colors", "range", "Num Colors").setRange(1, 5).setDefaultValue("3"));
454
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Color1", "color", "Color 1").setDefaultValue("#FF0000"));
455
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Color2", "color", "Color 2").setDefaultValue("#00FF00"));
456
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Color3", "color", "Color 3").setDefaultValue("#0000FF"));
457
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Color4", "color", "Color 4").setDefaultValue("#FFFFFF"));
458
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Color5", "color", "Color 5").setDefaultValue("#000000"));
459 2346 }
460 class BarsRunningEffect : public RunningEffect {
461 public:
462 36 BarsRunningEffect(PixelOverlayModel* m, const std::string& ae, const std::vector<std::string>& args) :
463 RunningEffect(m),
464 36 autoEnable(false) {
465
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 36 times.
36 if (args[0] == "Up") {
466 ✗ direction = DirectionEnum::UP;
467
3/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 30 times.
36 } else if (args[0] == "Down") {
468 6 direction = DirectionEnum::DOWN;
469
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 30 times.
30 } else if (args[0] == "Right") {
470 ✗ direction = DirectionEnum::RIGHT;
471 } else {
472 30 direction = DirectionEnum::LEFT;
473 }
474 // Every numeric arg is held to what update()/apply() can survive;
475 // the UI's ranges are not enforced for API, MQTT or preset callers.
476 // duration 0 made the position NaN and indexed `colors` with it,
477 // a huge iterations count made the subtract-one wrap loop spin
478 // forever (f -= 1 is a no-op past 2^24) with effectLock held, and
479 // numColors 0 left `colors` empty for apply() to index.
480 static const char* DEFAULT_COLORS[MAX_COLORS] = { "#FF0000", "#00FF00", "#0000FF", "#FFFFFF", "#000000" };
481
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 duration = std::clamp(argToLong(args, 1, 2000), 1L, (long)INT_MAX);
482
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 iterations = std::clamp(argToLong(args, 2, 1), 1L, (long)INT_MAX);
483
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 int colorRep = std::clamp(argToLong(args, 3, 1), 1L, (long)MAX_COLOR_REP);
484
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 int numColors = std::clamp(argToLong(args, 4, 3), 1L, (long)MAX_COLORS);
485
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 36 times.
72 while (colorRep) {
486
2/2
✓ Branch 0 taken 108 times.
✓ Branch 1 taken 36 times.
144 for (int x = 0; x < numColors; x++) {
487 108 size_t i = 5 + x;
488
5/14
✓ Branch 0 taken 108 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 108 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 108 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 108 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 108 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
108 colors.push_back(PixelOverlayManager::mapColor(i < args.size() ? args[i] : DEFAULT_COLORS[x]));
489 }
490 36 colorRep--;
491 }
492
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 PixelOverlayState st(ae);
493
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 if (st.getState() != PixelOverlayState::PixelState::Disabled) {
494 36 autoEnable = true;
495
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 model->setState(st);
496 }
497
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 startTime = GetMonotonicTimeMS();
498 36 done = false;
499
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 apply(0.0f);
500 36 }
501 static constexpr int MAX_COLORS = 5;
502 static constexpr int MAX_COLOR_REP = 100;
503 144 static long argToLong(const std::vector<std::string>& args, size_t i, long dflt) {
504
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 144 times.
144 if (i >= args.size()) {
505 ✗ return dflt;
506 }
507 144 char* end = nullptr;
508 144 long v = strtol(args[i].c_str(), &end, 10);
509
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 144 times.
144 return end == args[i].c_str() ? dflt : v;
510 }
511
512 150484 const std::string& name() const override {
513
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 150478 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
150484 static std::string NAME = "Bars";
514 150484 return NAME;
515 }
516 150479 virtual void toJson(Json::Value& v) const override {
517
3/6
✓ Branch 0 taken 150479 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150479 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 150479 times.
✗ Branch 5 not taken.
150479 v["name"] = name();
518
2/5
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 150476 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
150479 switch (direction) {
519 ✗ case DirectionEnum::UP:
520 ✗ v["direction"] = "Up";
521 ✗ break;
522 3 case DirectionEnum::DOWN:
523
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 v["direction"] = "Down";
524 3 break;
525 150476 case DirectionEnum::LEFT:
526
2/4
✓ Branch 0 taken 150476 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150476 times.
✗ Branch 3 not taken.
150476 v["direction"] = "Left";
527 150476 break;
528 ✗ case DirectionEnum::RIGHT:
529 ✗ v["direction"] = "Right";
530 ✗ break;
531 }
532
2/4
✓ Branch 0 taken 150479 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150479 times.
✗ Branch 3 not taken.
150479 v["duration"] = duration;
533
2/4
✓ Branch 0 taken 150479 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150479 times.
✗ Branch 3 not taken.
150479 v["iterations"] = iterations;
534
2/4
✓ Branch 0 taken 150479 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150479 times.
✗ Branch 3 not taken.
150479 v["colors"] = Json::Value(Json::arrayValue);
535
2/2
✓ Branch 0 taken 451437 times.
✓ Branch 1 taken 150479 times.
601916 for (auto& c : colors) {
536
3/6
✓ Branch 0 taken 451437 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 451437 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 451437 times.
✗ Branch 5 not taken.
451437 v["colors"].append(c);
537 }
538 150479 };
539
540 504070 void mapCoords(int m, int l, int w, int h, int& x, int& y) {
541
2/5
✗ Branch 0 not taken.
✓ Branch 1 taken 22830 times.
✓ Branch 2 taken 481240 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
504070 switch (direction) {
542 ✗ case DirectionEnum::UP:
543 ✗ y = m;
544 ✗ x = l;
545 ✗ break;
546 22830 case DirectionEnum::DOWN:
547 22830 y = h - m - 1;
548 22830 x = l;
549 22830 break;
550 481240 case DirectionEnum::LEFT:
551 481240 y = l;
552 481240 x = m;
553 481240 break;
554 ✗ case DirectionEnum::RIGHT:
555 ✗ y = l;
556 ✗ x = w - m - 1;
557 ✗ break;
558 ✗ default:
559 ✗ x = y = 0;
560 }
561 504070 }
562
563 1638 void apply(float pos) {
564 int w, h;
565 1638 model->getSize(w, h);
566 1638 int max = w;
567 1638 int len = h;
568
3/4
✓ Branch 0 taken 1638 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 514 times.
✓ Branch 3 taken 1124 times.
1638 if (direction == DirectionEnum::UP || direction == DirectionEnum::DOWN) {
569 514 max = h;
570 514 len = w;
571 }
572 1638 float numBars = colors.size();
573
2/2
✓ Branch 0 taken 33886 times.
✓ Branch 1 taken 1638 times.
35524 for (int m = 0; m < max; m++) {
574 33886 float f = m;
575 33886 f /= max;
576 33886 f += pos;
577
2/2
✓ Branch 0 taken 8615 times.
✓ Branch 1 taken 25271 times.
33886 if (f > 1.0) {
578 8615 f -= 1.0;
579 }
580 33886 f *= numBars;
581 33886 int cIdx = (int)f;
582
3/4
✓ Branch 0 taken 33886 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 33885 times.
33886 if (cIdx < 0 || cIdx >= numBars) {
583 1 cIdx = 0;
584 }
585 33886 uint32_t color = colors[cIdx];
586 33886 int r = (color >> 16) & 0xFF;
587 33886 int g = (color >> 8) & 0xFF;
588 33886 int b = color & 0xFF;
589
2/2
✓ Branch 0 taken 504070 times.
✓ Branch 1 taken 33886 times.
537956 for (int l = 0; l < len; l++) {
590 int x, y;
591 504070 mapCoords(m, l, w, h, x, y);
592
1/2
✓ Branch 0 taken 504070 times.
✗ Branch 1 not taken.
504070 model->setPixelValue(x, y, r, g, b);
593 }
594 }
595 1638 }
596 1603 virtual int32_t update() override {
597 1603 long long nowTime = GetMonotonicTimeMS();
598 1603 nowTime -= startTime;
599
2/2
✓ Branch 0 taken 1601 times.
✓ Branch 1 taken 2 times.
1603 if (nowTime <= duration) {
600 1601 float f = nowTime;
601 1601 f /= duration;
602 1601 f *= iterations;
603 1601 f = std::fmod(f, 1.0f);
604 1601 apply(f);
605
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 } else if (!done && autoEnable) {
606 // end
607 1 apply(1.0f);
608 1 done = true;
609 1 return EFFECT_AFTER_NEXT_OUTPUT;
610 } else {
611
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (autoEnable) {
612
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 model->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
613 } else {
614 ✗ apply(1.0f);
615 }
616 1 return 0;
617 }
618 1601 return 25;
619 }
620
621 enum class DirectionEnum {
622 UP,
623 DOWN,
624 LEFT,
625 RIGHT
626 } direction;
627 long long startTime = 0;
628 int iterations;
629 int duration;
630 std::vector<uint32_t> colors;
631 bool autoEnable;
632 bool done;
633 };
634
635 36 virtual bool apply(PixelOverlayModel* model, const std::string& autoEnable, const std::vector<std::string>& args) override {
636
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (args.size() < 5) {
637 ✗ LogInfo(VB_CHANNELOUT, "Bars Effect: not enough args\n");
638 ✗ return false;
639 }
640
2/6
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 36 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
36 BarsRunningEffect* re = new BarsRunningEffect(model, autoEnable, args);
641 36 model->setRunningEffect(re, 1);
642 36 return true;
643 }
644 };
645
646 class ImageMovementEffect : public RunningEffect {
647 public:
648 18 ImageMovementEffect(PixelOverlayModel* m) :
649 18 RunningEffect(m) {
650 18 }
651
652 58 virtual ~ImageMovementEffect() {
653
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
32 if (imageData) {
654 32 free(imageData);
655 }
656 58 }
657 235 const std::string& name() const override {
658
3/4
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 224 times.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
235 static std::string NAME = "Image";
659 235 return NAME;
660 }
661 4 virtual void toJson(Json::Value& v) const override {
662
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
4 v["name"] = name();
663 4 }
664 4238 void copyImageData(int xoff, int yoff) {
665
1/2
✓ Branch 0 taken 4238 times.
✗ Branch 1 not taken.
4238 if (imageData) {
666
1/2
✓ Branch 0 taken 4238 times.
✗ Branch 1 not taken.
4238 model->clearOverlayBuffer();
667 int h, w;
668 4238 model->getSize(w, h);
669 4238 int bpp = model->getBytesPerPixel();
670
1/2
✓ Branch 0 taken 4238 times.
✗ Branch 1 not taken.
4238 uint8_t* overlayBuf = model->getOverlayBuffer();
671
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4238 times.
4238 if (!overlayBuf) {
672 ✗ return;
673 }
674
2/2
✓ Branch 0 taken 156706 times.
✓ Branch 1 taken 4238 times.
160944 for (int y = 0; y < imageDataRows; ++y) {
675 156706 int ny = yoff + y;
676
4/4
✓ Branch 0 taken 111234 times.
✓ Branch 1 taken 45472 times.
✓ Branch 2 taken 45472 times.
✓ Branch 3 taken 65762 times.
156706 if (ny < 0 || ny >= h) {
677 90944 continue;
678 }
679
680 // imageData is always RGB (3 bytes per pixel)
681 65762 uint8_t* src = imageData + (y * imageDataCols * 3);
682 65762 uint8_t* dst = overlayBuf + (ny * w * bpp);
683 65762 int pixelsToCopy = imageDataCols;
684
685
2/2
✓ Branch 0 taken 61306 times.
✓ Branch 1 taken 4456 times.
65762 if (xoff < 0) {
686 61306 src -= xoff * 3;
687 61306 pixelsToCopy += xoff;
688
2/2
✓ Branch 0 taken 59976 times.
✓ Branch 1 taken 1330 times.
61306 if (pixelsToCopy >= w)
689 59976 pixelsToCopy = w;
690
2/2
✓ Branch 0 taken 4344 times.
✓ Branch 1 taken 112 times.
4456 } else if (xoff > 0) {
691 4344 dst += xoff * bpp;
692
2/2
✓ Branch 0 taken 4169 times.
✓ Branch 1 taken 175 times.
4344 if (pixelsToCopy > (w - xoff))
693 4169 pixelsToCopy = w - xoff;
694 } else {
695
2/2
✓ Branch 0 taken 93 times.
✓ Branch 1 taken 19 times.
112 if (pixelsToCopy >= w)
696 93 pixelsToCopy = w;
697 }
698
699
2/2
✓ Branch 0 taken 65634 times.
✓ Branch 1 taken 128 times.
65762 if (pixelsToCopy > 0) {
700
2/2
✓ Branch 0 taken 65619 times.
✓ Branch 1 taken 15 times.
65634 if (bpp == 3) {
701 65619 memcpy(dst, src, pixelsToCopy * 3);
702 } else {
703 // Expand RGB source to RGBW destination
704
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 15 times.
75 for (int p = 0; p < pixelsToCopy; p++) {
705 60 dst[p * bpp] = src[p * 3];
706 60 dst[p * bpp + 1] = src[p * 3 + 1];
707 60 dst[p * bpp + 2] = src[p * 3 + 2];
708
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 if (bpp > 3) {
709 60 dst[p * bpp + 3] = 0;
710 }
711 }
712 }
713 }
714 }
715
1/2
✓ Branch 0 taken 4238 times.
✗ Branch 1 not taken.
4238 model->flushOverlayBuffer();
716 }
717 }
718 4049 virtual int32_t update() override {
719
2/2
✓ Branch 0 taken 4031 times.
✓ Branch 1 taken 18 times.
4049 if (!done) {
720
6/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4027 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 4030 times.
4031 if (endTimeMS && GetMonotonicTimeMS() >= endTimeMS) {
721 // Duration expired mid-scroll. Wipe the partial frame so a
722 // timeout leaves the model blank, matching what the static path
723 // does, rather than freezing half an image on the display.
724 //
725 // Return a timed delay rather than EFFECT_AFTER_NEXT_OUTPUT:
726 // the completion handshake below itself waits on an output
727 // cycle, and returning AFTER_NEXT_OUTPUT here consumes the one
728 // our flush just produced, leaving the effect wedged holding
729 // the model enabled forever. This mirrors what the natural
730 // scroll-off end does - flush a frame, return a delay.
731 1 done = true;
732 1 model->clearOverlayBuffer();
733 1 model->flushOverlayBuffer();
734 1 return 1000 / std::max(1, speed);
735 }
736
2/2
✓ Branch 0 taken 3970 times.
✓ Branch 1 taken 60 times.
4030 if (direction == "R2L") {
737 3970 --x;
738
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3967 times.
3970 if (x <= (-imageDataCols)) {
739 3 done = true;
740 }
741
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 32 times.
60 } else if (direction == "L2R") {
742 28 ++x;
743
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 26 times.
28 if (x >= model->getWidth()) {
744 2 done = true;
745 }
746
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 24 times.
32 } else if (direction == "B2T") {
747 8 --y;
748
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (y <= (-imageDataRows)) {
749 1 done = true;
750 }
751
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 } else if (direction == "T2B") {
752 24 ++y;
753
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 21 times.
24 if (y >= model->getHeight()) {
754 3 done = true;
755 }
756 }
757 4030 copyImageData(x, y);
758 // A UI/command speed of 0 is in range for both the Text and Image
759 // effects; dividing by it raised SIGFPE and took fppd down.
760 4030 int msDelay = 1000 / std::max(1, speed);
761 4030 return msDelay;
762 }
763
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
18 if (disableWhenDone) {
764
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 8 times.
16 if (waitingToSetState) {
765
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 model->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
766 } else {
767 8 waitingToSetState = true;
768 8 return EFFECT_AFTER_NEXT_OUTPUT;
769 }
770 }
771 10 return EFFECT_DONE;
772 }
773
774 bool done = false;
775 bool waitingToSetState = false;
776 uint8_t* imageData = nullptr;
777 int imageDataRows = 0;
778 int imageDataCols = 0;
779
780 std::string direction;
781 int x = 0;
782 int y = 0;
783 int speed = 0;
784 bool disableWhenDone = false;
785
786 // Absolute stop time in ms, or 0 to run until the image scrolls off the
787 // model. Only the Image effect sets this; the Text effect leaves it 0 so
788 // its long-standing "scroll once, ignore Duration" behaviour is unchanged.
789 long long endTimeMS = 0;
790 };
791
792 // ---------------------------------------------------------------------------
793 // Text colour treatment
794 //
795 // The Text effect originally drew its message in one flat fill colour, and it
796 // still does exactly that whenever ColorMode is "Single" -- the default, and
797 // what every command, preset, cue and MQTT topic written before these
798 // arguments existed resolves to. Any other mode takes a second path: the
799 // message is rendered ONCE as a white-on-black coverage mask and the colour is
800 // painted through that mask by TextColorizer. Keeping the render and the
801 // colouring apart is what makes an animated mode affordable -- a repaint is one
802 // pass over the mask, not another trip through the font rasteriser.
803 // ---------------------------------------------------------------------------
804
805 /** Longest line the per-letter bands are measured glyph by glyph. */
806 static constexpr size_t MAX_MEASURED_GLYPHS = 160;
807 /** Repaint interval for an animated still (centred) block of text. */
808 static constexpr int32_t TEXT_COLOR_FRAME_MS = 25;
809 /** ColorSpeed that means one full palette cycle per second. */
810 static constexpr double TEXT_COLOR_SPEED_PER_CYCLE = 20.0;
811
812 /** Everything the Text effect's colour arguments resolve to. */
813 struct TextColorSpec {
814 TextColor::Mode mode = TextColor::Mode::Single;
815 TextColor::Palette palette;
816 int colorSpeed = 0; ///< 0 holds a still frame
817 uint32_t seed = 0; ///< fixes the Random modes' shuffle for one application
818
819 /** True when the painted result changes over time, so it needs repainting. */
820 526 bool animated() const {
821
3/4
✓ Branch 0 taken 480 times.
✓ Branch 1 taken 46 times.
✓ Branch 2 taken 480 times.
✗ Branch 3 not taken.
526 return colorSpeed > 0 && TextColor::IsAnimated(mode);
822 }
823 /** True for the original one-fill-colour path. */
824 480 bool plain() const {
825 480 return mode == TextColor::Mode::Single;
826 }
827 };
828
829 /**
830 * Animation position for a spec, wrapped into [0, 1).
831 *
832 * ColorSpeed 20 is one full palette cycle per second, so the argument's 1..100
833 * range runs from a slow drift up to five cycles a second.
834 */
835 344 static double textColorPhase(const TextColorSpec& spec, long long startMS) {
836
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 344 times.
344 if (!spec.animated()) {
837 ✗ return 0.0;
838 }
839 344 double cycles = ((double)(GetMonotonicTimeMS() - startMS) / 1000.0) *
840 344 ((double)spec.colorSpeed / TEXT_COLOR_SPEED_PER_CYCLE);
841 344 return cycles - std::floor(cycles);
842 }
843
844 /**
845 * Collapse a rendered RGB frame to an 8-bit coverage mask. The colour modes
846 * draw the glyphs white on black, so the brightest channel IS the coverage the
847 * font rasteriser produced, anti-aliased edges included.
848 */
849 28 static void rgbToCoverageMask(const uint8_t* rgb, size_t pixels, uint8_t* mask) {
850
2/2
✓ Branch 0 taken 43952 times.
✓ Branch 1 taken 28 times.
43980 for (size_t i = 0; i < pixels; ++i) {
851 43952 const uint8_t* p = rgb + i * 3;
852
1/2
✓ Branch 0 taken 43952 times.
✗ Branch 1 not taken.
43952 mask[i] = std::max({ p[0], p[1], p[2] });
853 }
854 28 }
855
856 240 static Magick::Color magickColor(int r, int g, int b) {
857 240 return Magick::Color(Magick::Color::scaleDoubleToQuantum(r / 255.0),
858 240 Magick::Color::scaleDoubleToQuantum(g / 255.0),
859 240 Magick::Color::scaleDoubleToQuantum(b / 255.0));
860 }
861
862 /** Split on newlines, always yielding at least one (possibly empty) line. */
863 240 static std::vector<std::string> splitTextLines(const std::string& msg) {
864 240 std::vector<std::string> lines;
865 240 size_t last = 0;
866
2/2
✓ Branch 0 taken 483711 times.
✓ Branch 1 taken 240 times.
483951 for (size_t x = 0; x < msg.length(); x++) {
867
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 483708 times.
483711 if (msg[x] == '\n') {
868
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 lines.push_back(msg.substr(last, x - last));
869 3 last = x + 1;
870 }
871 }
872
2/4
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
240 lines.push_back(msg.substr(last));
873 240 return lines;
874 ✗ }
875
876 46 static bool isTextSpace(char c) {
877
4/6
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 35 times.
46 return c == ' ' || c == '\t' || c == '\r';
878 }
879
880 5 static std::string trimTextLine(const std::string& s) {
881 5 size_t b = 0;
882 5 size_t e = s.size();
883
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
5 while (b < e && isTextSpace(s[b])) {
884 ✗ ++b;
885 }
886
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
5 while (e > b && isTextSpace(s[e - 1])) {
887 ✗ --e;
888 }
889 5 return s.substr(b, e - b);
890 }
891
892 /** Byte offsets of each UTF-8 character boundary, including 0 and size(). */
893 5 static std::vector<size_t> utf8Boundaries(const std::string& s) {
894 5 std::vector<size_t> b;
895
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 b.push_back(0);
896 5 size_t i = 0;
897
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 5 times.
41 while (i < s.size()) {
898 36 unsigned char c = (unsigned char)s[i];
899 36 size_t len = 1;
900
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if ((c & 0xE0) == 0xC0) {
901 ✗ len = 2;
902
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 } else if ((c & 0xF0) == 0xE0) {
903 ✗ len = 3;
904
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 } else if ((c & 0xF8) == 0xF0) {
905 ✗ len = 4;
906 }
907 36 i = std::min(s.size(), i + len);
908
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 b.push_back(i);
909 }
910 5 return b;
911 ✗ }
912
913 36 static bool boundaryIsSpace(const std::string& s, const std::vector<size_t>& bounds, size_t k) {
914
3/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 25 times.
36 return (bounds[k + 1] - bounds[k]) == 1 && isTextSpace(s[bounds[k]]);
915 }
916
917 /**
918 * Cumulative text width at every character boundary of `line`.
919 *
920 * One fontTypeMetrics() call per character is the only way to get this out of
921 * GraphicsMagick, so a long line is divided evenly instead: per-letter bands on
922 * a hundred-character banner are decorative rather than exact, and a hundred
923 * font passes on a Pi are not worth the difference.
924 */
925 5 static std::vector<double> measurePrefixWidths(Magick::Image& img, const std::string& line,
926 const std::vector<size_t>& bounds) {
927 5 size_t n = bounds.size() - 1;
928
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::vector<double> prefix(n + 1, 0.0);
929
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (n == 0) {
930 ✗ return prefix;
931 }
932
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Magick::TypeMetric tm;
933
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 img.fontTypeMetrics(line, &tm);
934
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 double total = tm.textWidth();
935
936
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (n > MAX_MEASURED_GLYPHS) {
937 ✗ for (size_t k = 0; k <= n; ++k) {
938 ✗ prefix[k] = total * (double)k / (double)n;
939 }
940 ✗ return prefix;
941 }
942
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 5 times.
36 for (size_t k = 1; k < n; ++k) {
943
2/4
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
31 img.fontTypeMetrics(line.substr(0, bounds[k]), &tm);
944
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 prefix[k] = tm.textWidth();
945 }
946 5 prefix[n] = total;
947 // A prefix that ends in a space measures trimmed, which would run the ramp
948 // backwards; forcing it monotonic keeps a span from inverting.
949
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 5 times.
41 for (size_t k = 1; k <= n; ++k) {
950 36 prefix[k] = std::max(prefix[k], prefix[k - 1]);
951 }
952 5 return prefix;
953 5 }
954
955 /**
956 * Work out the ink box and, for the modes that need them, the span rectangles
957 * a Per Letter / Per Word / Per Line mode paints through.
958 *
959 * Lines and their extents come from the rendered PIXELS rather than the font:
960 * runs of rows holding ink are the lines, and each line's first and last lit
961 * column bound it. Only the one thing pixels cannot answer -- where inside a
962 * line one letter ends and the next begins -- is measured with font metrics,
963 * and even that is rescaled onto the line's real ink extent, so a centring or
964 * side-bearing discrepancy cannot walk the bands off the glyphs.
965 */
966 28 static TextColor::Layout buildTextLayout(const uint8_t* mask, int w, int h,
967 Magick::Image& img,
968 const std::vector<std::string>& lines,
969 TextColor::Mode mode) {
970 28 TextColor::Layout layout;
971
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 28 times.
28 if (!TextColor::InkBounds(mask, w, h, layout)) {
972 ✗ return layout; // nothing rendered; Colorize handles the empty case
973 }
974
8/10
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 23 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 22 times.
✓ Branch 9 taken 6 times.
51 if (!TextColor::NeedsLetterSpans(mode) && !TextColor::NeedsWordSpans(mode) &&
975
3/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 1 times.
23 !TextColor::NeedsLineSpans(mode)) {
976 22 return layout;
977 }
978
979
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 auto runs = TextColor::InkRowRuns(mask, w, h);
980
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (runs.empty()) {
981 ✗ return layout;
982 }
983
984
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 5 times.
6 if (TextColor::NeedsLineSpans(mode)) {
985 1 int idx = 0;
986
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 for (const auto& r : runs) {
987
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 layout.spans.push_back({ idx++, layout.x, layout.x + layout.w, r.first, r.second + 1 });
988 }
989 1 layout.spanCount = idx;
990 1 return layout;
991 }
992
993 // Letters and words have to be matched back to the source line that drew
994 // them. Blank lines contribute no ink, so they are dropped first.
995 5 std::vector<std::string> inked;
996
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
10 for (const auto& l : lines) {
997
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::string t = trimTextLine(l);
998
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (!t.empty()) {
999
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 inked.push_back(t);
1000 }
1001 5 }
1002
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (inked.size() != runs.size()) {
1003 // Adjacent lines whose ascenders and descenders touch merge into one
1004 // run. Rather than guess at the pairing, hand back no spans at all:
1005 // Colorize paints a gradient instead, which still shows the palette.
1006 ✗ return layout;
1007 }
1008
1009 5 int idx = 0;
1010
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
10 for (size_t li = 0; li < inked.size(); ++li) {
1011 5 int y0 = runs[li].first;
1012 5 int y1 = runs[li].second + 1;
1013 5 int ix0 = 0;
1014 5 int ix1 = 0;
1015
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if (!TextColor::InkColumnExtent(mask, w, h, y0, y1, ix0, ix1)) {
1016 ✗ continue;
1017 }
1018
1019 5 const std::string& line = inked[li];
1020
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::vector<size_t> bounds = utf8Boundaries(line);
1021 5 std::vector<double> prefix;
1022 try {
1023
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 prefix = measurePrefixWidths(img, line, bounds);
1024 ✗ } catch (...) {
1025 // A font that will not report metrics leaves the spans short;
1026 // Colorize falls back to a gradient rather than painting a partly
1027 // banded line.
1028 ✗ layout.spans.clear();
1029 ✗ layout.spanCount = 0;
1030 ✗ return layout;
1031 ✗ }
1032 5 double total = prefix.back();
1033
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (total <= 0.0) {
1034 ✗ continue;
1035 }
1036 5 double scale = (double)(ix1 - ix0 + 1) / total;
1037
1038 5 size_t nchars = bounds.size() - 1;
1039 5 size_t k = 0;
1040
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 5 times.
32 while (k < nchars) {
1041 27 size_t end = k + 1;
1042
3/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 20 times.
27 if (TextColor::NeedsWordSpans(mode)) {
1043
6/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 7 times.
10 while (end < nchars && !boundaryIsSpace(line, bounds, end)) {
1044 3 ++end;
1045 }
1046 }
1047 // Absorb the following run of spaces so consecutive spans tile with
1048 // no gap - an anti-aliased edge that bleeds past its own glyph then
1049 // still lands on a span rather than on the fallback colour.
1050 27 size_t ext = end;
1051
6/6
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 22 times.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 27 times.
33 while (ext < nchars && boundaryIsSpace(line, bounds, ext)) {
1052 6 ++ext;
1053 }
1054 27 int sx0 = ix0 + (int)std::lround(prefix[k] * scale);
1055 27 int sx1 = ix0 + (int)std::lround(prefix[ext] * scale);
1056
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (sx1 <= sx0) {
1057 ✗ sx1 = sx0 + 1;
1058 }
1059
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 layout.spans.push_back({ idx++, sx0, sx1, y0, y1 });
1060 27 k = ext;
1061 }
1062
2/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
5 }
1063 5 layout.spanCount = std::max(1, idx);
1064 5 return layout;
1065 6 }
1066
1067 /**
1068 * Repaints a still (Position: Center) block of text through an animated colour
1069 * mode. The glyphs never move, so the coverage mask is built once by the
1070 * effect and every frame after that is a pass over it - no font work and no
1071 * GraphicsMagick call at all.
1072 */
1073 class TextColorAnimationEffect : public RunningEffect {
1074 public:
1075 4 TextColorAnimationEffect(PixelOverlayModel* m) :
1076 4 RunningEffect(m) {
1077 4 }
1078 14 const std::string& name() const override {
1079
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
14 static std::string NAME = "Text";
1080 14 return NAME;
1081 }
1082 220 virtual int32_t update() override {
1083
6/6
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 178 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 40 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 218 times.
220 if (endTimeMS && GetMonotonicTimeMS() >= endTimeMS) {
1084 // The same two-step teardown StopRunningEffect performs: blank the
1085 // model, let one output cycle push that out, and only then drop the
1086 // state we auto-enabled.
1087 2 model->clearOverlayBuffer();
1088 2 model->flushOverlayBuffer();
1089
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (stopped) {
1090
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (disableWhenDone) {
1091
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 model->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
1092 }
1093 1 return EFFECT_DONE;
1094 }
1095 1 stopped = true;
1096 1 return EFFECT_AFTER_NEXT_OUTPUT;
1097 }
1098 218 paint();
1099 218 return TEXT_COLOR_FRAME_MS;
1100 }
1101 218 void paint() {
1102
3/6
✓ Branch 0 taken 218 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 218 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 218 times.
218 if (mask.empty() || rgb.size() != mask.size() * 3) {
1103 ✗ return;
1104 }
1105 436 TextColor::Colorize(mask.data(), width, height, layout, spec.mode, spec.palette,
1106 218 textColorPhase(spec, startMS), true, spec.seed, rgb.data());
1107 218 setModelDataFromRGB(model, rgb.data());
1108 }
1109
1110 std::vector<uint8_t> mask;
1111 std::vector<uint8_t> rgb;
1112 TextColor::Layout layout;
1113 TextColorSpec spec;
1114 int width = 0;
1115 int height = 0;
1116 long long startMS = 0;
1117 long long endTimeMS = 0;
1118 bool disableWhenDone = false;
1119 bool stopped = false;
1120 };
1121
1122 class TextEffect : public PixelOverlayEffect {
1123 public:
1124 2346 TextEffect() :
1125
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 PixelOverlayEffect("Text") {
1126
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Color", "color", "Color").setDefaultValue("#FF0000"));
1127
7/14
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2346 times.
✗ Branch 13 not taken.
18768 args.push_back(CommandArg("Font", "string", "Font").setContentListUrl("api/overlays/fonts", false));
1128
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("FontSize", "range", "FontSize").setRange(4, 100).setDefaultValue("18"));
1129
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("FontAntiAlias", "bool", "Anti-Aliased").setDefaultValue("false"));
1130
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
18768 args.push_back(CommandArg("Position", "string", "Position").setContentList({ "Center", "Right to Left", "Left to Right", "Bottom to Top", "Top to Bottom" }));
1131
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Speed", "range", "Scroll Speed").setRange(0, 200).setDefaultValue("10"));
1132
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Duration", "int", "Duration").setRange(-1, 2000).setDefaultValue("0"));
1133
1134 // keep text as last argument if possible as the MQTT commands will, by default, use the payload of the mqtt
1135 // msg as the last argument. Thus, this allows all of the above to be topic paths, but the text to be
1136 // sent in the payload
1137
5/10
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
14076 args.push_back(CommandArg("Text", "string", "Text").setAdjustable());
1138
1139 // ---- colour treatment -------------------------------------------
1140 // Deliberately AFTER Text, even though the form reads better with them
1141 // next to Color. Effect arguments are positional: every saved preset,
1142 // playlist entry, cue and MQTT topic naming this effect carries exactly
1143 // the eight arguments above IN THAT ORDER, and the MQTT convention of
1144 // putting the message in the payload works by appending it as the last
1145 // argument of a topic that carried the other seven. Inserting anything
1146 // ahead of Text silently re-points all of them. Appended and optional,
1147 // they are simply absent for those callers and resolve to "Single",
1148 // which is the behaviour this effect always had.
1149
4/8
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
16422 args.push_back(CommandArg("ColorMode", "string", "Color Mode", true)
1150
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 .setContentList(TextColor::ModeNames())
1151
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 .setDefaultValue("Single")
1152
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 .setSection("Color Effects")
1153
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 .setChildren(colorModeChildren())
1154
3/6
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
4692 .setHelp("<ul class='mb-0 ps-3 text-start'>"
1155 "<li><b>Single</b>: the whole message in Color.</li>"
1156 "<li><b>Gradient</b>: blend across the text, horizontally, "
1157 "vertically or corner to corner.</li>"
1158 "<li><b>Per Letter / Word / Line</b>: step through the palette, "
1159 "one colour per letter, word or line.</li>"
1160 "<li><b>Random Letter / Word</b>: as above but shuffled.</li>"
1161 "<li><b>Sparkle</b>: glitter the letters with bright flecks.</li>"
1162 "<li><b>Chase</b>: run a bright highlight along the text.</li>"
1163 "</ul>Sparkle and Chase need a Color Speed above 0 to move."));
1164
4/8
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
16422 args.push_back(CommandArg("Palette", "string", "Palette", true)
1165
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 .setContentList(TextColor::PaletteNames())
1166
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 .setDefaultValue("Custom")
1167
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 .setSection("Color Effects")
1168
3/6
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
4692 .setHelp("Where the colours come from. <b>Custom</b> uses Color plus the "
1169 "extra colours below; <b>Rainbow</b> sweeps the whole hue circle; "
1170 "the rest are ready-made sets."));
1171
4/8
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
16422 args.push_back(CommandArg("NumColors", "range", "Num Colors", true)
1172 2346 .setRange(1, 5)
1173
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 .setDefaultValue("2")
1174
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 .setSection("Color Effects")
1175
3/6
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
4692 .setHelp("How many colours the Custom palette uses, counting Color as the "
1176 "first. Ignored by the named palettes."));
1177
8/16
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2346 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 2346 times.
✗ Branch 15 not taken.
18768 args.push_back(CommandArg("Color2", "color", "Color 2", true).setDefaultValue("#0000FF").setSection("Color Effects"));
1178
8/16
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2346 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 2346 times.
✗ Branch 15 not taken.
18768 args.push_back(CommandArg("Color3", "color", "Color 3", true).setDefaultValue("#00FF00").setSection("Color Effects"));
1179
8/16
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2346 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 2346 times.
✗ Branch 15 not taken.
18768 args.push_back(CommandArg("Color4", "color", "Color 4", true).setDefaultValue("#FFFF00").setSection("Color Effects"));
1180
8/16
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2346 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 2346 times.
✗ Branch 15 not taken.
18768 args.push_back(CommandArg("Color5", "color", "Color 5", true).setDefaultValue("#FF00FF").setSection("Color Effects"));
1181
4/8
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
16422 args.push_back(CommandArg("ColorSpeed", "range", "Color Speed", true)
1182 2346 .setRange(0, 100)
1183
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 .setDefaultValue("0")
1184
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 .setSection("Color Effects")
1185
3/6
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
4692 .setHelp("How fast the colours move through the text. 0 holds a still "
1186 "frame; 20 is one full pass a second. Scrolling text animates its "
1187 "colours on the scroll's own frame rate, so a very low Scroll "
1188 "Speed also makes the colour change step."));
1189 2346 }
1190
1191 /** Which arguments are only worth showing once a colour mode is chosen. */
1192 2346 static std::map<std::string, std::vector<std::string>> colorModeChildren() {
1193 static const std::vector<std::string> SHOWN = {
1194 "Palette", "NumColors", "Color2", "Color3", "Color4", "Color5", "ColorSpeed"
1195
3/8
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
7038 };
1196 2346 std::map<std::string, std::vector<std::string>> m;
1197
3/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25806 times.
✓ Branch 3 taken 2346 times.
28152 for (const auto& n : TextColor::ModeNames()) {
1198
6/10
✓ Branch 0 taken 25806 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✓ Branch 3 taken 23460 times.
✓ Branch 4 taken 23460 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25806 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 25806 times.
✗ Branch 9 not taken.
25806 m[n] = (n == "Single") ? std::vector<std::string>() : SHOWN;
1199 }
1200 2346 return m;
1201 ✗ }
1202
1203 class TextMovementEffect : public ImageMovementEffect {
1204 public:
1205 5 TextMovementEffect(PixelOverlayModel* m) :
1206 5 ImageMovementEffect(m) {
1207 5 }
1208
1209 28 const std::string& name() const override {
1210
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 27 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
28 static std::string NAME = "Text";
1211 28 return NAME;
1212 }
1213
1214 /**
1215 * Scrolling already re-blits every frame, so an animated colour mode
1216 * only has to repaint the glyph buffer first. colorMask is empty for
1217 * Single (and for a still colour mode), in which case imageData is what
1218 * doText() painted once and nothing here touches it.
1219 */
1220 3919 virtual int32_t update() override {
1221
6/8
✓ Branch 0 taken 3919 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 126 times.
✓ Branch 3 taken 3793 times.
✓ Branch 4 taken 126 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 126 times.
✓ Branch 7 taken 3793 times.
3919 if (imageData && !colorMask.empty() && colorSpec.animated()) {
1222 126 TextColor::Colorize(colorMask.data(), imageDataCols, imageDataRows, colorLayout,
1223 126 colorSpec.mode, colorSpec.palette,
1224 126 textColorPhase(colorSpec, colorStartMS), true,
1225 colorSpec.seed, imageData);
1226 }
1227 3919 return ImageMovementEffect::update();
1228 }
1229
1230 std::vector<uint8_t> colorMask;
1231 TextColor::Layout colorLayout;
1232 TextColorSpec colorSpec;
1233 long long colorStartMS = 0;
1234 };
1235
1236 // GraphicsMagick throws on a font it cannot load, an allocation it cannot
1237 // make, and so on. The command dispatcher would catch it now, but not
1238 // before the model was left auto-enabled with nothing to turn it off.
1239 240 void doText(PixelOverlayModel* m,
1240 const std::string& msg,
1241 int r, int g, int b,
1242 const std::string& font,
1243 int fontSize,
1244 bool antialias,
1245 const std::string& position,
1246 int pixelsPerSecond,
1247 const std::string& autoEnable,
1248 int duration,
1249 const TextColorSpec& colorSpec) {
1250
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 const bool wasDisabled = m->getState().getState() == PixelOverlayState::PixelState::Disabled;
1251 try {
1252
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 doTextImpl(m, msg, r, g, b, font, fontSize, antialias, position, pixelsPerSecond,
1253 autoEnable, duration, colorSpec);
1254 ✗ } catch (const std::exception& e) {
1255 ✗ LogErr(VB_CHANNELOUT, "Text Overlay Effect - could not render text: %s\n", e.what());
1256 ✗ if (wasDisabled && m->getState().getState() != PixelOverlayState::PixelState::Disabled) {
1257 ✗ m->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
1258 }
1259 ✗ }
1260 240 }
1261 240 void doTextImpl(PixelOverlayModel* m,
1262 const std::string& msg,
1263 int r, int g, int b,
1264 const std::string& font,
1265 int fontSize,
1266 bool antialias,
1267 const std::string& position,
1268 int pixelsPerSecond,
1269 const std::string& autoEnable,
1270 int duration,
1271 const TextColorSpec& colorSpec) {
1272 240 bool disableWhenDone = false;
1273
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 PixelOverlayState st(autoEnable);
1274
6/8
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✓ Branch 5 taken 227 times.
✓ Branch 6 taken 13 times.
✓ Branch 7 taken 227 times.
240 if ((st.getState() != PixelOverlayState::PixelState::Disabled) && (m->getState().getState() == PixelOverlayState::PixelState::Disabled)) {
1275
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 m->setState(st);
1276 13 disableWhenDone = true;
1277 }
1278
1279 // Normalize a literal "\n" (backslash followed by 'n', as typed into a
1280 // single-line text field) into a real newline byte so it is treated the
1281 // same as an actual embedded newline for both sizing and rendering below.
1282 // A doubled backslash ("\\") escapes to a single literal backslash, so
1283 // text containing a literal "\n" sequence (e.g. a Windows path like
1284 // "c:\network") can be entered as "c:\\network" without being split.
1285 240 std::string normalizedMsg;
1286
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 normalizedMsg.reserve(msg.length());
1287
2/2
✓ Branch 0 taken 483711 times.
✓ Branch 1 taken 240 times.
483951 for (int x = 0; x < msg.length(); x++) {
1288
7/8
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 483707 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 483708 times.
483711 if (msg[x] == '\\' && (x < msg.length() - 1) && msg[x + 1] == 'n') {
1289
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 normalizedMsg += '\n';
1290 3 x++;
1291
6/8
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 483707 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 483707 times.
483708 } else if (msg[x] == '\\' && (x < msg.length() - 1) && msg[x + 1] == '\\') {
1292
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 normalizedMsg += '\\';
1293 1 x++;
1294 } else {
1295
1/2
✓ Branch 0 taken 483707 times.
✗ Branch 1 not taken.
483707 normalizedMsg += msg[x];
1296 }
1297 }
1298
1299 // Owned, so every return and any Magick throw below frees it.
1300
3/6
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 240 times.
✗ Branch 5 not taken.
240 std::unique_ptr<Magick::Image> image = std::make_unique<Magick::Image>(Magick::Geometry(m->getWidth(), m->getHeight()), Magick::Color("black"));
1301
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 image->quiet(true);
1302
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 image->depth(8);
1303
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 image->font(font);
1304
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 image->fontPointsize(fontSize);
1305
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 image->antiAlias(antialias);
1306
1307
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 std::vector<std::string> lines = splitTextLines(normalizedMsg);
1308 240 int maxWid = 0;
1309 240 int totalHi = 0;
1310 // Left holding the LAST line's metrics, which is what the Top to Bottom
1311 // start position below is measured from.
1312
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 Magick::TypeMetric metrics;
1313
2/2
✓ Branch 0 taken 243 times.
✓ Branch 1 taken 240 times.
483 for (const auto& line : lines) {
1314
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 image->fontTypeMetrics(line, &metrics);
1315
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 maxWid = std::max(maxWid, (int)metrics.textWidth());
1316
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 totalHi += (int)metrics.textHeight();
1317 }
1318
1319 // Empty text (or text consisting only of newlines) measures as zero width,
1320 // which would make the scrolling branch below construct an invalid image.
1321 240 maxWid = std::max(maxWid, 1);
1322 240 totalHi = std::max(totalHi, 1);
1323
1324 // Single draws straight in its fill colour, exactly as this effect
1325 // always has; every other mode draws white and is painted afterwards.
1326 240 const bool plainColor = colorSpec.plain();
1327
4/6
✓ Branch 0 taken 212 times.
✓ Branch 1 taken 28 times.
✓ Branch 2 taken 212 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 28 times.
✗ Branch 5 not taken.
240 Magick::Color fill = plainColor ? magickColor(r, g, b) : magickColor(255, 255, 255);
1328
1329
7/10
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 240 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 44 times.
✓ Branch 7 taken 196 times.
✓ Branch 8 taken 44 times.
✓ Branch 9 taken 196 times.
240 if (position == "Centered" || position == "Center") {
1330 // Before anything is drawn: a still frame is written straight to the
1331 // model, so a repainting Text effect from an earlier run has to go
1332 // first or it simply paints back over us.
1333
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 retireRunningTextEffect(m);
1334
1335
2/4
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44 times.
✗ Branch 3 not taken.
88 image->magick("RGB");
1336 // one shot, just draw the text and return
1337
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 image->fillColor(fill);
1338
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 image->antiAlias(antialias);
1339
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 image->strokeAntiAlias(antialias);
1340
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 image->annotate(normalizedMsg, Magick::CenterGravity);
1341
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 Magick::Blob blob;
1342
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 image->write(&blob);
1343
1344
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 25 times.
44 if (plainColor) {
1345
2/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
19 setModelDataFromRGB(m, (const uint8_t*)blob.data());
1346
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 14 times.
19 if (disableWhenDone) {
1347 5 int nd = 25;
1348
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (duration > 0) {
1349 5 nd = duration * 1000;
1350 }
1351
5/12
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 5 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
15 m->setRunningEffect(new StopRunningEffect(m, "Text", disableWhenDone), nd);
1352 }
1353 19 image.reset();
1354 19 return;
1355 }
1356
1357 25 int w = m->getWidth();
1358 25 int h = m->getHeight();
1359 25 size_t pixels = (size_t)w * h;
1360
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 25 times.
25 if (blob.length() < pixels * 3) {
1361 // magick("RGB") + depth(8) should always give exactly this, but
1362 // the mask pass reads every byte of it, so short-changing it
1363 // would be an out-of-bounds read rather than a stretched image.
1364 ✗ LogErr(VB_CHANNELOUT, "Text Overlay Effect - rendered %zu bytes, expected %zu\n",
1365 blob.length(), pixels * 3);
1366 ✗ image.reset();
1367 ✗ return;
1368 }
1369
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 std::vector<uint8_t> mask(pixels);
1370
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 rgbToCoverageMask((const uint8_t*)blob.data(), pixels, mask.data());
1371
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 TextColor::Layout layout = buildTextLayout(mask.data(), w, h, *image, lines, colorSpec.mode);
1372 25 image.reset();
1373
1374
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 std::vector<uint8_t> rgb(pixels * 3);
1375
1/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
50 TextColor::Colorize(mask.data(), w, h, layout, colorSpec.mode, colorSpec.palette,
1376
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 0.0, colorSpec.animated(), colorSpec.seed, rgb.data());
1377
1378
3/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 4 times.
25 if (!colorSpec.animated()) {
1379
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 setModelDataFromRGB(m, rgb.data());
1380
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (disableWhenDone) {
1381 ✗ int nd = 25;
1382 ✗ if (duration > 0) {
1383 ✗ nd = duration * 1000;
1384 }
1385 ✗ m->setRunningEffect(new StopRunningEffect(m, "Text", disableWhenDone), nd);
1386 }
1387 21 return;
1388 }
1389
1390
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
4 TextColorAnimationEffect* ef = new TextColorAnimationEffect(m);
1391 4 ef->mask = std::move(mask);
1392 4 ef->rgb = std::move(rgb);
1393 4 ef->layout = std::move(layout);
1394
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 ef->spec = colorSpec;
1395 4 ef->width = w;
1396 4 ef->height = h;
1397
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 ef->startMS = GetMonotonicTimeMS();
1398 4 ef->disableWhenDone = disableWhenDone;
1399 // Duration bounds an auto-enabled run only, which is the rule the
1400 // still centred path has always followed - without auto-enable the
1401 // text stays up until something else replaces it.
1402
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (disableWhenDone) {
1403
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ef->endTimeMS = ef->startMS + (duration > 0 ? (long long)duration * 1000 : 25);
1404 }
1405
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 m->setRunningEffect(ef, 1);
1406
8/8
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 21 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 21 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 21 times.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 40 times.
107 } else {
1407 196 image.reset();
1408 // movement
1409 // The strip is as wide as the whole message, so its size is the
1410 // caller's to choose: a long MQTT payload or a huge FontSize asked
1411 // GraphicsMagick for gigabytes (8 bytes a pixel), more than a
1412 // BeagleBone has. 4M pixels is a 64-row banner some 65000 wide.
1413 196 constexpr int64_t MAX_TEXT_PIXELS = 4 * 1024 * 1024;
1414
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 195 times.
196 if ((int64_t)maxWid * totalHi > MAX_TEXT_PIXELS) {
1415
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_CHANNELOUT, "Text Overlay Effect - text renders %dx%d, over the %lld pixel limit; not drawing it\n",
1416 maxWid, totalHi, (long long)MAX_TEXT_PIXELS);
1417
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (disableWhenDone) {
1418
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 m->setState(PixelOverlayState(PixelOverlayState::PixelState::Disabled));
1419 }
1420 1 return;
1421 }
1422
3/6
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 195 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 195 times.
✗ Branch 5 not taken.
195 Magick::Image image2(Magick::Geometry(maxWid, totalHi), Magick::Color("black"));
1423
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.quiet(true);
1424
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.depth(8);
1425
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.font(font);
1426
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.fontPointsize(fontSize);
1427
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.antiAlias(antialias);
1428
1429
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.fillColor(fill);
1430
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.antiAlias(antialias);
1431
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.strokeAntiAlias(antialias);
1432
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 image2.annotate(normalizedMsg, Magick::CenterGravity);
1433
1434 195 double y = (m->getHeight() / 2.0) - ((totalHi) / 2.0);
1435 195 double x = (m->getWidth() / 2.0) - (maxWid / 2.0);
1436
2/4
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 195 times.
✗ Branch 3 not taken.
195 if (position == "R2L") {
1437 195 x = m->getWidth();
1438 ✗ } else if (position == "L2R") {
1439 ✗ x = -maxWid;
1440 ✗ } else if (position == "B2T") {
1441 ✗ y = m->getHeight();
1442 ✗ } else if (position == "T2B") {
1443 ✗ y = -metrics.ascent();
1444 }
1445
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 uint8_t* newData = imageToRGBBuffer(image2);
1446
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 195 times.
195 if (!newData) {
1447 ✗ LogErr(VB_CHANNELOUT, "Text Overlay Effect - could not read rendered text pixels\n");
1448 ✗ return;
1449 }
1450
1451
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 int cols = image2.columns();
1452
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 int rows = image2.rows();
1453 195 std::vector<uint8_t> mask;
1454 195 TextColor::Layout layout;
1455
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 192 times.
195 if (!plainColor) {
1456
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 mask.resize((size_t)cols * rows);
1457
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 rgbToCoverageMask(newData, mask.size(), mask.data());
1458
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 layout = buildTextLayout(mask.data(), cols, rows, image2, lines, colorSpec.mode);
1459
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 TextColor::Colorize(mask.data(), cols, rows, layout, colorSpec.mode,
1460
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
3 colorSpec.palette, 0.0, colorSpec.animated(),
1461
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 colorSpec.seed, newData);
1462
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
3 if (!colorSpec.animated()) {
1463 // Nothing will repaint, so the mask is dead weight in the
1464 // running effect - imageData already holds the final pixels.
1465 2 mask.clear();
1466 }
1467 }
1468
1469
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
1470
2/2
✓ Branch 0 taken 190 times.
✓ Branch 1 taken 5 times.
195 TextMovementEffect* ef = dynamic_cast<TextMovementEffect*>(m->getRunningEffect());
1471
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 190 times.
195 if (ef == nullptr) {
1472
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 ef = new TextMovementEffect(m);
1473 5 ef->x = (int)x;
1474 5 ef->y = (int)y;
1475 }
1476 195 ef->speed = pixelsPerSecond;
1477 195 ef->disableWhenDone = disableWhenDone;
1478
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 ef->direction = position;
1479 // Scroll Speed's range starts at 0, and this is the same SIGFPE
1480 // ImageMovementEffect::update() already had to guard against - the
1481 // first tick was being scheduled here, before that guard ever ran.
1482 195 int32_t t = 1000 / std::max(1, pixelsPerSecond);
1483
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 195 times.
195 if (t == 0) {
1484 ✗ t = 1;
1485 }
1486 195 uint8_t* old = ef->imageData;
1487 195 ef->imageData = newData;
1488 195 ef->imageDataCols = cols;
1489 195 ef->imageDataRows = rows;
1490 195 ef->colorMask = std::move(mask);
1491 195 ef->colorLayout = std::move(layout);
1492
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 ef->colorSpec = colorSpec;
1493
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 ef->colorStartMS = GetMonotonicTimeMS();
1494
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 ef->copyImageData(ef->x, ef->y);
1495
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 m->setRunningEffect(ef, t);
1496
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 lock.unlock();
1497 195 free(old);
1498
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 }
1499
10/10
✓ Branch 0 taken 199 times.
✓ Branch 1 taken 41 times.
✓ Branch 2 taken 199 times.
✓ Branch 3 taken 41 times.
✓ Branch 4 taken 199 times.
✓ Branch 5 taken 41 times.
✓ Branch 6 taken 199 times.
✓ Branch 7 taken 41 times.
✓ Branch 8 taken 199 times.
✓ Branch 9 taken 41 times.
404 }
1500
1501 /**
1502 * Drop a Text effect that is still REPAINTING, leaving whatever it last
1503 * drew on the model, so a still centred message is not immediately painted
1504 * over by a scroll or an animated colour mode left over from an earlier
1505 * run.
1506 *
1507 * A pending StopRunningEffect is deliberately left alone even though it is
1508 * also named "Text": it is what returns an auto-enabled model to Disabled,
1509 * and cancelling it would strand the model enabled. Anything else on the
1510 * model - an Image scroll, a WLED effect - is likewise left to finish on
1511 * its own terms.
1512 */
1513 44 static void retireRunningTextEffect(PixelOverlayModel* m) {
1514 // effectLock is recursive, and clearRunningEffect() takes it the same
1515 // way setRunningEffect() does, so holding it across the check keeps the
1516 // decision and the removal atomic.
1517
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
44 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
1518 44 RunningEffect* cur = m->getRunningEffect();
1519
7/8
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 42 times.
✓ Branch 2 taken 44 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 42 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 42 times.
88 if (dynamic_cast<TextMovementEffect*>(cur) != nullptr ||
1520
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 42 times.
44 dynamic_cast<TextColorAnimationEffect*>(cur) != nullptr) {
1521
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 m->clearRunningEffect();
1522 }
1523 44 }
1524
1525 /** args[i] when it was supplied and is not blank, otherwise `dflt`. */
1526 359 static std::string argOr(const std::vector<std::string>& args, size_t i, const char* dflt) {
1527
5/6
✓ Branch 0 taken 358 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 358 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 358 times.
✓ Branch 5 taken 1 times.
359 if (i < args.size() && !args[i].empty()) {
1528 358 return args[i];
1529 }
1530
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 return dflt;
1531 }
1532
1533 /**
1534 * The colour arguments can arrive straight off an MQTT topic path or a
1535 * hand-written preset; a typo falls back to the caller's default rather
1536 * than mapColor()'s black.
1537 */
1538 35 static uint32_t safeMapColor(const std::string& c, uint32_t dflt) {
1539 uint32_t color;
1540
3/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✓ Branch 3 taken 1 times.
70 return PixelOverlayManager::parseColor(c, color) ? color : dflt;
1541 }
1542
1543 /**
1544 * Arguments 8 and up are the colour treatment and are all optional: a
1545 * caller that stops at Text lands on Single with the one Color, which is
1546 * this effect's original behaviour.
1547 */
1548 240 static TextColorSpec parseColorSpec(const std::vector<std::string>& args, uint32_t baseColor) {
1549 // Color2..Color5, matching the declared defaults so an MQTT topic that
1550 // names a mode but no extra colours still gets a usable palette.
1551 static const char* COLOR_NAMES[] = { "#0000FF", "#00FF00", "#FFFF00", "#FF00FF" };
1552 static const uint32_t COLOR_VALUES[] = { 0x0000FF, 0x00FF00, 0xFFFF00, 0xFF00FF };
1553 240 TextColorSpec spec;
1554
2/4
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
240 spec.mode = TextColor::ParseMode(argOr(args, 8, "Single"));
1555
2/2
✓ Branch 0 taken 212 times.
✓ Branch 1 taken 28 times.
240 if (spec.plain()) {
1556
1/2
✓ Branch 0 taken 212 times.
✗ Branch 1 not taken.
212 spec.palette.colors.push_back(baseColor);
1557 212 return spec;
1558 }
1559
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 int numColors = std::clamp(std::atoi(argOr(args, 10, "2").c_str()), 1, 5);
1560 28 std::vector<uint32_t> custom;
1561
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 custom.push_back(baseColor);
1562
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 28 times.
63 for (int i = 1; i < numColors; ++i) {
1563
3/6
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35 times.
✗ Branch 5 not taken.
35 custom.push_back(safeMapColor(argOr(args, 10 + i, COLOR_NAMES[i - 1]), COLOR_VALUES[i - 1]));
1564 }
1565
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 spec.palette = TextColor::BuildPalette(argOr(args, 9, "Custom"), custom);
1566
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 spec.colorSpeed = std::clamp(std::atoi(argOr(args, 15, "0").c_str()), 0, 100);
1567 // One seed per application, so the Random modes hold their shuffle for
1568 // as long as the text is up instead of re-rolling on every repaint.
1569
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 spec.seed = (uint32_t)GetTimeMS();
1570 28 return spec;
1571 28 }
1572
1573 243 virtual bool apply(PixelOverlayModel* model, const std::string& autoEnable, const std::vector<std::string>& args) override {
1574
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 std::string color = args[0];
1575
1/2
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
243 unsigned int cint = PixelOverlayManager::mapColor(color);
1576
2/4
✓ Branch 0 taken 243 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 243 times.
✗ Branch 3 not taken.
243 std::string font = PixelOverlayManager::INSTANCE.mapFont(args[1]);
1577
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 240 times.
243 if (font.empty()) {
1578
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string warn = "Text Overlay Effect - Could not find font \'" + args[1] + "\'";
1579
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 WarningHolder::AddWarning(warn);
1580 3 return false;
1581 3 }
1582 240 int fontSize = std::atoi(args[2].c_str());
1583
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 240 times.
240 if (fontSize < 4) {
1584 ✗ fontSize = 12;
1585 }
1586 // Points; even this renders far taller than any model.
1587 240 fontSize = std::min(fontSize, 1000);
1588
4/8
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 240 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 240 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 240 times.
240 bool aa = args[3] == "true" || args[3] == "1";
1589
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 std::string position = MapOverlayPosition(args[4]);
1590 240 int pps = std::atoi(args[5].c_str());
1591 240 int duration = std::atoi(args[6].c_str());
1592
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 std::string msg = args[7];
1593
1594
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
240 doText(model,
1595 msg,
1596 240 (cint >> 16) & 0xFF,
1597 240 (cint >> 8) & 0xFF,
1598
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
240 cint & 0xFF,
1599 font,
1600 fontSize,
1601 aa,
1602 position,
1603 pps,
1604 autoEnable,
1605 duration,
1606
1/2
✓ Branch 0 taken 240 times.
✗ Branch 1 not taken.
480 parseColorSpec(args, cint));
1607 240 return true;
1608 243 }
1609 };
1610
1611 class ImageEffect : public PixelOverlayEffect {
1612 public:
1613 2346 ImageEffect() :
1614
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 PixelOverlayEffect("Image") {
1615
7/14
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2346 times.
✗ Branch 13 not taken.
18768 args.push_back(CommandArg("Scaling", "string", "Scaling").setContentList({ "Scale to Fit", "Crop to Fill", "Stretch", "None" }).setDefaultValue("Scale to Fit"));
1616
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
18768 args.push_back(CommandArg("Position", "string", "Position").setContentList({ "Center", "Right to Left", "Left to Right", "Bottom to Top", "Top to Bottom" }));
1617
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Speed", "range", "Scroll Speed").setRange(0, 200).setDefaultValue("10"));
1618
6/12
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
14076 args.push_back(CommandArg("Duration", "int", "Duration (0 = leave up)").setRange(0, 2000).setDefaultValue("0"));
1619
1620 // keep the filename as the last argument so the MQTT commands can, by
1621 // default, carry it as the payload while everything above is a topic
1622 // path. Same reasoning as the Text effect's Text argument.
1623
7/14
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2346 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2346 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2346 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 2346 times.
✗ Branch 13 not taken.
18768 args.push_back(CommandArg("Image", "string", "Image").setContentListUrl("api/overlays/images", false).setAdjustable());
1624 2346 }
1625
1626 // Compose an arbitrarily sized RGB image into a model-sized RGB frame,
1627 // centred, clipping whatever falls outside. Used for the static case so a
1628 // "None"-scaled or odd-sized image can never run off the end of setData's
1629 // width*height*bpp buffer.
1630 5 void centerIntoFrame(const uint8_t* src, int srcCols, int srcRows,
1631 std::vector<uint8_t>& frame, int w, int h) {
1632 5 int xoff = (w - srcCols) / 2;
1633 5 int yoff = (h - srcRows) / 2;
1634
1635
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 5 times.
22 for (int y = 0; y < srcRows; y++) {
1636 17 int ny = yoff + y;
1637
4/4
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 15 times.
17 if (ny < 0 || ny >= h) {
1638 2 continue;
1639 }
1640 15 int sx = 0;
1641 15 int dx = xoff;
1642 15 int count = srcCols;
1643
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 10 times.
15 if (dx < 0) {
1644 5 sx = -dx;
1645 5 count += dx;
1646 5 dx = 0;
1647 }
1648
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 10 times.
15 if (count > (w - dx)) {
1649 5 count = w - dx;
1650 }
1651
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (count <= 0) {
1652 ✗ continue;
1653 }
1654 15 memcpy(&frame[((size_t)ny * w + dx) * 3],
1655 15 src + (((size_t)y * srcCols + sx) * 3),
1656 15 (size_t)count * 3);
1657 }
1658 5 }
1659
1660 19 bool doImage(PixelOverlayModel* m,
1661 const std::string& path,
1662 const std::string& scaling,
1663 const std::string& position,
1664 int pixelsPerSecond,
1665 const std::string& autoEnable,
1666 int duration) {
1667
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 18 times.
19 if (path.empty()) {
1668
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
3 WarningHolder::AddWarningTimeout(60, 34, "Image Overlay Effect - no image specified");
1669 1 return false;
1670 }
1671
1672
11/32
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 18 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 18 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 18 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 18 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 18 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 18 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 18 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 18 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 18 times.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
54 std::string fullPath = startsWith(path, "/") ? path : FPP_DIR_IMAGE("/" + path);
1673
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 18 times.
18 if (!FileExists(fullPath)) {
1674 ✗ WarningHolder::AddWarningTimeout(60, 34, "Image Overlay Effect - file not found: " + fullPath);
1675 ✗ LogErr(VB_CHANNELOUT, "Image Overlay Effect - file not found: %s\n", fullPath.c_str());
1676 ✗ return false;
1677 }
1678
1679 18 int w = m->getWidth();
1680 18 int h = m->getHeight();
1681
6/8
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 9 times.
18 bool horizontal = (position == "R2L") || (position == "L2R");
1682
1683 // Decode and scale before touching the model or the effect slot: a slow
1684 // read here should not leave a half-applied effect behind.
1685
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 Magick::Image image;
1686 try {
1687
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 image.quiet(true); // squelch warning exceptions
1688
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 image.read(fullPath);
1689
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 image.autoOrient();
1690
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 image.type(Magick::TrueColorType);
1691
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 image.depth(8);
1692
1693
8/10
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 10 times.
✓ Branch 7 taken 3 times.
✓ Branch 8 taken 15 times.
✓ Branch 9 taken 3 times.
18 if (position == "Center" || scaling != "Scale to Fit") {
1694
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 ScaleOverlayImage(image, w, h, scaling);
1695 } else {
1696 // Scrolling with aspect preserved: match the axis we are not
1697 // travelling along and let the other run past the model edge.
1698
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 scaleToCrossAxis(image, w, h, horizontal);
1699 }
1700 ✗ } catch (Magick::Exception& error_) {
1701 ✗ LogErr(VB_CHANNELOUT, "GraphicsMagick exception reading %s: %s\n", fullPath.c_str(), error_.what());
1702 ✗ WarningHolder::AddWarningTimeout(60, 34, "Image Overlay Effect - could not read " + fullPath);
1703 ✗ return false;
1704 ✗ } catch (const std::exception& ex) {
1705 // Not everything thrown here is a Magick::Exception (std::bad_alloc,
1706 // filesystem errors), and an escape would unwind through the
1707 // command dispatch holding the models lock.
1708 ✗ LogErr(VB_CHANNELOUT, "Exception preparing image %s: %s\n", fullPath.c_str(), ex.what());
1709 ✗ WarningHolder::AddWarningTimeout(60, 34, "Image Overlay Effect - could not read " + fullPath);
1710 ✗ return false;
1711 ✗ }
1712
1713 18 bool disableWhenDone = false;
1714
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 PixelOverlayState st(autoEnable);
1715
6/8
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 15 times.
✓ Branch 7 taken 3 times.
18 if ((st.getState() != PixelOverlayState::PixelState::Disabled) && (m->getState().getState() == PixelOverlayState::PixelState::Disabled)) {
1716
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 m->setState(st);
1717 15 disableWhenDone = true;
1718 }
1719
1720
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 uint8_t* imageRGB = imageToRGBBuffer(image);
1721
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18 times.
18 if (!imageRGB) {
1722 ✗ LogErr(VB_CHANNELOUT, "Image Overlay Effect - could not read pixels from %s\n", fullPath.c_str());
1723 ✗ return false;
1724 }
1725
1726
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 int cols = image.columns();
1727
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 int rows = image.rows();
1728
1729
3/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 13 times.
18 if (position == "Center") {
1730
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::vector<uint8_t> frame((size_t)w * h * 3, 0);
1731 5 centerIntoFrame(imageRGB, cols, rows, frame, w, h);
1732 5 free(imageRGB);
1733
1734 // We are taking the model over, so drop any effect still running on
1735 // it: its next tick would clear or overwrite the image we are about
1736 // to draw. The Text effect gets this incidentally because it always
1737 // installs a StopRunningEffect here; with Duration 0 meaning "leave
1738 // it up" we install nothing, so the eviction has to be explicit.
1739 // The scrolling path below needs no equivalent - setRunningEffect
1740 // deletes the previous effect for us.
1741 {
1742
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
1743
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (m->getRunningEffect()) {
1744
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 m->setRunningEffect(nullptr, 25); // one no-op tick, then dropped
1745 }
1746 5 }
1747
1748
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 setModelDataFromRGB(m, frame.data());
1749
1750 // Duration > 0 clears the image (and disables the model if we were
1751 // the one that enabled it) after that many seconds; 0 leaves it up
1752 // until something else changes the model. This deliberately
1753 // differs from the Text effect, where Duration is honoured only in
1754 // the auto-enable case and a Duration of 0 wipes the model after
1755 // 25ms - which for an image means the default arguments would draw
1756 // and immediately erase. Text's behaviour is left alone so
1757 // existing presets do not shift under people.
1758
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (duration > 0) {
1759
5/12
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3 m->setRunningEffect(new StopRunningEffect(m, "Image", disableWhenDone), duration * 1000);
1760 }
1761 5 return true;
1762 5 }
1763
1764 // Scrolling: hand the pixels to the same mover the Text effect uses.
1765 13 double y = (h / 2.0) - (rows / 2.0);
1766 13 double x = (w / 2.0) - (cols / 2.0);
1767
3/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 6 times.
13 if (position == "R2L") {
1768 7 x = w;
1769
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
6 } else if (position == "L2R") {
1770 2 x = -cols;
1771
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
4 } else if (position == "B2T") {
1772 1 y = h;
1773
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 } else if (position == "T2B") {
1774 3 y = -rows;
1775 }
1776
1777
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::unique_lock<std::recursive_mutex> lock(m->getRunningEffectMutex());
1778
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 ImageMovementEffect* ef = new ImageMovementEffect(m);
1779 13 ef->x = (int)x;
1780 13 ef->y = (int)y;
1781 13 ef->speed = std::max(1, pixelsPerSecond);
1782 13 ef->disableWhenDone = disableWhenDone;
1783
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 ef->direction = position;
1784
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (duration > 0) {
1785
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ef->endTimeMS = GetMonotonicTimeMS() + ((long long)duration * 1000);
1786 }
1787 13 ef->imageData = imageRGB; // effect owns it from here, frees in its dtor
1788 13 ef->imageDataCols = cols;
1789 13 ef->imageDataRows = rows;
1790
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 ef->copyImageData(ef->x, ef->y);
1791
1792 13 int32_t t = 1000 / ef->speed;
1793
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 if (t == 0) {
1794 ✗ t = 1;
1795 }
1796
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 m->setRunningEffect(ef, t);
1797 13 return true;
1798 18 }
1799
1800 19 virtual bool apply(PixelOverlayModel* model, const std::string& autoEnable, const std::vector<std::string>& args) override {
1801
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (args.size() < 5) {
1802 ✗ LogInfo(VB_CHANNELOUT, "Image Effect: not enough args\n");
1803 ✗ return false;
1804 }
1805
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::string scaling = args[0];
1806
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::string position = MapOverlayPosition(args[1]);
1807 19 int pps = std::atoi(args[2].c_str());
1808 19 int duration = std::atoi(args[3].c_str());
1809
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 std::string file = args[4];
1810
1811
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 return doImage(model, file, scaling, position, pps, autoEnable, duration);
1812 19 }
1813 };
1814
1815 class StopEffect : public PixelOverlayEffect {
1816 public:
1817 2346 StopEffect() :
1818
2/4
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
4692 PixelOverlayEffect("Stop Effects") {
1819 2346 }
1820 34947 virtual bool apply(PixelOverlayModel* model, const std::string& autoEnable, const std::vector<std::string>& args) override {
1821 // The deferred stop blanks an enabled model for one output pass before
1822 // disabling it, so its lights don't hold the effect's last frame. A
1823 // disabled model is on no lights, and with nothing else active the
1824 // output thread is idle, so that pass never came: the stop stayed
1825 // queued on the model and wiped and disabled whatever next enabled it
1826 // (a Fill minutes later). Stop it here instead.
1827
3/4
✓ Branch 0 taken 34947 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 123 times.
✓ Branch 3 taken 34824 times.
34947 if (model->getState().getState() == PixelOverlayState::PixelState::Disabled) {
1828 123 model->clearRunningEffect();
1829 123 model->clearOverlayBuffer();
1830 123 model->flushOverlayBuffer();
1831 123 return true;
1832 }
1833
4/10
✓ Branch 0 taken 34824 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34824 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 34824 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 34824 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
104472 model->setRunningEffect(new StopRunningEffect(model, "Stop Effects", autoEnable), 25);
1834 34824 return true;
1835 }
1836 };
1837
1838 class PixelOverlayEffectHolder {
1839 public:
1840 2346 PixelOverlayEffectHolder() {
1841
4/10
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2346 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
2346 add(new ColorFadeEffect());
1842
4/10
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2346 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
2346 add(new BarsEffect());
1843
4/10
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2346 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
2346 add(new TextEffect());
1844
4/10
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2346 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
2346 add(new ImageEffect());
1845
1846
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 std::list<PixelOverlayEffect*> wled = WLEDEffect::getWLEDEffects();
1847
2/2
✓ Branch 0 taken 499698 times.
✓ Branch 1 taken 2346 times.
502044 for (auto a : wled) {
1848
1/2
✓ Branch 0 taken 499698 times.
✗ Branch 1 not taken.
499698 add(a);
1849 }
1850
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 effectNames.sort();
1851
1852 // The WLED JSON API's numbering: the same sorted list with each
1853 // retired effect still in the place it sorted to, as "RSVD".
1854
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 std::list<std::string> retired = WLEDEffect::getRetiredWLEDEffectNames();
1855
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 wledAPINames = effectNames;
1856
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 wledAPINames.insert(wledAPINames.end(), retired.begin(), retired.end());
1857
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 wledAPINames.sort();
1858
2/2
✓ Branch 0 taken 511428 times.
✓ Branch 1 taken 2346 times.
513774 for (auto& n : wledAPINames) {
1859
3/4
✓ Branch 0 taken 511428 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✓ Branch 3 taken 509082 times.
511428 if (std::find(retired.begin(), retired.end(), n) != retired.end()) {
1860
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 n = "RSVD";
1861 }
1862 }
1863
1864
3/8
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2346 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
2346 StopEffect* pe = new StopEffect();
1865
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 effectNames.push_front(pe->name);
1866
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 wledAPINames.push_front(pe->name);
1867
1/2
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
2346 effects[pe->name] = pe;
1868 2346 }
1869 458 ~PixelOverlayEffectHolder() {
1870
2/2
✓ Branch 0 taken 99844 times.
✓ Branch 1 taken 458 times.
100302 for (auto& a : effects) {
1871
1/2
✓ Branch 0 taken 99844 times.
✗ Branch 1 not taken.
99844 delete a.second;
1872 }
1873 458 effects.clear();
1874 458 WLEDEffect::cleanupWLEDEffects();
1875 458 }
1876 509082 void add(PixelOverlayEffect* pe) {
1877 509082 effects[pe->name] = pe;
1878 509082 effectNames.push_back(pe->name);
1879 509082 wledAPINames.push_back(pe->name);
1880 509082 ++generation;
1881 509082 }
1882 ✗ void remove(PixelOverlayEffect* pe) {
1883 ✗ effects.erase(pe->name);
1884 ✗ auto it = std::find(effectNames.begin(), effectNames.end(), pe->name);
1885 ✗ if (it != effectNames.end()) {
1886 ✗ effectNames.erase(it);
1887 }
1888 ✗ it = std::find(wledAPINames.begin(), wledAPINames.end(), pe->name);
1889 ✗ if (it != wledAPINames.end()) {
1890 ✗ wledAPINames.erase(it);
1891 }
1892 ✗ ++generation;
1893 ✗ }
1894 40616 PixelOverlayEffect* get(const std::string& n) {
1895 // find(), not operator[]: GET /api/overlays/effects/<name> reaches
1896 // this from several HTTP threads at once, and inserting a null entry
1897 // for each unknown name raced the map (and grew it for good).
1898
1/2
✓ Branch 0 taken 40616 times.
✗ Branch 1 not taken.
40616 auto it = effects.find(n);
1899
2/2
✓ Branch 0 taken 88 times.
✓ Branch 1 taken 40528 times.
40616 return it == effects.end() ? nullptr : it->second;
1900 }
1901 29 const std::list<std::string>& names() const {
1902 29 return effectNames;
1903 }
1904 499766 const std::list<std::string>& apiNames() const {
1905 499766 return wledAPINames;
1906 }
1907 2103 uint64_t gen() const {
1908 2103 return generation;
1909 }
1910
1911 private:
1912 std::map<std::string, PixelOverlayEffect*> effects;
1913 std::list<std::string> effectNames;
1914 std::list<std::string> wledAPINames;
1915 std::atomic<uint64_t> generation{ 0 };
1916 };
1917
1918 static PixelOverlayEffectHolder EFFECTS;
1919
1920 40616 PixelOverlayEffect* PixelOverlayEffect::GetPixelOverlayEffect(const std::string& name) {
1921 40616 return EFFECTS.get(name);
1922 }
1923
1924 29 const std::list<std::string>& PixelOverlayEffect::GetPixelOverlayEffects() {
1925 29 return EFFECTS.names();
1926 }
1927
1928 499766 const std::list<std::string>& WLEDEffect::getWLEDAPIEffectNames() {
1929 499766 return EFFECTS.apiNames();
1930 }
1931
1932 2103 uint64_t PixelOverlayEffect::GetPixelOverlayEffectsGeneration() {
1933 2103 return EFFECTS.gen();
1934 }
1935
1936 ✗ void PixelOverlayEffect::AddPixelOverlayEffect(PixelOverlayEffect* effect) {
1937 ✗ EFFECTS.add(effect);
1938 ✗ }
1939 ✗ void PixelOverlayEffect::RemovePixelOverlayEffect(PixelOverlayEffect* effect) {
1940 ✗ EFFECTS.remove(effect);
1941 ✗ }
1942