|
| 1 | +/* |
| 2 | + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information |
| 3 | + * |
| 4 | + * This program is free software; you can redistribute it and/or modify it |
| 5 | + * under the terms of the GNU General Public License as published by the |
| 6 | + * Free Software Foundation; either version 2 of the License, or (at your |
| 7 | + * option) any later version. |
| 8 | + * |
| 9 | + * This program is distributed in the hope that it will be useful, but WITHOUT |
| 10 | + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 11 | + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
| 12 | + * more details. |
| 13 | + * |
| 14 | + * You should have received a copy of the GNU General Public License along |
| 15 | + * with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | + */ |
| 17 | + |
| 18 | +#include "zm_catch2.h" |
| 19 | + |
| 20 | +#include "zm_config.h" |
| 21 | +#include "zm_image.h" |
| 22 | +#include "zm_rgb.h" |
| 23 | + |
| 24 | +extern "C" { |
| 25 | +#include <libavutil/imgutils.h> |
| 26 | +} |
| 27 | + |
| 28 | +#include <cstring> |
| 29 | + |
| 30 | +namespace { |
| 31 | + |
| 32 | +// Image::Initialise() loads the timestamp font from the DB-backed config, |
| 33 | +// which the test binary doesn't have; point it at the test fixture font. |
| 34 | +void bootstrap_image_config() { |
| 35 | + config.font_file_location = "data/fonts/04_valid.zmfnt"; |
| 36 | +} |
| 37 | + |
| 38 | +struct Planes { |
| 39 | + uint8_t *data[4] = {nullptr, nullptr, nullptr, nullptr}; |
| 40 | + int stride[4] = {0, 0, 0, 0}; |
| 41 | +}; |
| 42 | + |
| 43 | +// View an Image's buffer with the same layout Image uses internally |
| 44 | +// (av_image_fill_arrays, align 32). |
| 45 | +Planes plane_view(Image &image, AVPixelFormat fmt, int w, int h) { |
| 46 | + Planes planes; |
| 47 | + REQUIRE(av_image_fill_arrays(planes.data, planes.stride, image.Buffer(), |
| 48 | + fmt, w, h, 32) > 0); |
| 49 | + return planes; |
| 50 | +} |
| 51 | + |
| 52 | +} // namespace |
| 53 | + |
| 54 | +// Regression: Image::Rotate/Flip computed chroma plane dimensions with |
| 55 | +// AV_CEIL_RSHIFT on unsigned operands. The macro's runtime form |
| 56 | +// -((-(a)) >> (b)) is only valid for signed types; with unsigned width it |
| 57 | +// yields 2^31 + width/2, sending the chroma loops billions of samples out |
| 58 | +// of bounds (segfault on every rotated planar image - decoder thread crash |
| 59 | +// on any monitor with a rotated orientation). |
| 60 | +TEST_CASE("Image::Rotate YUV420P", "[image]") { |
| 61 | + bootstrap_image_config(); |
| 62 | + const int w = 1280, h = 720; |
| 63 | + Image image(w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P); |
| 64 | + REQUIRE(image.Buffer() != nullptr); |
| 65 | + |
| 66 | + // Distinct fill per plane + a marker pixel in each |
| 67 | + Planes src = plane_view(image, AV_PIX_FMT_YUV420P, w, h); |
| 68 | + memset(src.data[0], 0x10, static_cast<size_t>(src.stride[0]) * h); |
| 69 | + memset(src.data[1], 0x20, static_cast<size_t>(src.stride[1]) * (h / 2)); |
| 70 | + memset(src.data[2], 0x30, static_cast<size_t>(src.stride[2]) * (h / 2)); |
| 71 | + src.data[0][20 * src.stride[0] + 10] = 200; // luma (10, 20) |
| 72 | + src.data[1][30 * src.stride[1] + 40] = 210; // U (40, 30) |
| 73 | + src.data[2][50 * src.stride[2] + 60] = 220; // V (60, 50) |
| 74 | + |
| 75 | + SECTION("rotate 90") { |
| 76 | + image.Rotate(90); |
| 77 | + REQUIRE(image.Width() == static_cast<unsigned int>(h)); |
| 78 | + REQUIRE(image.Height() == static_cast<unsigned int>(w)); |
| 79 | + |
| 80 | + Planes dst = plane_view(image, AV_PIX_FMT_YUV420P, h, w); |
| 81 | + // 90deg: (sx, sy) -> (dx, dy) = (src_h-1-sy, sx) |
| 82 | + REQUIRE(dst.data[0][10 * dst.stride[0] + (h - 1 - 20)] == 200); |
| 83 | + // chroma plane is (w/2 x h/2): (40, 30) -> (h/2-1-30, 40) |
| 84 | + REQUIRE(dst.data[1][40 * dst.stride[1] + (h / 2 - 1 - 30)] == 210); |
| 85 | + REQUIRE(dst.data[2][60 * dst.stride[2] + (h / 2 - 1 - 50)] == 220); |
| 86 | + } |
| 87 | + |
| 88 | + SECTION("rotate 180") { |
| 89 | + image.Rotate(180); |
| 90 | + REQUIRE(image.Width() == static_cast<unsigned int>(w)); |
| 91 | + REQUIRE(image.Height() == static_cast<unsigned int>(h)); |
| 92 | + |
| 93 | + Planes dst = plane_view(image, AV_PIX_FMT_YUV420P, w, h); |
| 94 | + // 180deg: (sx, sy) -> (src_w-1-sx, src_h-1-sy) |
| 95 | + REQUIRE(dst.data[0][(h - 1 - 20) * dst.stride[0] + (w - 1 - 10)] == 200); |
| 96 | + REQUIRE(dst.data[1][(h / 2 - 1 - 30) * dst.stride[1] + (w / 2 - 1 - 40)] == 210); |
| 97 | + REQUIRE(dst.data[2][(h / 2 - 1 - 50) * dst.stride[2] + (w / 2 - 1 - 60)] == 220); |
| 98 | + } |
| 99 | + |
| 100 | + SECTION("rotate 270") { |
| 101 | + image.Rotate(270); |
| 102 | + REQUIRE(image.Width() == static_cast<unsigned int>(h)); |
| 103 | + REQUIRE(image.Height() == static_cast<unsigned int>(w)); |
| 104 | + |
| 105 | + Planes dst = plane_view(image, AV_PIX_FMT_YUV420P, h, w); |
| 106 | + // 270deg: (sx, sy) -> (dx, dy) = (sy, src_w-1-sx) |
| 107 | + REQUIRE(dst.data[0][(w - 1 - 10) * dst.stride[0] + 20] == 200); |
| 108 | + REQUIRE(dst.data[1][(w / 2 - 1 - 40) * dst.stride[1] + 30] == 210); |
| 109 | + REQUIRE(dst.data[2][(w / 2 - 1 - 60) * dst.stride[2] + 50] == 220); |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +TEST_CASE("Image::Rotate YUV420P odd dimensions", "[image]") { |
| 114 | + bootstrap_image_config(); |
| 115 | + // Odd luma dims exercise the ceiling in the chroma plane size: 101x57 |
| 116 | + // luma -> 51x29 chroma. |
| 117 | + const int w = 101, h = 57; |
| 118 | + Image image(w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P); |
| 119 | + |
| 120 | + Planes src = plane_view(image, AV_PIX_FMT_YUV420P, w, h); |
| 121 | + const int cw = (w + 1) / 2, ch = (h + 1) / 2; |
| 122 | + memset(src.data[0], 0x10, static_cast<size_t>(src.stride[0]) * h); |
| 123 | + memset(src.data[1], 0x20, static_cast<size_t>(src.stride[1]) * ch); |
| 124 | + memset(src.data[2], 0x30, static_cast<size_t>(src.stride[2]) * ch); |
| 125 | + // Marker in the LAST chroma column/row - the part flooring would drop |
| 126 | + src.data[1][(ch - 1) * src.stride[1] + (cw - 1)] = 211; |
| 127 | + |
| 128 | + image.Rotate(90); |
| 129 | + REQUIRE(image.Width() == static_cast<unsigned int>(h)); |
| 130 | + REQUIRE(image.Height() == static_cast<unsigned int>(w)); |
| 131 | + |
| 132 | + Planes dst = plane_view(image, AV_PIX_FMT_YUV420P, h, w); |
| 133 | + // (cw-1, ch-1) -> (ch-1-(ch-1), cw-1) = (0, cw-1) |
| 134 | + REQUIRE(dst.data[1][(cw - 1) * dst.stride[1] + 0] == 211); |
| 135 | +} |
| 136 | + |
| 137 | +TEST_CASE("Image::Flip YUV420P", "[image]") { |
| 138 | + bootstrap_image_config(); |
| 139 | + const int w = 640, h = 480; |
| 140 | + Image image(w, h, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P); |
| 141 | + |
| 142 | + Planes src = plane_view(image, AV_PIX_FMT_YUV420P, w, h); |
| 143 | + memset(src.data[0], 0x10, static_cast<size_t>(src.stride[0]) * h); |
| 144 | + memset(src.data[1], 0x20, static_cast<size_t>(src.stride[1]) * (h / 2)); |
| 145 | + memset(src.data[2], 0x30, static_cast<size_t>(src.stride[2]) * (h / 2)); |
| 146 | + src.data[0][20 * src.stride[0] + 10] = 200; |
| 147 | + src.data[1][30 * src.stride[1] + 40] = 210; |
| 148 | + |
| 149 | + SECTION("horizontal") { |
| 150 | + image.Flip(true); |
| 151 | + Planes dst = plane_view(image, AV_PIX_FMT_YUV420P, w, h); |
| 152 | + REQUIRE(dst.data[0][20 * dst.stride[0] + (w - 1 - 10)] == 200); |
| 153 | + REQUIRE(dst.data[1][30 * dst.stride[1] + (w / 2 - 1 - 40)] == 210); |
| 154 | + } |
| 155 | + |
| 156 | + SECTION("vertical") { |
| 157 | + image.Flip(false); |
| 158 | + Planes dst = plane_view(image, AV_PIX_FMT_YUV420P, w, h); |
| 159 | + REQUIRE(dst.data[0][(h - 1 - 20) * dst.stride[0] + 10] == 200); |
| 160 | + REQUIRE(dst.data[1][(h / 2 - 1 - 30) * dst.stride[1] + 40] == 210); |
| 161 | + } |
| 162 | +} |
0 commit comments