vdecoderjpeg_web_logo

VDecoderJPEG C++ library

v1.1.0

Table of contents

Overview

The VDecoderJPEG C++ library provides a standalone software JPEG and Motion JPEG (MJPEG) video decoder for Linux, Windows, and embedded platforms. Key features:

  • Zero external dependencies: Contains complete, self-contained implementations of JPEG marker parsing (SOI, SOF0, SOF1, DQT, DHT, SOS, DRI, RSTm, EOI and the APP0 / APP14 color space markers), entropy data reading with byte stuffing removal, Huffman entropy decoding with two-level direct prefix lookup tables, an integer 8x8 inverse discrete cosine transform (IDCT), chroma conversion to 4:2:0 and uncompressed frame assembly. Zero third-party media frameworks or OS-specific decoding services are required.
  • Pure C++ Source Architecture for Certification: Built 100% from pure C++17 source code with native SIMD intrinsics (AVX2 and SSE4.1 on x86 / x86-64, NEON on ARM64) and portable C++ code for everything else, including a complete portable fallback of every SIMD kernel. Uses no precompiled libraries, preloaded shared objects, or runtime dynamic loading (.dll, .so, LoadLibrary, dlopen), meeting all strict software certification standards.
  • Runs on any CPU, builds with any C++17 compiler: the SIMD kernels are compiled per source file and selected at run time (AVX2 -> SSE4.1 -> portable C++ on x86, NEON on ARM64, portable C++ on every other architecture), so a binary never requires an instruction set the CPU does not have. See SIMD acceleration and portability.
  • Cross-platform: Windows (MSVC 2022, x86 and x86-64), Linux x86 / x86-64 (GCC, Clang) and Linux ARM64 / AArch64 (GCC, Clang, NEON). Any other platform with a C++17 compiler and std::thread is built with the portable code. The decoded output is bit-identical on all platforms, SIMD levels and thread counts.
  • Supported streams: Baseline and extended sequential (SOF0, SOF1) 8-bit Huffman-coded JPEG / MJPEG streams with one interleaved scan that contains all components: 4:2:0 (YCbCr, luma sampling 2x2), 4:2:2 (YCbCr 2x1), 4:4:0 (YCbCr 1x2), 4:4:4 (YCbCr 1x1) - also with larger sampling factors in the same ratios, at most 10 blocks per MCU - and Grayscale (single component). Image size from 1 x 1 up to 65535 x 65535 with at most 268,435,456 pixels (for example 16384 x 16384). Supports restart markers (DRI and RST0–RST7), 8-bit and 16-bit quantization tables and fill bytes before markers. Frames without DHT segments (typical for MJPEG) are decoded with the typical Huffman tables of ITU-T T.81 Annex K; tables of previous frames stay valid until they are redefined or RESET is executed. Not supported, transcode(...) returns false: progressive, lossless, arithmetic-coded, hierarchical, multi-scan and 12-bit streams, CMYK / YCCK (4 components), RGB-coded frames (Adobe color transform 0, or component identifiers ‘R’, ‘G’, ‘B’ without JFIF marker), other subsampling (for example 4:1:1), frames whose height is defined by a DNL marker and damaged frames (invalid marker segments or tables, invalid Huffman codes, missing restart markers, entropy data that ends before the last MCU).
  • NV12 Output: Decodes JPEG frames into standard NV12 uncompressed video frames (Y plane followed by interleaved UV plane), ready for display or downstream computer vision / AI processing. Chroma of 4:2:2, 4:4:0 and 4:4:4 frames is converted to 4:2:0 by averaging 2 samples (4:2:2 vertically, 4:4:0 horizontally) or 2x2 samples (4:4:4) with rounding; grayscale frames get neutral chroma (128). NV12 requires even dimensions, so a frame with odd width and/or height is padded by one column and/or one row of decoded samples (the output size is the JPEG size rounded up to an even number).
  • Single-threaded by default, optional multi-threading: By default (CUSTOM_2 = 1.0f) the decoder works single-threaded with zero background thread overhead. Optionally (CUSTOM_2 = 0.0f for automatic, or N >= 2) the calling thread decodes the entropy data row of MCUs by row of MCUs while worker threads reconstruct the decoded rows (inverse DCT and NV12 assembly) in parallel, achieving significant acceleration while maintaining 100% bit-identical output.
  • Standard VCodec Interface: Inherits from the open-source cr::video::VCodec interface (3rdparty/VCodec), enabling drop-in compatibility across the RapidPixel SDK ecosystem.
  • C++17 Architecture: Adheres strictly to the PIMPL pattern, shielding public headers from internal decoder structures and ensuring rapid compilation and clean ABI stability.

The VDecoderJPEG class depends on the VCodec class, which determines video frame structures. The example application uses SimpleUI for window display and drawing, and a lightweight built-in Motion JPEG stream parser (JpegParser). The library uses the C++17 standard.

Versions

Table 1 - Library versions.

Version Release date What’s new
1.0.0 30.09.2026 - First version.
1.1.0 06.10.2026 - Decoder rewritten, about 35 % faster.
- Range checks of all marker segments and entropy data, limited memory use for damaged files.
- NOTICE and README section with the origin of the inverse DCT method (IJG license).
- No compiler warnings (MSVC /W4, GCC, Clang).

Library files

The repository structure is organized as follows:

CMakeLists.txt ---------------- Main CMake build configuration.
README.md --------------------- Documentation and usage guide.
3rdparty ---------------------- External dependencies.
    CMakeLists.txt ------------ CMake inclusion script for third-party libraries.
    VCodec -------------------- VCodec interface and Frame submodule.
src --------------------------- Library source code.
    CMakeLists.txt ------------ CMake script for VDecoderJPEG library.
    VDecoderJPEG.h ------------ Public library interface (PIMPL).
    VDecoderJPEG.cpp ---------- Public library implementation wrapper.
    VDecoderJPEGVersion.h ----- Version definition header.
    VDecoderJPEGVersion.h.in -- Template for CMake version generation.
    impl ---------------------- Internal decoder implementation files.
        VDecoderJPEGImpl.h ---- Internal PIMPL class declaration.
        VDecoderJPEGImpl.cpp -- PIMPL class implementation & frame buffer management.
        JpegEngine.h ---------- Decoder engine and internal JPEG data types.
        JpegEngine.cpp -------- Marker parsing, Huffman decoding, portable IDCT and NV12 conversion, kernel selection, threads.
        JpegKernels.h --------- IDCT arithmetic shared by all kernel sets, table of the reconstruction kernels.
        x86 ------------------- x86 SIMD kernels (compiled with their own instruction set flags) and CPU detection.
            JpegX86.h --------- Declarations of the x86 CPU checks and kernel sets.
            JpegCpuFeatures.cpp  CPUID / XGETBV checks (no instruction set flags).
            JpegKernelsSse41.cpp SSE4.1 IDCT and chroma conversion kernels.
            JpegKernelsAvx2.cpp  AVX2 IDCT and chroma conversion kernels.
example ----------------------- Standalone sample application.
    SimpleUI ------------------ Lightweight window and drawing library.
    JpegParser.h -------------- Standalone Motion JPEG stream parser header.
    JpegParser.cpp ------------ Standalone Motion JPEG stream parser implementation.
    CMakeLists.txt ------------ Example build configuration.
    main.cpp ------------------ Example demonstrating video decoding with VDecoderJPEG.

VDecoderJPEG class description

VDecoderJPEG class declaration

The VDecoderJPEG class is declared in VDecoderJPEG.h:

namespace cr
{
namespace video
{
class VDecoderJPEG : public VCodec
{
public:

    /// Class constructor.
    VDecoderJPEG();

    /// Class destructor.
    ~VDecoderJPEG() override;

    /// Get library version.
    static std::string getVersion();

    /// Decode JPEG video frame to NV12.
    bool transcode(Frame& src, Frame& dst) override;

    /// Set parameter value.
    bool setParam(VCodecParam id, float value) override;

    /// Get parameter value.
    float getParam(VCodecParam id) override;

    /// Execute codec command.
    bool executeCommand(VCodecCommand id) override;
};
}
}

getVersion method

The getVersion() method returns a string with the current version of the library:

static std::string getVersion();

Example usage:

std::cout << "VDecoderJPEG version: " << cr::video::VDecoderJPEG::getVersion() << std::endl;

Terminal output:

VDecoderJPEG version: 1.1.0

transcode method

The transcode(…) method decodes an incoming JPEG frame into an uncompressed NV12 video frame.

bool transcode(Frame& src, Frame& dst) override;
Parameter Type Description
src Frame& Source compressed video frame. Field src.fourcc must be Fourcc::JPEG (or bitstream starting with SOI marker 0xFF 0xD8). Data pointer src.data points to one complete JPEG image (SOI marker first) and src.size specifies its size in bytes: src.data must point to at least src.size readable bytes, the decoder reads nothing beyond them. src.width and src.height are not used.
dst Frame& Destination uncompressed video frame. Upon successful decode, the decoder populates dst.data with raw NV12 pixel data (Y plane followed by interleaved UV plane), sets dst.width, dst.height, dst.size = width * height * 3 / 2 and dst.fourcc = Fourcc::NV12, and copies frameId and sourceId from src. The width and height are rounded up to even numbers (NV12 requirement). If dst.data is not null and dst.size is at least the NV12 size, the existing buffer is reused (dst.data must point to at least dst.size writable bytes), otherwise a new buffer is allocated. dst is left unchanged if decoding fails.

Returns: true if a frame was successfully decoded; false if the stream is unsupported, damaged or truncated (invalid markers or tables, missing restart marker, entropy data that ends before the last MCU) or if memory can not be allocated. The decoding time of a frame is proportional to its number of blocks; a frame whose entropy data is shorter than 2 bits per block is rejected before its buffers are allocated.

setParam method

The setParam(…) method configures decoder parameters.

bool setParam(VCodecParam id, float value) override;
Parameter Type Description
id VCodecParam Parameter identifier from the VCodecParam enum.
value float Value to assign to the parameter.

Supported parameters:

  • VCodecParam::LOG_LEVEL: Logging verbosity, integer values are accepted and stored (default 0). The decoder does not print or log anything.
  • VCodecParam::TYPE: Codec type, integer values are accepted and stored (default 0). The decoder is always a software decoder.
  • VCodecParam::CUSTOM_1: Reserved for custom user configurations, stored only (default 0.0f).
  • VCodecParam::CUSTOM_2: Number of decoding threads (default 1.0f):
    • 1.0f - single-threaded decoding (default): everything is decoded in the thread which calls transcode(...), no worker threads are created.
    • 0.0f - automatic: worker thread pool depending on hardware concurrency (at most 16 threads).
    • N >= 2 - worker pool with N threads (at most 32 threads are used).
    • Negative and non-finite values are rejected. If a worker thread can not be created, false is returned and the decoder stays single-threaded (CUSTOM_2 reads 1.0f).
    • The output is bit-identical for every value.
  • VCodecParam::CUSTOM_3: Color range control for output NV12 frame (default 1.0f):
    • 1.0f: Automatic (default). The luma of every frame is analysed (every 4th sample of every 4th row, every 2nd for sizes up to 256). If the luma span is at least 8 levels and fewer than 0.2% of the analysed samples are outside [14..238], the frame is treated as limited range and luma [16..235] and chroma [16..240] are expanded to Full Range [0..255] during NV12 assembly (values outside the limited range are clipped). Frames with more samples outside [14..238], as typical for full-range content, are not changed. The decision is made for every frame independently, so a full-range frame without dark and bright samples can be expanded too: use 0.0f if the source is known to be full range.
    • 0.0f: Disabled / pass-through: pass-through raw decoded samples without range expansion.
    • 2.0f: Forced expansion: always expand Limited Range to Full Range.
    • Other values are rejected.

Returns: true if the parameter was accepted, false otherwise.

getParam method

The getParam(…) method queries parameter values from the decoder.

float getParam(VCodecParam id) override;

Returns: Value of the specified parameter, or -1.0f if unsupported.

executeCommand method

The executeCommand(…) method sends control commands to the decoder.

bool executeCommand(VCodecCommand id) override;

Supported commands:

  • VCodecCommand::RESET: Clears internal decoder state: the quantization and Huffman tables of previous frames are forgotten and the frame buffers are released, ready for a new stream.

Returns: true if the command was executed successfully.

A VDecoderJPEG object is not thread-safe: call its methods from one thread at a time (the worker threads are internal). Several objects can be used in parallel.

Build and connect to your project

Typical commands to build VDecoderJPEG library:

cd VDecoderJPEG
mkdir build
cd build
cmake ..
make

If you want to connect the VDecoderJPEG library to your CMake project as source code, you can do the following. For example, if your repository has the structure:

CMakeLists.txt
src
    CMakeLists.txt
    yourLib.h
    yourLib.cpp

Create folder 3rdparty in your repository and copy VDecoderJPEG folder there. The new structure of your repository:

CMakeLists.txt
src
    CMakeLists.txt
    yourLib.h
    yourLib.cpp
3rdparty
    VDecoderJPEG

Create a CMakeLists.txt file in the 3rdparty folder. The CMakeLists.txt file should contain:

cmake_minimum_required(VERSION 3.13)

################################################################################
## 3RD-PARTY
## dependencies for the project
################################################################################
project(3rdparty LANGUAGES CXX)

################################################################################
## SETTINGS
## basic 3rd-party settings before use
################################################################################
# To inherit the top-level architecture when the project is used as a submodule.
SET(PARENT ${PARENT}_YOUR_PROJECT_3RDPARTY)
# Disable self-overwriting of parameters inside included subdirectories.
SET(${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF CACHE BOOL "" FORCE)

################################################################################
## INCLUDING SUBDIRECTORIES
## Adding subdirectories according to the 3rd-party configuration
################################################################################
add_subdirectory(VDecoderJPEG)

Next, you need to include the 3rdparty folder in the main CMakeLists.txt file of your repository. Add the following line at the end of your main CMakeLists.txt:

add_subdirectory(3rdparty)

Next, you need to include the VDecoderJPEG library in your src/CMakeLists.txt file:

target_link_libraries(${PROJECT_NAME} VDecoderJPEG)

Include the public header in your code:

#include "VDecoderJPEG.h"

Simple example

The example application (example/main.cpp) demonstrates opening a Motion JPEG video file (*.mjpeg), decoding frames to NV12, converting to YUV format, and rendering using SimpleUI:

#include <cstring>
#include <iostream>
#include <string>
#include "VDecoderJPEG.h"
#include "JpegParser.h"
#include "SimpleUi.h"

// Link namespaces.
using namespace std;
using namespace cr::ui;
using namespace cr::video;

// Entry point.
int main(int argc, char* argv[])
{
    // Print library version.
    cout << "VDecoderJPEG v" << VDecoderJPEG::getVersion() << endl;

    // Get input Motion JPEG file path.
    string videoPath;
    if (argc > 1)
    {
        videoPath = argv[1];
    }
    else
    {
        cout << "Enter path to video file (*.mjpeg): ";
        getline(cin, videoPath);
    }
    if (!videoPath.empty() && (videoPath.front() == '"' || videoPath.front() == '\''))
        videoPath.erase(0, 1);
    if (!videoPath.empty() && (videoPath.back() == '"' || videoPath.back() == '\''))
        videoPath.pop_back();
    if (videoPath.empty())
        videoPath = "test.mjpeg";

    // Open Motion JPEG stream file.
    JpegParser parser;
    if (!parser.open(videoPath))
    {
        cerr << "Failed to open video file: " << videoPath << endl;
        return -1;
    }

    // Initialize decoder and UI window.
    VDecoderJPEG decoder;
    SimpleUiWindow window("VDecoderJPEG - Video");
    window.show();

    // Input JPEG frame.
    Frame jpegFrame(640, 480, Fourcc::JPEG);
    // Decoded NV12 frame.
    Frame nv12Frame(640, 480, Fourcc::NV12);
    // Converted YUV frame.
    Frame yuvFrame(640, 480, Fourcc::YUV24);

    // Decode and display video frames.
    while (window.isOpen() && parser.getFrame(jpegFrame))
    {
        // Decode JPEG frame to NV12 format.
        if (!decoder.transcode(jpegFrame, nv12Frame))
            continue;

        // Convert NV12 frame to YUV format with only Y channel.
        if (nv12Frame.width != yuvFrame.width || nv12Frame.height != yuvFrame.height)
            yuvFrame = Frame(nv12Frame.width, nv12Frame.height, Fourcc::YUV24);
        memset(yuvFrame.data, 128, yuvFrame.size);
        for (int i = 0; i < yuvFrame.size; i = i + 3)
            yuvFrame.data[i] = nv12Frame.data[i / 3];

        // Render frame and process UI events (ESC to exit).
        window.render(yuvFrame);
        if (window.getKey(30) == 27)
            break;
    }

    return 0;
}

SIMD acceleration and portability

The decoder is plain C++17 with a complete portable implementation of every sample reconstruction routine (inverse DCT, chroma conversion to NV12). SIMD kernels are optional accelerators of these routines; the kernel set is chosen once per process, when the first decoder is created:

CPU Kernels used How the level is chosen
x86 / x86-64 AVX2 -> SSE4.1 -> portable C++ CPUID. AVX2 additionally requires OS support for the YMM registers (OSXSAVE + XGETBV).
ARM64 / AArch64 NEON (chroma interleaving) + portable C++ At compile time, NEON is always available on AArch64.
Any other CPU portable C++ No intrinsics are compiled.

A binary built on a machine with AVX2 therefore also starts and decodes on a CPU with only SSE4.1 or even only SSE2 (checked with emulated Haswell, Nehalem, Penryn and Core 2 (Conroe) CPUs), and the decoded output is bit-identical for every level. The entropy decoding and all other code are built without instruction set flags; the SSE4.1 kernels make the decoding about 20 % faster than the portable code on a CPU without AVX2.

Instruction set flags are applied per source file, never globally:

Source files Flags
src/impl/x86/JpegKernelsSse41.cpp -msse4.1 (GCC / Clang). MSVC needs no flag.
src/impl/x86/JpegKernelsAvx2.cpp -mavx2 (GCC / Clang), /arch:AVX2 (MSVC).
All other files, including the CPU detection, the kernel selection and the portable code No instruction set flags (no -march=native, no global -mavx2 or /arch:AVX2).

CMake checks with check_cxx_source_compiles that the target is x86, that the CPU detection compiles and that the compiler can build the SSE4.1 and AVX2 kernels. If it can not (very old compiler, unusual toolchain, universal macOS binary), these kernels are left out, a message is printed during configuration, and the next level is used. Options:

Option / macro Effect
-DVDECODERJPEG_ENABLE_SIMD=OFF Builds the portable decoder only (defines VDECODERJPEG_NO_SIMD, also switches off NEON).
VDECODERJPEG_NO_SSE41, VDECODERJPEG_NO_AVX2 Leave out an x86 kernel set. CMake defines them automatically when a compiler check fails.

The macros are private compile definitions of the VDecoderJPEG target. Without CMake, compile all src/**/*.cpp files as C++17, add the flags from the table to the listed files (or define the macros above) and link the system thread library (-pthread).


Table of contents