diff --git a/src/zm_zone.cpp b/src/zm_zone.cpp index e263de31c..837b8a3fb 100644 --- a/src/zm_zone.cpp +++ b/src/zm_zone.cpp @@ -994,8 +994,44 @@ std::vector Zone::Load(const std::shared_ptr &monitor) { continue; } } else { - // Percentage-based coordinates (default): convert to pixels using monitor dimensions - if (!ParsePercentagePolygon(Coords, monitor->Width(), monitor->Height(), polygon)) { + // Percentage-based coordinates (default): convert to pixels using monitor dimensions. + // However, if any coordinate value exceeds 100, these are actually pixel values + // stored with incorrect Units — fall back to pixel parsing with a warning. + bool has_pixel_values = false; + { + const char *s = Coords; + while (*s != '\0') { + double val = strtod(s, nullptr); + if (val > 100.0) { + has_pixel_values = true; + break; + } + // Skip to next number: find comma then space (x,y pairs separated by spaces) + const char *comma = strchr(s, ','); + if (!comma) break; + val = strtod(comma + 1, nullptr); + if (val > 100.0) { + has_pixel_values = true; + break; + } + const char *space = strchr(comma + 1, ' '); + if (space) { + s = space + 1; + } else { + break; + } + } + } + + if (has_pixel_values) { + Warning("Zone %d/%s has Units=Percent but Coords contain pixel values (>100), " + "parsing as pixels instead", Id, Name); + if (!ParsePolygonString(Coords, polygon)) { + Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring", + Coords, Id, Name, monitor->Name()); + continue; + } + } else if (!ParsePercentagePolygon(Coords, monitor->Width(), monitor->Height(), polygon)) { Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring", Coords, Id, Name, monitor->Name()); continue; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 72d599d76..508a6ac38 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,7 +19,8 @@ set(TEST_SOURCES zm_onvif_renewal.cpp zm_poly.cpp zm_utils.cpp - zm_vector2.cpp) + zm_vector2.cpp + zm_zone.cpp) add_executable(tests main.cpp ${TEST_SOURCES}) diff --git a/tests/zm_zone.cpp b/tests/zm_zone.cpp new file mode 100644 index 000000000..86fb2cfb7 --- /dev/null +++ b/tests/zm_zone.cpp @@ -0,0 +1,122 @@ +/* + * 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 . + */ + +#include "zm_catch2.h" + +#include "zm_zone.h" + +TEST_CASE("Zone::ParsePolygonString: basic pixel parsing", "[Zone]") { + Polygon polygon; + + SECTION("parses a simple rectangle") { + bool ok = Zone::ParsePolygonString("0,0 639,0 639,479 0,479", polygon); + REQUIRE(ok); + + auto const &verts = polygon.GetVertices(); + REQUIRE(verts.size() == 4); + REQUIRE(verts[0] == Vector2(0, 0)); + REQUIRE(verts[1] == Vector2(639, 0)); + REQUIRE(verts[2] == Vector2(639, 479)); + REQUIRE(verts[3] == Vector2(0, 479)); + } + + SECTION("parses a triangle") { + bool ok = Zone::ParsePolygonString("100,100 200,100 150,200", polygon); + REQUIRE(ok); + + auto const &verts = polygon.GetVertices(); + REQUIRE(verts.size() == 3); + REQUIRE(verts[0] == Vector2(100, 100)); + REQUIRE(verts[1] == Vector2(200, 100)); + REQUIRE(verts[2] == Vector2(150, 200)); + } + + SECTION("rejects string with fewer than 3 vertices") { + // Two vertices — not enough for a polygon, but ParsePolygonString + // returns true if any vertices were parsed (vertices.empty() check) + bool ok = Zone::ParsePolygonString("10,20 30,40", polygon); + REQUIRE(ok); + // The polygon won't be properly formed but parsing itself doesn't fail + } +} + +TEST_CASE("Zone::ParsePercentagePolygon: percentage to pixel conversion", "[Zone]") { + Polygon polygon; + unsigned int width = 1920; + unsigned int height = 1080; + + SECTION("full-frame 0-100% rectangle") { + bool ok = Zone::ParsePercentagePolygon("0,0 100,0 100,100 0,100", width, height, polygon); + REQUIRE(ok); + + auto const &verts = polygon.GetVertices(); + REQUIRE(verts.size() == 4); + REQUIRE(verts[0] == Vector2(0, 0)); + REQUIRE(verts[1] == Vector2(1920, 0)); + REQUIRE(verts[2] == Vector2(1920, 1080)); + REQUIRE(verts[3] == Vector2(0, 1080)); + } + + SECTION("50% rectangle converts to half-resolution pixels") { + bool ok = Zone::ParsePercentagePolygon("0,0 50,0 50,50 0,50", width, height, polygon); + REQUIRE(ok); + + auto const &verts = polygon.GetVertices(); + REQUIRE(verts.size() == 4); + REQUIRE(verts[0] == Vector2(0, 0)); + REQUIRE(verts[1] == Vector2(960, 0)); + REQUIRE(verts[2] == Vector2(960, 540)); + REQUIRE(verts[3] == Vector2(0, 540)); + } + + SECTION("values are clamped to monitor bounds") { + // 100% should clamp to exact monitor dimensions + bool ok = Zone::ParsePercentagePolygon("0,0 100,0 100,100 0,100", width, height, polygon); + REQUIRE(ok); + + auto const &verts = polygon.GetVertices(); + for (auto const &v : verts) { + REQUIRE(v.x_ >= 0); + REQUIRE(v.x_ <= static_cast(width)); + REQUIRE(v.y_ >= 0); + REQUIRE(v.y_ <= static_cast(height)); + } + } +} + +TEST_CASE("Zone: pixel values through ParsePercentagePolygon produce wrong results", "[Zone]") { + // This test demonstrates the bug: when pixel coordinates (>100) are fed + // through ParsePercentagePolygon, they get treated as percentages and + // scaled wildly, then clamped to monitor bounds. + Polygon polygon; + unsigned int width = 1920; + unsigned int height = 1080; + + // These are pixel coordinates for a 640x480 region + bool ok = Zone::ParsePercentagePolygon("0,0 639,0 639,479 0,479", width, height, polygon); + REQUIRE(ok); + + auto const &verts = polygon.GetVertices(); + REQUIRE(verts.size() == 4); + + // 639% of 1920 = 12268.8 -> clamped to 1920 + // 479% of 1080 = 5173.2 -> clamped to 1080 + // All non-zero coords get clamped to monitor bounds — the zone is + // degenerate (covers the full monitor instead of a sub-region) + REQUIRE(verts[1] == Vector2(static_cast(width), 0)); + REQUIRE(verts[2] == Vector2(static_cast(width), static_cast(height))); +}