| 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 |