Commit Graph
300 Commits
Author SHA1 Message Date
Isaac ConnorandClaude Opus 4.7 2b55ed45d1 fix: nullptr-safe pix fmt name in Colourise/Deinterlace_4Field error paths (PR #4788)
- Image::Colourise sizing-failure Error: av_image_get_buffer_size /
  av_image_get_linesize return negative for AV_PIX_FMT_NONE and
  unrecognised formats, so this branch fires exactly when
  av_get_pix_fmt_name(p_req_pixfmt) can return nullptr. Switched to
  zm_get_pix_fmt_name.
- Image::Deinterlace_4Field fallback Panic: same issue — fires when
  imagePixFormat is outside our dispatch set, where av_get_pix_fmt_name
  may return nullptr. Switched to zm_get_pix_fmt_name.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 10:01:13 -04:00
Isaac ConnorandClaude Opus 4.7 965936eaf8 fix: pass delta8_* fn ptrs directly; zm_get_pix_fmt_name in Delta panic (PR #4788)
- Image::Delta: pass delta8_bgr/rgb/argb/abgr/bgra/rgba/gray8 directly to
  delta_row() instead of dereferencing with *. They're already function
  pointers; the deref is redundant and would be undefined behaviour if
  any pointer were null (Initialise() sets them, but the explicit *
  obscures the intent and adds nothing).
- Switched the Delta fallback Panic from av_get_pix_fmt_name() to the
  nullptr-safe zm_get_pix_fmt_name() wrapper. The Panic fires exactly
  when imagePixFormat is unrecognised, where the raw function would
  return nullptr.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 21:26:55 -04:00
Isaac ConnorandClaude Opus 4.7 12d864e772 fix: Deinterlace_Discard OOB write on odd heights (PR #4788)
The y += 2 loop ran while `y < height`, but copied even row y into odd
row y+1. For odd image heights, the final iteration had y == height-1
and wrote to row height — past the end of the image buffer in all three
colour branches (GRAY8/YUV-Y plane, RGB24, RGB32).

Stop one short of the last row (y < height - 1) so y+1 stays in
bounds; the orphan last row in odd-height images is left untouched
(consistent with the (even, odd) pairing this discard pass is doing).
Also added a guard for height < 2 to keep the unsigned `height - 1`
calculation from wrapping.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 20:10:39 -04:00
Isaac ConnorandClaude Opus 4.7 6ecf51061a fix: more actionable error messages in GetImage/getSnapshot/WriteBuffer (PR #4788)
- Monitor::GetImage and Monitor::getSnapshot bounds-check branches were
  logging "Image Buffer has not been allocated", but by the time control
  reaches them the empty-vector check has already passed — the buffer IS
  allocated, the index is just out of range. Replaced with explicit
  "index N out of range (image_buffer.size() = M)" so debugging starts
  from the right place.
- Image::WriteBuffer unsupported-format error only printed `colours`,
  but the mapping in zm_pixformat_from_colours depends on both `colours`
  and `subpixelorder`, and planar formats legitimately have colours=1.
  Print both fields so a bad (colours, subpixelorder) pair is
  immediately identifiable.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 22:12:26 -04:00
Isaac ConnorandClaude Opus 4.7 9e19c6689c fix: Image(AVFrame*) ctor delegates to default ctor before AssignDirect (PR #4788)
The Image(const AVFrame*) constructor only initialised blend_buffer_
and blend_buffer_size_ before calling AssignDirect(frame). AssignDirect
now calls DumpImgBuffer() on both the failure (invalidate) and success
paths to release any previously-owned buffer — but in the
freshly-constructed Image, buffer/buffertype/allocation are
uninitialised, so DumpImgBuffer would read garbage and potentially
free an invalid pointer.

Use constructor delegation so Image() runs first (zero-initialises
every member, sets buffertype = DONTFREE), making the subsequent
DumpImgBuffer call a no-op on a known-empty Image.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 21:23:37 -04:00
Isaac ConnorandClaude Opus 4.7 de3638ec48 fix: Image::Assign(Image&) handle smaller-source linesize; explicit pixdesc.h (PR #4788)
- Image::Assign(const Image&): when the source linesize was *smaller*
  than the destination's (e.g. src came from a held-buffer ctor with a
  packed device stride while dst is FFALIGN'd to 32 from
  av_image_get_buffer_size), the function fell through to the flat
  memcpy of `size` bytes — over-reading the source by
  (linesize - image.linesize) * height bytes. Undefined behaviour;
  could crash or copy unrelated memory. Generalised the per-row branch
  to handle both directions: trigger whenever linesize != image.linesize
  and copy min(src, dst) bytes per row, so neither buffer is read or
  written past its capacity. Planar formats still refuse explicitly
  since per-row would lose chroma; the error message now reads "vs"
  instead of ">" to match the broader condition.
- Added an explicit `#include <libavutil/pixdesc.h>` in zm_image.cpp.
  The recent Rotate/Flip rewrites call av_pix_fmt_desc_get /
  av_pix_fmt_count_planes / use AVPixFmtDescriptor, which previously
  only compiled thanks to transitive includes from zm_ffmpeg.h →
  libavutil/imgutils.h. Make the dependency explicit.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 20:13:05 -04:00
Isaac ConnorandClaude Opus 4.7 bc88781a08 fix: Image::Assign(uint8_t*) handle packed source -> aligned dest (PR #4788)
The raw-buffer Assign overload previously validated buffer_size against
av_image_get_buffer_size(..., align=32) and then memcpy'd `size` bytes
flat. But callers feed it packed source buffers — Camera::ImageSize() is
now derived with align=1 to describe device buffers (V4L2 mmap, raw RTP,
etc.) — so for non-32-aligned widths:
  * the size check spuriously rejected valid packed source buffers, and
  * if relaxed, the flat memcpy would have read past the source.

Validate the source size against the packed (align=1) layout and the
destination size/allocation against the aligned (align=32) layout, then
copy plane-by-plane via av_image_copy. av_image_fill_arrays gives us
both layouts' per-plane pointers and strides, and av_image_copy walks
each plane with its own src/dst linesize so per-row padding never
over-reads the source.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 10:30:49 -04:00
Isaac ConnorandClaude Opus 4.7 bc8be20e68 fix: use canonical imagePixFormat in Image::Assign(AVFrame*) (PR #4788)
The destination format for the same-format fast path was derived via the
deprecated AVPixFormat() getter, which re-derives from the legacy
(colours, subpixelorder) pair. If those fields ever drift out of sync
with imagePixFormat — or hit the GRAY8/YUV420P alias collision — the
fast path would compare against the wrong target and either fall
through to sws_scale unnecessarily or, worse, av_image_copy the wrong
layout. Use imagePixFormat directly and drop the manual
av_get_pix_fmt_name nullptr dance in the Debug log in favour of the
zm_get_pix_fmt_name wrapper.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 22:16:52 -04:00
Isaac ConnorandClaude Opus 4.7 bfe9c4ad4a fix: free previously-owned buffer in AssignDirect(AVFrame); nullptr-safe logs (PR #4788)
- Image::AssignDirect(const AVFrame*): both the invalidate() failure
  path and the success path overwrote buffer/buffertype without freeing
  any previously-owned buffer. If the Image currently owned a
  ZM_BUFTYPE_ZM/MALLOC/NEW allocation, that memory was leaked on every
  AssignDirect(frame) call. Added DumpImgBuffer() at the top of
  invalidate() and immediately before the success-path buffer
  reassignment; DumpBuffer is a no-op for DONTFREE buffers, so this is
  safe whether the Image previously owned its memory or wrapped a
  caller's buffer.
- Switched five remaining av_get_pix_fmt_name() calls flagged by review
  to zm_get_pix_fmt_name():
  * Monitor::GetAlarmImage warnings (zm_monitor.cpp:1432, 1438-1439)
  * Monitor::ReadShmFrame warnings (zm_monitor.cpp:3042, 3049-3050)
  * Camera ctor fallback Error (zm_camera.cpp:90)
  All can be reached with AV_PIX_FMT_NONE or unrecognised formats where
  av_get_pix_fmt_name returns nullptr, which is undefined when fed to %s.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 21:39:16 -04:00
Isaac ConnorandClaude Opus 4.7 90c6bc0b42 fix: guard SHM pixformat publish on assign failure (PR #4788)
- Monitor::WriteShmFrame: image_pixelformats[index] was being recorded
  even when image_buffer[index]->Assign(*capture_image) silently left
  the slot untouched (Assign returns void; held-buffer undersize and
  unknown src format both early-return without mutating the dest).
  Readers would then adopt the newly-published format while the slot
  still held the previous frame's bytes, producing garble. Check the
  dest's post-Assign PixFormat against the source's and only publish on
  match; warn and keep the previously-published format otherwise so
  readers continue interpreting the slot bytes correctly.
- Monitor::WriteAlarmImage: same fix for alarm_image — verify Assign
  adopted src.PixFormat() before publishing *alarm_image_pixelformat.
- Image::AVPixFormat(AVPixelFormat) setter: switched the four
  format-name logs to the nullptr-safe zm_get_pix_fmt_name wrapper.
  The first Error branch triggers exactly when new_pixelformat is
  unrecognised, and av_get_pix_fmt_name returns nullptr for those.
- monitor.php: removed the manual translate fallback for
  DeprecatedColoursSetting. web/includes/lang.php always merges
  en_gb.php as a fallback when the active language differs, so
  translate() never returns the bare key once en_gb.php defines it.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:24:42 -04:00
Isaac ConnorandClaude Opus 4.7 f794e260cc fix: per-row Overlay; align image_pixelformats SHM pointer (PR #4788)
- Image::Overlay (no-offset variant): rewrite all eight format branches
  to walk row-by-row using each image's own linesize. The previous
  linear walk with `buffer + size` as the end pointer drifted across
  rows whenever this->linesize != image.linesize (which can happen for
  images constructed via held-buffer ctors at a different stride) and
  also walked into the destination's chroma planes for planar YUV
  destinations whose `size` covers chroma. Inner loops bound by `width`
  per row in every branch, so per-row padding is left untouched.
- Monitor::connect: align the image_pixelformats SHM base address up
  to alignof(AVPixelFormat) before casting. shared_images +
  2*image_buffer_count*image_size can be misaligned when image_size
  isn't a multiple of alignof(AVPixelFormat) (e.g. GRAY8 with odd
  width sourced from camera->ImageSize() before the SHM upper-bound
  applies). An unaligned AVPixelFormat* is undefined behaviour on
  strict-alignment ISAs and slow even on x86. The +64-byte padding
  reserved in mem_size already covers the small shift.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 13:54:31 -04:00
Isaac ConnorandClaude Opus 4.7 36effc8bb5 fix: stride-aware Delta and Deinterlace_4Field (PR #4788)
The delta8_* and *_deinterlace_4field_* SIMD helpers process pixel runs
or 2D blocks without any concept of per-row stride. With the
FFALIGN(linesize, 32) layout, an Image's buffer may have per-row padding
(linesize > width*bpp), and feeding the helpers the raw buffer pointer
made them treat padding bytes as image data — wrong motion deltas and
visibly broken deinterlacing on non-32-aligned widths.

- Image::Delta: drive each delta8_* helper one row at a time, passing
  `width` pixels per call with `buffer + y*src_linesize`,
  `image.buffer + y*img_linesize`, and `pdiff + y*dst_linesize`. No
  copies; just a stride-aware caller-side loop.
- Image::Deinterlace_4Field: the 4-field helpers internally use the
  passed `width` as their row stride, so a per-row loop wouldn't
  preserve their inter-row processing. Pack both inputs into
  tightly-laid-out (linesize == width*bpp) temp buffers, run the
  helper, then copy only the data bytes back into the padded source
  buffer (padding stays intact). Fast path skips the copy when linesize
  already equals width*bpp on both images (the 32-aligned-width case).

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:35:32 -04:00
Isaac ConnorandClaude Opus 4.7 2846970614 fix: PopulateFrame error handling + canonical pix fmt in Scale (PR #4788)
- Image::Assign(AVFrame*) fast path: PopulateFrame can fail
  (av_buffer_create / av_image_fill_arrays errors); calling av_image_copy
  on an unpopulated temp_frame is undefined. Check the return and bail
  out cleanly on failure.
- Image::Scale: use the canonical imagePixFormat directly instead of
  the deprecated AVPixFormat() getter (which re-derives via the legacy
  (colours, subpixelorder) pair and would hit the GRAY8/YUV420P alias
  collision if those drift).

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:38:18 -04:00
Isaac ConnorandClaude Opus 4.7 a5bf39d413 fix: keep DeColourise/AssignDirect(AVFrame) state self-consistent (PR #4788)
- Image::DeColourise: updating only imagePixFormat=GRAY8 left size still
  at the previous RGB sizing and linesize at the previous RGB stride.
  Downstream ops that allocate via size (Flip/Rotate) under-allocate,
  and row-stride loops that use linesize address the wrong rows. Now
  recomputes size and linesize from the new GRAY8 layout via
  av_image_get_buffer_size/av_image_get_linesize and calls
  update_function_pointers().
- Image::AssignDirect(AVFrame*): never called update_function_pointers()
  after the format change, so an Image that previously held a different
  format kept stale fptr_delta/fptr_blend/fptr_convert bindings and
  subsequent ops took the wrong optimized path. Added the call at the
  end of the success path.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:15:36 -04:00
Isaac ConnorandClaude Opus 4.7 070704d92c fix: ceiling chroma dims in Rotate/Flip; clearer Assign error (PR #4788)
- Image::Rotate / Image::Flip: chroma plane dimensions were computed as
  `width >> log2_chroma_w` / `height >> log2_chroma_h`, which floors.
  For odd luma widths/heights the last chroma column/row was skipped,
  leaving U/V samples unrotated/unflipped on odd-sized frames. Use
  AV_CEIL_RSHIFT to match FFmpeg's plane dimension convention.
- Image::Assign(const Image&): the AV_PIX_FMT_NONE guard's error
  message said "unexpected colours per pixel" and printed image.colours,
  which is misleading — planar formats legitimately have colours==1.
  Report the real cause (missing AVPixelFormat metadata) plus the
  legacy (colours, subpixelorder) for context.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:15:39 -04:00
Isaac ConnorandClaude Opus 4.7 3592fbb2b4 fix: dest linesize in HighlightEdges, ceiling chroma dims in MaskPrivacy (PR #4788)
- Image::HighlightEdges: the RGB24 and RGB32 output branches addressed
  the destination buffer as `high_buff + ((y*linesize + lo_x) * 3|4)`,
  where `linesize` is the *source* (GRAY8) stride. For non-32-aligned
  widths the source GRAY8 stride and destination RGB24/RGB32 stride
  differ in their padding, so multiplying the source stride by 3 or 4
  doesn't yield the destination's row offset and writes spill past
  high_buff on the last row. Read the destination's linesize from
  high_image and use that for phigh. The neighbour-pixel lookups also
  used `p ± width`; switched to `p ± src_linesize` so we follow the
  actual source row stride. All three branches (GRAY8/RGB24/RGB32) now
  pick neighbours via src_linesize.
- Image::MaskPrivacy: chroma plane dimensions were `width/2` and
  `height/2` (truncating). For odd width/height the planar layout in
  AVFrame uses ceil(W/2) / ceil(H/2), so the last column or row of
  chroma was never neutralised, leaving a strip with the source's
  original hue along the right/bottom edge — defeats the privacy mask.
  Use (W+1)/2 and (H+1)/2; existing in-loop guards on the Y indices
  already clamp the odd-edge lookup to the bitmap bounds.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 06:30:23 -04:00
Isaac ConnorandClaude Opus 4.7 e5446d3af5 fix: report AVPixelFormat in Panic messages for actionable diagnosis (PR #4788)
Six format-dispatch fallbacks were still printing only the legacy
`colours` value, but the dispatch in each spot is now keyed off
pixelFormat/imagePixFormat. With the GRAY8/YUV420P alias collision
(both colours=1) the legacy value frequently doesn't identify the true
format, so a "Unexpected colours: 1" log on a misroute is useless.

Updated each Panic to include the AVPixelFormat enum value and its
human-readable name (via av_get_pix_fmt_name), plus the legacy
(colours, subpixelorder) for context:
- RemoteCameraRtsp constructor
- FfmpegCamera constructor
- VncCamera constructor
- LocalCamera conversion-selection branch
- VideoStream::SetupCodec mpeg helper
- Image::Delta unknown-format fallback

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:03:33 -04:00
Isaac ConnorandClaude Opus 4.7 e6ed0e027d fix: linesize/planar correctness for Overlay, Rotate, Flip, AssignDirect(AVFrame) (PR #4788)
- Image::Overlay(image, lo_x, lo_y): rebase source and destination
  pointers per row using each image's own linesize. The previous
  sequential psrc++ drifted across rows whenever linesize >
  width*colours (i.e. non-32-aligned widths).
- Image::Rotate / Image::Flip: rewrite as plane-aware via
  av_image_fill_arrays. Each plane is rotated/flipped independently
  using its own dimensions and stride, so planar YUV (YUV420P/J420P
  and YUV422P/J422P for 180/hflip/vflip; YUV420P/J420P additionally
  for 90/270) no longer loses chroma. YUV422P 90/270 is explicitly
  refused — the chroma subsampling would transpose to vertical-only,
  which isn't the same AVPixelFormat. Packed RGB24/RGB32/GRAY8 go
  through the same helper with bpp set per format. Inner helpers
  use linesize for stride, so non-32-aligned widths no longer drift
  into per-row padding.
- Image::AssignDirect(AVFrame*): derive the wrapped size from the
  AVFrame's own linesize[] via av_image_fill_pointers rather than
  av_image_get_buffer_size(..., 32) — decoder-allocated frames may
  have a different alignment, so the 32-aligned size could overstate
  the actual buffer. Also sanity-check that data[1]/data[2]/data[3]
  are contiguous from data[0] (a non-contiguous AVFrame can't be
  wrapped with a single Image::buffer pointer). Both failure paths
  fully invalidate the Image (the existing helper).

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:11:22 -04:00
Isaac ConnorandClaude Opus 4.7 667b00b30a fix: address remaining width-vs-linesize and planar chroma issues (PR #4788)
Continuation of the FFALIGN(linesize, 32) consumer fixes. Alignment is
kept for SIMD performance; only the addressing in consumers is fixed.

- Image::Outline: both branches (dx>=dy and dx<dy, all three colour
  paths) now compute pixel addresses as buffer + y*linesize + x*colours
  instead of width-based offsets, so lines aren't drawn into per-row
  padding for non-32-aligned widths.
- Image::Fill(Rgb, density, Polygon): the scan-line polygon filler
  computes each scanline base as buffer + scan_line*linesize + lo_x*bpp
  in all three colour branches.
- Image::Deinterlace_Linear / Deinterlace_Blend /
  Deinterlace_Blend_CustomRatio: all three colour branches address rows
  by y*linesize (instead of y*width, y*width*3, y*width<<2), so they
  blend the correct rows for non-32-aligned widths. Inner loops still
  process exactly `width` pixels per row; per-row padding is untouched.
- Image::MaskPrivacy: after masking the Y plane, also neutralise the
  chroma (U/V) samples covering masked pixels for planar YUV formats
  (YUV420P/J420P/YUV422P/J422P). Previously, masking only the Y plane
  left chroma carrying the original hue, so source colour bled through
  the privacy mask — a real privacy leak when the capture format is
  planar YUV. Conservative rule: if any covered Y bit is masked, set
  the chroma sample to 128 (neutral). Plane pointers/strides come from
  av_image_fill_arrays so the math works for any aligned plane layout.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:03:18 -04:00
Isaac ConnorandClaude Opus 4.7 6a1c4e7b6e fix: fully invalidate Image on AssignDirect(AVFrame) format failure (PR #4788)
When av_image_get_buffer_size or zm_colours_from_pixformat rejects the
AVFrame's format, AssignDirect(AVFrame) previously cleared
size/linesize/colours but left width/height assigned and buffer pointing
into frame->data[0]. A caller that ignored the error log could still
walk width*height pixels through that pointer — and once the AVFrame is
freed the buffer dangles. Clear width/height/pixels/buffer too so the
Image is unambiguously empty after a failed assign. Reorder validation
to run before any state mutation so a successful assign also leaves
every field consistent in a single step.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:51:25 -04:00
Isaac ConnorandClaude Opus 4.7 77edeca57c fix: use linesize for row stride in pixel ops (PR #4788)
The FFALIGN(linesize, 32) introduced by the AVPixelFormat migration adds
per-row padding for non-32-aligned widths. Functions that walked pixel
buffers using width*bytes_per_pixel as the row stride wrote into padding,
shifted rows sideways, or under-allocated output buffers for those
widths. Convert each affected pixel op to use the Image's linesize as
the per-row stride. Alignment is kept for SIMD/perf; only the consumer
addressing is fixed.

- Image::MaskPrivacy: reset the row pointer to `buffer + y*linesize`
  each iteration instead of monotonically incrementing across rows.
- Image::Annotate (GRAY8/RGB24/RGB32 branches): use linesize for the
  per-row advance and char-block reset. The RGB32 branch additionally
  uses `linesize / sizeof(Rgb)` as the Rgb-typed stride.
- Image::Fill and Image::Fill(colour, density, ...): compute each row's
  base address as `buffer + y*linesize + lo_x*colours` instead of
  `colours*(y*width + lo_x)`.
- Image::Colourise: allocate the output buffer with
  av_image_get_buffer_size(target_fmt, w, h, 32) and copy row by row
  using src_linesize / dst_linesize. The previous tightly-packed
  allocation undersized the buffer (so AssignDirect would reject it)
  and smeared pixels across row boundaries for non-aligned widths.
- Image::Deinterlace_Discard (all three colour branches): index source
  and destination rows by y*linesize instead of (y*width)*bytes.
- Monitor::CheckSignal: convert the random linear index to (x, y) and
  address as y*LineSize() + x*bytes_per_pixel, so samples don't read
  per-row padding or land on the wrong row.
- Monitor::Capture signal-loss path: rewrote the publish-order comment
  so it matches the actual code (WriteShmFrame, then timestamp, then
  last_write_time, then last_write_index as the commit step).

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:17:56 -04:00
Isaac ConnorandClaude Opus 4.7 158d9433d3 fix: address PR #4788 review comments on planar sizing and SHM bounds
- Image::Assign(raw buffer overload): derive new_size from
  av_image_get_buffer_size(pix_fmt, w, h, 32) instead of
  width*height*colours, which undercounts planar formats (YUV420P/J420P/
  YUV422P/J422P) and would silently truncate U/V planes. Update linesize
  in the same branch so LineSize() consumers (JPEG encode, overlay) stay
  in sync with the new format. Reject AV_PIX_FMT_NONE and negative
  av_image_get_buffer_size returns up-front.
- Image::AVPixFormat(AVPixelFormat) setter: validate the requested
  format via zm_colours_from_pixformat() and check both av_image_*
  return values before mutating any state. On failure, leave
  imagePixFormat/size/linesize untouched so callers can recover instead
  of inheriting wrapped-unsigned junk.
- Image::Assign(const Image&) stride-mismatch branch: refuse planar
  formats (YUV420P/J420P/YUV422P/J422P) explicitly. The per-row copy
  only touches the Y plane; doing it silently on planar input leaves
  U/V uninitialised in the destination and produces solid-green output
  downstream. Caller must reconvert or reallocate.
- Monitor::connect: size SHM slots to an upper bound across every
  supported AVPixelFormat (RGBA at w*h, align=32) instead of the
  monitor's configured camera->ImageSize(). Prevents "Held buffer is
  undersized" failures when the no-conversion pipeline transports a
  format larger than the legacy DB Colours selection (e.g. YUV422P or
  RGBA into a GRAY8-configured monitor). Store the chosen capacity on
  the Monitor as shm_slot_size.
- Monitor::ReadShmFrame: validate image_pixelformats[index] both as a
  recognised enum AND that its av_image_get_buffer_size fits
  shm_slot_size before calling AVPixFormat(fmt). Treats a corrupted or
  torn SHM value as ignorable instead of letting it overrun the held
  slot. Single zm_colours_from_pixformat() call (no longer duplicated).
- web/.../monitor.php: inline fallback for
  translate('DeprecatedColoursSetting') so non-en_gb locales render an
  English string instead of the literal key name.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:10:22 -04:00
Isaac ConnorandClaude Opus 4.7 d6a146e1d7 fix: address PR #4788 review comments on SHM publish ordering and overlay bounds
- Image::Overlay: in the 1-byte-per-pixel branch, bound the loop by
  std::min(height*linesize, image.height*image.linesize) instead of
  `buffer + size`. For planar YUV destinations `size` includes chroma
  planes, while GRAY8 sources have only Y bytes; the old loop read past
  image.buffer and clobbered the destination's chroma. Restricting to the
  smaller of the two Y-plane spans keeps the overlay in the luma plane
  and never over-reads the source.
- Monitor::ReadShmFrame: image_pixelformats[] lives in SHM and is written
  by another process, so its value must be treated as untrusted. Validate
  via zm_colours_from_pixformat() before passing into AVPixFormat(fmt) —
  an unsupported enum value would make av_image_get_buffer_size return
  negative and wrap into the Image's unsigned size/linesize members.
  Compare against PixFormat() (canonical imagePixFormat) instead of the
  deprecated AVPixFormat() getter.
- Monitor::Capture signal-loss path: reorder SHM publishing so the slot
  bytes and per-slot shared_timestamps[index] are written first, then
  last_write_time is set from packet->timestamp (the fresh value, not
  the stale tv_sec the slot held from its previous occupant), and
  last_write_index is assigned last as the commit step. Readers gate on
  last_write_index, so all per-slot state must be visible before that
  final assignment.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:40:00 -04:00
Isaac ConnorandClaude Opus 4.7 1fe29202f5 fix: address PR #4788 review comments on AVPixelFormat migration
- Image::Assign: when source linesize > destination linesize, copy only
  the destination's row capacity per line; previously copied
  image.linesize bytes into rows of smaller linesize, overflowing the
  destination buffer on the last row.
- Monitor::CheckSignal: dispatch the 1-byte-per-pixel branch via
  zm_bytes_per_pixel(pix_fmt) == 1 so YUV422P/J422P are sampled on the
  Y plane like GRAY8/YUV420P, instead of falling through and reporting
  "no signal".
- Monitor::WriteShmFrame: record image_pixelformats[index] via
  capture_image->PixFormat() (canonical imagePixFormat) instead of
  AVPixFormat(), which re-derives from the deprecated
  (colours, subpixelorder) pair and could propagate stale metadata.
- Monitor::connect: rewrite stale comment that claimed readers don't
  need per-slot pixformat adoption; readers MUST call ReadShmFrame()
  to adopt the actual format zmc wrote.
- Camera::Camera: guard linesize/imagesize derivation against
  AV_PIX_FMT_NONE and negative returns from av_image_get_linesize /
  av_image_get_buffer_size, falling back to width * colours stride so
  unsigned wrap-around can't break SHM sizing.

refs #4788

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 19:36:20 -04:00
Isaac ConnorandClaude Opus 4.7 45a467107b fix: planar YUV correctness + alignment in Image methods
Several Image methods were silently producing wrong output for planar
YUV formats and for any format-width pair where the natural linesize
isn't a multiple of 32. Each surfaced as a different visual artefact
once the SHM started carrying YUV420P/RGB32/etc. through to the
JPEG encoders without an upfront convert-to-RGB32 step:

- Image::Assign(const Image &): sized new_size from
  image.height * image.linesize, which counts only the Y plane for
  planar layouts. Use image.size, which is populated from
  av_image_get_buffer_size and includes chroma. Without this an
  Assign'd YUV420P image left Cb/Cr at zero — solid green output via
  YCbCr-to-RGB later.
- Image::AssignDirect(width,height,colours,...): computed
  new_buffer_size as W*H*p_colours and linesize as W*p_colours, both
  wrong for planar (1.5x undersize) and for non-32-aligned RGB widths
  (the actual buffer stride after sws_scale + av_image_fill_arrays
  with align=32 is FFALIGN(W*bpp, 32), not W*bpp). Use
  av_image_get_buffer_size and FFALIGN(av_image_get_linesize(...), 32)
  so the recorded size/linesize match the real buffer layout.
  Mismatched linesize produced the diagonal-shift artefact in
  RGB32 streams at scaled widths like 1094.
- Image::WriteBuffer: same FFALIGN linesize fix; it was using the
  unaligned natural linesize too.
- Image::Scale(new_width, new_height): scale_buffer was sized
  (new_W+1)*(new_H+1)*colours which undercounts planar formats.
  SWScale::Convert correctly checks the buffer against
  av_image_get_buffer_size and returns an error, but the caller
  ignored the return value and AssignDirect'd the empty/uninit
  buffer anyway. Size with av_image_get_buffer_size, check the
  Convert return, free and bail on failure.
- Image::Scale(factor): the hand-rolled pixel-doubling/decimation
  loop treated the buffer as packed `colours`-bytes-per-pixel —
  scaled only the Y plane and dropped chroma. Drop the loop and
  delegate to Scale(new_width, new_height), which now uses sws_scale
  for all formats.
- Image::EncodeJpeg + Image::WriteJpeg: format dispatch and
  scanline writer had no planar-YUV support. WriteJpeg's existing
  branch unpacked the buffer as packed YUYV 4:2:2 with offset
  scanline*W*2, which read W*H/2 bytes past the end of any YUV420P
  buffer — a latent crash that became reachable once image_buffer
  could carry YUV420P data, segfaulting from the Event thread on
  every event-frame write. Add a planar branch that uses
  av_image_fill_arrays for plane pointers and per-plane linesizes
  and feeds JCS_YCbCr scanlines built from the Y/U/V planes —
  works for both 4:2:0 (YUV420P/YUVJ420P) and 4:2:2 (YUV422P/YUVJ422P).
- Image::Overlay: the warning fired on a benign GRAY8-on-YUV420P
  case (both report colours=1 due to the GRAY8/YUV420P alias
  collision in zm_rgb.h, but their imagePixFormat differs). Reframe
  the check so it only warns when imagePixFormat actually matches
  but the ZM (colours, subpixelorder) metadata diverges — i.e. a
  real format-tracking bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:03:53 -04:00
Isaac Connor 7b21371083 Reapply "fix: replace ZM_COLOUR system with AVPixelFormat for format dispatch"
This reverts commit 60b6b37c60.
2026-05-03 12:24:43 -04:00
Isaac Connor 60b6b37c60 Revert "fix: replace ZM_COLOUR system with AVPixelFormat for format dispatch" 2026-05-02 18:26:48 -04:00
Isaac ConnorandClaude Opus 4.7 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
Isaac ConnorandClaude Opus 4.7 0eb36d0c52 fix: silence spurious Overlay subpixel-order warning post-AVPixelFormat
Image::Overlay() warned when (colours == image.colours &&
subpixelorder != image.subpixelorder), which made sense when
(colours, subpixelorder) was the canonical format identifier.

Now that imagePixFormat is canonical, the check produces false
positives in a normal code path: zm_monitor.cpp's analysis pass calls
analysis_image->Overlay(*(zone.AlarmImage())) where the destination is
YUV420P (colours=1 via the GRAY8/YUV420P=1 alias collision in
zm_rgb.h, subpixelorder=ZM_SUBPIX_ORDER_YUV420P=11) and the source is
the zone's GRAY8 alarm mask (colours=1, subpixelorder=NONE=2). The
overlay dispatch below already handles this correctly via
zm_bytes_per_pixel(imagePixFormat) == 1 on both sides — only the Y
plane of the dest is touched, leaving chroma untouched, which is
exactly the intent. The warning was just noise.

Reframe the check around imagePixFormat: warn only when the
AVPixelFormat actually matches but the ZM (colours, subpixelorder)
metadata diverges, which would indicate a real format-tracking bug.
The new message also names the AVPixelFormat for context, instead of
two opaque integers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:33:20 -04:00
Isaac ConnorandClaude Opus 4.7 e74916ad0e fix: PR #4742 review feedback + drop stray u_buffer assignments
Two issues flagged by Copilot review on the AVPixelFormat-migration PR,
plus one build fix that was needed to land them:

1. zm_local_camera.cpp set subpixelorder to BGR for V4L2_PIX_FMT_RGB24
   captures. V4L2_PIX_FMT_RGB24 is byte-order R,G,B in memory and is
   mapped to AV_PIX_FMT_RGB24 by getFfPixFormatFromV4lPalette earlier
   in the same file, so the matching ZM subpixel order is RGB. Setting
   BGR meant red and blue were swapped in the captured image whenever
   a V4L2 camera was configured with the RGB24 palette. Long-standing
   bug — preserved unchanged through the AVPixelFormat migration —
   now fixed to ZM_SUBPIX_ORDER_RGB.

2. Image::AssignDirect(const AVFrame*) called zm_colours_from_pixformat
   without checking the return value, leaving colours/subpixelorder at
   their previous values for any unsupported AVPixelFormat. Wrap the
   call and on failure put the Image into an explicit invalid state
   (AV_PIX_FMT_NONE plus zeroed colours/subpixelorder) so the
   inconsistency surfaces immediately instead of producing wrong-format
   reads downstream.

3. Drop the u_buffer = ... / v_buffer = ... assignments inside
   Image::Assign()'s identity-copy path. Those members exist on the
   ai_server lineage but not on master, so the PR branch did not
   compile against master as-is. av_image_copy reads the planes
   directly out of temp_frame->data, so the assignments were not
   load-bearing — they look like leftover state-tracking that didn't
   survive the upstreaming. Comment notes why the lines were removed.

Tests pass: 76 cases, 778 assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:36:43 -04:00
Isaac ConnorandClaude Opus 4.6 76ec0ae1de fix: skip sws_scale for identity conversions in Image::Assign
Use av_image_copy instead of sws_scale when source and dest format +
dimensions match. Eliminates unnecessary per-frame pixel processing
for passthrough formats (e.g. YUVJ422P from MJPEG cameras).

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 13:04:44 -04:00
Isaac ConnorandClaude Opus 4.6 53683cff79 fix: use zm_bytes_per_pixel for pixel-rendering dispatch instead of GRAY8-only
The migration from colours==ZM_COLOUR_GRAY8 to imagePixFormat==GRAY8
broke YUV420P handling: the old check matched both GRAY8 and YUV420P
(both had colours==1) but the new check only matched GRAY8.

For pixel-rendering operations (Annotate, Fill, Outline, DrawLine,
Rotate, Flip, Delta, MaskPrivacy, Deinterlace, Overlay) that work on
the Y-plane of any 1-byte-per-pixel format, replace the GRAY8-only
check with zm_bytes_per_pixel(imagePixFormat)==1. This covers GRAY8,
YUV420P, YUVJ420P, YUV422P, and YUVJ422P.

Format-identification sites (JPEG encoding, Colourise, DeColourise)
that genuinely distinguish grayscale from YUV are left unchanged.

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 21:15:48 -04:00
Isaac ConnorandClaude Opus 4.6 4c999c0fe8 fix: sync imagePixFormat in WriteBuffer, Assign, and AssignDirect
After migrating format dispatch from colours to imagePixFormat,
several methods that update colours/subpixelorder were not also
updating imagePixFormat, leaving it stale. This caused format
misidentification downstream — e.g. DecodeJpeg falling back to
RGB24 while imagePixFormat still claimed RGBA, producing vertical
lines and washed-out colors in the live stream.

Fix WriteBuffer, Assign(buffer), Assign(Image), AssignDirect(buffer),
and AssignDirect(AVFrame) to keep imagePixFormat in sync.

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 12:18:03 -04:00
Isaac ConnorandClaude Opus 4.6 b416aa5293 fix: replace ZM_COLOUR_*/ZM_SUBPIX_ORDER_* with AVPixelFormat for format dispatch
ZM_COLOUR_GRAY8, ZM_COLOUR_YUV420P, and ZM_COLOUR_YUVJ420P were all
defined to 1, making format identification via colours ambiguous.
LocalCamera misidentified YUV420P as GRAY8, causing V4L2 MJPEG cameras
to decode to grayscale via expensive sws_scale conversion.

Replace the legacy ZM_COLOUR_*/ZM_SUBPIX_ORDER_* integer pair with
AVPixelFormat as the single source of truth for pixel format dispatch:

- Add src/zm_pixformat.h with central format helpers:
  zm_pixformat_from_colours, zm_colours_from_pixformat,
  zm_bytes_per_pixel, zm_db_colours_to_pixformat, zm_is_rgb32,
  zm_is_rgb24, zm_is_yuv420
- Add AVPixelFormat pixelFormat member + PixelFormat() accessor to Camera
- Add PixFormat() accessor to Image, delegate AVPixFormat methods
  to shared helpers
- Migrate all ~100 format dispatch comparisons in zm_image.cpp,
  zm_local_camera.cpp, zm_ffmpeg_camera.cpp, zm_remote_camera_rtsp.cpp,
  zm_libvlc_camera.cpp, zm_libvnc_camera.cpp, zm_monitor.cpp,
  zm_mpeg.cpp from colours/subpixelorder checks to imagePixFormat/
  AVPixelFormat checks
- Deprecate GetFFMPEGPixelFormat, delegate to zm_pixformat_from_colours
- Fix DeColourise bug: imagePixFormat was not updated to GRAY8
- Deprecate ZM_COLOUR_* and ZM_SUBPIX_ORDER_* constants in zm_rgb.h
- Add deprecation notice on Monitor.Colours web UI dropdown
- Add 13 Catch2 test cases (105 assertions) for format mapping helpers

refs #4735

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:42:06 -04:00
Isaac ConnorandClaude Opus 4.6 83ed47abf4 fix: scan-line polygon fill incorrectly filled non-convex polygon gaps
Image::Fill(Polygon) implements scan-line polygon fill but iterated
through active edges one at a time instead of in pairs. For convex
polygons (always exactly 2 active edges per scan line) this happened
to work, but for non-convex polygons it would fill the gaps between
concave sections.

A banana-shaped zone, for example, would have its inner concave area
incorrectly marked as inside the zone, causing motion detection to
trigger on the area the user explicitly drew the zone to avoid.

Fix by stepping the iterator by 2 to fill between pairs of edges
following the standard parity rule for scan-line polygon fill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:33:52 -04:00
Isaac ConnorandClaude Opus 4.6 4d40ca4edf fix: add www-data to video group and font path fallback refs #4642
Add www-data to video and dialout groups in Debian/Ubuntu postinst
scripts so zmc can access /dev/video* devices on fresh installs.
RedHat packaging already handled this via gpasswd in %post.

Add compile-time ZM_FONTDIR to zm_config_data.h.in and use it as a
fallback in zm_image.cpp when the configured font_file_location is not
found, fixing font loading failures caused by stale DB config values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:19:20 -05:00
Isaac ConnorandClaude Opus 4.6 03c42d4816 fix: use original colour format for DeColourise() dispatch
colours and subpixelorder were set to GRAY8/NONE before the
format detection logic, so RGB32 SSSE3/fast paths were never
reached and subpixel order switches always hit the default case.
Save originals before overwriting and set output values after
conversion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:39:28 -05:00
Isaac ConnorandClaude Opus 4.6 701d477ea8 perf: reuse persistent blend buffer in Image::Blend()
Replace per-frame AllocBuffer/free cycle with a persistent
blend_buffer_ member that is allocated once and reused across
calls. For non-holdbuffer images, swap buffer pointers instead
of freeing and reallocating. Eliminates ~8MB alloc+free per
frame for 1080p RGBA.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:32:56 -05:00
Isaac ConnorandClaude Opus 4.6 4a4b6b25c5 fix: correct broken pointer logic in Image::Merge() and Image::Highlight()
Merge() reset pdest and psrc to buffer base on every pixel iteration,
so only buffer[0] was ever written. Move pdest outside the loop and
use direct array indexing for source buffers.

Highlight() had the same pointer reset bug plus iterated over size
(total bytes) instead of pixels, and the unsigned diff calculation
wrapped around instead of computing absolute difference. Fix pointer
management, loop bound, and diff comparison.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:32:56 -05:00
Isaac ConnorandClaude Opus 4.5 ecb0ed4122 fix: return error when av_buffer_create fails in Image::PopulateFrame
Previously, when av_buffer_create() failed and returned nullptr, the code
only issued a Warning and continued, assigning the null pointer to the
frame buffer and returning success. Now properly returns -1 on failure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:48:03 -05:00
Isaac Connor bef2ea5b9e Allocate the temp frame inside Assign, instead of keeping it around. 2026-01-22 20:54:00 -05:00
Isaac Connor 32a842e81e Return instead of panic in Delta and Blend 2026-01-19 14:40:28 -06:00
Isaac Connor 9ee497f708 Add toString 2026-01-16 15:44:30 -05:00
Isaac Connor 17a633087c Create a constructor that takes an allocation parameter so that we can give a larger allocation than buffer size 2026-01-16 15:37:10 -05:00
Isaac Connor ec913cfc50 When overlaying a greyscale onto rgb32, only set the red field so we don't get white. 2026-01-16 15:29:39 -05:00
Isaac Connor 82554e0f20 Fix compiler complain 2025-12-28 13:09:37 -05:00
Isaac Connor 027fcdaf58 Add debugging to figure out cause of crash 2025-10-23 12:37:08 -04:00
Isaac Connor 643fe97b25 Deprecate zm_av_frame_alloc 2025-09-18 14:48:52 -04:00
Isaac Connor eaf34434e3 Add constructor and assign direct for avframe 2025-09-18 09:59:39 -04:00
Isaac Connor e1898c483b Adjust debug level add AVPixelFormat Image::AVPixFormat(AVPixelFormat new_pixelformat) 2025-09-18 09:42:03 -04:00