Files
zoneminder/tests/zm_image_linesize.cpp
Isaac Connor 3b633a281b fix: Rotate/Flip honor borrowed source linesize; size Flip dest for 32-aligned planes
get_y_image() wraps the decoder Y plane zero-copy but recorded
FFALIGN(width,32) as the Image stride instead of the frame's real
linesize[0]. For widths that are not a multiple of 32 the decoder packs
the plane tighter than FFALIGN, so:

- Image::Rotate/Flip re-derived the source stride via
  av_image_fill_arrays(...,32) and read past the end of the borrowed
  plane (and skewed Y-channel motion analysis, which reads the same
  buffer).
- Image::Flip sized its destination from this->size, which is tight for a
  borrowed plane and smaller than the 32-aligned layout the planes are
  written at, overrunning the destination.

Record the frame's real linesize in get_y_image(); use the Image's own
linesize for the source plane-0 stride in Rotate/Flip; size Flip's
destination from av_image_get_buffer_size(...,32). All are no-ops for
self-consistent ZM-allocated (32-aligned) images.

tests/zm_image_linesize.cpp: Rotate 90/180/270 and Flip H/V over a tight,
non-32-aligned GRAY8 source verifying correct output.

refs #4788

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 09:35:40 -04:00

146 lines
5.3 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_config.h"
#include "zm_image.h"
#include "zm_rgb.h"
#include <cstdint>
#include <vector>
// get_y_image() wraps a decoder Y plane whose real stride (linesize[0]) can be
// smaller than FFALIGN(width,32). Image::Rotate/Flip must use the source
// Image's own linesize, not a re-derived 32-aligned stride, or they read past
// the end of the borrowed plane (crash) and skew the output. Width 15 is
// deliberately not a multiple of 32 (FFALIGN(15,32)=32) and the source is
// packed tight (linesize == width), exactly like the decoder's Y plane.
// width*height=150 keeps every pixel value distinct in a byte so any stride
// drift is detectable.
namespace {
constexpr unsigned int kW = 15;
constexpr unsigned int kH = 10;
// Image::Initialise() dereferences config.font_file_location, which is null in
// the unit-test harness (no zm.conf loaded). Give it a non-null value so the
// first Image construction doesn't throw before exercising the rotate/flip code.
void EnsureImageInit() {
if (!config.font_file_location) config.font_file_location = "";
}
// Backing allocation larger than the tight 15x10 plane so that a regression
// (an over-read using stride FFALIGN(15,32)=32) stays inside our allocation and
// corrupts the result rather than segfaulting the test runner.
std::vector<uint8_t> MakeTightGray8Plane() {
std::vector<uint8_t> backing(32 * kH + 64, 0);
for (unsigned int y = 0; y < kH; y++)
for (unsigned int x = 0; x < kW; x++)
backing[y * kW + x] = static_cast<uint8_t>(y * kW + x);
return backing;
}
} // namespace
TEST_CASE("Image::Rotate 180 honors a non-32-aligned source linesize", "[Image]") {
EnsureImageInit();
std::vector<uint8_t> backing = MakeTightGray8Plane();
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
img.Rotate(180);
REQUIRE(img.Width() == kW);
REQUIRE(img.Height() == kH);
for (unsigned int y = 0; y < kH; y++) {
for (unsigned int x = 0; x < kW; x++) {
const uint8_t expected = static_cast<uint8_t>((kH - 1 - y) * kW + (kW - 1 - x));
REQUIRE(*img.Buffer(x, y) == expected);
}
}
}
TEST_CASE("Image::Flip horizontal honors a non-32-aligned source linesize", "[Image]") {
EnsureImageInit();
std::vector<uint8_t> backing = MakeTightGray8Plane();
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
img.Flip(true);
REQUIRE(img.Width() == kW);
REQUIRE(img.Height() == kH);
for (unsigned int y = 0; y < kH; y++) {
for (unsigned int x = 0; x < kW; x++) {
const uint8_t expected = static_cast<uint8_t>(y * kW + (kW - 1 - x));
REQUIRE(*img.Buffer(x, y) == expected);
}
}
}
TEST_CASE("Image::Flip vertical honors a non-32-aligned source linesize", "[Image]") {
EnsureImageInit();
std::vector<uint8_t> backing = MakeTightGray8Plane();
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
img.Flip(false);
REQUIRE(img.Width() == kW);
REQUIRE(img.Height() == kH);
for (unsigned int y = 0; y < kH; y++) {
for (unsigned int x = 0; x < kW; x++) {
const uint8_t expected = static_cast<uint8_t>((kH - 1 - y) * kW + x);
REQUIRE(*img.Buffer(x, y) == expected);
}
}
}
// 90/270 also swap dimensions (dst is kH wide x kW tall), exercising both the
// source-stride fix and the destination dimension/stride handling.
TEST_CASE("Image::Rotate 90 honors a non-32-aligned source linesize", "[Image]") {
EnsureImageInit();
std::vector<uint8_t> backing = MakeTightGray8Plane();
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
img.Rotate(90);
REQUIRE(img.Width() == kH);
REQUIRE(img.Height() == kW);
// 90deg: dst(X,Y) == src(x=Y, y=kH-1-X)
for (unsigned int Y = 0; Y < kW; Y++) {
for (unsigned int X = 0; X < kH; X++) {
const uint8_t expected = static_cast<uint8_t>((kH - 1 - X) * kW + Y);
REQUIRE(*img.Buffer(X, Y) == expected);
}
}
}
TEST_CASE("Image::Rotate 270 honors a non-32-aligned source linesize", "[Image]") {
EnsureImageInit();
std::vector<uint8_t> backing = MakeTightGray8Plane();
Image img(kW, /*linesize*/ kW, kH, /*colours*/ 1, ZM_SUBPIX_ORDER_NONE, backing.data(), /*padding*/ 0);
img.Rotate(270);
REQUIRE(img.Width() == kH);
REQUIRE(img.Height() == kW);
// 270deg: dst(X,Y) == src(x=kW-1-Y, y=X)
for (unsigned int Y = 0; Y < kW; Y++) {
for (unsigned int X = 0; X < kH; X++) {
const uint8_t expected = static_cast<uint8_t>(X * kW + (kW - 1 - Y));
REQUIRE(*img.Buffer(X, Y) == expected);
}
}
}