diff --git a/src/zm_camera.cpp b/src/zm_camera.cpp index cdd66527f32..3f3c8c62a33 100644 --- a/src/zm_camera.cpp +++ b/src/zm_camera.cpp @@ -41,6 +41,7 @@ Camera::Camera( height(p_height), colours(p_colours), subpixelorder(p_subpixelorder), + pixelFormat(zm_pixformat_from_colours(p_colours, p_subpixelorder)), brightness(p_brightness), hue(p_hue), colour(p_colour), @@ -63,9 +64,37 @@ Camera::Camera( mLastAudioDTS(AV_NOPTS_VALUE), bytes(0), mIsPrimed(false) { - linesize = width * colours; + // Camera::linesize/imagesize describe the *device-side* buffer that + // capture paths copy from (V4L2 mmap buffers, raw RTP frames, etc). + // Those buffers are tightly packed at the driver/source stride, not + // 32-byte aligned, so use align=1 here. ZM's internal Image buffers + // and SHM slots independently apply their own 32-byte alignment for + // SIMD performance — that decoupling is the source of the size-mismatch + // Fatal in LocalCamera::PrimeCapture when widths aren't 32-aligned. + // + // Guard against an unknown (colours, subpixelorder) pair that produced + // pixelFormat == AV_PIX_FMT_NONE, or any case where av_image_get_* + // returns a negative size — assigning those to unsigned would wrap to a + // huge value and break SHM sizing and downstream allocations. pixels = width * height; - imagesize = static_cast(height) * linesize; + if (pixelFormat == AV_PIX_FMT_NONE) { + Error("Camera: unknown pixel format from colours=%u subpixelorder=%u; falling back to width*colours stride", + p_colours, p_subpixelorder); + linesize = width * colours; + imagesize = static_cast(height) * linesize; + } else { + int raw_linesize = av_image_get_linesize(pixelFormat, width, 0); + int raw_imagesize = av_image_get_buffer_size(pixelFormat, width, height, 1); + if (raw_linesize < 0 || raw_imagesize < 0) { + Error("Camera: av_image_get_* returned %d/%d for pixelFormat=%s; falling back", + raw_linesize, raw_imagesize, zm_get_pix_fmt_name(pixelFormat)); + linesize = width * colours; + imagesize = static_cast(height) * linesize; + } else { + linesize = static_cast(raw_linesize); + imagesize = static_cast(raw_imagesize); + } + } Debug(2, "New camera id: %d width: %d line size: %d height: %d colours: %d subpixelorder: %d capture: %d, size: %llu", monitor->Id(), width, linesize, height, colours, subpixelorder, capture, imagesize); @@ -101,7 +130,7 @@ AVStream *Camera::getVideoStream() { mVideoStream->time_base = (AVRational) {1, 1000000}; // microseconds as base frame rate mVideoStream->codecpar->width = width; mVideoStream->codecpar->height = height; - mVideoStream->codecpar->format = GetFFMPEGPixelFormat(colours, subpixelorder); + mVideoStream->codecpar->format = pixelFormat; mVideoStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; mVideoStream->codecpar->codec_id = AV_CODEC_ID_NONE; Debug(1, "Allocating avstream %p %p %d", mVideoStream, mVideoStream->codecpar, mVideoStream->codecpar->codec_id); diff --git a/src/zm_camera.h b/src/zm_camera.h index 21e46cf2e9c..dbfaa447fcc 100644 --- a/src/zm_camera.h +++ b/src/zm_camera.h @@ -21,6 +21,7 @@ #define ZM_CAMERA_H #include "zm_image.h" +#include "zm_pixformat.h" #include #include @@ -42,8 +43,9 @@ class Camera { uint16_t width; uint16_t height; unsigned int linesize; - unsigned int colours; - unsigned int subpixelorder; + unsigned int colours; // DEPRECATED: use pixelFormat + unsigned int subpixelorder; // DEPRECATED: use pixelFormat + AVPixelFormat pixelFormat; unsigned int pixels; unsigned long long imagesize; int brightness; @@ -98,8 +100,9 @@ class Camera { unsigned int Width() const { return width; } unsigned int LineSize() const { return linesize; } unsigned int Height() const { return height; } - unsigned int Colours() const { return colours; } - unsigned int SubpixelOrder() const { return subpixelorder; } + unsigned int Colours() const { return colours; } // DEPRECATED + unsigned int SubpixelOrder() const { return subpixelorder; } // DEPRECATED + AVPixelFormat PixelFormat() const { return pixelFormat; } unsigned int Pixels() const { return pixels; } unsigned long long ImageSize() const { return imagesize; } unsigned int Bytes() const { return bytes; }; diff --git a/src/zm_ffmpeg.cpp b/src/zm_ffmpeg.cpp index eecb0664221..b9e97a4a7cc 100644 --- a/src/zm_ffmpeg.cpp +++ b/src/zm_ffmpeg.cpp @@ -20,6 +20,7 @@ #include "zm_ffmpeg.h" #include "zm_logger.h" +#include "zm_pixformat.h" #include "zm_rgb.h" #include "zm_utils.h" @@ -327,46 +328,9 @@ void FFMPEGDeInit() { bInit = false; } +// DEPRECATED: use zm_pixformat_from_colours() from zm_pixformat.h instead. enum _AVPIXELFORMAT GetFFMPEGPixelFormat(unsigned int p_colours, unsigned p_subpixelorder) { - enum _AVPIXELFORMAT pf; - - Debug(8,"Colours: %d SubpixelOrder: %d",p_colours,p_subpixelorder); - - switch (p_colours) { - case ZM_COLOUR_RGB24: - if(p_subpixelorder == ZM_SUBPIX_ORDER_BGR) { - /* BGR subpixel order */ - pf = AV_PIX_FMT_BGR24; - } else { - /* Assume RGB subpixel order */ - pf = AV_PIX_FMT_RGB24; - } - break; - case ZM_COLOUR_RGB32: - if (p_subpixelorder == ZM_SUBPIX_ORDER_ARGB) { - /* ARGB subpixel order */ - pf = AV_PIX_FMT_ARGB; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - pf = AV_PIX_FMT_ABGR; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - pf = AV_PIX_FMT_BGRA; - } else { - /* Assume RGBA subpixel order */ - pf = AV_PIX_FMT_RGBA; - } - break; - case ZM_COLOUR_GRAY8: - pf = AV_PIX_FMT_GRAY8; - break; - default: - Panic("Unexpected colours: %d", p_colours); - pf = AV_PIX_FMT_GRAY8; /* Just to shush gcc variable may be unused warning */ - break; - } - - return pf; + return zm_pixformat_from_colours(p_colours, p_subpixelorder); } #if LIBAVUTIL_VERSION_CHECK(56, 0, 0, 17, 100) diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index c39f4924a22..728b419dafd 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -94,17 +94,18 @@ FfmpegCamera::FfmpegCamera( /* Has to be located inside the constructor so other components such as zma * will receive correct colours and subpixel order */ - if ( colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; - imagePixFormat = AV_PIX_FMT_RGBA; - } else if ( colours == ZM_COLOUR_RGB24 ) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if ( zm_is_rgb24(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGB; - imagePixFormat = AV_PIX_FMT_RGB24; - } else if ( colours == ZM_COLOUR_GRAY8 ) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if ( pixelFormat == AV_PIX_FMT_GRAY8 ) { subpixelorder = ZM_SUBPIX_ORDER_NONE; - imagePixFormat = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %d", colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } packet = av_packet_ptr{av_packet_alloc()}; @@ -525,7 +526,7 @@ int FfmpegCamera::OpenFfmpeg() { mVideoCodec->name, av_hwdevice_get_type_name(type), av_hwdevice_get_type_name(config->device_type), - av_get_pix_fmt_name(config->pix_fmt) + zm_get_pix_fmt_name(config->pix_fmt) ); } } // end foreach hwconfig @@ -534,7 +535,7 @@ int FfmpegCamera::OpenFfmpeg() { #endif if (hw_pix_fmt != AV_PIX_FMT_NONE) { Debug(1, "Selected hw_pix_fmt %d %s", - hw_pix_fmt, av_get_pix_fmt_name(hw_pix_fmt)); + hw_pix_fmt, zm_get_pix_fmt_name(hw_pix_fmt)); mVideoCodecContext->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL; //if (!lavc_param->check_hw_profile) diff --git a/src/zm_ffmpeg_camera.h b/src/zm_ffmpeg_camera.h index e45f5516eb6..611cbc38676 100644 --- a/src/zm_ffmpeg_camera.h +++ b/src/zm_ffmpeg_camera.h @@ -54,8 +54,6 @@ class FfmpegCamera : public Camera { int frameCount; - _AVPIXELFORMAT imagePixFormat; - bool use_hwaccel; //will default to on if hwaccel specified, will get turned off if there is a failure #if HAVE_LIBAVUTIL_HWCONTEXT_H AVBufferRef *hw_device_ctx = nullptr; diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 3fbb458f59c..095fc2ae931 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -23,6 +23,9 @@ #include "zm_poly.h" #include "zm_swscale.h" #include "zm_utils.h" + +#include + #include #include #include @@ -309,9 +312,12 @@ Image::Image(const AVFrame *frame, int p_width, int p_height) : this->Assign(frame); } -Image::Image(const AVFrame *frame) { - blend_buffer_ = nullptr; - blend_buffer_size_ = 0; +Image::Image(const AVFrame *frame) : Image() { + // Delegate to the default ctor first so buffer/buffertype/allocation + // are fully initialised before AssignDirect runs. AssignDirect's failure + // and success paths both call DumpImgBuffer() to release any previously + // owned buffer; without this delegation those calls would read + // uninitialised members and potentially free a garbage pointer. AssignDirect(frame); } @@ -361,14 +367,48 @@ bool Image::Assign(const AVFrame *frame) { } zm_dump_video_frame(frame, "source frame in Image::Assign"); - // Desired format - AVPixelFormat format = (AVPixelFormat)AVPixFormat(); + // imagePixFormat is the canonical destination format; the deprecated + // AVPixFormat() getter re-derives via (colours, subpixelorder) and would + // pick the wrong swscale target if those legacy fields drift out of sync + // (e.g. the GRAY8/YUV420P alias collision). + const AVPixelFormat format = imagePixFormat; + const AVPixelFormat src_fmt = static_cast(frame->format); + + // If source and destination format + dimensions match, do a direct plane + // copy instead of running through sws_scale. This avoids the overhead of + // the swscale pipeline for identity conversions (e.g. YUVJ422P→YUVJ422P). + if (src_fmt == format + && frame->width == static_cast(width) + && frame->height == static_cast(height)) { + Debug(4, "Same format %s %dx%d, using av_image_copy", + zm_get_pix_fmt_name(format), width, height); + av_frame_ptr temp_frame{av_frame_alloc()}; + if (!temp_frame) { + Error("Unable to allocate destination frame"); + return false; + } + // PopulateFrame can fail (av_buffer_create / av_image_fill_arrays errors); + // calling av_image_copy on an unpopulated temp_frame is undefined. + if (PopulateFrame(temp_frame.get()) < 0) { + Error("PopulateFrame failed; skipping av_image_copy fast path"); + return false; + } + temp_frame->pts = frame->pts; + // u_buffer/v_buffer are not members of Image on master; av_image_copy + // reads the planes directly from temp_frame->data, so we don't need to + // mirror them onto the Image either. + av_image_copy(temp_frame->data, temp_frame->linesize, + (const uint8_t **)frame->data, frame->linesize, + format, width, height); + update_function_pointers(); + return true; + } + sws_convert_context = sws_getCachedContext( sws_convert_context, - frame->width, frame->height, (AVPixelFormat)frame->format, + frame->width, frame->height, src_fmt, width, height, format, SWS_BICUBIC, - //SWS_POINT | SWS_BITEXACT, nullptr, nullptr, nullptr); if (sws_convert_context == nullptr) { Error("Unable to create conversion context"); @@ -691,12 +731,11 @@ uint8_t* Image::WriteBuffer( const unsigned int p_colours, const unsigned int p_subpixelorder) { - if ( p_colours != ZM_COLOUR_GRAY8 - && - p_colours != ZM_COLOUR_RGB24 - && - p_colours != ZM_COLOUR_RGB32 ) { - Error("WriteBuffer called with unexpected colours: %d", p_colours); + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + if (p_pixfmt == AV_PIX_FMT_NONE) { + Error("WriteBuffer called with unsupported (colours=%u, subpixelorder=%u) pair; " + "no AVPixelFormat mapping", + p_colours, p_subpixelorder); return nullptr; } @@ -707,7 +746,29 @@ uint8_t* Image::WriteBuffer( if ( p_width != width || p_height != height || p_colours != colours || p_subpixelorder != subpixelorder ) { - unsigned int newsize = (p_width * p_height) * p_colours; + // Derive size/linesize from the AVPixelFormat. Using p_width * p_colours + // is wrong for planar formats: with the GRAY8/YUV420P alias collision, + // p_colours=1 for YUV420P/YUV422P, but those formats need ~1.5x/2x more + // bytes for the chroma planes. + int av_size = av_image_get_buffer_size(p_pixfmt, p_width, p_height, 32); + if (av_size < 0) { + Error("WriteBuffer: av_image_get_buffer_size failed for fmt=%d %ux%u", + p_pixfmt, p_width, p_height); + return nullptr; + } + // FFALIGN to 32 to match what av_image_get_buffer_size with align=32 + // assumes for the buffer layout, and what sws_scale writes when it + // produces this format/width pair. Using the unaligned natural linesize + // here would mismatch the buffer's actual row stride and cause + // diagonal-shift artefacts in any consumer that walks the buffer at + // Image::linesize stride (e.g. EncodeJpeg's non-planar path). + int av_linesize = FFALIGN(av_image_get_linesize(p_pixfmt, p_width, 0), 32); + if (av_linesize < 0) { + Error("WriteBuffer: av_image_get_linesize failed for fmt=%d width=%u", + p_pixfmt, p_width); + return nullptr; + } + unsigned int newsize = static_cast(av_size); if ( buffer == nullptr ) { AllocImgBuffer(newsize); @@ -727,8 +788,9 @@ uint8_t* Image::WriteBuffer( width = p_width; height = p_height; colours = p_colours; - linesize = p_width * p_colours; + linesize = static_cast(av_linesize); subpixelorder = p_subpixelorder; + imagePixFormat = p_pixfmt; pixels = height*width; size = newsize; } // end if need to re-alloc buffer @@ -737,25 +799,98 @@ uint8_t* Image::WriteBuffer( } void Image::AssignDirect(const AVFrame *frame) { + auto invalidate = [&]() { + // Release any previously-owned buffer before overwriting buffer/buffertype; + // a held (DONTFREE) buffer is left in place by DumpImgBuffer, so this is + // safe whether the Image currently owns its memory or wraps somebody + // else's. Without this step a ZM_BUFTYPE_ZM allocation would be leaked + // every time AssignDirect(frame) hits the invalidate path. + DumpImgBuffer(); + width = 0; + height = 0; + pixels = 0; + buffer = nullptr; + imagePixFormat = AV_PIX_FMT_NONE; + colours = 0; + subpixelorder = 0; + size = 0; + allocation = 0; + linesize = 0; + holdbuffer = true; + buffertype = ZM_BUFTYPE_DONTFREE; + }; + + AVPixelFormat frame_fmt = static_cast(frame->format); + unsigned int probe_colours, probe_subpix; + // Validate the format against our supported set before touching any state. + if (!zm_colours_from_pixformat(frame_fmt, probe_colours, probe_subpix)) { + Error("AssignDirect: unsupported pixel format %d on frame %dx%d; " + "fully invalidating Image", frame->format, frame->width, frame->height); + invalidate(); + return; + } + if (!frame->data[0]) { + Error("AssignDirect: AVFrame has null data[0]; fully invalidating Image"); + invalidate(); + return; + } + + // av_image_get_buffer_size(..., align=32) assumes 32-byte stride + // alignment, but the AVFrame may have been allocated with a different + // alignment by the decoder. Derive the size from the *actual* frame + // strides instead — av_image_fill_pointers computes expected per-plane + // offsets given a tightly-packed buffer using the frame's linesize[]; the + // last (plane+1) offset is the total contiguous span. We also sanity- + // check that the frame's plane pointers really are contiguous starting + // at data[0] (a non-contiguous AVFrame can't be wrapped with a single + // Image::buffer pointer). + uint8_t *expected_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int frame_strides[4] = { + frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3] + }; + int total_size = av_image_fill_pointers(expected_planes, frame_fmt, frame->height, + frame->data[0], frame_strides); + if (total_size <= 0) { + Error("AssignDirect: av_image_fill_pointers returned %d for %s %dx%d; invalidating", + total_size, av_get_pix_fmt_name(frame_fmt), frame->width, frame->height); + invalidate(); + return; + } + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame_fmt); + const int plane_count = desc ? av_pix_fmt_count_planes(frame_fmt) : 1; + for (int p = 1; p < plane_count && p < 4; p++) { + if (frame->data[p] && frame->data[p] != expected_planes[p]) { + Error("AssignDirect: AVFrame plane %d not contiguous with data[0] " + "(expected %p, got %p) for %s %dx%d; invalidating", + p, expected_planes[p], frame->data[p], + av_get_pix_fmt_name(frame_fmt), frame->width, frame->height); + invalidate(); + return; + } + } + + // Release any previously-owned buffer before we replace it with + // frame->data[0]. DumpImgBuffer is a no-op for DONTFREE buffers, so this + // is safe whether the Image currently owns its memory or wraps somebody + // else's. Without this step a ZM_BUFTYPE_ZM allocation would be leaked + // every time AssignDirect(frame) succeeds. + DumpImgBuffer(); width = frame->width; height = frame->height; buffer = frame->data[0]; linesize = frame->linesize[0]; - allocation = size = av_image_get_buffer_size(static_cast(frame->format), frame->width, frame->height, 32); - switch(static_cast(frame->format)) { - case AV_PIX_FMT_RGBA: - subpixelorder = ZM_SUBPIX_ORDER_RGBA; - colours = ZM_COLOUR_RGB32; - break; - case AV_PIX_FMT_YUV420P: - colours = ZM_COLOUR_GRAY8; - break; - default: - Debug(1, "Unimplemented format"); - break; - } - buffertype = ZM_BUFTYPE_DONTFREE; + imagePixFormat = frame_fmt; + colours = probe_colours; + subpixelorder = probe_subpix; + allocation = size = static_cast(total_size); pixels = width * height; + holdbuffer = true; + buffertype = ZM_BUFTYPE_DONTFREE; + // Re-bind format-specific function pointers (delta/blend/convert + // dispatch) to the new format. Without this, an Image that previously + // held a different format keeps its old fptr_* and subsequent ops take + // the wrong optimized path. + update_function_pointers(); } @@ -781,16 +916,32 @@ void Image::AssignDirect( return; } - if ( p_colours != ZM_COLOUR_GRAY8 && p_colours != ZM_COLOUR_RGB24 && p_colours != ZM_COLOUR_RGB32 ) { + AVPixelFormat new_pix_fmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + if (new_pix_fmt == AV_PIX_FMT_NONE) { Error("Attempt to directly assign buffer with unexpected colours per pixel: %d", p_colours); return; } - size_t new_buffer_size = static_cast(p_width) * p_height * p_colours; + // p_width * p_height * p_colours undercounts planar formats (YUV420P needs + // ~1.5x; with the GRAY8 alias colours=1 here would otherwise treat it as + // GRAY8 size W*H). Use av_image_* so the size and linesize match what the + // pixel format actually requires. + int av_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 32); + // FFALIGN the linesize to 32 to match the buffer's actual row stride + // (av_image_get_buffer_size with align=32 assumes this layout, and + // sws_scale writes in this layout). Storing the unaligned natural + // linesize would cause downstream consumers that walk the buffer at + // Image::linesize stride to drift sideways every row. + int av_linesize = FFALIGN(av_image_get_linesize(new_pix_fmt, p_width, 0), 32); + if (av_size < 0 || av_linesize < 0) { + Error("AssignDirect: av_image sizing failed for %s %ux%u", av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); + return; + } + size_t new_buffer_size = static_cast(av_size); if ( buffer_size < new_buffer_size ) { - Error("Attempt to directly assign buffer from an undersized buffer of size: %zu, needed %dx%d*%d colours = %zu", - buffer_size, p_width, p_height, p_colours, new_buffer_size); + Error("Attempt to directly assign buffer from an undersized buffer of size: %zu, needed %dx%d %s = %zu", + buffer_size, p_width, p_height, av_get_pix_fmt_name(new_pix_fmt), new_buffer_size); return; } @@ -801,7 +952,7 @@ void Image::AssignDirect( } else { /* Copy into the held buffer */ if ( new_buffer != buffer ) { - (*fptr_imgbufcpy)(buffer, new_buffer, size); + (*fptr_imgbufcpy)(buffer, new_buffer, new_buffer_size); } /* Free the new buffer */ DumpBuffer(new_buffer, p_buffertype); @@ -817,8 +968,9 @@ void Image::AssignDirect( width = p_width; height = p_height; colours = p_colours; - linesize = width * colours; + linesize = static_cast(av_linesize); subpixelorder = p_subpixelorder; + imagePixFormat = new_pix_fmt; pixels = width * height; size = new_buffer_size; update_function_pointers(); @@ -837,22 +989,37 @@ void Image::Assign( return; } - unsigned int new_size = p_width * p_height * p_colours; - if ( buffer_size < new_size ) { - Error("Attempt to assign buffer from an undersized buffer of size: %zu", buffer_size); - return; - } - if ( !p_height || !p_width ) { Error("Attempt to assign buffer with invalid width or height: %d %d", p_width, p_height); return; } - if ( p_colours != ZM_COLOUR_GRAY8 && p_colours != ZM_COLOUR_RGB24 && p_colours != ZM_COLOUR_RGB32 ) { + AVPixelFormat new_pix_fmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + if (new_pix_fmt == AV_PIX_FMT_NONE) { Error("Attempt to assign buffer with unexpected colours per pixel: %d", p_colours); return; } + // Source and destination may use different alignment. Callers pass + // device-side buffers sized at align=1 (e.g. Camera::ImageSize() is now + // packed) while ZM's internal Image keeps a 32-byte-aligned layout for + // SIMD. Validate the source size against the packed layout and the + // destination/allocation against the aligned layout, then copy + // plane-by-plane so per-row padding doesn't read past the source. + int packed_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 1); + int aligned_size = av_image_get_buffer_size(new_pix_fmt, p_width, p_height, 32); + if (packed_size < 0 || aligned_size < 0) { + Error("av_image_get_buffer_size failed (%d/%d) for %s %ux%u", + packed_size, aligned_size, av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); + return; + } + if (buffer_size < static_cast(packed_size)) { + Error("Attempt to assign buffer from an undersized buffer of size: %zu (need %d packed)", + buffer_size, packed_size); + return; + } + const unsigned int new_size = static_cast(aligned_size); + if ( !buffer || p_width != width || p_height != height || p_colours != colours || p_subpixelorder != subpixelorder ) { if ( holdbuffer && buffer ) { @@ -872,28 +1039,58 @@ void Image::Assign( pixels = width*height; colours = p_colours; subpixelorder = p_subpixelorder; + imagePixFormat = new_pix_fmt; size = new_size; + // Keep linesize in sync with the new format. av_image_get_linesize is + // signed; the format validation above guarantees a recognised format + // here so the call will not return a negative value. + linesize = FFALIGN(av_image_get_linesize(new_pix_fmt, p_width, 0), 32); + } + + if (new_buffer != buffer) { + // Plane-aware copy: src is laid out packed (align=1), dst is laid out + // aligned (align=32). av_image_copy walks each plane using its own + // src/dst linesize so per-row padding doesn't read past the source. + uint8_t *src_data[4] = {nullptr, nullptr, nullptr, nullptr}; + int src_strides[4] = {0, 0, 0, 0}; + uint8_t *dst_data[4] = {nullptr, nullptr, nullptr, nullptr}; + int dst_strides[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(src_data, src_strides, + const_cast(new_buffer), + new_pix_fmt, p_width, p_height, 1) < 0 + || av_image_fill_arrays(dst_data, dst_strides, buffer, + new_pix_fmt, p_width, p_height, 32) < 0) { + Error("Assign: av_image_fill_arrays failed for %s %ux%u", + av_get_pix_fmt_name(new_pix_fmt), p_width, p_height); + return; + } + av_image_copy(dst_data, dst_strides, + const_cast(src_data), src_strides, + new_pix_fmt, p_width, p_height); } - - if ( new_buffer != buffer ) - (*fptr_imgbufcpy)(buffer, new_buffer, size); update_function_pointers(); } void Image::Assign(const Image &image) { - unsigned int new_size = image.height * image.linesize; + // image.size is the AVPixelFormat-aware buffer size (av_image_get_buffer_size, + // including chroma planes for planar YUV); image.height * image.linesize is + // only the Y plane for planar formats and would silently leave U/V planes + // uninitialised in the destination — producing a Cb=Cr=0 "solid green" image + // when the dest is later interpreted as YCbCr. + unsigned int new_size = image.size; if ( image.buffer == nullptr ) { Error("Attempt to assign image with an empty buffer"); return; } - if ( image.colours != ZM_COLOUR_GRAY8 - && - image.colours != ZM_COLOUR_RGB24 - && - image.colours != ZM_COLOUR_RGB32 ) { - Error("Attempt to assign image with unexpected colours per pixel: %d", image.colours); + if (image.imagePixFormat == AV_PIX_FMT_NONE) { + // Real cause is unset/unknown AVPixelFormat metadata, not the + // (colours, subpixelorder) values themselves — those can legitimately + // be 1/something for planar YUV. Report the underlying state. + Error("Attempt to assign image with missing AVPixelFormat metadata " + "(imagePixFormat=AV_PIX_FMT_NONE, legacy colours=%u subpixelorder=%u)", + image.colours, image.subpixelorder); return; } @@ -904,7 +1101,7 @@ void Image::Assign(const Image &image) { if ( holdbuffer && buffer ) { if ( new_size > allocation ) { - Error("Held buffer is undersized for assigned buffer"); + Error("Held buffer is undersized for assigned buffer (need %u, have %lu)", new_size, allocation); return; } } else { @@ -919,23 +1116,51 @@ void Image::Assign(const Image &image) { pixels = width*height; colours = image.colours; subpixelorder = image.subpixelorder; + imagePixFormat = image.imagePixFormat; size = new_size; linesize = image.linesize; update_function_pointers(); } if ( image.buffer != buffer ) { - if (image.linesize > linesize) { - Debug(1, "Must copy line by line due to different line size %d != %d", image.linesize, linesize); + if (image.linesize != linesize) { + // Dimensions/colours/subpixelorder all match but linesizes disagree. + // This happens when src and dst use different alignment conventions + // (e.g. dst is FFALIGN'd to 32 from av_image_get_buffer_size while + // src came from a held-buffer ctor with a packed driver stride). + // + // For planar formats (YUV420P/J420P/YUV422P/J422P) a per-row copy of + // just the Y plane would leave U/V uninitialised in the destination, + // producing solid-green output downstream. Plane-aware copying via + // av_image_copy_plane would be needed and the caller may as well + // reconvert; refuse explicitly. + if (zm_is_yuv420(image.imagePixFormat) + || image.imagePixFormat == AV_PIX_FMT_YUV422P + || image.imagePixFormat == AV_PIX_FMT_YUVJ422P) { + Error("Image::Assign: planar format %s with stride mismatch (src %d vs dst %d) " + "is not supported by the per-row copy; chroma planes would be lost. " + "Caller must convert or reallocate.", + av_get_pix_fmt_name(image.imagePixFormat), image.linesize, linesize); + return; + } + // Packed formats: copy min(src, dst) bytes per row so we never write + // past the destination row capacity AND never read past the source + // row capacity. Source padding (when src > dst) is discarded; dest + // padding (when src < dst) is left untouched. Both directions are + // safe — the previous flat-memcpy path silently over-read the + // source when image.linesize < linesize. + const unsigned int copy_bytes = std::min(image.linesize, linesize); + Debug(1, "Must copy line by line due to different line size %d != %d (copy %u)", + image.linesize, linesize, copy_bytes); uint8_t *src_ptr = image.buffer; uint8_t *dst_ptr = buffer; for (unsigned int i=0; i< image.height; i++) { - (*fptr_imgbufcpy)(dst_ptr, src_ptr, image.linesize); + (*fptr_imgbufcpy)(dst_ptr, src_ptr, copy_bytes); src_ptr += image.linesize; dst_ptr += linesize; } } else { - Debug(4, "Doing full copy line size %d != %d", image.linesize, linesize); + Debug(4, "Doing full copy line size %d == %d, %u bytes", image.linesize, linesize, size); (*fptr_imgbufcpy)(buffer, image.buffer, size); } } @@ -947,13 +1172,15 @@ Image *Image::HighlightEdges( unsigned int p_subpixelorder, const Box *limits ) { - if ( colours != ZM_COLOUR_GRAY8 ) { + if ( imagePixFormat != AV_PIX_FMT_GRAY8 ) { Panic("Attempt to highlight image edges when colours = %d", colours); } /* Convert the colour's RGBA subpixel order into the image's subpixel order */ colour = rgb_convert(colour, p_subpixelorder); + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); + /* Create a new image of the target format */ Image *high_image = new Image(width, height, p_colours, p_subpixelorder); uint8_t* high_buff = high_image->WriteBuffer(width, height, p_colours, p_subpixelorder); @@ -966,40 +1193,38 @@ Image *Image::HighlightEdges( unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; - if ( p_colours == ZM_COLOUR_GRAY8 ) { + // Source (this) is GRAY8 with src_linesize per row. The destination Image + // has its own (possibly different) row stride; use that for high_buff + // addressing so non-32-aligned widths don't overflow the destination on + // the last row, and use src_linesize when looking up neighbour pixels + // (p ± linesize) so we follow the actual row stride rather than `width`. + const unsigned int src_linesize = linesize; + const unsigned int dst_linesize = high_image->LineSize(); + + if ( p_pixfmt == AV_PIX_FMT_GRAY8 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - const uint8_t* p = buffer + (y * linesize) + lo_x; - uint8_t* phigh = high_buff + (y * linesize) + lo_x; + const uint8_t* p = buffer + (y * src_linesize) + lo_x; + uint8_t* phigh = high_buff + (y * dst_linesize) + lo_x; for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh++ ) { bool edge = false; if ( *p ) { - edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) || (y > 0 && !*(p-width)) || (y < (height-1) && !*(p+width)); -#if 0 - if ( !edge && x > 0 && !*(p-1) ) edge = true; - if ( !edge && x < (width-1) && !*(p+1) ) edge = true; - if ( !edge && y > 0 && !*(p-width) ) edge = true; - if ( !edge && y < (height-1) && !*(p+width) ) edge = true; -#endif + edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) + || (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize)); } if ( edge ) { *phigh = colour; } } } - } else if ( p_colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(p_pixfmt) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - const uint8_t* p = buffer + (y * linesize) + lo_x; - uint8_t* phigh = high_buff + (((y * linesize) + lo_x) * 3); + const uint8_t* p = buffer + (y * src_linesize) + lo_x; + uint8_t* phigh = high_buff + (y * dst_linesize) + lo_x * 3; for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh += 3 ) { bool edge = false; if ( *p ) { - edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) || (y > 0 && !*(p-width)) || (y < (height-1) && !*(p+width)); -#if 0 - if ( !edge && x > 0 && !*(p-1) ) edge = true; - if ( !edge && x < (width-1) && !*(p+1) ) edge = true; - if ( !edge && y > 0 && !*(p-width) ) edge = true; - if ( !edge && y < (height-1) && !*(p+width) ) edge = true; -#endif + edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) + || (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize)); } if ( edge ) { RED_PTR_RGBA(phigh) = RED_VAL_RGBA(colour); @@ -1008,20 +1233,15 @@ Image *Image::HighlightEdges( } } } - } else if ( p_colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(p_pixfmt) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - const uint8_t* p = buffer + (y * linesize) + lo_x; - Rgb* phigh = (Rgb*)(high_buff + (((y * linesize) + lo_x) * 4)); + const uint8_t* p = buffer + (y * src_linesize) + lo_x; + Rgb* phigh = (Rgb*)(high_buff + (y * dst_linesize) + (lo_x << 2)); for ( unsigned int x = lo_x; x <= hi_x; x++, p++, phigh++ ) { bool edge = false; if ( *p ) { - edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) || (y > 0 && !*(p-width)) || (y < (height-1) && !*(p+width)); -#if 0 - if ( !edge && x > 0 && !*(p-1) ) edge = true; - if ( !edge && x < (width-1) && !*(p+1) ) edge = true; - if ( !edge && y > 0 && !*(p-width) ) edge = true; - if ( !edge && y < (height-1) && !*(p+width) ) edge = true; -#endif + edge = (x > 0 && !*(p-1)) || (x < (width-1) && !*(p+1)) + || (y > 0 && !*(p-src_linesize)) || (y < (height-1) && !*(p+src_linesize)); } if ( edge ) { *phigh = colour; @@ -1084,6 +1304,7 @@ bool Image::WriteRaw(const std::string &filename) const { bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsigned int p_subpixelorder) { unsigned int new_width, new_height, new_colours, new_subpixelorder; + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); if (!readjpg_dcinfo) { readjpg_dcinfo = new jpeg_decompress_struct; @@ -1131,22 +1352,20 @@ bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsign height = new_height; } - switch (p_colours) { - case ZM_COLOUR_GRAY8: + if (p_pixfmt == AV_PIX_FMT_GRAY8) { readjpg_dcinfo->out_color_space = JCS_GRAYSCALE; new_colours = ZM_COLOUR_GRAY8; new_subpixelorder = ZM_SUBPIX_ORDER_NONE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(p_pixfmt)) { #ifdef JCS_EXTENSIONS new_colours = ZM_COLOUR_RGB32; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGRA) { + if (p_pixfmt == AV_PIX_FMT_BGRA) { readjpg_dcinfo->out_color_space = JCS_EXT_BGRX; new_subpixelorder = ZM_SUBPIX_ORDER_BGRA; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ARGB) { + } else if (p_pixfmt == AV_PIX_FMT_ARGB) { readjpg_dcinfo->out_color_space = JCS_EXT_XRGB; new_subpixelorder = ZM_SUBPIX_ORDER_ARGB; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ABGR) { + } else if (p_pixfmt == AV_PIX_FMT_ABGR) { readjpg_dcinfo->out_color_space = JCS_EXT_XBGR; new_subpixelorder = ZM_SUBPIX_ORDER_ABGR; } else { @@ -1154,20 +1373,22 @@ bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsign readjpg_dcinfo->out_color_space = JCS_EXT_RGBX; new_subpixelorder = ZM_SUBPIX_ORDER_RGBA; } - break; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a RGB32 buffer, reading into a RGB24 buffer instead."); + new_colours = ZM_COLOUR_RGB24; + readjpg_dcinfo->out_color_space = JCS_RGB; + new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif - case ZM_COLOUR_RGB24: - default: + } else { + /* Assume RGB24/BGR24 */ new_colours = ZM_COLOUR_RGB24; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGR) { + if (p_pixfmt == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS readjpg_dcinfo->out_color_space = JCS_EXT_BGR; new_subpixelorder = ZM_SUBPIX_ORDER_BGR; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a BGR24 buffer, reading into a RGB24 buffer instead."); - cinfo->out_color_space = JCS_RGB; + readjpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif } else { @@ -1182,8 +1403,7 @@ bool Image::ReadJpeg(const std::string &filename, unsigned int p_colours, unsign readjpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; } - break; - } // end switch p_colours + } // end format dispatch if (WriteBuffer(new_width, new_height, new_colours, new_subpixelorder) == nullptr) { Error("Failed requesting writeable buffer for reading JPEG image."); @@ -1231,7 +1451,7 @@ bool Image::WriteJpeg(const std::string &filename, SystemTimePoint timestamp, bool on_blocking_abort) const { - if (config.colour_jpeg_files && (colours == ZM_COLOUR_GRAY8)) { + if (config.colour_jpeg_files && (imagePixFormat == AV_PIX_FMT_GRAY8)) { Image temp_image(*this); temp_image.Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); return temp_image.WriteJpeg(filename, quality_override, timestamp, on_blocking_abort); @@ -1297,28 +1517,25 @@ bool Image::WriteJpeg(const std::string &filename, cinfo->image_width = width; /* image width and height, in pixels */ cinfo->image_height = height; - switch (colours) { - case ZM_COLOUR_GRAY8: + if (imagePixFormat == AV_PIX_FMT_GRAY8) { cinfo->input_components = 1; cinfo->in_color_space = JCS_GRAYSCALE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(imagePixFormat)) { #ifdef JCS_EXTENSIONS cinfo->input_components = 4; - if (subpixelorder == ZM_SUBPIX_ORDER_RGBA) { + if (imagePixFormat == AV_PIX_FMT_RGBA) { cinfo->in_color_space = JCS_EXT_RGBX; - } else if (subpixelorder == ZM_SUBPIX_ORDER_BGRA) { + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { cinfo->in_color_space = JCS_EXT_BGRX; - } else if (subpixelorder == ZM_SUBPIX_ORDER_ARGB) { + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { cinfo->in_color_space = JCS_EXT_XRGB; - } else if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) { + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { cinfo->in_color_space = JCS_EXT_XBGR; } else { Warning("Unknown subpixelorder %d", subpixelorder); /* Assume RGBA */ cinfo->in_color_space = JCS_EXT_RGBX; } - break; #else Error("libjpeg-turbo is required for JPEG encoding directly from RGB32 source"); jpeg_abort_compress(cinfo); @@ -1327,10 +1544,14 @@ bool Image::WriteJpeg(const std::string &filename, fclose(outfile); return false; #endif - case ZM_COLOUR_RGB24: - default: + } else if (imagePixFormat == AV_PIX_FMT_YUV420P || imagePixFormat == AV_PIX_FMT_YUVJ420P + || imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P) { + cinfo->input_components = 3; + cinfo->in_color_space = JCS_YCbCr; + } else { + /* Assume RGB24/BGR24 */ cinfo->input_components = 3; - if (subpixelorder == ZM_SUBPIX_ORDER_BGR) { + if (imagePixFormat == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS cinfo->in_color_space = JCS_EXT_BGR; #else @@ -1341,21 +1562,11 @@ bool Image::WriteJpeg(const std::string &filename, fclose(outfile); return false; #endif - } else if (subpixelorder == ZM_SUBPIX_ORDER_YUV420P) { - cinfo->in_color_space = JCS_YCbCr; } else { /* Assume RGB */ - /* - #ifdef JCS_EXTENSIONS - cinfo->out_color_space = JCS_EXT_RGB; - #else - cinfo->out_color_space = JCS_RGB; - #endif - */ cinfo->in_color_space = JCS_RGB; } - break; - } // end switch(colours) + } // end format dispatch jpeg_set_defaults(cinfo); jpeg_set_quality(cinfo, quality, FALSE); @@ -1398,21 +1609,42 @@ bool Image::WriteJpeg(const std::string &filename, jpeg_write_marker(cinfo, EXIF_CODE, (const JOCTET *) exiftimes, sizeof(exiftimes)); } - if (subpixelorder == ZM_SUBPIX_ORDER_YUV420P) { - std::vector tmprowbuf(width * 3); - JSAMPROW row_pointer = &tmprowbuf[0]; /* pointer to a single row */ + if (imagePixFormat == AV_PIX_FMT_YUV420P || imagePixFormat == AV_PIX_FMT_YUVJ420P + || imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P) { + // Planar YUV: interleave Y/U/V plane rows into per-scanline YCbCr triples. + // The previous code here unpacked the buffer as if it were packed YUYV + // 4:2:2, which read way past the W*H*1.5 YUV420P buffer end on every + // frame — a latent crash that became reachable when image_buffer was + // pinned to YUV420P. Use av_image_fill_arrays for plane offsets so this + // works for both 4:2:0 and 4:2:2 layouts. + uint8_t *plane[4] = {nullptr, nullptr, nullptr, nullptr}; + int plane_linesizes[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(plane, plane_linesizes, buffer, imagePixFormat, width, height, 32) < 0) { + Error("WriteJpeg: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + jpeg_abort_compress(cinfo); + fl.l_type = F_UNLCK; + fcntl(raw_fd, F_SETLK, &fl); + fclose(outfile); + return false; + } + const bool is_422 = (imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P); + const unsigned chroma_v_shift = is_422 ? 0u : 1u; + + std::vector tmprow(static_cast(width) * 3); + JSAMPROW row_ptr = tmprow.data(); + while (cinfo->next_scanline < cinfo->image_height) { - unsigned i, j; - unsigned offset = cinfo->next_scanline * cinfo->image_width * 2; //offset to the correct row - for (i = 0, j = 0; i < cinfo->image_width * 2; i += 4, j += 6) { //input strides by 4 bytes, output strides by 6 (2 pixels) - tmprowbuf[j + 0] = buffer[offset + i + 0]; // Y (unique to this pixel) - tmprowbuf[j + 1] = buffer[offset + i + 1]; // U (shared between pixels) - tmprowbuf[j + 2] = buffer[offset + i + 3]; // V (shared between pixels) - tmprowbuf[j + 3] = buffer[offset + i + 2]; // Y (unique to this pixel) - tmprowbuf[j + 4] = buffer[offset + i + 1]; // U (shared between pixels) - tmprowbuf[j + 5] = buffer[offset + i + 3]; // V (shared between pixels) + const unsigned y = cinfo->next_scanline; + const unsigned cy = y >> chroma_v_shift; + const uint8_t *yrow = plane[0] + static_cast(y) * plane_linesizes[0]; + const uint8_t *urow = plane[1] + static_cast(cy) * plane_linesizes[1]; + const uint8_t *vrow = plane[2] + static_cast(cy) * plane_linesizes[2]; + for (unsigned x = 0; x < width; ++x) { + tmprow[x * 3 + 0] = yrow[x]; + tmprow[x * 3 + 1] = urow[x >> 1]; + tmprow[x * 3 + 2] = vrow[x >> 1]; } - jpeg_write_scanlines(cinfo, &row_pointer, 1); + jpeg_write_scanlines(cinfo, &row_ptr, 1); } } else { JSAMPROW row_pointer = buffer; /* pointer to a single row */ @@ -1435,6 +1667,7 @@ bool Image::WriteJpeg(const std::string &filename, bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p_colours, unsigned int p_subpixelorder) { unsigned int new_width, new_height, new_colours, new_subpixelorder; + AVPixelFormat p_pixfmt = zm_pixformat_from_colours(p_colours, p_subpixelorder); if (!decodejpg_dcinfo) { decodejpg_dcinfo = new jpeg_decompress_struct; @@ -1473,22 +1706,20 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p width, height, new_width, new_height); } - switch (p_colours) { - case ZM_COLOUR_GRAY8: + if (p_pixfmt == AV_PIX_FMT_GRAY8) { decodejpg_dcinfo->out_color_space = JCS_GRAYSCALE; new_colours = ZM_COLOUR_GRAY8; new_subpixelorder = ZM_SUBPIX_ORDER_NONE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(p_pixfmt)) { #ifdef JCS_EXTENSIONS new_colours = ZM_COLOUR_RGB32; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGRA) { + if (p_pixfmt == AV_PIX_FMT_BGRA) { decodejpg_dcinfo->out_color_space = JCS_EXT_BGRX; new_subpixelorder = ZM_SUBPIX_ORDER_BGRA; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ARGB) { + } else if (p_pixfmt == AV_PIX_FMT_ARGB) { decodejpg_dcinfo->out_color_space = JCS_EXT_XRGB; new_subpixelorder = ZM_SUBPIX_ORDER_ARGB; - } else if (p_subpixelorder == ZM_SUBPIX_ORDER_ABGR) { + } else if (p_pixfmt == AV_PIX_FMT_ABGR) { decodejpg_dcinfo->out_color_space = JCS_EXT_XBGR; new_subpixelorder = ZM_SUBPIX_ORDER_ABGR; } else { @@ -1496,20 +1727,22 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p decodejpg_dcinfo->out_color_space = JCS_EXT_RGBX; new_subpixelorder = ZM_SUBPIX_ORDER_RGBA; } - break; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a RGB32 buffer, reading into a RGB24 buffer instead."); + new_colours = ZM_COLOUR_RGB24; + decodejpg_dcinfo->out_color_space = JCS_RGB; + new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif - case ZM_COLOUR_RGB24: - default: + } else { + /* Assume RGB24/BGR24 */ new_colours = ZM_COLOUR_RGB24; - if (p_subpixelorder == ZM_SUBPIX_ORDER_BGR) { + if (p_pixfmt == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS decodejpg_dcinfo->out_color_space = JCS_EXT_BGR; new_subpixelorder = ZM_SUBPIX_ORDER_BGR; #else Warning("libjpeg-turbo is required for reading a JPEG directly into a BGR24 buffer, reading into a RGB24 buffer instead."); - cinfo->out_color_space = JCS_RGB; + decodejpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; #endif } else { @@ -1524,8 +1757,7 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p decodejpg_dcinfo->out_color_space = JCS_RGB; new_subpixelorder = ZM_SUBPIX_ORDER_RGB; } - break; - } // end switch + } // end format dispatch if (WriteBuffer(new_width, new_height, new_colours, new_subpixelorder) == nullptr) { Error("Failed requesting writeable buffer for reading JPEG image."); @@ -1547,7 +1779,7 @@ bool Image::DecodeJpeg(const JOCTET *inbuffer, int inbuffer_size, unsigned int p } bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_override) const { - if ( config.colour_jpeg_files && (colours == ZM_COLOUR_GRAY8) ) { + if ( config.colour_jpeg_files && (imagePixFormat == AV_PIX_FMT_GRAY8) ) { Image temp_image(*this); temp_image.Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); return temp_image.EncodeJpeg(outbuffer, outbuffer_size, quality_override); @@ -1572,37 +1804,41 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov cinfo->image_width = width; /* image width and height, in pixels */ cinfo->image_height = height; - switch ( colours ) { - case ZM_COLOUR_GRAY8: + const bool is_yuv_planar = + (imagePixFormat == AV_PIX_FMT_YUV420P || imagePixFormat == AV_PIX_FMT_YUVJ420P + || imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P); + + if (imagePixFormat == AV_PIX_FMT_GRAY8) { cinfo->input_components = 1; cinfo->in_color_space = JCS_GRAYSCALE; - break; - case ZM_COLOUR_RGB32: + } else if (zm_is_rgb32(imagePixFormat)) { #ifdef JCS_EXTENSIONS cinfo->input_components = 4; - if ( subpixelorder == ZM_SUBPIX_ORDER_RGBA ) { + if (imagePixFormat == AV_PIX_FMT_RGBA) { cinfo->in_color_space = JCS_EXT_RGBX; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { cinfo->in_color_space = JCS_EXT_BGRX; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_ARGB ) { + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { cinfo->in_color_space = JCS_EXT_XRGB; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_ABGR ) { + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { cinfo->in_color_space = JCS_EXT_XBGR; } else { Warning("unknown subpixelorder %d", subpixelorder); /* Assume RGBA */ cinfo->in_color_space = JCS_EXT_RGBX; } - break; #else Error("libjpeg-turbo is required for JPEG encoding directly from RGB32 source"); jpeg_abort_compress(cinfo); return false; #endif - case ZM_COLOUR_RGB24: - default: + } else if (is_yuv_planar) { + cinfo->input_components = 3; + cinfo->in_color_space = JCS_YCbCr; + } else { + /* Assume RGB24/BGR24 */ cinfo->input_components = 3; - if ( subpixelorder == ZM_SUBPIX_ORDER_BGR ) { + if (imagePixFormat == AV_PIX_FMT_BGR24) { #ifdef JCS_EXTENSIONS cinfo->in_color_space = JCS_EXT_BGR; #else @@ -1612,17 +1848,9 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov #endif } else { /* Assume RGB */ - /* - #ifdef JCS_EXTENSIONS - cinfo->out_color_space = JCS_EXT_RGB; - #else - cinfo->out_color_space = JCS_RGB; - #endif - */ cinfo->in_color_space = JCS_RGB; } - break; - } // end switch + } // end format dispatch jpeg_set_defaults(cinfo); jpeg_set_quality(cinfo, quality, FALSE); @@ -1630,10 +1858,44 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, size_t *outbuffer_size, int quality_ov jpeg_start_compress(cinfo, TRUE); - JSAMPROW row_pointer = buffer; - while ( cinfo->next_scanline < cinfo->image_height ) { - jpeg_write_scanlines(cinfo, &row_pointer, 1); - row_pointer += linesize; + if (is_yuv_planar) { + // Planar YUV: interleave Y/U/V plane rows into per-scanline YCbCr triples + // for libjpeg's JCS_YCbCr input. av_image_fill_arrays computes plane + // pointers and per-plane linesizes from the AVPixelFormat, so this works + // for both 4:2:0 (YUV420P/YUVJ420P) and 4:2:2 (YUV422P/YUVJ422P) without + // hand-coded plane offsets. + uint8_t *plane[4] = {nullptr, nullptr, nullptr, nullptr}; + int plane_linesizes[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(plane, plane_linesizes, buffer, imagePixFormat, width, height, 32) < 0) { + Error("EncodeJpeg: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + jpeg_abort_compress(cinfo); + return false; + } + const bool is_422 = (imagePixFormat == AV_PIX_FMT_YUV422P || imagePixFormat == AV_PIX_FMT_YUVJ422P); + const unsigned chroma_v_shift = is_422 ? 0u : 1u; // 4:2:0 halves vertical too + + std::vector tmprow(static_cast(width) * 3); + JSAMPROW row_ptr = tmprow.data(); + + while (cinfo->next_scanline < cinfo->image_height) { + const unsigned y = cinfo->next_scanline; + const unsigned cy = y >> chroma_v_shift; + const uint8_t *yrow = plane[0] + static_cast(y) * plane_linesizes[0]; + const uint8_t *urow = plane[1] + static_cast(cy) * plane_linesizes[1]; + const uint8_t *vrow = plane[2] + static_cast(cy) * plane_linesizes[2]; + for (unsigned x = 0; x < width; ++x) { + tmprow[x * 3 + 0] = yrow[x]; + tmprow[x * 3 + 1] = urow[x >> 1]; // chroma is 2x horizontal subsampled in 4:2:0/4:2:2 + tmprow[x * 3 + 2] = vrow[x >> 1]; + } + jpeg_write_scanlines(cinfo, &row_ptr, 1); + } + } else { + JSAMPROW row_pointer = buffer; + while ( cinfo->next_scanline < cinfo->image_height ) { + jpeg_write_scanlines(cinfo, &row_pointer, 1); + row_pointer += linesize; + } } jpeg_finish_compress(cinfo); @@ -1709,159 +1971,144 @@ void Image::Overlay( const Image &image ) { width, height, image.width, image.height); } - if ( colours == image.colours && subpixelorder != image.subpixelorder ) { - Warning("Attempt to overlay images of same format but with different subpixel order %d != %d.", - subpixelorder, image.subpixelorder); - } - - /* Grayscale on top of grayscale - complete */ - if ( colours == ZM_COLOUR_GRAY8 && image.colours == ZM_COLOUR_GRAY8 ) { - const uint8_t* const max_ptr = buffer+size; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( *psrc ) { - *pdest = *psrc; + // Pre-AVPixelFormat the (colours, subpixelorder) pair was the canonical + // format identifier and a colours-match-but-subpixelorder-mismatch warning + // was meaningful. With imagePixFormat now canonical the old check fires + // false positives whenever GRAY8 (colours=1, subpixelorder=NONE=2) is + // overlaid onto YUV420P (colours=1 due to the GRAY8/YUV420P alias + // collision, subpixelorder=YUV420P=11) — the dispatch below handles this + // case correctly via zm_bytes_per_pixel(...) == 1. Only warn if the + // AVPixelFormat actually matches but the ZM metadata diverges, which would + // indicate a real format-tracking bug. + if (imagePixFormat == image.imagePixFormat + && (colours != image.colours || subpixelorder != image.subpixelorder)) { + Warning("Overlay: imagePixFormat matches (%s) but ZM (colours,subpixelorder) " + "diverges: (%u,%u) vs (%u,%u) — stale metadata?", + av_get_pix_fmt_name(imagePixFormat), + colours, subpixelorder, image.colours, image.subpixelorder); + } + + // Drive every branch row-by-row using each image's own linesize. Walking + // linearly with `buffer + size` as the end was unsafe in two ways: + // (a) source and destination linesizes can differ (e.g. one came in via + // AssignDirect with a held buffer at a non-FFALIGN stride), so a shared + // index drifted across rows; (b) the destination's `size` includes chroma + // planes for planar YUV destinations, so iterating to `buffer + size` + // walked past the source's Y plane and clobbered the destination's + // chroma. After Colourise() the destination's linesize is updated to the + // new format's stride, so we re-read it inside each branch. + + /* Grayscale/YUV420 on top of grayscale/YUV420 - complete */ + if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { + // Overlay only the luma/primary plane. Width is shared (panic above). + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc++, pdest++) { + if (*psrc) *pdest = *psrc; } - pdest++; - psrc++; } - /* RGB24 on top of grayscale - convert to same format first - complete */ - } else if ( colours == ZM_COLOUR_GRAY8 && image.colours == ZM_COLOUR_RGB24 ) { + /* RGB24 on top of grayscale/YUV420 - convert to same format first - complete */ + } else if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_is_rgb24(image.imagePixFormat) ) { Colourise(image.colours, image.subpixelorder); - const uint8_t* const max_ptr = buffer+size; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc) ) { - RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); - GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); - BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc += 3, pdest += 3) { + if (RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc)) { + RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); + GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); + BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + } } - pdest += 3; - psrc += 3; } - /* RGB32 on top of grayscale - convert to same format first - complete */ - } else if ( colours == ZM_COLOUR_GRAY8 && image.colours == ZM_COLOUR_RGB32 ) { + /* RGB32 on top of grayscale/YUV420 - convert to same format first - complete */ + } else if ( zm_bytes_per_pixel(imagePixFormat) == 1 && zm_is_rgb32(image.imagePixFormat) ) { Colourise(image.colours, image.subpixelorder); - const Rgb* const max_ptr = (Rgb*)(buffer+size); - const Rgb* prsrc = (Rgb*)image.buffer; - Rgb* prdest = (Rgb*)buffer; - - if ( subpixelorder == ZM_SUBPIX_ORDER_RGBA || subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { - /* RGB\BGR\RGBA\BGRA subpixel order - Alpha byte is last */ - while ( prdest < max_ptr) { - if ( RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc) ) { - *prdest = *prsrc; - } - prdest++; - prsrc++; - } - } else { - /* ABGR\ARGB subpixel order - Alpha byte is first */ - while ( prdest < max_ptr) { - if ( RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc) ) { - *prdest = *prsrc; + const bool alpha_last = (imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA); + for (unsigned int y = 0; y < height; y++) { + const Rgb *prsrc = (const Rgb *)(image.buffer + y * image.linesize); + Rgb *prdest = (Rgb *)(buffer + y * linesize); + for (unsigned int x = 0; x < width; x++, prsrc++, prdest++) { + if (alpha_last) { + if (RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc)) + *prdest = *prsrc; + } else { + if (RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc)) + *prdest = *prsrc; } - prdest++; - prsrc++; } } - /* Grayscale on top of RGB24 - complete */ - } else if ( colours == ZM_COLOUR_RGB24 && image.colours == ZM_COLOUR_GRAY8 ) { - const uint8_t* const max_ptr = buffer+size; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( *psrc ) { - RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = *psrc; + /* Grayscale/YUV420 on top of RGB24 - complete */ + } else if ( zm_is_rgb24(imagePixFormat) && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc++, pdest += 3) { + if (*psrc) { + RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = *psrc; + } } - pdest += 3; - psrc++; } /* RGB24 on top of RGB24 - not complete. need to take care of different subpixel orders */ - } else if ( colours == ZM_COLOUR_RGB24 && image.colours == ZM_COLOUR_RGB24 ) { - const uint8_t* const max_ptr = buffer+size; - const uint8_t* psrc = image.buffer; - uint8_t* pdest = buffer; - - while ( pdest < max_ptr ) { - if ( RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc) ) { - RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); - GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); - BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + } else if ( zm_is_rgb24(imagePixFormat) && zm_is_rgb24(image.imagePixFormat) ) { + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + uint8_t *pdest = buffer + y * linesize; + for (unsigned int x = 0; x < width; x++, psrc += 3, pdest += 3) { + if (RED_PTR_RGBA(psrc) || GREEN_PTR_RGBA(psrc) || BLUE_PTR_RGBA(psrc)) { + RED_PTR_RGBA(pdest) = RED_PTR_RGBA(psrc); + GREEN_PTR_RGBA(pdest) = GREEN_PTR_RGBA(psrc); + BLUE_PTR_RGBA(pdest) = BLUE_PTR_RGBA(psrc); + } } - pdest += 3; - psrc += 3; } /* RGB32 on top of RGB24 - TO BE DONE */ - } else if ( colours == ZM_COLOUR_RGB24 && image.colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb24(imagePixFormat) && zm_is_rgb32(image.imagePixFormat) ) { Error("Overlay of RGB32 on top of RGB24 is not supported."); - /* Grayscale on top of RGB32 - complete */ - } else if ( colours == ZM_COLOUR_RGB32 && image.colours == ZM_COLOUR_GRAY8 ) { - const Rgb* const max_ptr = (Rgb*)(buffer+size); - Rgb* prdest = (Rgb*)buffer; - const uint8_t* psrc = image.buffer; - - if ( subpixelorder == ZM_SUBPIX_ORDER_RGBA || subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { - /* RGBA\BGRA subpixel order - Alpha byte is last */ - while ( prdest < max_ptr ) { - if ( *psrc ) { - RED_PTR_RGBA(prdest) = *psrc; - //RED_PTR_RGBA(prdest) = GREEN_PTR_RGBA(prdest) = BLUE_PTR_RGBA(prdest) = *psrc; - } - prdest++; - psrc++; - } - } else { - /* ABGR\ARGB subpixel order - Alpha byte is first */ - while ( prdest < max_ptr ) { - if ( *psrc ) { - RED_PTR_ABGR(prdest) = GREEN_PTR_ABGR(prdest) = BLUE_PTR_ABGR(prdest) = *psrc; + /* Grayscale/YUV420 on top of RGB32 - complete */ + } else if ( zm_is_rgb32(imagePixFormat) && zm_bytes_per_pixel(image.imagePixFormat) == 1 ) { + const bool alpha_last = (imagePixFormat == AV_PIX_FMT_RGBA || imagePixFormat == AV_PIX_FMT_BGRA); + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = image.buffer + y * image.linesize; + Rgb *prdest = (Rgb *)(buffer + y * linesize); + for (unsigned int x = 0; x < width; x++, psrc++, prdest++) { + if (*psrc) { + if (alpha_last) { + RED_PTR_RGBA(prdest) = *psrc; + } else { + RED_PTR_ABGR(prdest) = GREEN_PTR_ABGR(prdest) = BLUE_PTR_ABGR(prdest) = *psrc; + } } - prdest++; - psrc++; } } /* RGB24 on top of RGB32 - TO BE DONE */ - } else if ( colours == ZM_COLOUR_RGB32 && image.colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb32(imagePixFormat) && zm_is_rgb24(image.imagePixFormat) ) { Error("Overlay of RGB24 on top of RGB32 is not supported."); /* RGB32 on top of RGB32 - not complete. need to take care of different subpixel orders */ - } else if ( colours == ZM_COLOUR_RGB32 && image.colours == ZM_COLOUR_RGB32 ) { - const Rgb* const max_ptr = (Rgb*)(buffer+size); - Rgb* prdest = (Rgb*)buffer; - const Rgb* prsrc = (Rgb*)image.buffer; - - if ( image.subpixelorder == ZM_SUBPIX_ORDER_RGBA || image.subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { - /* RGB\BGR\RGBA\BGRA subpixel order - Alpha byte is last */ - while ( prdest < max_ptr ) { - if ( RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc) ) { - *prdest = *prsrc; - } - prdest++; - prsrc++; - } - } else { - /* ABGR\ARGB subpixel order - Alpha byte is first */ - while ( prdest < max_ptr ) { - if ( RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc) ) { - *prdest = *prsrc; + } else if ( zm_is_rgb32(imagePixFormat) && zm_is_rgb32(image.imagePixFormat) ) { + const bool alpha_last = (image.imagePixFormat == AV_PIX_FMT_RGBA || image.imagePixFormat == AV_PIX_FMT_BGRA); + for (unsigned int y = 0; y < height; y++) { + const Rgb *prsrc = (const Rgb *)(image.buffer + y * image.linesize); + Rgb *prdest = (Rgb *)(buffer + y * linesize); + for (unsigned int x = 0; x < width; x++, prsrc++, prdest++) { + if (alpha_last) { + if (RED_PTR_RGBA(prsrc) || GREEN_PTR_RGBA(prsrc) || BLUE_PTR_RGBA(prsrc)) + *prdest = *prsrc; + } else { + if (RED_PTR_ABGR(prsrc) || GREEN_PTR_ABGR(prsrc) || BLUE_PTR_ABGR(prsrc)) + *prdest = *prsrc; } - prdest++; - prsrc++; } } } @@ -1887,28 +2134,31 @@ void Image::Overlay( const Image &image, const unsigned int lo_x, const unsigned unsigned int hi_x = (lo_x+image.width)-1; unsigned int hi_y = (lo_y+image.height-1); - if ( colours == ZM_COLOUR_GRAY8 ) { - const uint8_t *psrc = image.buffer; + // Both buffers may carry per-row padding (linesize > width*colours after + // FFALIGN). Rebase source and destination per row using each image's + // own linesize so the sequential psrc++ advance can't drift across rows. + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - uint8_t *pdest = &buffer[(y*width)+lo_x]; + const uint8_t *psrc = image.buffer + (y - lo_y) * image.linesize; + uint8_t *pdest = buffer + y * linesize + lo_x; for ( unsigned int x = lo_x; x <= hi_x; x++ ) { *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { - const uint8_t *psrc = image.buffer; + } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - uint8_t *pdest = &buffer[colours*((y*width)+lo_x)]; + const uint8_t *psrc = image.buffer + (y - lo_y) * image.linesize; + uint8_t *pdest = buffer + y * linesize + lo_x * colours; for ( unsigned int x = lo_x; x <= hi_x; x++ ) { *pdest++ = *psrc++; *pdest++ = *psrc++; *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { - const Rgb *psrc = (Rgb*)(image.buffer); + } else if ( zm_is_rgb32(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - Rgb *pdest = (Rgb*)&buffer[((y*width)+lo_x)<<2]; + const Rgb *psrc = (const Rgb*)(image.buffer + (y - lo_y) * image.linesize); + Rgb *pdest = (Rgb*)(buffer + y * linesize + (lo_x << 2)); for ( unsigned int x = lo_x; x <= hi_x; x++ ) { *pdest++ = *psrc++; } @@ -2076,37 +2326,49 @@ bool Image::Delta(const Image &image, Image* targetimage) const { TimePoint start = std::chrono::steady_clock::now(); #endif - switch ( colours ) { - case ZM_COLOUR_RGB24: - if ( subpixelorder == ZM_SUBPIX_ORDER_BGR ) { - /* BGR subpixel order */ - (*delta8_bgr)(buffer, image.buffer, pdiff, pixels); - } else { - /* Assume RGB subpixel order */ - (*delta8_rgb)(buffer, image.buffer, pdiff, pixels); - } - break; - case ZM_COLOUR_RGB32: - if ( subpixelorder == ZM_SUBPIX_ORDER_ARGB ) { - /* ARGB subpixel order */ - (*delta8_argb)(buffer, image.buffer, pdiff, pixels); - } else if(subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - (*delta8_abgr)(buffer, image.buffer, pdiff, pixels); - } else if(subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - (*delta8_bgra)(buffer, image.buffer, pdiff, pixels); - } else { - /* Assume RGBA subpixel order */ - (*delta8_rgba)(buffer, image.buffer, pdiff, pixels); + // The delta8_* SIMD helpers process N contiguous bytes / pixels without + // any concept of row stride. With FFALIGN'd linesize, an image's buffer + // may have per-row padding (linesize > width*bpp), so passing the full + // pixel count would read padding bytes as image data and produce wrong + // motion deltas. Drive the helpers row by row instead — width pixels per + // row of `this`, of `image`, and of the GRAY8 target, each at their own + // linesize stride. + const unsigned int src_linesize = linesize; + const unsigned int img_linesize = image.linesize; + const unsigned int dst_linesize = targetimage->LineSize(); + + auto delta_row = [&](void (*fn)(const uint8_t *, const uint8_t *, uint8_t *, unsigned long)) { + for (unsigned int y = 0; y < height; y++) { + fn(buffer + y * src_linesize, + image.buffer + y * img_linesize, + pdiff + y * dst_linesize, + width); } - break; - case ZM_COLOUR_GRAY8: - (*delta8_gray8)(buffer, image.buffer, pdiff, pixels); - break; - default: - Panic("Delta called with unexpected colours: %d",colours); - break; + }; + + if (imagePixFormat == AV_PIX_FMT_BGR24) { + /* BGR subpixel order */ + delta_row(delta8_bgr); + } else if (zm_is_rgb24(imagePixFormat)) { + /* Assume RGB subpixel order */ + delta_row(delta8_rgb); + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { + /* ARGB subpixel order */ + delta_row(delta8_argb); + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { + /* ABGR subpixel order */ + delta_row(delta8_abgr); + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { + /* BGRA subpixel order */ + delta_row(delta8_bgra); + } else if (zm_is_rgb32(imagePixFormat)) { + /* Assume RGBA subpixel order */ + delta_row(delta8_rgba); + } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { + delta_row(delta8_gray8); + } else { + Panic("Delta called with unexpected pixel format %d (%s); legacy colours=%d", + imagePixFormat, zm_get_pix_fmt_name(imagePixFormat), colours); } #ifdef ZM_IMAGE_PROFILING @@ -2158,17 +2420,21 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour const uint8_t pixel_bw_col = pixel_colour & 0xff; const Rgb pixel_rgb_col = rgb_convert(pixel_colour,subpixelorder); - unsigned char *ptr = &buffer[0]; unsigned int i = 0; for ( unsigned int y = 0; y < height; y++ ) { - if ( colours == ZM_COLOUR_GRAY8 ) { + // Re-base the row pointer each iteration: with FFALIGN'd linesize there + // may be per-row padding between width*colours and linesize, and a + // monotonically-incremented ptr would walk into that padding (and + // misaddress subsequent rows). + unsigned char *ptr = buffer + y * linesize; + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int x = 0; x < width; x++, ptr++ ) { if ( p_bitmask[i] ) *ptr = pixel_bw_col; i++; } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int x = 0; x < width; x++, ptr += colours ) { if ( p_bitmask[i] ) { RED_PTR_RGBA(ptr) = pixel_r_col; @@ -2177,7 +2443,7 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour } i++; } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for ( unsigned int x = 0; x < width; x++, ptr += colours ) { Rgb *temp_ptr = (Rgb*)ptr; if ( p_bitmask[i] ) @@ -2189,6 +2455,57 @@ void Image::MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour return; } } // end foreach y + + // Planar YUV formats: the loop above masked only the Y plane, leaving the + // chroma (U/V) planes intact. Source colour bleeds through the mask + // because chroma still carries the original hue — a privacy leak. Set + // chroma samples covering any masked Y pixel to the neutral value (128). + // For YUV420P chroma is subsampled 2:2; for YUV422P it is 2:1 horizontal. + // Use the conservative rule: if any covered Y bit is set in the bitmask, + // neutralise the corresponding chroma sample. + const bool planar_420 = zm_is_yuv420(imagePixFormat); + const bool planar_422 = (imagePixFormat == AV_PIX_FMT_YUV422P + || imagePixFormat == AV_PIX_FMT_YUVJ422P); + if (planar_420 || planar_422) { + uint8_t *plane_ptrs[4] = {nullptr, nullptr, nullptr, nullptr}; + int plane_linesizes[4] = {0, 0, 0, 0}; + int fill_size = av_image_fill_arrays(plane_ptrs, plane_linesizes, buffer, + imagePixFormat, width, height, 32); + if (fill_size < 0 || !plane_ptrs[1] || !plane_ptrs[2]) { + Warning("MaskPrivacy: av_image_fill_arrays failed for %s; chroma not masked", + av_get_pix_fmt_name(imagePixFormat)); + return; + } + // Chroma plane dimensions use ceiling division so an odd width/height + // doesn't drop the last chroma column/row — that would leave a strip + // along the right/bottom edge with original colour bleeding through + // the privacy mask. + const unsigned int c_height = planar_420 ? (height + 1) / 2 : height; + const unsigned int c_width = (width + 1) / 2; + const int u_stride = plane_linesizes[1]; + const int v_stride = plane_linesizes[2]; + uint8_t *u_plane = plane_ptrs[1]; + uint8_t *v_plane = plane_ptrs[2]; + + for (unsigned int cy = 0; cy < c_height; cy++) { + for (unsigned int cx = 0; cx < c_width; cx++) { + const unsigned int y_x0 = cx * 2; + const unsigned int y_x1 = y_x0 + 1; + const unsigned int y_top = planar_420 ? cy * 2 : cy; + const unsigned int y_bot = planar_420 ? y_top + 1 : y_top; + bool mask_hit = p_bitmask[y_top * width + y_x0] + || (y_x1 < width && p_bitmask[y_top * width + y_x1]); + if (planar_420 && !mask_hit && y_bot < height) { + mask_hit = p_bitmask[y_bot * width + y_x0] + || (y_x1 < width && p_bitmask[y_bot * width + y_x1]); + } + if (mask_hit) { + u_plane[cy * u_stride + cx] = 128; + v_plane[cy * v_stride + cx] = 128; + } + } + } + } } /* RGB32 compatible: complete */ @@ -2228,8 +2545,11 @@ void Image::Annotate( for (const std::string &line : lines) { uint32 x = x0; - if (colours == ZM_COLOUR_GRAY8) { - uint8 *ptr = &buffer[(y * width) + x0]; + // Use linesize (which may include FFALIGN'd padding) for row stride + // rather than width*bytesPerPixel, so non-32-aligned widths don't + // skew annotation placement into per-row padding. + if (zm_bytes_per_pixel(imagePixFormat) == 1) { + uint8 *ptr = &buffer[y * linesize + x0]; for (char c : line) { for (uint64 cp_row : font_variant.GetCodepoint(c)) { if (bg_colour != kRGBTransparent) { @@ -2241,18 +2561,18 @@ void Image::Annotate( *(ptr + column_idx) = fg_colour & 0xff; cp_row = cp_row & (cp_row - 1); } - ptr += width; + ptr += linesize; } - ptr -= (width * char_height); + ptr -= (linesize * char_height); ptr += char_width; x += char_width; if (x >= width) { break; } } - } else if (colours == ZM_COLOUR_RGB24) { + } else if (zm_is_rgb24(imagePixFormat)) { constexpr uint8 bytesPerPixel = 3; - uint8 *ptr = &buffer[((y * width) + x0) * bytesPerPixel]; + uint8 *ptr = &buffer[y * linesize + x0 * bytesPerPixel]; for (char c : line) { for (uint64 cp_row : font_variant.GetCodepoint(c)) { @@ -2273,18 +2593,20 @@ void Image::Annotate( BLUE_PTR_RGBA(colour_ptr) = BLUE_VAL_RGBA(fg_colour); cp_row = cp_row & (cp_row - 1); } - ptr += width * bytesPerPixel; + ptr += linesize; } - ptr -= (width * char_height * bytesPerPixel); + ptr -= (linesize * char_height); ptr += char_width * bytesPerPixel; x += char_width; if (x >= width) { break; } } - } else if (colours == ZM_COLOUR_RGB32) { + } else if (zm_is_rgb32(imagePixFormat)) { constexpr uint8 bytesPerPixel = 4; - Rgb *ptr = reinterpret_cast(&buffer[((y * width) + x0) * bytesPerPixel]); + // Rgb is 4 bytes (rgb32); convert linesize bytes -> Rgb stride. + const unsigned int rgb_stride = linesize / sizeof(Rgb); + Rgb *ptr = reinterpret_cast(&buffer[y * linesize + x0 * bytesPerPixel]); for (char c : line) { for (uint64 cp_row : font_variant.GetCodepoint(c)) { @@ -2297,9 +2619,9 @@ void Image::Annotate( *(ptr + column_idx) = fg_rgb_col; cp_row = cp_row & (cp_row - 1); } - ptr += width; + ptr += rgb_stride; } - ptr -= (width * char_height); + ptr -= (rgb_stride * char_height); ptr += char_width; x += char_width; if (x >= width) { @@ -2336,54 +2658,78 @@ void Image::Timestamp(const char *label, SystemTimePoint when, const Vector2 &co void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqsubpixelorder) { Debug(9, "Colourise: Req colours: %u Req subpixel order: %u Current colours: %u Current subpixel order: %u",p_reqcolours,p_reqsubpixelorder,colours,subpixelorder); - if ( colours != ZM_COLOUR_GRAY8) { + if ( imagePixFormat != AV_PIX_FMT_GRAY8) { Warning("Target image is already colourised, colours: %u",colours); return; } - if ( p_reqcolours == ZM_COLOUR_RGB32 ) { - /* RGB32 */ - Rgb* new_buffer = (Rgb*)AllocBuffer(pixels*sizeof(Rgb)); + AVPixelFormat p_req_pixfmt = zm_pixformat_from_colours(p_reqcolours, p_reqsubpixelorder); + + // AssignDirect validates buffer_size against av_image_get_buffer_size(..., + // align=32) and uses an FFALIGN'd linesize. Allocate accordingly and copy + // row by row using the source (GRAY8) and destination (RGB) strides, so + // we don't undersize the output buffer for non-32-aligned widths and + // don't smear pixels across row boundaries. + int new_size_signed = av_image_get_buffer_size(p_req_pixfmt, width, height, 32); + int dst_linesize_signed = av_image_get_linesize(p_req_pixfmt, width, 0); + if (new_size_signed < 0 || dst_linesize_signed < 0) { + Error("Colourise: av_image sizing failed for %s %ux%u", + zm_get_pix_fmt_name(p_req_pixfmt), width, height); + return; + } + const size_t new_size = static_cast(new_size_signed); + const unsigned int dst_linesize = FFALIGN(dst_linesize_signed, 32); + const unsigned int src_linesize = linesize; - const uint8_t *psrc = buffer; - Rgb* pdest = new_buffer; - Rgb subpixel; - Rgb newpixel; + if ( zm_is_rgb32(p_req_pixfmt) ) { + /* RGB32 */ + Rgb* new_buffer = (Rgb*)AllocBuffer(new_size); - if ( p_reqsubpixelorder == ZM_SUBPIX_ORDER_ABGR || p_reqsubpixelorder == ZM_SUBPIX_ORDER_ARGB ) { + if ( p_req_pixfmt == AV_PIX_FMT_ABGR || p_req_pixfmt == AV_PIX_FMT_ARGB ) { /* ARGB\ABGR subpixel order. alpha byte is first (mem+0), so we need to shift the pixel left in the end */ - for ( unsigned int i=0; i < pixels; i++ ) { - newpixel = subpixel = psrc[i]; - newpixel = (newpixel<<8) | subpixel; - newpixel = (newpixel<<8) | subpixel; - pdest[i] = (newpixel<<8); + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = buffer + y * src_linesize; + Rgb *pdest = (Rgb *)((uint8_t *)new_buffer + y * dst_linesize); + for (unsigned int x = 0; x < width; x++) { + Rgb subpixel = psrc[x]; + Rgb newpixel = subpixel; + newpixel = (newpixel << 8) | subpixel; + newpixel = (newpixel << 8) | subpixel; + pdest[x] = (newpixel << 8); + } } } else { /* RGBA\BGRA subpixel order, alpha byte is last (mem+3) */ - for ( unsigned int i=0; i < pixels; i++ ) { - newpixel = subpixel = psrc[i]; - newpixel = (newpixel<<8) | subpixel; - newpixel = (newpixel<<8) | subpixel; - pdest[i] = newpixel; + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = buffer + y * src_linesize; + Rgb *pdest = (Rgb *)((uint8_t *)new_buffer + y * dst_linesize); + for (unsigned int x = 0; x < width; x++) { + Rgb subpixel = psrc[x]; + Rgb newpixel = subpixel; + newpixel = (newpixel << 8) | subpixel; + newpixel = (newpixel << 8) | subpixel; + pdest[x] = newpixel; + } } } /* Directly assign the new buffer and make sure it will be freed when not needed anymore */ - AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, (uint8_t*)new_buffer, pixels*4, ZM_BUFTYPE_ZM); + AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, (uint8_t*)new_buffer, new_size, ZM_BUFTYPE_ZM); - } else if ( p_reqcolours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(p_req_pixfmt) ) { /* RGB24 */ - uint8_t *new_buffer = AllocBuffer(pixels*3); - - uint8_t *pdest = new_buffer; - const uint8_t *psrc = buffer; + uint8_t *new_buffer = AllocBuffer(new_size); - for ( unsigned int i=0; i < (unsigned int)pixels; i++, pdest += 3 ) { - RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = psrc[i]; + for (unsigned int y = 0; y < height; y++) { + const uint8_t *psrc = buffer + y * src_linesize; + uint8_t *pdest = new_buffer + y * dst_linesize; + for (unsigned int x = 0; x < width; x++, pdest += 3) { + RED_PTR_RGBA(pdest) = GREEN_PTR_RGBA(pdest) = BLUE_PTR_RGBA(pdest) = psrc[x]; + } } /* Directly assign the new buffer and make sure it will be freed when not needed anymore */ - AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, new_buffer, pixels*3, ZM_BUFTYPE_ZM); + AssignDirect( width, height, p_reqcolours, p_reqsubpixelorder, new_buffer, new_size, ZM_BUFTYPE_ZM); } else { Error("Colourise called with unexpected colours: %d", colours); return; @@ -2392,10 +2738,10 @@ void Image::Colourise(const unsigned int p_reqcolours, const unsigned int p_reqs /* RGB32 compatible: complete */ void Image::DeColourise() { - const unsigned int src_colours = colours; + const AVPixelFormat src_pixfmt = imagePixFormat; const unsigned int src_subpixelorder = subpixelorder; - if ( src_colours == ZM_COLOUR_RGB32 && config.cpu_extensions && sse_version >= 35 ) { + if ( zm_is_rgb32(src_pixfmt) && config.cpu_extensions && sse_version >= 35 ) { /* Use SSSE3 functions */ switch (src_subpixelorder) { case ZM_SUBPIX_ORDER_BGRA: @@ -2414,7 +2760,7 @@ void Image::DeColourise() { } } else { /* Use standard functions */ - if ( src_colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(src_pixfmt) ) { if ( pixels % 16 ) { switch (src_subpixelorder) { case ZM_SUBPIX_ORDER_BGRA: @@ -2476,12 +2822,20 @@ void Image::DeColourise() { colours = ZM_COLOUR_GRAY8; subpixelorder = ZM_SUBPIX_ORDER_NONE; - size = width * height; + imagePixFormat = AV_PIX_FMT_GRAY8; + // Keep size and linesize consistent with the new format; downstream ops + // (Flip/Rotate allocate via size; row-stride loops use linesize) need both + // to match the GRAY8 32-byte-aligned layout, not the previous RGB sizing. + int new_size = av_image_get_buffer_size(AV_PIX_FMT_GRAY8, width, height, 32); + int new_linesize = av_image_get_linesize(AV_PIX_FMT_GRAY8, width, 0); + if (new_size > 0) size = static_cast(new_size); + if (new_linesize > 0) linesize = FFALIGN(new_linesize, 32); + update_function_pointers(); } /* RGB32 compatible: complete */ void Image::Fill( Rgb colour, const Box *limits ) { - if ( !(colours == ZM_COLOUR_GRAY8 || colours == ZM_COLOUR_RGB24 || colours == ZM_COLOUR_RGB32 ) ) { + if ( !(zm_bytes_per_pixel(imagePixFormat) == 1 || zm_is_rgb24(imagePixFormat) || zm_is_rgb32(imagePixFormat)) ) { Panic("Attempt to fill image with unexpected colours %d", colours); } @@ -2492,25 +2846,27 @@ void Image::Fill( Rgb colour, const Box *limits ) { unsigned int lo_y = limits ? limits->Lo().y_ : 0; unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; - if ( colours == ZM_COLOUR_GRAY8 ) { + // Use linesize as the row stride so non-32-aligned widths don't write + // into padding bytes or shift subsequent rows. + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[(y*width)+lo_x]; + unsigned char *p = &buffer[y * linesize + lo_x]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { *p = colour; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[colours*((y*width)+lo_x)]; + unsigned char *p = &buffer[y * linesize + lo_x * colours]; for ( unsigned int x = lo_x; x <= hi_x; x++, p += 3) { RED_PTR_RGBA(p) = RED_VAL_RGBA(colour); GREEN_PTR_RGBA(p) = GREEN_VAL_RGBA(colour); BLUE_PTR_RGBA(p) = BLUE_VAL_RGBA(colour); } } - } else if ( colours == ZM_COLOUR_RGB32 ) { /* RGB32 */ + } else if ( zm_is_rgb32(imagePixFormat) ) { /* RGB32 */ for ( unsigned int y = lo_y; y <= (unsigned int)hi_y; y++ ) { - Rgb *p = (Rgb*)&buffer[((y*width)+lo_x)<<2]; + Rgb *p = (Rgb*)&buffer[y * linesize + (lo_x << 2)]; for ( unsigned int x = lo_x; x <= (unsigned int)hi_x; x++, p++) { /* Fast, copies the entire pixel in a single pass */ @@ -2526,7 +2882,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { if ( density <= 1 ) return Fill(colour,limits); - if ( !(colours == ZM_COLOUR_GRAY8 || colours == ZM_COLOUR_RGB24 || colours == ZM_COLOUR_RGB32 ) ) { + if ( !(zm_bytes_per_pixel(imagePixFormat) == 1 || zm_is_rgb24(imagePixFormat) || zm_is_rgb32(imagePixFormat)) ) { Panic("Attempt to fill image with unexpected colours %d", colours); } @@ -2537,17 +2893,19 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { unsigned int lo_y = limits ? limits->Lo().y_ : 0; unsigned int hi_x = limits ? limits->Hi().x_ : width - 1; unsigned int hi_y = limits ? limits->Hi().y_ : height - 1; - if ( colours == ZM_COLOUR_GRAY8 ) { + // Use linesize as the row stride so non-32-aligned widths don't write + // into padding bytes or shift subsequent rows. + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[(y*width)+lo_x]; + unsigned char *p = &buffer[y * linesize + lo_x]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { if ( ( x == lo_x || x == hi_x || y == lo_y || y == hi_y ) || (!(x%density) && !(y%density) ) ) *p = colour; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - unsigned char *p = &buffer[colours*((y*width)+lo_x)]; + unsigned char *p = &buffer[y * linesize + lo_x * colours]; for ( unsigned int x = lo_x; x <= hi_x; x++, p += 3) { if ( ( x == lo_x || x == hi_x || y == lo_y || y == hi_y ) || (!(x%density) && !(y%density) ) ) { RED_PTR_RGBA(p) = RED_VAL_RGBA(colour); @@ -2556,9 +2914,9 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { } } } - } else if ( colours == ZM_COLOUR_RGB32 ) { /* RGB32 */ + } else if ( zm_is_rgb32(imagePixFormat) ) { /* RGB32 */ for ( unsigned int y = lo_y; y <= hi_y; y++ ) { - Rgb* p = (Rgb*)&buffer[((y*width)+lo_x)<<2]; + Rgb* p = (Rgb*)&buffer[y * linesize + (lo_x << 2)]; for ( unsigned int x = lo_x; x <= hi_x; x++, p++) { if ( ( x == lo_x || x == hi_x || y == lo_y || y == hi_y ) || (!(x%density) && !(y%density) ) ) @@ -2571,7 +2929,7 @@ void Image::Fill( Rgb colour, int density, const Box *limits ) { /* RGB32 compatible: complete */ void Image::Outline( Rgb colour, const Polygon &polygon ) { - if ( !(colours == ZM_COLOUR_GRAY8 || colours == ZM_COLOUR_RGB24 || colours == ZM_COLOUR_RGB32 ) ) { + if ( !(zm_bytes_per_pixel(imagePixFormat) == 1 || zm_is_rgb24(imagePixFormat) || zm_is_rgb32(imagePixFormat)) ) { Panic("Attempt to outline image with unexpected colours %d", colours); } @@ -2603,20 +2961,20 @@ void Image::Outline( Rgb colour, const Polygon &polygon ) { double x; int y, yinc = (y1(it->min_x); int32 hi_x = static_cast((it + 1)->min_x); - if (colours == ZM_COLOUR_GRAY8) { - uint8 *p = &buffer[(scan_line * width) + lo_x]; + if (zm_bytes_per_pixel(imagePixFormat) == 1) { + uint8 *p = &buffer[scan_line * linesize + lo_x]; for (int32 x = lo_x; x <= hi_x; x++, p++) { if (!(x % density)) { *p = colour; } } - } else if (colours == ZM_COLOUR_RGB24) { + } else if (zm_is_rgb24(imagePixFormat)) { constexpr uint8 bytesPerPixel = 3; - uint8 *ptr = &buffer[((scan_line * width) + lo_x) * bytesPerPixel]; + uint8 *ptr = &buffer[scan_line * linesize + lo_x * bytesPerPixel]; for (int32 x = lo_x; x <= hi_x; x++, ptr += bytesPerPixel) { if (!(x % density)) { @@ -2745,9 +3103,9 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { BLUE_PTR_RGBA(ptr) = BLUE_VAL_RGBA(colour); } } - } else if (colours == ZM_COLOUR_RGB32) { + } else if (zm_is_rgb32(imagePixFormat)) { constexpr uint8 bytesPerPixel = 4; - Rgb *ptr = reinterpret_cast(&buffer[((scan_line * width) + lo_x) * bytesPerPixel]); + Rgb *ptr = reinterpret_cast(&buffer[scan_line * linesize + lo_x * bytesPerPixel]); for (int32 x = lo_x; x <= hi_x; x++, ptr++) { if (!(x % density)) { @@ -2762,176 +3120,209 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) { } // end while } +namespace { +// Rotate `src_w` × `src_h` plane (bpp bytes per sample, src_linesize stride) +// into dst (dst_linesize stride) according to angle (90/180/270). +// For 90/270: dst dims are src_h × src_w. For 180: dst dims are src_w × src_h. +void rotate_plane(const uint8_t *src, int src_linesize, + unsigned int src_w, unsigned int src_h, + uint8_t *dst, int dst_linesize, + unsigned int bpp, int angle) { + if (angle == 90) { + // (sx, sy) -> (dx, dy) = (src_h-1-sy, sx). Equivalently, for each + // destination row dy=sx, write the column src_w-1-dy from the source. + for (unsigned int sy = 0; sy < src_h; sy++) { + const uint8_t *src_row = src + sy * src_linesize; + const unsigned int dx = src_h - 1 - sy; + for (unsigned int sx = 0; sx < src_w; sx++) { + const unsigned int dy = sx; + uint8_t *dst_px = dst + dy * dst_linesize + dx * bpp; + const uint8_t *src_px = src_row + sx * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; + } + } + } else if (angle == 180) { + // (sx, sy) -> (dx, dy) = (src_w-1-sx, src_h-1-sy). + for (unsigned int sy = 0; sy < src_h; sy++) { + const uint8_t *src_row = src + sy * src_linesize; + uint8_t *dst_row = dst + (src_h - 1 - sy) * dst_linesize; + for (unsigned int sx = 0; sx < src_w; sx++) { + const unsigned int dx = src_w - 1 - sx; + uint8_t *dst_px = dst_row + dx * bpp; + const uint8_t *src_px = src_row + sx * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; + } + } + } else if (angle == 270) { + // (sx, sy) -> (dx, dy) = (sy, src_w-1-sx). + for (unsigned int sy = 0; sy < src_h; sy++) { + const uint8_t *src_row = src + sy * src_linesize; + const unsigned int dx = sy; + for (unsigned int sx = 0; sx < src_w; sx++) { + const unsigned int dy = src_w - 1 - sx; + uint8_t *dst_px = dst + dy * dst_linesize + dx * bpp; + const uint8_t *src_px = src_row + sx * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; + } + } + } +} +} // namespace + void Image::Rotate(int angle) { angle %= 360; if ( !angle || angle%90 ) { return; } - unsigned int new_height = height; - unsigned int new_width = width; - uint8_t* rotate_buffer = AllocBuffer(size); - switch ( angle ) { - case 90 : { - new_height = width; - new_width = height; + // For 90°/270° the chroma subsampling pattern itself transposes. YUV420P + // has symmetric (2:2) subsampling and survives — the rotated chroma is + // still W/2 × H/2 of the rotated image. YUV422P has horizontal-only + // subsampling; after a 90°/270° rotation it would need vertical-only + // subsampling, which is not the same AVPixelFormat. Refuse rather than + // silently producing a non-conformant buffer. + const bool is_422 = (imagePixFormat == AV_PIX_FMT_YUV422P + || imagePixFormat == AV_PIX_FMT_YUVJ422P); + if ((angle == 90 || angle == 270) && is_422) { + Error("Rotate %d° not supported for %s (asymmetric chroma subsampling); " + "convert to RGB/YUV420P first", angle, av_get_pix_fmt_name(imagePixFormat)); + return; + } - unsigned int line_bytes = new_width*colours; - unsigned char *s_ptr = buffer; + const unsigned int new_width = (angle == 180) ? width : height; + const unsigned int new_height = (angle == 180) ? height : width; - if ( colours == ZM_COLOUR_GRAY8 ) { - for ( unsigned int i = new_width; i > 0; i-- ) { - unsigned char *d_ptr = rotate_buffer+(i-1); - for ( unsigned int j = new_height; j > 0; j-- ) { - *d_ptr = *s_ptr++; - d_ptr += line_bytes; - } - } - } else if ( colours == ZM_COLOUR_RGB32 ) { - Rgb* s_rptr = (Rgb*)s_ptr; - for ( unsigned int i = new_width; i; i-- ) { - Rgb* d_rptr = (Rgb*)(rotate_buffer+((i-1)<<2)); - for ( unsigned int j = new_height; j; j-- ) { - *d_rptr = *s_rptr++; - d_rptr += new_width; - } - } - } else { /* Assume RGB24 */ - for ( unsigned int i = new_width; i; i-- ) { - unsigned char *d_ptr = rotate_buffer+((i-1)*3); - for ( unsigned int j = new_height; j; j-- ) { - *d_ptr = *s_ptr++; - *(d_ptr+1) = *s_ptr++; - *(d_ptr+2) = *s_ptr++; - d_ptr += line_bytes; - } - } + int new_size_signed = av_image_get_buffer_size(imagePixFormat, new_width, new_height, 32); + if (new_size_signed < 0) { + Error("Rotate: av_image_get_buffer_size failed for %s %ux%u", + av_get_pix_fmt_name(imagePixFormat), new_width, new_height); + return; + } + const size_t new_size = static_cast(new_size_signed); + uint8_t* rotate_buffer = AllocBuffer(new_size); + + uint8_t *src_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int src_strides[4] = {0, 0, 0, 0}; + uint8_t *dst_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int dst_strides[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(src_planes, src_strides, buffer, imagePixFormat, width, height, 32) < 0 + || av_image_fill_arrays(dst_planes, dst_strides, rotate_buffer, imagePixFormat, new_width, new_height, 32) < 0) { + Error("Rotate: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(rotate_buffer, ZM_BUFTYPE_ZM); + return; + } + + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(imagePixFormat); + if (!desc) { + Error("Rotate: av_pix_fmt_desc_get failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(rotate_buffer, ZM_BUFTYPE_ZM); + return; + } + const bool planar = (desc->flags & AV_PIX_FMT_FLAG_PLANAR) != 0; + + if (planar) { + // Each plane has 1 byte per sample. Plane 0 is luma at full resolution; + // planes 1/2 are chroma subsampled by desc->log2_chroma_w/h. Use ceiling + // (AV_CEIL_RSHIFT) — flooring would drop the last chroma column/row for + // odd luma dimensions and leave part of U/V unrotated. + const unsigned int cw = AV_CEIL_RSHIFT(width, desc->log2_chroma_w); + const unsigned int ch = AV_CEIL_RSHIFT(height, desc->log2_chroma_h); + rotate_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], 1, angle); + rotate_plane(src_planes[1], src_strides[1], cw, ch, + dst_planes[1], dst_strides[1], 1, angle); + rotate_plane(src_planes[2], src_strides[2], cw, ch, + dst_planes[2], dst_strides[2], 1, angle); + } else { + // Packed format: single plane with bpp bytes per pixel. + const unsigned int bpp = zm_bytes_per_pixel(imagePixFormat); + if (bpp == 0) { + Error("Rotate: bytes_per_pixel unknown for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(rotate_buffer, ZM_BUFTYPE_ZM); + return; } - break; + rotate_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], bpp, angle); } - case 180 : { - unsigned char *s_ptr = buffer+size; - unsigned char *d_ptr = rotate_buffer; - if ( colours == ZM_COLOUR_GRAY8 ) { - while( s_ptr > buffer ) { - s_ptr--; - *d_ptr++ = *s_ptr; - } - } else if ( colours == ZM_COLOUR_RGB32 ) { - Rgb* s_rptr = (Rgb*)s_ptr; - Rgb* d_rptr = (Rgb*)d_ptr; - while( s_rptr > (Rgb*)buffer ) { - s_rptr--; - *d_rptr++ = *s_rptr; - } - } else { /* Assume RGB24 */ - while( s_ptr > buffer ) { - s_ptr -= 3; - *d_ptr++ = *s_ptr; - *d_ptr++ = *(s_ptr+1); - *d_ptr++ = *(s_ptr+2); + AssignDirect(new_width, new_height, colours, subpixelorder, rotate_buffer, new_size, ZM_BUFTYPE_ZM); +} // void Image::Rotate(int angle) + +namespace { +// Flip a single plane (bpp bytes per sample) into dst. leftright=true is +// horizontal flip; leftright=false is vertical flip. src/dst dimensions +// stay the same; only the order of pixels/rows changes. +void flip_plane(const uint8_t *src, int src_linesize, + unsigned int plane_w, unsigned int plane_h, + uint8_t *dst, int dst_linesize, + unsigned int bpp, bool leftright) { + if (leftright) { + for (unsigned int y = 0; y < plane_h; y++) { + const uint8_t *src_row = src + y * src_linesize; + uint8_t *dst_row = dst + y * dst_linesize; + for (unsigned int x = 0; x < plane_w; x++) { + const uint8_t *src_px = src_row + x * bpp; + uint8_t *dst_px = dst_row + (plane_w - 1 - x) * bpp; + for (unsigned int b = 0; b < bpp; b++) dst_px[b] = src_px[b]; } } - break; - } - case 270 : { - new_height = width; - new_width = height; - - unsigned int line_bytes = new_width*colours; - unsigned char *s_ptr = buffer+size; - - if ( colours == ZM_COLOUR_GRAY8 ) { - for ( unsigned int i = new_width; i > 0; i-- ) { - unsigned char *d_ptr = rotate_buffer+(i-1); - for ( unsigned int j = new_height; j > 0; j-- ) { - s_ptr--; - *d_ptr = *s_ptr; - d_ptr += line_bytes; - } - } - } else if ( colours == ZM_COLOUR_RGB32 ) { - Rgb* s_rptr = (Rgb*)s_ptr; - for ( unsigned int i = new_width; i > 0; i-- ) { - Rgb* d_rptr = (Rgb*)(rotate_buffer+((i-1)<<2)); - for ( unsigned int j = new_height; j > 0; j-- ) { - s_rptr--; - *d_rptr = *s_rptr; - d_rptr += new_width; - } - } - } else { /* Assume RGB24 */ - for ( unsigned int i = new_width; i > 0; i-- ) { - unsigned char *d_ptr = rotate_buffer+((i-1)*3); - for ( unsigned int j = new_height; j > 0; j-- ) { - *(d_ptr+2) = *(--s_ptr); - *(d_ptr+1) = *(--s_ptr); - *d_ptr = *(--s_ptr); - d_ptr += line_bytes; - } - } + } else { + // Vertical flip: row y maps to row (plane_h - 1 - y); copy bpp*plane_w + // bytes per row (the data portion; padding bytes are left untouched). + const size_t row_data_bytes = static_cast(plane_w) * bpp; + for (unsigned int y = 0; y < plane_h; y++) { + memcpy(dst + (plane_h - 1 - y) * dst_linesize, + src + y * src_linesize, + row_data_bytes); } - break; } - } - - AssignDirect(new_width, new_height, colours, subpixelorder, rotate_buffer, size, ZM_BUFTYPE_ZM); -} // void Image::Rotate(int angle) +} +} // namespace -/* RGB32 compatible: complete */ +/* RGB32 compatible: complete; planar YUV (YUV420P/J420P/YUV422P/J422P) flipped per-plane */ void Image::Flip( bool leftright ) { uint8_t* flip_buffer = AllocBuffer(size); - unsigned int line_bytes = width*colours; - unsigned int line_bytes2 = 2*line_bytes; - if ( leftright ) { - // Horizontal flip, left to right - unsigned char *s_ptr = buffer+line_bytes; - unsigned char *d_ptr = flip_buffer; - unsigned char *max_d_ptr = flip_buffer + size; - - if ( colours == ZM_COLOUR_GRAY8 ) { - while( d_ptr < max_d_ptr ) { - for ( unsigned int j = 0; j < width; j++ ) { - s_ptr--; - *d_ptr++ = *s_ptr; - } - s_ptr += line_bytes2; - } - } else if ( colours == ZM_COLOUR_RGB32 ) { - Rgb* s_rptr = (Rgb*)s_ptr; - Rgb* d_rptr = (Rgb*)flip_buffer; - Rgb* max_d_rptr = (Rgb*)max_d_ptr; - while( d_rptr < max_d_rptr ) { - for ( unsigned int j = 0; j < width; j++ ) { - s_rptr--; - *d_rptr++ = *s_rptr; - } - s_rptr += width * 2; - } - } else { /* Assume RGB24 */ - while( d_ptr < max_d_ptr ) { - for ( unsigned int j = 0; j < width; j++ ) { - s_ptr -= 3; - *d_ptr++ = *s_ptr; - *d_ptr++ = *(s_ptr+1); - *d_ptr++ = *(s_ptr+2); - } - s_ptr += line_bytes2; - } - } + uint8_t *src_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int src_strides[4] = {0, 0, 0, 0}; + uint8_t *dst_planes[4] = {nullptr, nullptr, nullptr, nullptr}; + int dst_strides[4] = {0, 0, 0, 0}; + if (av_image_fill_arrays(src_planes, src_strides, buffer, imagePixFormat, width, height, 32) < 0 + || av_image_fill_arrays(dst_planes, dst_strides, flip_buffer, imagePixFormat, width, height, 32) < 0) { + Error("Flip: av_image_fill_arrays failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(flip_buffer, ZM_BUFTYPE_ZM); + return; + } + + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(imagePixFormat); + if (!desc) { + Error("Flip: av_pix_fmt_desc_get failed for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(flip_buffer, ZM_BUFTYPE_ZM); + return; + } + const bool planar = (desc->flags & AV_PIX_FMT_FLAG_PLANAR) != 0; + + if (planar) { + // Ceiling division so odd luma dimensions don't drop the last chroma + // column/row. + const unsigned int cw = AV_CEIL_RSHIFT(width, desc->log2_chroma_w); + const unsigned int ch = AV_CEIL_RSHIFT(height, desc->log2_chroma_h); + flip_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], 1, leftright); + flip_plane(src_planes[1], src_strides[1], cw, ch, + dst_planes[1], dst_strides[1], 1, leftright); + flip_plane(src_planes[2], src_strides[2], cw, ch, + dst_planes[2], dst_strides[2], 1, leftright); } else { - // Vertical flip, top to bottom - unsigned char *s_ptr = buffer+(height*line_bytes); - unsigned char *d_ptr = flip_buffer; - - while ( s_ptr > buffer ) { - s_ptr -= line_bytes; - memcpy(d_ptr, s_ptr, line_bytes); - d_ptr += line_bytes; + const unsigned int bpp = zm_bytes_per_pixel(imagePixFormat); + if (bpp == 0) { + Error("Flip: bytes_per_pixel unknown for %s", av_get_pix_fmt_name(imagePixFormat)); + DumpBuffer(flip_buffer, ZM_BUFTYPE_ZM); + return; } + flip_plane(src_planes[0], src_strides[0], width, height, + dst_planes[0], dst_strides[0], bpp, leftright); } AssignDirect(width, height, colours, subpixelorder, flip_buffer, size, ZM_BUFTYPE_ZM); @@ -2940,21 +3331,32 @@ void Image::Flip( bool leftright ) { void Image::Scale(const unsigned int new_width, const unsigned int new_height) { if (width == new_width and height == new_height) return; - // Why larger than we need? - size_t scale_buffer_size = static_cast(new_width+1) * (new_height+1) * colours; + // imagePixFormat is the canonical source of truth; the deprecated + // AVPixFormat() getter re-derives via (colours, subpixelorder) and would + // hit the GRAY8/YUV420P alias collision if those legacy fields ever drift. + const AVPixelFormat format = imagePixFormat; + // (new_width+1)*(new_height+1)*colours undercounts planar formats: for + // YUV420P, colours=1 due to the GRAY8 alias, but the buffer needs + // ~1.5x for chroma. Use av_image_get_buffer_size so this works for any + // AVPixelFormat. SWScale::Convert checks the buffer size against this + // and errors out if undersized. + int new_size = av_image_get_buffer_size(format, new_width, new_height, 32); + if (new_size < 0) { + Error("Scale: av_image_get_buffer_size failed for %s %ux%u", av_get_pix_fmt_name(format), new_width, new_height); + return; + } + size_t scale_buffer_size = static_cast(new_size); uint8_t* scale_buffer = AllocBuffer(scale_buffer_size); - AVPixelFormat format = AVPixFormat(); SWScale swscale; swscale.init(); - swscale.Convert( buffer, allocation, - scale_buffer, - scale_buffer_size, - format, format, - width, - height, - new_width, - new_height); + if (swscale.Convert(buffer, allocation, scale_buffer, scale_buffer_size, + format, format, width, height, new_width, new_height) < 0) { + Error("Scale: sws_scale conversion failed (%ux%u %s -> %ux%u)", + width, height, av_get_pix_fmt_name(format), new_width, new_height); + DumpBuffer(scale_buffer, ZM_BUFTYPE_ZM); + return; + } AssignDirect(new_width, new_height, colours, subpixelorder, scale_buffer, scale_buffer_size, ZM_BUFTYPE_ZM); } @@ -2967,115 +3369,54 @@ void Image::Scale(const unsigned int factor) { return; } + // Delegate to the (new_width, new_height) variant, which uses sws_scale. + // The previous hand-rolled pixel-doubling/decimation loops here did not + // understand planar YUV layouts (only the Y plane was scaled, the U/V + // planes were dropped), so a YUV420P image came out garbled. unsigned int new_width = (width*factor)/ZM_SCALE_BASE; unsigned int new_height = (height*factor)/ZM_SCALE_BASE; - - // Why larger than we need? - size_t scale_buffer_size = static_cast(new_width+1) * (new_height+1) * colours; - - uint8_t* scale_buffer = AllocBuffer(scale_buffer_size); - - if ( factor > ZM_SCALE_BASE ) { - unsigned char *pd = scale_buffer; - unsigned int wc = width*colours; - unsigned int nwc = new_width*colours; - unsigned int h_count = ZM_SCALE_BASE/2; - unsigned int last_h_index = 0; - unsigned int last_w_index = 0; - for ( unsigned int y = 0; y < height; y++ ) { - unsigned char *ps = &buffer[y*wc]; - unsigned int w_count = ZM_SCALE_BASE/2; - last_w_index = 0; - for ( unsigned int x = 0; x < width; x++ ) { - w_count += factor; - unsigned int w_index = w_count/ZM_SCALE_BASE; - for (unsigned int f = last_w_index; f < w_index; f++ ) { - for ( unsigned int c = 0; c < colours; c++ ) { - *pd++ = *(ps+c); - } - } - ps += colours; - last_w_index = w_index; - } - h_count += factor; - unsigned int h_index = h_count/ZM_SCALE_BASE; - for ( unsigned int f = last_h_index+1; f < h_index; f++ ) { - memcpy(pd, pd-nwc, nwc); - pd += nwc; - } - last_h_index = h_index; - } // end foreach line - new_width = last_w_index; - new_height = last_h_index; - } else { - unsigned char *pd = scale_buffer; - unsigned int wc = width*colours; - unsigned int h_count = factor/2; - unsigned int last_h_index = 0; - unsigned int last_w_index = 0; - for ( unsigned int y = 0; y < height; y++ ) { - h_count += factor; - unsigned int h_index = h_count/ZM_SCALE_BASE; - if ( h_index > last_h_index ) { - unsigned int w_count = factor/2; - unsigned int w_index; - last_w_index = 0; - - unsigned char *ps = &buffer[y*wc]; - for ( unsigned int x = 0; x < width; x++ ) { - w_count += factor; - w_index = w_count/ZM_SCALE_BASE; - - if ( w_index > last_w_index ) { - for ( unsigned int c = 0; c < colours; c++ ) { - *pd++ = *ps++; - } - } else { - ps += colours; - } - last_w_index = w_index; - } - } - last_h_index = h_index; - } - new_width = last_w_index; - new_height = last_h_index; - } // end foreach line - AssignDirect(new_width, new_height, colours, subpixelorder, scale_buffer, scale_buffer_size, ZM_BUFTYPE_ZM); + Scale(new_width, new_height); } void Image::Deinterlace_Discard() { /* Simple deinterlacing. Copy the even lines into the odd lines */ // ICON: These can be drastically improved. But who cares? - if ( colours == ZM_COLOUR_GRAY8 ) { + // Use linesize as the per-row byte stride so non-32-aligned widths copy + // from/to the correct row offsets. Still copies `width` pixels per row + // (the padding bytes are untouched). Stop one short of the last row + // when height is odd — y == height-1 would write to row height which + // is past the buffer. + if (height < 2) return; + const unsigned int last_src = static_cast(height) - 1; + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { const uint8_t *psrc; uint8_t *pdest; - for (unsigned int y = 0; y < (unsigned int)height; y += 2) { - psrc = buffer + (y * width); - pdest = buffer + ((y+1) * width); + for (unsigned int y = 0; y < last_src; y += 2) { + psrc = buffer + y * linesize; + pdest = buffer + (y + 1) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { const uint8_t *psrc; uint8_t *pdest; - for (unsigned int y = 0; y < (unsigned int)height; y += 2) { - psrc = buffer + ((y * width) * 3); - pdest = buffer + (((y+1) * width) * 3); + for (unsigned int y = 0; y < last_src; y += 2) { + psrc = buffer + y * linesize; + pdest = buffer + (y + 1) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pdest++ = *psrc++; *pdest++ = *psrc++; *pdest++ = *psrc++; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { const Rgb *psrc; Rgb *pdest; - for (unsigned int y = 0; y < (unsigned int)height; y += 2) { - psrc = (Rgb*)(buffer + ((y * width) << 2)); - pdest = (Rgb*)(buffer + (((y+1) * width) << 2)); + for (unsigned int y = 0; y < last_src; y += 2) { + psrc = (Rgb*)(buffer + y * linesize); + pdest = (Rgb*)(buffer + (y + 1) * linesize); for (unsigned int x = 0; x < (unsigned int)width; x++) { *pdest++ = *psrc++; } @@ -3091,26 +3432,29 @@ void Image::Deinterlace_Linear() { const uint8_t *pbelow, *pabove; uint8_t *pcurrent; - if ( colours == ZM_COLOUR_GRAY8 ) { + // Use linesize as the per-row byte stride so non-32-aligned widths read + // and write the correct rows. The inner loops still process `width` + // pixels per row; per-row padding bytes are not touched. + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { - pabove = buffer + ((y-1) * width); - pbelow = buffer + ((y+1) * width); - pcurrent = buffer + (y * width); + pabove = buffer + (y - 1) * linesize; + pbelow = buffer + (y + 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = (*pabove++ + *pbelow++) >> 1; } } /* Special case for the last line */ - pcurrent = buffer + ((height-1) * width); - pabove = buffer + ((height-2) * width); + pcurrent = buffer + (height - 1) * linesize; + pabove = buffer + (height - 2) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = *pabove++; } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { - pabove = buffer + (((y-1) * width) * 3); - pbelow = buffer + (((y+1) * width) * 3); - pcurrent = buffer + ((y * width) * 3); + pabove = buffer + (y - 1) * linesize; + pbelow = buffer + (y + 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = (*pabove++ + *pbelow++) >> 1; *pcurrent++ = (*pabove++ + *pbelow++) >> 1; @@ -3118,18 +3462,18 @@ void Image::Deinterlace_Linear() { } } /* Special case for the last line */ - pcurrent = buffer + (((height-1) * width) * 3); - pabove = buffer + (((height-2) * width) * 3); + pcurrent = buffer + (height - 1) * linesize; + pabove = buffer + (height - 2) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = *pabove++; *pcurrent++ = *pabove++; *pcurrent++ = *pabove++; } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)(height-1); y += 2) { - pabove = buffer + (((y-1) * width) << 2); - pbelow = buffer + (((y+1) * width) << 2); - pcurrent = buffer + ((y * width) << 2); + pabove = buffer + (y - 1) * linesize; + pbelow = buffer + (y + 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = (*pabove++ + *pbelow++) >> 1; *pcurrent++ = (*pabove++ + *pbelow++) >> 1; @@ -3138,8 +3482,8 @@ void Image::Deinterlace_Linear() { } } /* Special case for the last line */ - pcurrent = buffer + (((height-1) * width) << 2); - pabove = buffer + (((height-2) * width) << 2); + pcurrent = buffer + (height - 1) * linesize; + pabove = buffer + (height - 2) * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pcurrent++ = *pabove++; *pcurrent++ = *pabove++; @@ -3157,19 +3501,22 @@ void Image::Deinterlace_Blend() { uint8_t *pabove, *pcurrent; - if ( colours == ZM_COLOUR_GRAY8 ) { + // Use linesize as the per-row byte stride so non-32-aligned widths read + // and write the correct rows. The inner loops still process `width` + // pixels per row; per-row padding bytes are not touched. + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + ((y-1) * width); - pcurrent = buffer + (y * width); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pabove = (*pabove + *pcurrent) >> 1; *pcurrent++ = *pabove++; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) * 3); - pcurrent = buffer + ((y * width) * 3); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pabove = (*pabove + *pcurrent) >> 1; *pcurrent++ = *pabove++; @@ -3179,10 +3526,10 @@ void Image::Deinterlace_Blend() { *pcurrent++ = *pabove++; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) << 2); - pcurrent = buffer + ((y * width) << 2); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { *pabove = (*pabove + *pcurrent) >> 1; *pcurrent++ = *pabove++; @@ -3214,10 +3561,12 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { Error("Deinterlace called with invalid blend ratio"); } - if ( colours == ZM_COLOUR_GRAY8 ) { + // Use linesize as the per-row byte stride so non-32-aligned widths read + // and write the correct rows. Inner loops process `width` pixels. + if ( zm_bytes_per_pixel(imagePixFormat) == 1 ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + ((y-1) * width); - pcurrent = buffer + (y * width); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { subpix1 = ((*pabove - *pcurrent)>>divider) + *pcurrent; subpix2 = ((*pcurrent - *pabove)>>divider) + *pabove; @@ -3225,10 +3574,10 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { *pabove++ = subpix2; } } - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) * 3); - pcurrent = buffer + ((y * width) * 3); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { subpix1 = ((*pabove - *pcurrent)>>divider) + *pcurrent; subpix2 = ((*pcurrent - *pabove)>>divider) + *pabove; @@ -3244,10 +3593,10 @@ void Image::Deinterlace_Blend_CustomRatio(int divider) { *pabove++ = subpix2; } } - } else if ( colours == ZM_COLOUR_RGB32 ) { + } else if ( zm_is_rgb32(imagePixFormat) ) { for (unsigned int y = 1; y < (unsigned int)height; y += 2) { - pabove = buffer + (((y-1) * width) << 2); - pcurrent = buffer + ((y * width) << 2); + pabove = buffer + (y - 1) * linesize; + pcurrent = buffer + y * linesize; for (unsigned int x = 0; x < (unsigned int)width; x++) { subpix1 = ((*pabove - *pcurrent)>>divider) + *pcurrent; subpix2 = ((*pcurrent - *pabove)>>divider) + *pabove; @@ -3279,41 +3628,76 @@ void Image::Deinterlace_4Field(const Image* next_image, unsigned int threshold) Panic( "Attempt to deinterlace different sized images, expected %dx%dx%d %d, got %dx%dx%d %d", width, height, colours, subpixelorder, next_image->width, next_image->height, next_image->colours, next_image->subpixelorder); } - switch(colours) { - case ZM_COLOUR_RGB24: { - if(subpixelorder == ZM_SUBPIX_ORDER_BGR) { - /* BGR subpixel order */ - std_deinterlace_4field_bgr(buffer, next_image->buffer, threshold, width, height); - } else { - /* Assume RGB subpixel order */ - std_deinterlace_4field_rgb(buffer, next_image->buffer, threshold, width, height); + // The *_deinterlace_4field_* SIMD helpers use `width` as the row stride + // internally and have no concept of per-row padding. With FFALIGN'd + // linesize the source row stride may exceed width*bpp, so feeding them + // the held buffers directly would treat padding bytes as image data and + // corrupt the output for non-32-aligned widths. Pack both inputs into + // tightly-laid-out temp buffers (linesize == width*bpp), run the helper, + // then copy only the data bytes back into our linesize-padded buffer + // (leaving padding untouched). The fast path — no copies — is used + // whenever linesize already equals width*bpp on both images, which is + // the common case for 32-aligned widths. + const unsigned int bpp = (zm_bytes_per_pixel(imagePixFormat) == 1) + ? 1 + : zm_bytes_per_pixel(imagePixFormat); + const size_t row_data_bytes = static_cast(width) * bpp; + const bool needs_pack = (linesize != row_data_bytes) || (next_image->linesize != row_data_bytes); + + uint8_t *col1 = buffer; + uint8_t *col2 = next_image->buffer; + uint8_t *tmp1 = nullptr; + uint8_t *tmp2 = nullptr; + if (needs_pack) { + const size_t packed_size = row_data_bytes * height; + tmp1 = AllocBuffer(packed_size); + tmp2 = AllocBuffer(packed_size); + for (unsigned int y = 0; y < height; y++) { + memcpy(tmp1 + y * row_data_bytes, buffer + y * linesize, row_data_bytes); + memcpy(tmp2 + y * row_data_bytes, next_image->buffer + y * next_image->linesize, row_data_bytes); } - break; - } - case ZM_COLOUR_RGB32: { - if(subpixelorder == ZM_SUBPIX_ORDER_ARGB) { - /* ARGB subpixel order */ - (*fptr_deinterlace_4field_argb)(buffer, next_image->buffer, threshold, width, height); - } else if(subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - (*fptr_deinterlace_4field_abgr)(buffer, next_image->buffer, threshold, width, height); - } else if(subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - (*fptr_deinterlace_4field_bgra)(buffer, next_image->buffer, threshold, width, height); - } else { - /* Assume RGBA subpixel order */ - (*fptr_deinterlace_4field_rgba)(buffer, next_image->buffer, threshold, width, height); + col1 = tmp1; + col2 = tmp2; + } + + if (imagePixFormat == AV_PIX_FMT_BGR24) { + /* BGR subpixel order */ + std_deinterlace_4field_bgr(col1, col2, threshold, width, height); + } else if (zm_is_rgb24(imagePixFormat)) { + /* Assume RGB subpixel order */ + std_deinterlace_4field_rgb(col1, col2, threshold, width, height); + } else if (imagePixFormat == AV_PIX_FMT_ARGB) { + /* ARGB subpixel order */ + (*fptr_deinterlace_4field_argb)(col1, col2, threshold, width, height); + } else if (imagePixFormat == AV_PIX_FMT_ABGR) { + /* ABGR subpixel order */ + (*fptr_deinterlace_4field_abgr)(col1, col2, threshold, width, height); + } else if (imagePixFormat == AV_PIX_FMT_BGRA) { + /* BGRA subpixel order */ + (*fptr_deinterlace_4field_bgra)(col1, col2, threshold, width, height); + } else if (zm_is_rgb32(imagePixFormat)) { + /* Assume RGBA subpixel order */ + (*fptr_deinterlace_4field_rgba)(col1, col2, threshold, width, height); + } else if (zm_bytes_per_pixel(imagePixFormat) == 1) { + (*fptr_deinterlace_4field_gray8)(col1, col2, threshold, width, height); + } else { + if (needs_pack) { + DumpBuffer(tmp1, ZM_BUFTYPE_ZM); + DumpBuffer(tmp2, ZM_BUFTYPE_ZM); } - break; - } - case ZM_COLOUR_GRAY8: - (*fptr_deinterlace_4field_gray8)(buffer, next_image->buffer, threshold, width, height); - break; - default: - Panic("Deinterlace_4Field called with unexpected colours: %d",colours); - break; + Panic("Deinterlace_4Field called with unexpected pixel format %d (%s)", + imagePixFormat, zm_get_pix_fmt_name(imagePixFormat)); } + if (needs_pack) { + // Copy the processed Y/primary plane back into our padded buffer, + // touching only the data bytes per row so existing padding is intact. + for (unsigned int y = 0; y < height; y++) { + memcpy(buffer + y * linesize, tmp1 + y * row_data_bytes, row_data_bytes); + } + DumpBuffer(tmp1, ZM_BUFTYPE_ZM); + DumpBuffer(tmp2, ZM_BUFTYPE_ZM); + } } @@ -5464,54 +5848,36 @@ __attribute__((noinline)) void std_deinterlace_4field_abgr(uint8_t* col1, uint8_ } AVPixelFormat Image::AVPixFormat() const { - if ( colours == ZM_COLOUR_RGB32 ) { - return AV_PIX_FMT_RGBA; - } else if ( colours == ZM_COLOUR_RGB24 ) { - if ( subpixelorder == ZM_SUBPIX_ORDER_BGR) { - return AV_PIX_FMT_BGR24; - } else { - return AV_PIX_FMT_RGB24; - } - } else if ( colours == ZM_COLOUR_GRAY8 ) { - return AV_PIX_FMT_GRAY8; - } else { - Error("Unknown colours (%d)",colours); - return AV_PIX_FMT_RGBA; - } + return zm_pixformat_from_colours(colours, subpixelorder); } AVPixelFormat Image::AVPixFormat(AVPixelFormat new_pixelformat) { - switch (new_pixelformat) { - case AV_PIX_FMT_YUVJ420P: - colours = ZM_COLOUR_YUVJ420P; - subpixelorder = ZM_SUBPIX_ORDER_YUVJ420P; - break; - case AV_PIX_FMT_YUV420P: - colours = ZM_COLOUR_YUV420P; - subpixelorder = ZM_SUBPIX_ORDER_YUV420P; - break; - case AV_PIX_FMT_RGBA: - colours = ZM_COLOUR_RGB32; - subpixelorder = ZM_SUBPIX_ORDER_RGBA; - break; - case AV_PIX_FMT_BGR24: - colours = ZM_COLOUR_RGB24; - subpixelorder = ZM_SUBPIX_ORDER_BGR; - break; - case AV_PIX_FMT_RGB24: - colours = ZM_COLOUR_RGB24; - subpixelorder = ZM_SUBPIX_ORDER_RGB; - break; - case AV_PIX_FMT_GRAY8: - colours = ZM_COLOUR_GRAY8; - subpixelorder = ZM_SUBPIX_ORDER_NONE; - break; - default: - Error("Unknown pixelformat %d %s", new_pixelformat, av_get_pix_fmt_name(new_pixelformat)); - } - Debug(4, "Old size: %d, old pixelformat %d", size, imagePixFormat); - size = av_image_get_buffer_size(new_pixelformat, width, height, 32); - Debug(4, "New size: %d new pixelformat %d", size, new_pixelformat); - linesize = FFALIGN(av_image_get_linesize(new_pixelformat, width, 0), 32); - return imagePixFormat = new_pixelformat; + // Reject unrecognised formats up-front: av_image_get_buffer_size / + // av_image_get_linesize return negative for them, and assigning that into + // unsigned size/linesize would wrap to a huge value and corrupt later + // bounds checks. On failure, leave the Image's format/size/linesize + // untouched so callers can recover or skip the slot. + unsigned int probe_colours, probe_subpix; + if (!zm_colours_from_pixformat(new_pixelformat, probe_colours, probe_subpix)) { + Error("Image::AVPixFormat: refusing to set unknown pixelformat %d %s; keeping current %s", + new_pixelformat, zm_get_pix_fmt_name(new_pixelformat), + zm_get_pix_fmt_name(imagePixFormat)); + return imagePixFormat; + } + int new_size = av_image_get_buffer_size(new_pixelformat, width, height, 32); + int new_linesize = av_image_get_linesize(new_pixelformat, width, 0); + if (new_size < 0 || new_linesize < 0) { + Error("Image::AVPixFormat: av_image_get_* returned %d/%d for %s; keeping current %s", + new_size, new_linesize, zm_get_pix_fmt_name(new_pixelformat), + zm_get_pix_fmt_name(imagePixFormat)); + return imagePixFormat; + } + Debug(4, "Old size: %u, old pixelformat %s", size, zm_get_pix_fmt_name(imagePixFormat)); + colours = probe_colours; + subpixelorder = probe_subpix; + imagePixFormat = new_pixelformat; + size = static_cast(new_size); + linesize = FFALIGN(new_linesize, 32); + Debug(4, "New size: %u new pixelformat %s", size, zm_get_pix_fmt_name(new_pixelformat)); + return imagePixFormat; } diff --git a/src/zm_image.h b/src/zm_image.h index 2ffc671922c..985d7f7ad38 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -24,6 +24,7 @@ #include "zm_jpeg.h" #include "zm_logger.h" #include "zm_mem_utils.h" +#include "zm_pixformat.h" #include "zm_rgb.h" #include "zm_time.h" #include "zm_vector2.h" @@ -176,8 +177,9 @@ class Image { inline unsigned int Size() const { return size; } std::string Filename() const { return filename_; } - AVPixelFormat AVPixFormat() const; - AVPixelFormat AVPixFormat(AVPixelFormat); + AVPixelFormat PixFormat() const { return imagePixFormat; } + AVPixelFormat AVPixFormat() const; // DEPRECATED: use PixFormat() + AVPixelFormat AVPixFormat(AVPixelFormat); // Sets imagePixFormat and updates colours/subpixelorder inline uint8_t* Buffer() { return buffer; } inline const uint8_t* Buffer() const { return buffer; } diff --git a/src/zm_libvlc_camera.cpp b/src/zm_libvlc_camera.cpp index 49fe07aacdd..f66651a87ee 100644 --- a/src/zm_libvlc_camera.cpp +++ b/src/zm_libvlc_camera.cpp @@ -145,16 +145,19 @@ LibvlcCamera::LibvlcCamera( mOptArgV = nullptr; /* Has to be located inside the constructor so other components such as zma will receive correct colours and subpixel order */ - if ( colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_BGRA; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); mTargetChroma = "RV32"; mBpp = 4; - } else if ( colours == ZM_COLOUR_RGB24 ) { + } else if ( zm_is_rgb24(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_BGR; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); mTargetChroma = "RV24"; mBpp = 3; - } else if ( colours == ZM_COLOUR_GRAY8 ) { + } else if ( pixelFormat == AV_PIX_FMT_GRAY8 ) { subpixelorder = ZM_SUBPIX_ORDER_NONE; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); mTargetChroma = "GREY"; mBpp = 1; } else { diff --git a/src/zm_libvnc_camera.cpp b/src/zm_libvnc_camera.cpp index 6059788d315..876ebd6db7a 100644 --- a/src/zm_libvnc_camera.cpp +++ b/src/zm_libvnc_camera.cpp @@ -117,17 +117,21 @@ VncCamera::VncCamera( mPort(port), mUser(user), mPass(pass) { - if (colours == ZM_COLOUR_RGB32) { + if (zm_is_rgb32(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; mImgPixFmt = AV_PIX_FMT_RGBA; - } else if (colours == ZM_COLOUR_RGB24) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if (zm_is_rgb24(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGB; mImgPixFmt = AV_PIX_FMT_RGB24; - } else if (colours == ZM_COLOUR_GRAY8) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if (pixelFormat == AV_PIX_FMT_GRAY8) { subpixelorder = ZM_SUBPIX_ORDER_NONE; mImgPixFmt = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %d", colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } if (capture) { diff --git a/src/zm_local_camera.cpp b/src/zm_local_camera.cpp index e17bd0b5b55..69011d6f94f 100644 --- a/src/zm_local_camera.cpp +++ b/src/zm_local_camera.cpp @@ -323,25 +323,32 @@ LocalCamera::LocalCamera( /* Try to find a match for the selected palette and target colourspace */ /* RGB32 palette and 32bit target colourspace */ - if (palette == V4L2_PIX_FMT_RGB32 && colours == ZM_COLOUR_RGB32) { + if (palette == V4L2_PIX_FMT_RGB32 && zm_is_rgb32(pixelFormat)) { conversion_type = 0; subpixelorder = ZM_SUBPIX_ORDER_ARGB; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* BGR32 palette and 32bit target colourspace */ - } else if (palette == V4L2_PIX_FMT_BGR32 && colours == ZM_COLOUR_RGB32) { + } else if (palette == V4L2_PIX_FMT_BGR32 && zm_is_rgb32(pixelFormat)) { conversion_type = 0; subpixelorder = ZM_SUBPIX_ORDER_BGRA; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* RGB24 palette and 24bit target colourspace */ - } else if (palette == V4L2_PIX_FMT_RGB24 && colours == ZM_COLOUR_RGB24) { + } else if (palette == V4L2_PIX_FMT_RGB24 && zm_is_rgb24(pixelFormat)) { conversion_type = 0; - conversion_type = 0; - subpixelorder = ZM_SUBPIX_ORDER_BGR; + // V4L2_PIX_FMT_RGB24 is byte-order R,G,B in memory (maps to AV_PIX_FMT_RGB24 + // in getFfPixFormatFromV4lPalette above), so the subpixel order must be RGB. + // Setting BGR here was a long-standing bug that swapped red and blue on + // RGB24-capture cameras. + subpixelorder = ZM_SUBPIX_ORDER_RGB; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* Grayscale palette and grayscale target colourspace */ - } else if (palette == V4L2_PIX_FMT_GREY && colours == ZM_COLOUR_GRAY8) { + } else if (palette == V4L2_PIX_FMT_GREY && pixelFormat == AV_PIX_FMT_GRAY8) { conversion_type = 0; subpixelorder = ZM_SUBPIX_ORDER_NONE; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); /* Unable to find a solution for the selected palette and target colourspace. Conversion required. Notify the user of performance penalty */ } else { if (capture) { @@ -353,17 +360,21 @@ LocalCamera::LocalCamera( /* Try using swscale for the conversion */ conversion_type = 1; Debug(2, "Using swscale for image conversion"); - if (colours == ZM_COLOUR_RGB32) { + if (zm_is_rgb32(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; imagePixFormat = AV_PIX_FMT_RGBA; - } else if (colours == ZM_COLOUR_RGB24) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if (zm_is_rgb24(pixelFormat)) { subpixelorder = ZM_SUBPIX_ORDER_RGB; imagePixFormat = AV_PIX_FMT_RGB24; - } else if (colours == ZM_COLOUR_GRAY8) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if (pixelFormat == AV_PIX_FMT_GRAY8) { subpixelorder = ZM_SUBPIX_ORDER_NONE; imagePixFormat = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %u",colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%u subpixelorder=%u", + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } if (capture) { if (!sws_isSupportedInput(capturePixFormat)) { @@ -376,7 +387,7 @@ LocalCamera::LocalCamera( } } /* Our YUYV->Grayscale conversion is a lot faster than swscale's */ - if (colours == ZM_COLOUR_GRAY8 && palette == V4L2_PIX_FMT_YUYV) { + if (pixelFormat == AV_PIX_FMT_GRAY8 && palette == V4L2_PIX_FMT_YUYV) { conversion_type = 2; } @@ -388,13 +399,15 @@ LocalCamera::LocalCamera( if (conversion_type == 2) { Debug(2,"Using ZM for image conversion"); - if ( palette == V4L2_PIX_FMT_RGB32 && colours == ZM_COLOUR_GRAY8 ) { + if ( palette == V4L2_PIX_FMT_RGB32 && pixelFormat == AV_PIX_FMT_GRAY8 ) { conversion_fptr = &std_convert_argb_gray8; subpixelorder = ZM_SUBPIX_ORDER_NONE; - } else if (palette == V4L2_PIX_FMT_BGR32 && colours == ZM_COLOUR_GRAY8) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_BGR32 && pixelFormat == AV_PIX_FMT_GRAY8) { conversion_fptr = &std_convert_bgra_gray8; subpixelorder = ZM_SUBPIX_ORDER_NONE; - } else if (palette == V4L2_PIX_FMT_YUYV && colours == ZM_COLOUR_GRAY8) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_YUYV && pixelFormat == AV_PIX_FMT_GRAY8) { /* Fast YUYV->Grayscale conversion by extracting the Y channel */ if (config.cpu_extensions && sse_version >= 35) { conversion_fptr = &ssse3_convert_yuyv_gray8; @@ -404,24 +417,31 @@ LocalCamera::LocalCamera( Debug(2,"Using standard YUYV->grayscale fast conversion"); } subpixelorder = ZM_SUBPIX_ORDER_NONE; - } else if (palette == V4L2_PIX_FMT_YUYV && colours == ZM_COLOUR_RGB24) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_YUYV && zm_is_rgb24(pixelFormat)) { conversion_fptr = &zm_convert_yuyv_rgb; subpixelorder = ZM_SUBPIX_ORDER_RGB; - } else if (palette == V4L2_PIX_FMT_YUYV && colours == ZM_COLOUR_RGB32) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_YUYV && zm_is_rgb32(pixelFormat)) { conversion_fptr = &zm_convert_yuyv_rgba; subpixelorder = ZM_SUBPIX_ORDER_RGBA; - } else if (palette == V4L2_PIX_FMT_RGB555 && colours == ZM_COLOUR_RGB24) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB555 && zm_is_rgb24(pixelFormat)) { conversion_fptr = &zm_convert_rgb555_rgb; subpixelorder = ZM_SUBPIX_ORDER_RGB; - } else if (palette == V4L2_PIX_FMT_RGB555 && colours == ZM_COLOUR_RGB32) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB555 && zm_is_rgb32(pixelFormat)) { conversion_fptr = &zm_convert_rgb555_rgba; subpixelorder = ZM_SUBPIX_ORDER_RGBA; - } else if (palette == V4L2_PIX_FMT_RGB565 && colours == ZM_COLOUR_RGB24) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB565 && zm_is_rgb24(pixelFormat)) { conversion_fptr = &zm_convert_rgb565_rgb; subpixelorder = ZM_SUBPIX_ORDER_RGB; - } else if (palette == V4L2_PIX_FMT_RGB565 && colours == ZM_COLOUR_RGB32) { + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); + } else if (palette == V4L2_PIX_FMT_RGB565 && zm_is_rgb32(pixelFormat)) { conversion_fptr = &zm_convert_rgb565_rgba; subpixelorder = ZM_SUBPIX_ORDER_RGBA; + pixelFormat = zm_pixformat_from_colours(colours, subpixelorder); } else { Fatal("Unable to find a suitable format conversion for the selected palette and target colorspace."); } @@ -878,11 +898,12 @@ uint32_t LocalCamera::AutoSelectFormat(int p_colours) { int nIndexUsed = -1; unsigned int n_preferedformats = 0; const uint32_t* preferedformats; - if ( p_colours == ZM_COLOUR_RGB32 ) { + AVPixelFormat p_pixfmt = zm_db_colours_to_pixformat(p_colours); + if ( zm_is_rgb32(p_pixfmt) ) { /* 32bit */ preferedformats = prefered_rgb32_formats; n_preferedformats = sizeof(prefered_rgb32_formats) / sizeof(uint32_t); - } else if ( p_colours == ZM_COLOUR_GRAY8 ) { + } else if ( p_pixfmt == AV_PIX_FMT_GRAY8 ) { /* Grayscale */ preferedformats = prefered_gray8_formats; n_preferedformats = sizeof(prefered_gray8_formats) / sizeof(uint32_t); diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 89cc72b645e..813b93d28f5 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -299,6 +299,9 @@ Monitor::Monitor() : video_store_data(nullptr), shared_timestamps(nullptr), shared_images(nullptr), + image_pixelformats(nullptr), + alarm_image_pixelformat(nullptr), + shm_slot_size(0), video_stream_id(-1), audio_stream_id(-1), video_fifo(nullptr), @@ -522,6 +525,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { camera_height = atoi(dbrow[col]); col++; colours = atoi(dbrow[col]); + // colours from DB is legacy {1,3,4}. Derive the actual pixel format. col++; palette = atoi(dbrow[col]); col++; @@ -990,7 +994,22 @@ bool Monitor::connect() { Warning("Already connected. Please call disconnect first."); } if (!camera) LoadCamera(); + // SHM slot size must be an upper bound across every AVPixelFormat the + // no-conversion pipeline can transport, not just the monitor's configured + // colours. If the camera is configured as GRAY8 (1 byte/pixel) but the + // decoder hands us YUV422P (2 bytes/pixel) or RGBA (4 bytes/pixel), the + // capture-side Assign would fail with "Held buffer is undersized". Size + // the slot for the largest supported format (RGBA at 4 bytes/pixel with + // 32-byte alignment) and fall back to camera->ImageSize() if that probe + // fails, so we never shrink the slot. size_t image_size = camera->ImageSize(); + int upper_bound = av_image_get_buffer_size(AV_PIX_FMT_RGBA, width, height, 32); + if (upper_bound > 0 && static_cast(upper_bound) > image_size) { + Debug(1, "SHM slot sized for upper bound %d (was camera->ImageSize()=%zu)", + upper_bound, image_size); + image_size = static_cast(upper_bound); + } + shm_slot_size = image_size; mem_size = sizeof(SharedData) + sizeof(TriggerData) + (zone_count * sizeof(int)) // Per zone scores @@ -998,8 +1017,16 @@ bool Monitor::connect() { + (image_buffer_count*sizeof(struct timeval)) + (image_buffer_count*image_size) + (image_buffer_count*image_size) // alarm_images - + (image_buffer_count*sizeof(AVPixelFormat)) // - + 64; /* Padding used to permit aligning the images buffer to 64 byte boundary */ + + (image_buffer_count*sizeof(AVPixelFormat)) // per-slot capture pix fmt + + sizeof(AVPixelFormat) // alarm_image pix fmt (cross-process sync) + // Padding covers two independent alignment adjustments: + // * up to 63 bytes to push shared_images to a 64-byte boundary + // * up to alignof(AVPixelFormat)-1 bytes to push + // image_pixelformats to its required alignment after the + // image_size-stride run of bytes. + // Reserve the worst case so neither adjustment can run past the + // mapped region. + + 63 + (alignof(AVPixelFormat) - 1); Debug(1, "SharedData=%zu " @@ -1119,19 +1146,48 @@ bool Monitor::connect() { image_buffer.resize(image_buffer_count); for (int32_t i = 0; i < image_buffer_count; i++) { - image_buffer[i] = new Image(width, height, camera->Colours(), camera->SubpixelOrder(), &(shared_images[i*image_size])); + // The initial format is a placeholder. The actual SHM bytes can be in + // any AVPixelFormat zm_pixformat supports — zmc records the per-slot + // format via image_pixelformats[index] in WriteShmFrame, and ReadShmFrame + // applies it to image_buffer[i] before each consumer access. image_size + // is sized above to the RGBA upper bound so any supported format fits + // the held SHM slot regardless of the monitor's configured colours. + image_buffer[i] = new Image(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P, + &(shared_images[i*image_size]), image_size, 0); image_buffer[i]->HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ } - alarm_image.AssignDirect(width, height, camera->Colours(), camera->SubpixelOrder(), - &(shared_images[image_buffer_count*image_size]), - image_size, - ZM_BUFTYPE_DONTFREE - ); + // alarm_image follows the per-slot format convention. Initial format is a + // placeholder; consumers should sync via GetAlarmImage() which reads the + // cross-process *alarm_image_pixelformat. + alarm_image.AssignDirect(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P, + &(shared_images[image_buffer_count*image_size]), image_size, ZM_BUFTYPE_DONTFREE); alarm_image.HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ if (alarm_image.Buffer() + image_size > mem_ptr + mem_size) { Warning("We will exceed memsize by %td bytes!", (alarm_image.Buffer() + image_size) - (mem_ptr + mem_size)); } - image_pixelformats = (AVPixelFormat *)(shared_images + (image_buffer_count*image_size)); + // Layout in SHM is: image_buffer_count*image_size for image_buffer slots, + // then image_buffer_count*image_size for alarm_image slots, THEN the + // image_pixelformats[image_buffer_count] array, THEN the single + // alarm_image_pixelformat slot. Placing image_pixelformats at + // +1*image_buffer_count*image_size would collide with the alarm_image + // buffer region — zmc's writes to image_pixelformats[index] would corrupt + // the alarm image, and zms would read alarm-image bytes back as + // AVPixelFormat enum values, producing per-frame garble. + // + // image_size may not be a multiple of alignof(AVPixelFormat) (for + // example GRAY8 with odd width when image_size comes from + // camera->ImageSize()). Casting an unaligned address to AVPixelFormat* + // is undefined behaviour on strict-alignment ISAs (and slow even on x86), + // so round the offset up. mem_size reserves + // 63 + (alignof(AVPixelFormat) - 1) bytes of slack so the combined + // 64-byte alignment of shared_images and this pixformat alignment both + // fit inside the mapped region. + uintptr_t pixfmt_addr = reinterpret_cast( + shared_images + (2 * image_buffer_count * image_size)); + const uintptr_t pixfmt_align = alignof(AVPixelFormat); + pixfmt_addr = (pixfmt_addr + pixfmt_align - 1) & ~(pixfmt_align - 1); + image_pixelformats = reinterpret_cast(pixfmt_addr); + alarm_image_pixelformat = image_pixelformats + image_buffer_count; if (purpose == CAPTURE) { memset(mem_ptr, 0, mem_size); @@ -1158,6 +1214,13 @@ bool Monitor::connect() { shared_data->alarm_y = -1; shared_data->format = camera->SubpixelOrder(); shared_data->imagesize = camera->ImageSize(); + // memset zeroed image_pixelformats/alarm_image_pixelformat above; 0 is + // AV_PIX_FMT_YUV420P, not the AV_PIX_FMT_NONE sentinel readers expect + // for "format not yet published". Initialise explicitly. + for (int32_t i = 0; i < image_buffer_count; i++) { + image_pixelformats[i] = AV_PIX_FMT_NONE; + } + *alarm_image_pixelformat = AV_PIX_FMT_NONE; shared_data->alarm_cause[0] = 0; shared_data->video_fifo_path[0] = 0; shared_data->audio_fifo_path[0] = 0; @@ -1225,6 +1288,14 @@ bool Monitor::connect() { Error("Shared data not initialised by capture daemon for monitor %s", name.c_str()); return false; } + // image_buffer[] is constructed above with a hardcoded YUV420P placeholder + // format because we don't yet know which AVPixelFormat zmc has written into + // each SHM slot. Readers (zms, zma, etc.) MUST call ReadShmFrame() before + // interpreting a slot's bytes — it adopts the per-slot AVPixelFormat from + // image_pixelformats[] (written by zmc in WriteShmFrame) and updates the + // Image's imagePixFormat, colours, subpixelorder, size and linesize + // together. Direct image_buffer[i] access without ReadShmFrame() will + // mis-interpret any slot whose actual format differs from the placeholder. // We set these here because otherwise the first fps calc is meaningless last_fps_time = std::chrono::system_clock::now(); @@ -1347,28 +1418,93 @@ void Monitor::AddPrivacyBitmask() { } Image *Monitor::GetAlarmImage() { + // alarm_image's bytes live in SHM and are written by the capture/analysis + // process; alarm_image_pixelformat carries the format that process used. + // Without this sync, a reader process (zms) would interpret the bytes + // with whatever placeholder format alarm_image was constructed with — + // producing garbled output whenever the writer used RGB24/RGBA/etc. + if (alarm_image_pixelformat != nullptr) { + AVPixelFormat fmt = *alarm_image_pixelformat; + if (fmt != AV_PIX_FMT_NONE && alarm_image.PixFormat() != fmt) { + unsigned int probe_colours, probe_subpix; + if (!zm_colours_from_pixformat(fmt, probe_colours, probe_subpix)) { + Warning("GetAlarmImage: ignoring unsupported pixelformat %d; keeping current %s", + fmt, zm_get_pix_fmt_name(alarm_image.PixFormat())); + } else { + int required = av_image_get_buffer_size(fmt, width, height, 32); + if (required < 0 || static_cast(required) > shm_slot_size) { + Warning("GetAlarmImage: format %s requires %d bytes but slot capacity is %zu; " + "keeping current %s", + zm_get_pix_fmt_name(fmt), required, shm_slot_size, + zm_get_pix_fmt_name(alarm_image.PixFormat())); + } else { + alarm_image.AVPixFormat(fmt); + } + } + } + } return &alarm_image; } +void Monitor::WriteAlarmImage(const Image &src) { + // Mirror WriteShmFrame's contract for alarm_image: copy bytes then + // publish the canonical AVPixelFormat so reader processes can interpret + // the SHM correctly via GetAlarmImage(). + // + // Only publish *alarm_image_pixelformat if Assign actually adopted the + // source format. Image::Assign silently leaves the destination untouched + // on failure (held-buffer undersize, unknown src format), so publishing + // a new format whose bytes never landed would make readers misinterpret + // the previous alarm-image contents. + const AVPixelFormat src_fmt = src.PixFormat(); + alarm_image.Assign(src); + if (alarm_image_pixelformat != nullptr) { + if (alarm_image.PixFormat() == src_fmt) { + *alarm_image_pixelformat = src_fmt; + } else { + Warning("WriteAlarmImage: assign failed (dst fmt %s != src fmt %s); " + "keeping previously published pixelformat", + zm_get_pix_fmt_name(alarm_image.PixFormat()), + zm_get_pix_fmt_name(src_fmt)); + } + } +} + int Monitor::GetImage(int32_t index, int scale) { if (index < 0 || index > image_buffer_count) { Debug(1, "Invalid index %d passed. image_buffer_count = %d", index, image_buffer_count); index = shared_data->last_write_index; } - if (!image_buffer.size() or static_cast(index) >= image_buffer.size()) { + if (!image_buffer.size()) { Error("Image Buffer has not been allocated"); return -1; } - if ( index == image_buffer_count ) { + // Check the sentinel BEFORE the bounds check: when last_write_index is + // still image_buffer_count (no frames written yet), index equals the + // sentinel which also equals image_buffer.size(), so a plain bounds + // check would mask the intended "no images" branch and log a misleading + // "buffer not allocated" error. + if (index == image_buffer_count) { Error("Unable to generate image, no images in buffer"); return 0; } + if (static_cast(index) >= image_buffer.size()) { + Error("GetImage: index %d out of range (image_buffer.size() = %zu)", + index, image_buffer.size()); + return -1; + } + // Route the slot read through ReadShmFrame so the per-slot AVPixelFormat + // recorded by the capture process is adopted on image_buffer[index] + // before its bytes are interpreted. Otherwise the JPEG would be encoded + // using the placeholder format set at attach time and produce garbled + // output whenever the slot's actual format differs. + Image *src = ReadShmFrame(index); std::string filename = stringtf("Monitor%u.jpg", id); // If we are going to be modifying the snapshot before writing, then we need to copy it if ((scale != ZM_SCALE_BASE) || (!config.timestamp_on_capture)) { Image image; - image.Assign(*image_buffer[index]); + image.Assign(*src); if (scale != ZM_SCALE_BASE) { image.Scale(scale); @@ -1379,29 +1515,40 @@ int Monitor::GetImage(int32_t index, int scale) { } return image.WriteJpeg(filename); } else { - return image_buffer[index]->WriteJpeg(filename); + return src->WriteJpeg(filename); } } -std::shared_ptr Monitor::getSnapshot(int index) const { +std::shared_ptr Monitor::getSnapshot(int index) { if ((index < 0) || (index >= image_buffer_count)) { index = shared_data->last_write_index; } - if (!image_buffer.size() or static_cast(index) >= image_buffer.size()) { + if (!image_buffer.size()) { Error("Image Buffer has not been allocated"); return nullptr; } - if (index != image_buffer_count) { - std::shared_ptr packet = std::make_shared (image_buffer[index], - SystemTimePoint(zm::chrono::duration_cast(shared_timestamps[index]))); - return packet; - } else { + // Sentinel before bounds: index == image_buffer_count means "no frames + // written yet" (last_write_index is still the sentinel). Without this + // ordering the bounds check below would fire first and log a misleading + // "buffer not allocated" error. + if (index == image_buffer_count) { Error("Unable to generate image, no images in buffer"); + return nullptr; + } + if (static_cast(index) >= image_buffer.size()) { + Error("getSnapshot: index %d out of range (image_buffer.size() = %zu)", + index, image_buffer.size()); + return nullptr; } - return nullptr; + // ReadShmFrame syncs image_buffer[index] to the per-slot format that + // zmc wrote, so the ZMPacket consumer can read its bytes in the + // correct format instead of the placeholder set at attach time. + Image *src = ReadShmFrame(index); + return std::make_shared(src, + SystemTimePoint(zm::chrono::duration_cast(shared_timestamps[index]))); } -SystemTimePoint Monitor::GetTimestamp(int index) const { +SystemTimePoint Monitor::GetTimestamp(int index) { std::shared_ptr packet = getSnapshot(index); if (packet) return packet->timestamp; @@ -1685,7 +1832,7 @@ void Monitor::DumpZoneImage(const char *zone_string) { } } - if ( zone_image->Colours() == ZM_COLOUR_GRAY8 ) { + if (zone_image->PixFormat() == AV_PIX_FMT_GRAY8) { zone_image->Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); } @@ -1748,7 +1895,8 @@ bool Monitor::CheckSignal(const Image *image) { const uint8_t *buffer = image->Buffer(); int pixels = image->Pixels(); int width = image->Width(); - int colours = image->Colours(); + int linesize = image->LineSize(); + AVPixelFormat pix_fmt = image->PixFormat(); int index = 0; for (int i = 0; i < signal_check_points; i++) { @@ -1767,29 +1915,38 @@ bool Monitor::CheckSignal(const Image *image) { } } - if (colours == ZM_COLOUR_GRAY8) { - if (*(buffer+index) != grayscale_val) + // `index` is a pixel index in [0, width*height). Convert to (x, y) and + // use linesize for the row stride — buffer+index would read into per-row + // padding (or the wrong row) whenever linesize > width*bytes_per_pixel. + const int y = index / width; + const int x = index % width; + + if (zm_bytes_per_pixel(pix_fmt) == 1) { + // GRAY8 plus all planar YUV variants we transport (420P/J420P/422P/J422P). + // For planar formats the Y plane is the first byte at each pixel index. + if (*(buffer + y * linesize + x) != grayscale_val) return true; - } else if (colours == ZM_COLOUR_RGB24) { - const uint8_t *ptr = buffer+(index*colours); + } else if (zm_is_rgb24(pix_fmt)) { + const uint8_t *ptr = buffer + y * linesize + x * 3; - if (usedsubpixorder == ZM_SUBPIX_ORDER_BGR) { + if (pix_fmt == AV_PIX_FMT_BGR24) { if ((RED_PTR_BGRA(ptr) != red_val) || (GREEN_PTR_BGRA(ptr) != green_val) || (BLUE_PTR_BGRA(ptr) != blue_val)) return true; } else { - /* Assume RGB */ + /* Assume RGB24 */ if ((RED_PTR_RGBA(ptr) != red_val) || (GREEN_PTR_RGBA(ptr) != green_val) || (BLUE_PTR_RGBA(ptr) != blue_val)) return true; } - } else if (colours == ZM_COLOUR_RGB32) { - if (usedsubpixorder == ZM_SUBPIX_ORDER_ARGB || usedsubpixorder == ZM_SUBPIX_ORDER_ABGR) { - if (ARGB_ABGR_ZEROALPHA(*(((const Rgb*)buffer)+index)) != ARGB_ABGR_ZEROALPHA(colour_val)) + } else if (zm_is_rgb32(pix_fmt)) { + const Rgb *ptr = (const Rgb *)(buffer + y * linesize + x * 4); + if (pix_fmt == AV_PIX_FMT_ARGB || pix_fmt == AV_PIX_FMT_ABGR) { + if (ARGB_ABGR_ZEROALPHA(*ptr) != ARGB_ABGR_ZEROALPHA(colour_val)) return true; } else { /* Assume RGBA or BGRA */ - if (RGBA_BGRA_ZEROALPHA(*(((const Rgb*)buffer)+index)) != RGBA_BGRA_ZEROALPHA(colour_val)) + if (RGBA_BGRA_ZEROALPHA(*ptr) != RGBA_BGRA_ZEROALPHA(colour_val)) return true; } } @@ -2147,7 +2304,7 @@ bool Monitor::Analyse() { } else { Debug(1, "No image to ref yet"); } - alarm_image.Assign(*(packet->image)); + WriteAlarmImage(*(packet->image)); } else { // didn't assign, do motion detection maybe and blending definitely if (!(analysis_image_count % (motion_frame_skip+1))) { @@ -2207,7 +2364,7 @@ bool Monitor::Analyse() { if (hasAnalysisViewers()) { // These extra copies are expensive, so only do it if we have viewers. - alarm_image.Assign(*(packet->analysis_image ? packet->analysis_image : packet->image)); + WriteAlarmImage(*(packet->analysis_image ? packet->analysis_image : packet->image)); } if (analysis_image == ANALYSISIMAGE_YCHANNEL) { @@ -2697,10 +2854,16 @@ int Monitor::Capture() { Image *capture_image = new Image(width, height, camera->Colours(), camera->SubpixelOrder()); capture_image->Fill(signalcolor); shared_data->signal = false; - shared_data->last_write_index = index; - shared_data->last_write_time = shared_timestamps[index].tv_sec; - image_buffer[index]->Assign(*capture_image); + // Publish in dependency order: slot bytes, then per-slot timestamp, + // then last_write_time (fresh from packet->timestamp, not the stale + // tv_sec the slot held from its previous occupant), then + // last_write_index as the commit step. Readers gate on + // last_write_index, so every piece of per-slot state they consume + // must be visible before this final assignment. + WriteShmFrame(index, capture_image); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); + shared_data->last_write_time = std::chrono::system_clock::to_time_t(packet->timestamp); + shared_data->last_write_index = index; delete capture_image; shared_data->image_count++; // What about timestamping it? @@ -2837,6 +3000,77 @@ bool Monitor::setupConvertContext(const AVFrame *input_frame, const Image *image return (convert_context != nullptr); } +void Monitor::WriteShmFrame(unsigned int index, Image *capture_image) { + // No conversion at the SHM-write side. zmc records the format the bytes + // were actually written in via image_pixelformats[index]; consumers in + // other processes (zms etc.) sync image_buffer[index] from that array + // before reading via ReadShmFrame, so the SHM transports any format + // Image can represent without a sws_scale step. This is the central + // no-conversion promise of the AVPixelFormat migration. + // + // Record imagePixFormat directly via PixFormat() — AVPixFormat() re-derives + // from the deprecated (colours, subpixelorder) pair and could propagate + // stale metadata. + // + // Only publish image_pixelformats[index] if Assign actually adopted the + // source format. Image::Assign returns void and silently leaves the + // destination untouched on failure (held-buffer undersize, unknown src + // format, etc.); publishing a new format whose bytes never landed in the + // slot would make readers misinterpret the previous slot contents. + const AVPixelFormat src_fmt = capture_image->PixFormat(); + image_buffer[index]->Assign(*capture_image); + if (image_buffer[index]->PixFormat() == src_fmt) { + image_pixelformats[index] = src_fmt; + } else { + Warning("WriteShmFrame: slot %u assign failed (dst fmt %s != src fmt %s); " + "keeping previously published pixelformat", + index, zm_get_pix_fmt_name(image_buffer[index]->PixFormat()), + zm_get_pix_fmt_name(src_fmt)); + } +} + +Image *Monitor::ReadShmFrame(unsigned int index) { + // Adopt the per-slot format zmc recorded into image_pixelformats[]. zms + // (and zma, etc.) construct image_buffer[i] at attach time with whatever + // initial format was convenient; the actual bytes the capture daemon + // wrote into the slot can be in any AVPixelFormat that zm_pixformat + // supports, varying per-slot and across reconnections. AVPixFormat() + // updates imagePixFormat, colours, subpixelorder, size and linesize + // together so the Image object consistently interprets the SHM bytes. + // No-op when the slot's format already matches. + // + // image_pixelformats[] lives in SHM and is written by another process — + // treat it as untrusted: an uninitialised slot, a torn write, or a + // mismatched zmc/zms build could leave an arbitrary enum value here. + // Two checks before applying: + // 1. The format must be in our supported set; av_image_get_buffer_size + // returns negative for unrecognised formats and would wrap into + // Image's unsigned size/linesize. + // 2. The format's required buffer size must fit shm_slot_size; even a + // supported format (e.g. RGBA at a larger width than expected) could + // otherwise let subsequent reads/writes run past the held slot. + AVPixelFormat fmt = image_pixelformats[index]; + if (fmt == AV_PIX_FMT_NONE || image_buffer[index]->PixFormat() == fmt) { + return image_buffer[index]; + } + unsigned int probe_colours, probe_subpix; + if (!zm_colours_from_pixformat(fmt, probe_colours, probe_subpix)) { + Warning("ReadShmFrame: ignoring unsupported pixelformat %d in slot %u; keeping current %s", + fmt, index, zm_get_pix_fmt_name(image_buffer[index]->PixFormat())); + return image_buffer[index]; + } + int required = av_image_get_buffer_size(fmt, width, height, 32); + if (required < 0 || static_cast(required) > shm_slot_size) { + Warning("ReadShmFrame: format %s requires %d bytes but slot %u capacity is %zu; " + "keeping current %s to avoid overrun", + zm_get_pix_fmt_name(fmt), required, index, shm_slot_size, + zm_get_pix_fmt_name(image_buffer[index]->PixFormat())); + return image_buffer[index]; + } + image_buffer[index]->AVPixFormat(fmt); + return image_buffer[index]; +} + void Monitor::applyOrientation(Image *image) { if (orientation == ROTATE_0) return; @@ -3086,7 +3320,39 @@ bool Monitor::Decode() { } if (!packet->image) { - packet->image = new Image(camera_width, camera_height, camera->Colours(), camera->SubpixelOrder()); + // Pick the most pipeline-friendly format Image can represent. If the + // decoder's native format is one Image supports, capture into that + // directly so sws_scale becomes a no-op identity copy via av_image_copy + // (Image::Assign(AVFrame*) takes that fast path on src_fmt == format). + // Otherwise fall back to YUV420P, which is universally supported and + // the smallest planar option. + // + // Cross-process consistency with zms is maintained per-slot via + // image_pixelformats[index] (written by WriteShmFrame, read on the + // zms side before each frame is consumed) — not by pinning the SHM + // to a single format here. + unsigned int native_colours, native_subpixelorder; + AVPixelFormat native_fmt = static_cast(packet->in_frame->format); + const char *native_fmt_name = av_get_pix_fmt_name(native_fmt); + if (!native_fmt_name) native_fmt_name = "unknown"; + + bool can_passthrough = (native_fmt == AV_PIX_FMT_YUV420P + || native_fmt == AV_PIX_FMT_YUVJ420P + || native_fmt == AV_PIX_FMT_YUV422P + || native_fmt == AV_PIX_FMT_YUVJ422P + || native_fmt == AV_PIX_FMT_GRAY8 + || zm_is_rgb24(native_fmt) + || zm_is_rgb32(native_fmt)); + + if (can_passthrough && zm_colours_from_pixformat(native_fmt, native_colours, native_subpixelorder)) { + Debug(1, "Using native frame format %s", native_fmt_name); + } else { + Debug(1, "Converting %s to yuv420p for pipeline", native_fmt_name); + native_colours = ZM_COLOUR_GRAY8; + native_subpixelorder = ZM_SUBPIX_ORDER_YUV420P; + } + + packet->image = new Image(camera_width, camera_height, native_colours, native_subpixelorder); bool have_converter = convert_context || setupConvertContext(packet->in_frame.get(), packet->image); if (have_converter) { @@ -3147,11 +3413,10 @@ bool Monitor::Decode() { TimestampImage(capture_image, packet->timestamp); } - // Write to shared image buffer + // Write to shared image buffer. unsigned int index = (shared_data->last_write_index + 1) % image_buffer_count; decoding_image_count++; - image_buffer[index]->Assign(*capture_image); - image_pixelformats[index] = capture_image->AVPixFormat(); + WriteShmFrame(index, capture_image); shared_timestamps[index] = zm::chrono::duration_cast(packet->timestamp.time_since_epoch()); shared_data->signal = signal_check_points ? CheckSignal(capture_image) : true; shared_data->last_write_index = index; diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 4778d25e938..31048228fca 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -659,6 +659,8 @@ class Monitor : public std::enable_shared_from_this { unsigned char *shared_images; std::vector image_buffer; AVPixelFormat *image_pixelformats; + AVPixelFormat *alarm_image_pixelformat; // cross-process format for alarm_image + size_t shm_slot_size; // per-slot byte capacity, sized to RGBA upper bound int video_stream_id; // will be filled in PrimeCapture int audio_stream_id; // will be filled in PrimeCapture @@ -932,9 +934,12 @@ class Monitor : public std::enable_shared_from_this { const std::string &getONVIF_Options() const { return onvif_options; }; Image *GetAlarmImage(); + // Writer-side helper: copies src into alarm_image and publishes its + // AVPixelFormat so reader processes can correctly interpret the SHM bytes. + void WriteAlarmImage(const Image &src); int GetImage(int32_t index=-1, int scale=100); - std::shared_ptr getSnapshot( int index=-1 ) const; - SystemTimePoint GetTimestamp(int index = -1) const; + std::shared_ptr getSnapshot( int index=-1 ); + SystemTimePoint GetTimestamp(int index = -1); void UpdateAdaptiveSkip(); useconds_t GetAnalysisRate(); Microseconds GetAnalysisUpdateDelay() const { return analysis_update_delay; } @@ -992,6 +997,16 @@ class Monitor : public std::enable_shared_from_this { bool CheckSignal( const Image *image ); bool Analyse(); bool setupConvertContext(const AVFrame *input_frame, const Image *image); + // Write capture_image into image_buffer[index] without conversion and + // record its AVPixelFormat in image_pixelformats[index] so reading + // processes can adopt that format via ReadShmFrame. + void WriteShmFrame(unsigned int index, Image *capture_image); + + // Read-side counterpart: ensures image_buffer[index]'s metadata matches + // the format zmc wrote via image_pixelformats[index] before returning + // it. Use this from zms / zma / event paths instead of touching + // image_buffer[index] directly. + Image *ReadShmFrame(unsigned int index); void applyOrientation(Image *image); bool applyDeinterlacing(std::shared_ptr &packet, Image *capture_image); bool Decode(); diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index 48ba5420fbe..0af045d1056 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -662,7 +662,7 @@ void MonitorStream::runStream() { if (!was_paused) { int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; Debug(1, "Saving paused image from index %d",index); - paused_image = new Image(*monitor->image_buffer[index]); + paused_image = new Image(*monitor->ReadShmFrame(index)); paused_timestamp = SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index])); } } else if (paused_image) { @@ -775,12 +775,12 @@ void MonitorStream::runStream() { send_image = monitor->GetAlarmImage(); if (!send_image) { Debug(1, "Falling back"); - send_image = monitor->image_buffer[index]; + send_image = monitor->ReadShmFrame(index); } } else { //AVPixelFormat pixformat = monitor->image_pixelformats[index]; //Debug(1, "Sending regular image index %d, pix format is %d %s", index, pixformat, av_get_pix_fmt_name(pixformat)); - send_image = monitor->image_buffer[index]; + send_image = monitor->ReadShmFrame(index); } if (!sendFrame(send_image, last_frame_timestamp)) { @@ -848,7 +848,7 @@ void MonitorStream::runStream() { temp_image_buffer[temp_index].timestamp = SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index])); - monitor->image_buffer[index]->WriteJpeg(temp_image_buffer[temp_index].file_name, config.jpeg_file_quality); + monitor->ReadShmFrame(index)->WriteJpeg(temp_image_buffer[temp_index].file_name, config.jpeg_file_quality); temp_write_index = MOD_ADD(temp_write_index, 1, temp_image_buffer_count); if (temp_write_index == temp_read_index) { // Go back to live viewing @@ -1003,8 +1003,8 @@ void MonitorStream::SingleImage(int scale) { int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; AVPixelFormat pixformat = monitor->image_pixelformats[index]; - Debug(1, "Sending regular image index %d, pix format is %d %s", index, pixformat, av_get_pix_fmt_name(pixformat)); - Image *snap_image = monitor->image_buffer[index]; + Debug(1, "Sending regular image index %d, pix format is %d %s", index, pixformat, zm_get_pix_fmt_name(pixformat)); + Image *snap_image = monitor->ReadShmFrame(index); if (!config.timestamp_on_capture) { monitor->TimestampImage(snap_image, SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index]))); diff --git a/src/zm_mpeg.cpp b/src/zm_mpeg.cpp index d1c10047f40..4ff8b819727 100644 --- a/src/zm_mpeg.cpp +++ b/src/zm_mpeg.cpp @@ -20,6 +20,7 @@ #include "zm_mpeg.h" #include "zm_logger.h" +#include "zm_pixformat.h" #include "zm_rgb.h" #include "zm_time.h" @@ -58,38 +59,10 @@ int VideoStream::SetupCodec( int bitrate, double frame_rate ) { - /* ffmpeg format matching */ - switch (colours) { - case ZM_COLOUR_RGB24: - if (subpixelorder == ZM_SUBPIX_ORDER_BGR) { - /* BGR subpixel order */ - pf = AV_PIX_FMT_BGR24; - } else { - /* Assume RGB subpixel order */ - pf = AV_PIX_FMT_RGB24; - } - break; - case ZM_COLOUR_RGB32: - if (subpixelorder == ZM_SUBPIX_ORDER_ARGB) { - /* ARGB subpixel order */ - pf = AV_PIX_FMT_ARGB; - } else if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) { - /* ABGR subpixel order */ - pf = AV_PIX_FMT_ABGR; - } else if (subpixelorder == ZM_SUBPIX_ORDER_BGRA) { - /* BGRA subpixel order */ - pf = AV_PIX_FMT_BGRA; - } else { - /* Assume RGBA subpixel order */ - pf = AV_PIX_FMT_RGBA; - } - break; - case ZM_COLOUR_GRAY8: - pf = AV_PIX_FMT_GRAY8; - break; - default: - Panic("Unexpected colours: %d",colours); - break; + pf = zm_pixformat_from_colours(colours, subpixelorder); + if (pf == AV_PIX_FMT_NONE) { + Panic("Unsupported (colours, subpixelorder) pair: colours=%d subpixelorder=%d", + colours, subpixelorder); } if (strcmp("rtp", of->name) == 0) { diff --git a/src/zm_pixformat.h b/src/zm_pixformat.h new file mode 100644 index 00000000000..c53068f59cc --- /dev/null +++ b/src/zm_pixformat.h @@ -0,0 +1,164 @@ +// +// ZoneMinder Pixel Format Helpers +// Copyright (C) 2026 ZoneMinder +// +// 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. +// + +#ifndef ZM_PIXFORMAT_H +#define ZM_PIXFORMAT_H + +#include "zm_ffmpeg.h" +#include "zm_rgb.h" + +#include + +// +// Central pixel format conversion helpers. +// +// These replace the legacy ZM_COLOUR_* / ZM_SUBPIX_ORDER_* integer pair with +// AVPixelFormat as the single source of truth. The ZM_COLOUR_* constants +// (1=GRAY8, 3=RGB24, 4=RGB32) are retained only as bytes-per-pixel values for +// backwards compatibility with pixel-stride arithmetic. +// + +// Forward mapping: (ZM colours, ZM subpixelorder) -> AVPixelFormat +inline AVPixelFormat zm_pixformat_from_colours(unsigned int colours, unsigned int subpixelorder) { + if (colours == ZM_COLOUR_GRAY8) { + if (subpixelorder == ZM_SUBPIX_ORDER_YUV420P) return AV_PIX_FMT_YUV420P; + if (subpixelorder == ZM_SUBPIX_ORDER_YUVJ420P) return AV_PIX_FMT_YUVJ420P; + if (subpixelorder == ZM_SUBPIX_ORDER_YUV422P) return AV_PIX_FMT_YUV422P; + if (subpixelorder == ZM_SUBPIX_ORDER_YUVJ422P) return AV_PIX_FMT_YUVJ422P; + return AV_PIX_FMT_GRAY8; + } + if (colours == ZM_COLOUR_RGB24) { + if (subpixelorder == ZM_SUBPIX_ORDER_BGR) return AV_PIX_FMT_BGR24; + return AV_PIX_FMT_RGB24; + } + if (colours == ZM_COLOUR_RGB32) { + if (subpixelorder == ZM_SUBPIX_ORDER_ARGB) return AV_PIX_FMT_ARGB; + if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) return AV_PIX_FMT_ABGR; + if (subpixelorder == ZM_SUBPIX_ORDER_BGRA) return AV_PIX_FMT_BGRA; + return AV_PIX_FMT_RGBA; + } + return AV_PIX_FMT_NONE; +} + +// Inverse mapping: AVPixelFormat -> (ZM colours, ZM subpixelorder) +// Returns true if the format is recognised, false otherwise (out params untouched). +inline bool zm_colours_from_pixformat(AVPixelFormat fmt, + unsigned int &colours, + unsigned int &subpixelorder) { + switch (fmt) { + case AV_PIX_FMT_GRAY8: + colours = ZM_COLOUR_GRAY8; + subpixelorder = ZM_SUBPIX_ORDER_NONE; + return true; + case AV_PIX_FMT_YUV420P: + colours = ZM_COLOUR_GRAY8; + subpixelorder = ZM_SUBPIX_ORDER_YUV420P; + return true; + case AV_PIX_FMT_YUVJ420P: + colours = ZM_COLOUR_GRAY8; + subpixelorder = ZM_SUBPIX_ORDER_YUVJ420P; + return true; + case AV_PIX_FMT_YUV422P: + colours = ZM_COLOUR_GRAY8; // Y-plane is 1 byte/pixel + subpixelorder = ZM_SUBPIX_ORDER_YUV422P; + return true; + case AV_PIX_FMT_YUVJ422P: + colours = ZM_COLOUR_GRAY8; // Y-plane is 1 byte/pixel + subpixelorder = ZM_SUBPIX_ORDER_YUVJ422P; + return true; + case AV_PIX_FMT_RGB24: + colours = ZM_COLOUR_RGB24; + subpixelorder = ZM_SUBPIX_ORDER_RGB; + return true; + case AV_PIX_FMT_BGR24: + colours = ZM_COLOUR_RGB24; + subpixelorder = ZM_SUBPIX_ORDER_BGR; + return true; + case AV_PIX_FMT_RGBA: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_RGBA; + return true; + case AV_PIX_FMT_BGRA: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_BGRA; + return true; + case AV_PIX_FMT_ARGB: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_ARGB; + return true; + case AV_PIX_FMT_ABGR: + colours = ZM_COLOUR_RGB32; + subpixelorder = ZM_SUBPIX_ORDER_ABGR; + return true; + default: + return false; + } +} + +// Bytes-per-pixel in the primary buffer. For planar YUV formats this is the +// Y-plane stride (1), not the overall average. +inline unsigned int zm_bytes_per_pixel(AVPixelFormat fmt) { + switch (fmt) { + case AV_PIX_FMT_GRAY8: + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUVJ422P: + return 1; + case AV_PIX_FMT_RGB24: + case AV_PIX_FMT_BGR24: + return 3; + case AV_PIX_FMT_RGBA: + case AV_PIX_FMT_BGRA: + case AV_PIX_FMT_ARGB: + case AV_PIX_FMT_ABGR: + return 4; + default: + return 0; + } +} + +// Map the persisted DB Monitors.Colours value to an AVPixelFormat. +// Valid DB values are {1, 3, 4}. +inline AVPixelFormat zm_db_colours_to_pixformat(int db_colours) { + switch (db_colours) { + case ZM_COLOUR_GRAY8: return AV_PIX_FMT_GRAY8; + case ZM_COLOUR_RGB24: return AV_PIX_FMT_RGB24; + case ZM_COLOUR_RGB32: return AV_PIX_FMT_RGBA; + default: return AV_PIX_FMT_NONE; + } +} + +// Format-family predicates. Prefer these over ZM_COLOUR_* comparisons when +// dispatching on pixel format. +inline bool zm_is_rgb32(AVPixelFormat fmt) { + return fmt == AV_PIX_FMT_RGBA + || fmt == AV_PIX_FMT_BGRA + || fmt == AV_PIX_FMT_ARGB + || fmt == AV_PIX_FMT_ABGR; +} + +inline bool zm_is_rgb24(AVPixelFormat fmt) { + return fmt == AV_PIX_FMT_RGB24 || fmt == AV_PIX_FMT_BGR24; +} + +inline bool zm_is_yuv420(AVPixelFormat fmt) { + return fmt == AV_PIX_FMT_YUV420P || fmt == AV_PIX_FMT_YUVJ420P; +} + +// av_get_pix_fmt_name returns nullptr for unknown formats (including +// AV_PIX_FMT_NONE). Passing nullptr to a %s format specifier is undefined, +// so always go through this wrapper when logging. +inline const char *zm_get_pix_fmt_name(AVPixelFormat fmt) { + const char *name = av_get_pix_fmt_name(fmt); + return name ? name : "unknown"; +} + +#endif // ZM_PIXFORMAT_H diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index 501d9aa49cb..41c4f769bcd 100644 --- a/src/zm_remote_camera_rtsp.cpp +++ b/src/zm_remote_camera_rtsp.cpp @@ -69,17 +69,21 @@ RemoteCameraRtsp::RemoteCameraRtsp( } /* Has to be located inside the constructor so other components such as zma will receive correct colours and subpixel order */ - if ( colours == ZM_COLOUR_RGB32 ) { + if ( zm_is_rgb32(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGBA; imagePixFormat = AV_PIX_FMT_RGBA; - } else if ( colours == ZM_COLOUR_RGB24 ) { + pixelFormat = AV_PIX_FMT_RGBA; + } else if ( zm_is_rgb24(pixelFormat) ) { subpixelorder = ZM_SUBPIX_ORDER_RGB; imagePixFormat = AV_PIX_FMT_RGB24; - } else if ( colours == ZM_COLOUR_GRAY8 ) { + pixelFormat = AV_PIX_FMT_RGB24; + } else if ( pixelFormat == AV_PIX_FMT_GRAY8 ) { subpixelorder = ZM_SUBPIX_ORDER_NONE; imagePixFormat = AV_PIX_FMT_GRAY8; + pixelFormat = AV_PIX_FMT_GRAY8; } else { - Panic("Unexpected colours: %d", colours); + Panic("Unexpected pixel format %d (%s); legacy colours=%d subpixelorder=%d", + pixelFormat, zm_get_pix_fmt_name(pixelFormat), colours, subpixelorder); } } // end RemoteCameraRtsp::RemoteCameraRtsp(...) diff --git a/src/zm_rgb.h b/src/zm_rgb.h index 66b1c169c78..eb40e76b88c 100644 --- a/src/zm_rgb.h +++ b/src/zm_rgb.h @@ -100,15 +100,18 @@ constexpr Rgb kRGBTransparent = 0x01000000; // #define RGB_FASTLUM_SINGLE_ITU601(v) ((RED(v)+RED(v)+RED(v)+BLUE(v)+GREEN(v)+GREEN(v)+GREEN(v)+GREEN(v))>>3) // #define RGB_FASTLUM_VALUES_ITU601(ra,ga,ba) (((ra)+(ra)+(ra)+(ba)+(ga)+(ga)+(ga)+(ga))>>3) -/* ZM colours */ +// DEPRECATED: ZM_COLOUR_* and ZM_SUBPIX_ORDER_* are being replaced by +// AVPixelFormat throughout the codebase. Use the helpers in zm_pixformat.h for +// format identification. These values are retained only for byte-per-pixel +// stride arithmetic and backwards compatibility with the DB Monitors.Colours +// column which stores {1, 3, 4}. #define ZM_COLOUR_RGB32 4 #define ZM_COLOUR_RGB24 3 #define ZM_COLOUR_GRAY8 1 -#define ZM_COLOUR_YUV420P 1 -#define ZM_COLOUR_YUVJ420P 1 +#define ZM_COLOUR_YUV420P 1 // DEPRECATED: collides with GRAY8; use AVPixelFormat +#define ZM_COLOUR_YUVJ420P 1 // DEPRECATED: collides with GRAY8; use AVPixelFormat -/* Subpixel ordering */ -/* Based on byte order naming. For example, for ARGB (on both little endian or big endian) byte+0 should be alpha, byte+1 should be red, and so on. */ +// DEPRECATED: Subpixel ordering constants. Use AVPixelFormat directly. #define ZM_SUBPIX_ORDER_NONE 2 #define ZM_SUBPIX_ORDER_RGB 6 #define ZM_SUBPIX_ORDER_BGR 5 @@ -118,9 +121,11 @@ constexpr Rgb kRGBTransparent = 0x01000000; #define ZM_SUBPIX_ORDER_ARGB 10 #define ZM_SUBPIX_ORDER_YUV420P 11 #define ZM_SUBPIX_ORDER_YUVJ420P 12 +#define ZM_SUBPIX_ORDER_YUV422P 13 +#define ZM_SUBPIX_ORDER_YUVJ422P 14 -/* A macro to use default subpixel order for a specified colour. */ -/* for grayscale it will use NONE, for 3 colours it will use R,G,B, for 4 colours it will use R,G,B,A */ +// DEPRECATED: Use zm_pixformat_from_colours() in zm_pixformat.h instead. +// Only valid for DB-persisted colours values {1, 3, 4}. #define ZM_SUBPIX_ORDER_DEFAULT_FOR_COLOUR(c) ((c)<<1) /* Convert RGB colour value into BGR\ARGB\ABGR */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 508a6ac380b..0360a5a8707 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,7 @@ set(TEST_SOURCES zm_crypt.cpp zm_font.cpp zm_onvif_renewal.cpp + zm_pixformat.cpp zm_poly.cpp zm_utils.cpp zm_vector2.cpp diff --git a/tests/zm_pixformat.cpp b/tests/zm_pixformat.cpp new file mode 100644 index 00000000000..aa7fad03467 --- /dev/null +++ b/tests/zm_pixformat.cpp @@ -0,0 +1,201 @@ +/* + * 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_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); +} diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 3d00ba7e802..b9db66cd0b8 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -673,6 +673,7 @@ 'StorageScheme' => 'Scheme', 'StreamReplayBuffer' => 'Stream Replay Image Buffer', 'TargetColorspace' => 'Target colorspace', + 'DeprecatedColoursSetting' => 'Deprecated - will be auto-detected in a future release', 'TimeDelta' => 'Time Delta', 'TimelineTip1' => 'Pass your mouse over the graph to view a snapshot image and event details.', // Added 2013.08.15. 'TimelineTip2' => 'Click on the coloured sections of the graph, or the image, to view the event.', // Added 2013.08.15. diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 8c04e2d713a..d2f61cf380a 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -907,6 +907,7 @@ class="nav-link Colours()) ?> + ()