Files
zoneminder/tests/zm_pixformat.cpp
Isaac Connor 11544d86e3 fix: PR #4742 round 2 review feedback
Eight Copilot comments from the second review pass:

1. monitor.php (#3): "Deprecated - will be auto-detected..." note next
   to TargetColorspace bypassed translate(). Added DeprecatedColoursSetting
   key to en_gb and routed the deprecation note through translate() so it
   localises with the rest of the form.

2. tests/zm_pixformat.cpp (#4): zm_colours_from_pixformat / round-trip
   tests didn't cover the new YUV422P/YUVJ422P entries (added by
   02e6be6b4). Added explicit assertions in both test cases — bumps
   pixformat coverage from 105 to 115 assertions.

3. zm_image.cpp WriteBuffer (#5): linesize and size were derived from
   p_width * p_colours, which undercounts planar YUV* (where p_colours=1
   via the GRAY8 alias collision but actual buffer needs ~1.5x/2x for
   chroma). Use av_image_get_buffer_size and av_image_get_linesize for
   the AVPixelFormat instead, with bail-out on either failing.

4. zm_image.cpp AssignDirect (#6, #7): av_image_get_buffer_size returns
   int and can be negative; assigning that into unsigned size/allocation
   wrapped to a huge value. Check the return first, treat negative as the
   same "unsupported format" failure as zm_colours_from_pixformat
   returning false, and reset size/allocation/linesize/pixels to 0
   (alongside imagePixFormat=NONE/colours=0/subpixelorder=0) so the
   Image is left in a single coherent invalid state instead of partially
   stale.

5. zm_image.cpp Assign (#8): av_get_pix_fmt_name(format) can return
   nullptr (e.g. AV_PIX_FMT_NONE / unknown); passing that into
   Debug(..., "%s", ...) would segfault. Capture once with a fallback
   string before logging.

6. zm_monitor.cpp Capture path (#9): same nullptr issue with two Debug
   calls — capture native_fmt_name once with fallback.

7. zm_monitor.cpp can_passthrough comment (#10): comment claimed
   YUVJ422P would be converted to YUV420P because Image drops chroma,
   but can_passthrough now allows YUV422P/YUVJ422P passthrough since
   02e6be6b4 added 4:2:2 support. Updated the comment to describe the
   current behavior (full 4:2:0 + 4:2:2 planar passthrough plus GRAY8
   and RGB24/32) so the code and the rationale agree.

Tests: 76 cases, 788 assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:59:51 -04:00

202 lines
8.2 KiB
C++

/*
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "zm_catch2.h"
#include "zm_pixformat.h"
TEST_CASE("zm_pixformat_from_colours: GRAY8 family", "[pixformat]") {
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE) == AV_PIX_FMT_GRAY8);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P) == AV_PIX_FMT_YUV420P);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUVJ420P) == AV_PIX_FMT_YUVJ420P);
}
TEST_CASE("zm_pixformat_from_colours: RGB24 family", "[pixformat]") {
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB) == AV_PIX_FMT_RGB24);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_BGR) == AV_PIX_FMT_BGR24);
}
TEST_CASE("zm_pixformat_from_colours: RGB32 family", "[pixformat]") {
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_RGBA) == AV_PIX_FMT_RGBA);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_BGRA) == AV_PIX_FMT_BGRA);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_ARGB) == AV_PIX_FMT_ARGB);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_RGB32, ZM_SUBPIX_ORDER_ABGR) == AV_PIX_FMT_ABGR);
}
TEST_CASE("zm_pixformat_from_colours: unknown returns NONE", "[pixformat]") {
REQUIRE(zm_pixformat_from_colours(0, 0) == AV_PIX_FMT_NONE);
REQUIRE(zm_pixformat_from_colours(2, 0) == AV_PIX_FMT_NONE);
REQUIRE(zm_pixformat_from_colours(5, 0) == AV_PIX_FMT_NONE);
}
TEST_CASE("zm_colours_from_pixformat: maps each supported format", "[pixformat]") {
unsigned int c = 0, s = 0;
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_GRAY8, c, s));
REQUIRE(c == ZM_COLOUR_GRAY8);
REQUIRE(s == ZM_SUBPIX_ORDER_NONE);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUV420P, c, s));
REQUIRE(c == ZM_COLOUR_GRAY8); // collision: same value as ZM_COLOUR_YUV420P
REQUIRE(s == ZM_SUBPIX_ORDER_YUV420P);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUVJ420P, c, s));
REQUIRE(c == ZM_COLOUR_GRAY8); // collision
REQUIRE(s == ZM_SUBPIX_ORDER_YUVJ420P);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUV422P, c, s));
REQUIRE(c == ZM_COLOUR_GRAY8); // collision: 4:2:2 also aliases to GRAY8
REQUIRE(s == ZM_SUBPIX_ORDER_YUV422P);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_YUVJ422P, c, s));
REQUIRE(c == ZM_COLOUR_GRAY8); // collision
REQUIRE(s == ZM_SUBPIX_ORDER_YUVJ422P);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_RGB24, c, s));
REQUIRE(c == ZM_COLOUR_RGB24);
REQUIRE(s == ZM_SUBPIX_ORDER_RGB);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_BGR24, c, s));
REQUIRE(c == ZM_COLOUR_RGB24);
REQUIRE(s == ZM_SUBPIX_ORDER_BGR);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_RGBA, c, s));
REQUIRE(c == ZM_COLOUR_RGB32);
REQUIRE(s == ZM_SUBPIX_ORDER_RGBA);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_BGRA, c, s));
REQUIRE(c == ZM_COLOUR_RGB32);
REQUIRE(s == ZM_SUBPIX_ORDER_BGRA);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_ARGB, c, s));
REQUIRE(c == ZM_COLOUR_RGB32);
REQUIRE(s == ZM_SUBPIX_ORDER_ARGB);
REQUIRE(zm_colours_from_pixformat(AV_PIX_FMT_ABGR, c, s));
REQUIRE(c == ZM_COLOUR_RGB32);
REQUIRE(s == ZM_SUBPIX_ORDER_ABGR);
}
TEST_CASE("zm_colours_from_pixformat: unknown returns false and leaves out params untouched", "[pixformat]") {
unsigned int c = 42, s = 99;
REQUIRE_FALSE(zm_colours_from_pixformat(AV_PIX_FMT_YUV444P, c, s));
REQUIRE(c == 42);
REQUIRE(s == 99);
}
TEST_CASE("round-trip: from_colours(colours_from_pixformat(fmt)) == fmt", "[pixformat]") {
const AVPixelFormat formats[] = {
AV_PIX_FMT_GRAY8,
AV_PIX_FMT_YUV420P,
AV_PIX_FMT_YUVJ420P,
AV_PIX_FMT_YUV422P,
AV_PIX_FMT_YUVJ422P,
AV_PIX_FMT_RGB24,
AV_PIX_FMT_BGR24,
AV_PIX_FMT_RGBA,
AV_PIX_FMT_BGRA,
AV_PIX_FMT_ARGB,
AV_PIX_FMT_ABGR,
};
for (AVPixelFormat fmt : formats) {
unsigned int c = 0, s = 0;
REQUIRE(zm_colours_from_pixformat(fmt, c, s));
REQUIRE(zm_pixformat_from_colours(c, s) == fmt);
}
}
TEST_CASE("zm_bytes_per_pixel: primary-buffer stride", "[pixformat]") {
// GRAY8 and YUV planar Y-plane all have 1-byte stride in the primary buffer
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_GRAY8) == 1);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_YUV420P) == 1);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_YUVJ420P) == 1);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_RGB24) == 3);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_BGR24) == 3);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_RGBA) == 4);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_BGRA) == 4);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_ARGB) == 4);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_ABGR) == 4);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_NONE) == 0);
REQUIRE(zm_bytes_per_pixel(AV_PIX_FMT_YUV444P) == 0);
}
TEST_CASE("zm_db_colours_to_pixformat: maps DB values 1/3/4", "[pixformat]") {
REQUIRE(zm_db_colours_to_pixformat(ZM_COLOUR_GRAY8) == AV_PIX_FMT_GRAY8);
REQUIRE(zm_db_colours_to_pixformat(ZM_COLOUR_RGB24) == AV_PIX_FMT_RGB24);
REQUIRE(zm_db_colours_to_pixformat(ZM_COLOUR_RGB32) == AV_PIX_FMT_RGBA);
REQUIRE(zm_db_colours_to_pixformat(0) == AV_PIX_FMT_NONE);
REQUIRE(zm_db_colours_to_pixformat(2) == AV_PIX_FMT_NONE);
}
TEST_CASE("zm_is_rgb32: matches 4-byte packed RGB formats only", "[pixformat]") {
REQUIRE(zm_is_rgb32(AV_PIX_FMT_RGBA));
REQUIRE(zm_is_rgb32(AV_PIX_FMT_BGRA));
REQUIRE(zm_is_rgb32(AV_PIX_FMT_ARGB));
REQUIRE(zm_is_rgb32(AV_PIX_FMT_ABGR));
REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_RGB24));
REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_BGR24));
REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_GRAY8));
REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_YUV420P));
REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_YUVJ420P));
REQUIRE_FALSE(zm_is_rgb32(AV_PIX_FMT_NONE));
}
TEST_CASE("zm_is_rgb24: matches 3-byte packed RGB formats only", "[pixformat]") {
REQUIRE(zm_is_rgb24(AV_PIX_FMT_RGB24));
REQUIRE(zm_is_rgb24(AV_PIX_FMT_BGR24));
REQUIRE_FALSE(zm_is_rgb24(AV_PIX_FMT_RGBA));
REQUIRE_FALSE(zm_is_rgb24(AV_PIX_FMT_GRAY8));
REQUIRE_FALSE(zm_is_rgb24(AV_PIX_FMT_YUV420P));
}
TEST_CASE("zm_is_yuv420: matches planar YUV 4:2:0 formats only", "[pixformat]") {
REQUIRE(zm_is_yuv420(AV_PIX_FMT_YUV420P));
REQUIRE(zm_is_yuv420(AV_PIX_FMT_YUVJ420P));
REQUIRE_FALSE(zm_is_yuv420(AV_PIX_FMT_GRAY8));
REQUIRE_FALSE(zm_is_yuv420(AV_PIX_FMT_YUV444P));
REQUIRE_FALSE(zm_is_yuv420(AV_PIX_FMT_RGBA));
}
// Regression test for the ZM_COLOUR_GRAY8 == ZM_COLOUR_YUV420P collision.
// The collision is real in zm_rgb.h (both defined to 1) but the AVPixelFormat
// path must disambiguate correctly via subpixelorder, and user-selectable DB
// colours must not be mistaken for the internal YUV420P format.
TEST_CASE("regression: GRAY8/YUV420P collision does not confuse the pipeline", "[pixformat]") {
// The collision itself
REQUIRE(ZM_COLOUR_GRAY8 == ZM_COLOUR_YUV420P);
REQUIRE(ZM_COLOUR_GRAY8 == ZM_COLOUR_YUVJ420P);
// But the format helpers disambiguate by subpixelorder
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE) == AV_PIX_FMT_GRAY8);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUV420P) == AV_PIX_FMT_YUV420P);
REQUIRE(zm_pixformat_from_colours(ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_YUVJ420P) == AV_PIX_FMT_YUVJ420P);
// DB value 1 (user-selected "8BitGrey") unambiguously maps to GRAY8, not YUV420P
REQUIRE(zm_db_colours_to_pixformat(1) == AV_PIX_FMT_GRAY8);
REQUIRE(zm_db_colours_to_pixformat(1) != AV_PIX_FMT_YUV420P);
// DB value 4 (user-selected "32BitColour") maps to RGBA, not GRAY8
REQUIRE(zm_db_colours_to_pixformat(4) == AV_PIX_FMT_RGBA);
REQUIRE(zm_db_colours_to_pixformat(4) != AV_PIX_FMT_GRAY8);
}