From eaba4b02a1ac19f89c027d54a123d69229f2d73a Mon Sep 17 00:00:00 2001 From: Victor Derks Date: Tue, 23 Jul 2024 13:17:12 +0200 Subject: [PATCH] Add support to encode and decode mapping tables (#302) * Add support to write mapping tables to the JPEG byte stream. * Add API method to set the table ID for a component * Add method to create JPEG-LS file with mapping tables only. * Add support to read mapping tables * Parse table-ids from the scan segment * Extend API of charls_decoder_get_mapping_table_index to return -1 if index is not found * Add support to read mapping table continuation segments * Document new API methods * Add sample image from appendix H.4.5 "Example of a palletised image" / Figure H.10 to unit test * Update the get_mapping_table API to include size of the buffer --- .gitignore | 8 +- CHANGELOG.md | 16 +- CMakeLists.txt | 2 +- CharLS.sln.DotSettings | 2 + README.md | 1 - azure-pipelines.yml | 2 +- cpp.hint | 3 + default.ruleset | 1 + default.ruleset.md | 3 + include/charls/charls_jpegls_decoder.h | 179 ++++++++++++++++- include/charls/charls_jpegls_encoder.h | 100 +++++++++- include/charls/public_types.h | 59 +++++- spelling.dic | 3 + src/charls_jpegls_decoder.cpp | 129 +++++++++++- src/charls_jpegls_encoder.cpp | 125 +++++++++--- src/constants.h | 20 +- src/jpeg_stream_reader.cpp | 142 ++++++++++++- src/jpeg_stream_reader.h | 122 +++++++++++- src/jpeg_stream_writer.cpp | 54 ++++- src/jpeg_stream_writer.h | 40 +++- src/jpegls_error.cpp | 12 ++ src/util.h | 9 + test/compliance.cpp | 114 +++++++---- test/compliance.h | 1 + test/main.cpp | 5 +- unittest/charls_jpegls_decoder_test.cpp | 72 ++++--- unittest/charls_jpegls_encoder_test.cpp | 20 ++ unittest/compliance_test.cpp | 87 ++++++++ unittest/jpeg_stream_reader_test.cpp | 181 +++++++++++++++++ unittest/jpeg_stream_writer_test.cpp | 129 ++++++++++-- unittest/jpeg_test_stream_writer.h | 68 +++++++ unittest/jpegls_decoder_test.cpp | 255 ++++++++++++++++++++++-- unittest/jpegls_encoder_test.cpp | 214 +++++++++++++++++++- unittest/pch.h | 1 + 34 files changed, 1993 insertions(+), 186 deletions(-) diff --git a/.gitignore b/.gitignore index 7b59db5b..cc83c7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,10 @@ # GIT ignore file for CharLS -obj/ -bin/ -Win32/ -x64/ .vscode/ .vs/ .idea build*/ -[Dd]ebug/ -[Rr]elease/ -[Cc]hecked/ +cmake-build-debug/ TestResults/ vcpkg_installed/ *.opensdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f201b6..39510587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,22 @@ All notable changes to this project are documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [3.0.0] - +## [3.0.0] - UNRELEASED + +### Added + +- Support to encode and decode mapping tables. ### Changed -- Updated the source code to C++17. This is a breaking change, high version is updated to 3. -- encoding_options::include_pc_parameters_jai is not enabled by default anymore. This is a breaking change. +- BREAKING: Updated the minimal required C++ language version to C++17. +- BREAKING: encoding_options::include_pc_parameters_jai is not enabled by default anymore. + +### Removed + +- BREAKING: Legacy 1.x API methods have been removed. ## [2.4.2] - 2023-5-16 diff --git a/CMakeLists.txt b/CMakeLists.txt index 69229bf3..d35eca6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ # Copyright (c) Team CharLS. # SPDX-License-Identifier: BSD-3-Clause -cmake_minimum_required(VERSION 3.13...3.26) +cmake_minimum_required(VERSION 3.13...3.30) # Extract the version info from version.h file(READ "include/charls/version.h" version) diff --git a/CharLS.sln.DotSettings b/CharLS.sln.DotSettings index 60d91456..00537b47 100644 --- a/CharLS.sln.DotSettings +++ b/CharLS.sln.DotSettings @@ -98,6 +98,8 @@ True True True + True + True True True True diff --git a/README.md b/README.md index e0514a16..866fde84 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ The following JPEG-LS options are not supported by the CharLS implementation. Mo While technical possible all known JPEG-LS codecs put multi-component (color) images in a single scan or in multiple scans, but not use a mix of these in one file. * No support to encode\decode images with the height defined after the first scan (DNL marker). -* No support for JPEG-LS mapping tables (palette). * No support for point transform. Point transform is a lossly encoding mechanism and not used in lossless scenarios. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f5b72ee5..55b5935b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -52,7 +52,7 @@ jobs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' maximumCpuCount: true - msbuildArgs: -p:CHARLS_PROFILE=true -p:CHARLS_ALL_WARNINGS=true -p:VCToolsVersion=14.38.33130 + msbuildArgs: -p:CHARLS_PROFILE=true -p:CHARLS_ALL_WARNINGS=true - task: VSTest@2 inputs: diff --git a/cpp.hint b/cpp.hint index 2f80a657..087efeb7 100644 --- a/cpp.hint +++ b/cpp.hint @@ -15,6 +15,8 @@ #define CHARLS_NO_INLINE #define FORCE_INLINE #define MSVC_WARNING_SUPPRESS_NEXT_LINE(x) +#define MSVC_WARNING_SUPPRESS(x) +#define MSVC_WARNING_UNSUPPRESS #define CHARLS_IN #define CHARLS_IN_OPT #define CHARLS_IN_Z @@ -27,6 +29,7 @@ #define CHARLS_ATTRIBUTE(a) #define CHARLS_ATTRIBUTE_ACCESS(a) #define CHARLS_C_VOID +#define CHARLS_EXPORT #define CHARLS_CHECK_RETURN #define CHARLS_RET_MAY_BE_NULL #define USE_DECL_ANNOTATIONS diff --git a/default.ruleset b/default.ruleset index a0f0e4dc..007e71e1 100644 --- a/default.ruleset +++ b/default.ruleset @@ -11,6 +11,7 @@ + \ No newline at end of file diff --git a/default.ruleset.md b/default.ruleset.md index 93cb8e7b..2f3699a7 100644 --- a/default.ruleset.md +++ b/default.ruleset.md @@ -24,5 +24,8 @@ C26482: Only index into arrays using constant expressions. C26490: Don't use reinterpret_cast -> Rationale: required to cast unsigned char* to char*. +C26493: Don't use C-style casts (type.4). +-> Rationale: False positives in Visual Studio 2022 17.11.0 Preview 3.0 + C26494: Variable 'x' is uninitialized. Always initialize an object -> Rationale: many false warnings, other analyzers are better. diff --git a/include/charls/charls_jpegls_decoder.h b/include/charls/charls_jpegls_decoder.h index 9b112dad..1f36c6d0 100644 --- a/include/charls/charls_jpegls_decoder.h +++ b/include/charls/charls_jpegls_decoder.h @@ -11,6 +11,7 @@ #ifndef CHARLS_BUILD_AS_CPP_MODULE #include #include +#include #include #endif @@ -219,6 +220,79 @@ charls_jpegls_decoder_at_application_data(CHARLS_IN charls_jpegls_decoder* decod charls_at_application_data_handler handler, void* user_context) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull(1))); +/// +/// Returns the mapping table ID referenced by the component or 0 when no mapping table is used. +/// +/// +/// Function should be called after processing the complete JPEG-LS stream. +/// +/// Reference to the decoder instance. +/// Index of the component. +/// Output argument, will hold the mapping table ID when the function returns or 0. +/// The result of the operation: success or a failure code. +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_decoder_get_mapping_table_id(CHARLS_IN const charls_jpegls_decoder* decoder, int32_t component_index, + CHARLS_OUT int32_t* table_id) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); + +/// +/// Converts the mapping table ID to a mapping table index. +/// When the requested table is not present in the JPEG-LS stream the value -1 will be returned. +/// +/// +/// Function should be called after processing the complete JPEG-LS stream. +/// +/// Reference to the decoder instance. +/// Mapping table ID to lookup. +/// Output argument, will hold the mapping table index or -1 when the function returns. +/// The result of the operation: success or a failure code. +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_decoder_get_mapping_table_index(CHARLS_IN const charls_jpegls_decoder* decoder, int32_t table_id, + CHARLS_OUT int32_t* index) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); + +/// +/// Returns the count of mapping tables present in the JPEG-LS stream. +/// +/// +/// Function should be called after processing the complete JPEG-LS stream. +/// +/// Reference to the decoder instance. +/// Output argument, will hold the mapping table count when the function returns. +/// The result of the operation: success or a failure code. +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_decoder_get_mapping_table_count(CHARLS_IN const charls_jpegls_decoder* decoder, + CHARLS_OUT int32_t* count) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); + +/// +/// Returns information about a mapping table. +/// +/// +/// Function should be called after processing the complete JPEG-LS stream. +/// +/// Reference to the decoder instance. +/// Index of the requested mapping table. +/// Output argument, will hold the mapping table information when the function returns. +/// The result of the operation: success or a failure code. +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_decoder_get_mapping_table_info(CHARLS_IN const charls_jpegls_decoder* decoder, int32_t index, + CHARLS_OUT charls_table_info* table_info) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); + +/// +/// Returns a mapping table. +/// +/// +/// Function should be called after processing the complete JPEG-LS stream. +/// +/// Reference to the decoder instance. +/// Index of the requested mapping table. +/// Output argument, will hold the data of the mapping table when the function returns. +/// Length of the mapping table buffer in bytes. +/// The result of the operation: success or a failure code. +CHARLS_ATTRIBUTE_ACCESS((access(write_only, 3, 4))) +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_decoder_get_mapping_table(CHARLS_IN const charls_jpegls_decoder* decoder, int32_t index, + CHARLS_OUT_WRITES_BYTES(table_size_bytes) void* table_data, + size_t table_size_bytes) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); + #ifdef __cplusplus } // extern "C" @@ -524,9 +598,7 @@ class jpegls_decoder final /// Will decode the JPEG-LS byte stream set with source into the destination buffer. /// /// Byte array that holds the encoded bytes when the function returns. - /// - /// Length of the array in bytes. If the array is too small the function will return an error. - /// + /// Length of the destination buffer in bytes. /// Number of bytes to the next line in the buffer, when zero, decoder will compute it. /// An error occurred during the operation. CHARLS_ATTRIBUTE_ACCESS((access(write_only, 2, 3))) @@ -604,6 +676,107 @@ class jpegls_decoder final return *this; } + /// + /// Returns the mapping table ID referenced by the component or 0 when no mapping table is used. + /// + /// + /// Function should be called after processing the complete JPEG-LS stream. + /// + /// Index of the component. + /// The mapping table ID or 0 when no mapping table is referenced by the component. + /// An error occurred during the operation. + [[nodiscard]] + int32_t mapping_table_id(const int32_t component_index) const + { + int32_t table_id; + check_jpegls_errc(charls_decoder_get_mapping_table_id(decoder_.get(), component_index, &table_id)); + return table_id; + } + + /// + /// Converts the mapping table ID to a mapping table index. + /// + /// + /// Function should be called after processing the complete JPEG-LS stream. + /// + /// Mapping table ID to lookup. + /// + /// The index of the mapping table or an empty optional when the table is not present in the JPEG-LS stream. + /// + /// An error occurred during the operation. + [[nodiscard]] + std::optional mapping_table_index(const int32_t table_id) const + { + int32_t index; + check_jpegls_errc(charls_decoder_get_mapping_table_index(decoder_.get(), table_id, &index)); + return index == mapping_table_missing ? std::optional{} : index; + } + + /// + /// Returns the count of mapping tables present in the JPEG-LS stream. + /// + /// + /// Function should be called after processing the complete JPEG-LS stream. + /// + /// The number of mapping tables present in the JPEG-LS stream. + /// An error occurred during the operation. + [[nodiscard]] + int32_t mapping_table_count() const + { + int32_t count; + check_jpegls_errc(charls_decoder_get_mapping_table_count(decoder_.get(), &count)); + return count; + } + + /// + /// Returns information about a mapping table. + /// + /// + /// Function should be called after processing the complete JPEG-LS stream. + /// + /// Index of the requested mapping table. + /// Mapping table information + /// An error occurred during the operation. + [[nodiscard]] + table_info mapping_table_info(const int32_t index) const + { + table_info info; + check_jpegls_errc(charls_decoder_get_mapping_table_info(decoder_.get(), index, &info)); + return info; + } + + /// + /// Returns a mapping table. + /// + /// + /// Function should be called after processing the complete JPEG-LS stream. + /// + /// Index of the requested mapping table. + /// Output argument, will hold the data of mapping table when the function returns. + /// Length of the table buffer in bytes. + /// An error occurred during the operation. + CHARLS_ATTRIBUTE_ACCESS((access(write_only, 3, 4))) + void mapping_table(const int32_t index, CHARLS_OUT_WRITES_BYTES(table_size_bytes) void* table_data, + const size_t table_size_bytes) const + { + check_jpegls_errc(charls_decoder_get_mapping_table(decoder_.get(), index, table_data, table_size_bytes)); + } + + /// + /// Returns a mapping table. + /// + /// + /// Function should be called after processing the complete JPEG-LS stream. + /// + /// Index of the requested mapping table. + /// Output argument, will hold data of the mapping table when the function returns. + /// An error occurred during the operation. + template + void mapping_table(const int32_t index, Container& table_data) const + { + mapping_table(index, table_data.data(), table_data.size() * sizeof(ContainerValueType)); + } + private: [[nodiscard]] static charls_jpegls_decoder* create_decoder() diff --git a/include/charls/charls_jpegls_encoder.h b/include/charls/charls_jpegls_encoder.h index d09173d9..60627f86 100644 --- a/include/charls/charls_jpegls_encoder.h +++ b/include/charls/charls_jpegls_encoder.h @@ -110,6 +110,17 @@ charls_jpegls_encoder_set_color_transformation(CHARLS_IN charls_jpegls_encoder* charls_color_transformation color_transformation) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); +/// +/// Configures the table ID the encoder should reference when encoding a component. +/// The referenced table can be included in the stream or provided in another JPEG-LS abbreviated format stream. +/// +/// Reference to the encoder instance. +/// Index of the component. Component 0 is the start index. +/// Table ID that will be referenced by this component. +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_jpegls_encoder_set_table_id(CHARLS_IN charls_jpegls_encoder* encoder, int32_t component_index, + int32_t table_id) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); + /// /// Returns the size in bytes, that the encoder expects are needed to hold the encoded image. /// @@ -230,6 +241,25 @@ charls_jpegls_encoder_write_application_data(CHARLS_IN charls_jpegls_encoder* en CHARLS_IN_READS_BYTES(application_data_size_bytes) const void* application_data, size_t application_data_size_bytes) CHARLS_NOEXCEPT; +/// +/// Writes a mapping table to the destination. +/// During decoding a component can reference a mapping table. +/// +/// +/// No validation is performed if the table ID is unique and if the table size matches the required size. +/// During decoding the active maximum value determines the required size of the table. +/// +/// Reference to the encoder instance. +/// Table ID. Unique identifier of the mapping table in the range [1..255] +/// Size in bytes of a single table entry. +/// Byte array that holds the mapping table. +/// The size in bytes of the table data. +CHARLS_ATTRIBUTE_ACCESS((access(read_only, 4, 5))) +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_jpegls_encoder_write_table(CHARLS_IN charls_jpegls_encoder* encoder, int32_t table_id, int32_t entry_size, + CHARLS_IN_READS_BYTES(table_data_size_bytes) const void* table_data, + size_t table_data_size_bytes) CHARLS_NOEXCEPT; + /// /// Encodes the passed buffer with the source image data to the destination. /// @@ -248,6 +278,17 @@ charls_jpegls_encoder_encode_from_buffer(CHARLS_IN charls_jpegls_encoder* encode size_t source_size_bytes, uint32_t stride) CHARLS_NOEXCEPT CHARLS_ATTRIBUTE((nonnull)); + +/// +/// Creates a JPEG-LS stream in the abbreviated format that only contain mapping tables. +/// These tables must have been written to the stream first with the method charls_jpegls_encoder_write_table. +/// +/// Reference to the encoder instance. +/// The result of the operation: success or a failure code. +CHARLS_CHECK_RETURN CHARLS_API_IMPORT_EXPORT charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_jpegls_encoder_create_tables_only(CHARLS_IN charls_jpegls_encoder* encoder) CHARLS_NOEXCEPT + CHARLS_ATTRIBUTE((nonnull)); + /// /// Returns the size in bytes, that are written to the destination. /// @@ -378,6 +419,18 @@ class jpegls_encoder final return *this; } + /// + /// Configures the table ID the encoder should reference when encoding a component. + /// The referenced table can be included in the stream or provided in another JPEG-LS abbreviated format stream. + /// + /// Index of the component. Component 0 is the start index. + /// Table ID that will be referenced by this component. + jpegls_encoder& set_table_id(const int32_t component_index, const int32_t table_id) + { + check_jpegls_errc(charls_jpegls_encoder_set_table_id(encoder_.get(), component_index, table_id)); + return *this; + } + /// /// Returns the size in bytes, that the encoder expects are needed to hold the encoded image. /// @@ -459,7 +512,7 @@ class jpegls_encoder final /// The entry data of the directory entry. /// The size in bytes of the directory entry [0-65528]. template - CHARLS_ATTRIBUTE_ACCESS((access(read_only, 3, 4))) + CHARLS_ATTRIBUTE_ACCESS((access(read_only, 2, 3))) jpegls_encoder& write_spiff_entry(const IntDerivedType entry_tag, CHARLS_IN_READS_BYTES(entry_data_size_bytes) const void* entry_data, const size_t entry_data_size_bytes) @@ -524,6 +577,40 @@ class jpegls_encoder final return *this; } + /// + /// Writes a mapping table to the destination. + /// + /// + /// No validation is performed if the table ID is unique and if the table size matches the required size. + /// + /// Table ID. Unique identifier of the mapping table in the range [1..255] + /// Size in bytes of a single table entry. + /// Byte buffer that holds the mapping table. + /// The size of the buffer in bytes. + CHARLS_ATTRIBUTE_ACCESS((access(read_only, 4, 5))) + jpegls_encoder& write_table(const int32_t table_id, const int32_t entry_size, CHARLS_IN const void* table_data, + const size_t size) + { + check_jpegls_errc(charls_jpegls_encoder_write_table(encoder_.get(), table_id, entry_size, table_data, size)); + return *this; + } + + /// + /// Writes a mapping table to the destination. + /// + /// + /// No validation is performed if the table ID is unique and if the table size matches the required size. + /// + /// Table ID. Unique identifier of the mapping table in the range [1..255] + /// Size in bytes of a single table entry. + /// Buffer that holds the mapping table. + template + jpegls_encoder& write_table(const int32_t table_id, const int32_t entry_size, Container& table_container) + { + return write_table(table_id, entry_size, table_container.data(), + table_container.size() * sizeof(ContainerValueType)); + } + /// /// Encodes the passed buffer with the source image data to the destination. /// @@ -558,6 +645,17 @@ class jpegls_encoder final return encode(source_container.data(), source_container.size() * sizeof(ContainerValueType), stride); } + /// + /// Creates a JPEG-LS stream in abbreviated format that only contain mapping tables. + /// These tables should have been written to the stream first with the method write_table. + /// + /// The number of bytes written to the destination. + size_t create_tables_only() const + { + check_jpegls_errc(charls_jpegls_encoder_create_tables_only(encoder_.get())); + return bytes_written(); + } + /// /// Returns the size in bytes, that are written to the destination. /// diff --git a/include/charls/public_types.h b/include/charls/public_types.h index 8be53f06..28e7f4a5 100644 --- a/include/charls/public_types.h +++ b/include/charls/public_types.h @@ -56,6 +56,8 @@ enum charls_jpegls_errc CHARLS_JPEGLS_ERRC_CALLBACK_FAILED = 27, CHARLS_JPEGLS_ERRC_END_OF_IMAGE_MARKER_NOT_FOUND = 28, CHARLS_JPEGLS_ERRC_INVALID_SPIFF_HEADER = 29, + CHARLS_JPEGLS_ERRC_UNKNOWN_COMPONENT_ID = 30, + CHARLS_JPEGLS_ERRC_MAPPING_TABLES_AND_SPIFF_HEADER = 31, CHARLS_JPEGLS_ERRC_INVALID_ARGUMENT_WIDTH = 100, CHARLS_JPEGLS_ERRC_INVALID_ARGUMENT_HEIGHT = 101, CHARLS_JPEGLS_ERRC_INVALID_ARGUMENT_COMPONENT_COUNT = 102, @@ -75,6 +77,8 @@ enum charls_jpegls_errc CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_NEAR_LOSSLESS = 205, CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_JPEGLS_PRESET_PARAMETERS = 206, CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_COLOR_TRANSFORMATION = 207, + CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_MAPPING_TABLE_ID = 208, + CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_MAPPING_TABLE_CONTINUATION = 209 }; enum charls_interleave_mode @@ -163,6 +167,11 @@ enum charls_spiff_entry_tag CHARLS_SPIFF_ENTRY_TAG_SET_REFERENCE = 16 }; +enum charls_constants +{ + CHARLS_MAPPING_TABLE_MISSING = -1 +}; + #ifdef __cplusplus } // namespace charls::impl @@ -181,7 +190,7 @@ enum class [[nodiscard]] jpegls_errc success = impl::CHARLS_JPEGLS_ERRC_SUCCESS, /// - /// This error is returned when one of the arguments is invalid and no specific reason is available. + /// This error is returned when one of the passed arguments is invalid. /// invalid_argument = impl::CHARLS_JPEGLS_ERRC_INVALID_ARGUMENT, @@ -323,6 +332,16 @@ enum class [[nodiscard]] jpegls_errc /// invalid_spiff_header = impl::CHARLS_JPEGLS_ERRC_INVALID_SPIFF_HEADER, + /// + /// This error is returned when an unknown component ID in a scan is detected. + /// + unknown_component_id = impl::CHARLS_JPEGLS_ERRC_UNKNOWN_COMPONENT_ID, + + /// + /// This error is returned for stream with only mapping tables and a spiff header. + /// + mapping_tables_and_spiff_header = impl::CHARLS_JPEGLS_ERRC_MAPPING_TABLES_AND_SPIFF_HEADER, + /// /// The argument for the width parameter is outside the range [1, 65535]. /// @@ -420,7 +439,17 @@ enum class [[nodiscard]] jpegls_errc /// /// This error is returned when the stream contains an invalid color transformation segment or one that doesn't match with frame info. /// - invalid_parameter_color_transformation = impl::CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_COLOR_TRANSFORMATION + invalid_parameter_color_transformation = impl::CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_COLOR_TRANSFORMATION, + + /// + /// This error is returned when the stream contains a mapping table with an invalid ID. + /// + invalid_parameter_mapping_table_id = impl::CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_MAPPING_TABLE_ID, + + /// + /// This error is returned when the stream contains an invalid mapping table continuation. + /// + invalid_parameter_mapping_table_continuation = impl::CHARLS_JPEGLS_ERRC_INVALID_PARAMETER_MAPPING_TABLE_CONTINUATION }; @@ -803,6 +832,8 @@ enum class spiff_entry_tag : uint32_t set_reference = impl::CHARLS_SPIFF_ENTRY_TAG_SET_REFERENCE }; +constexpr int mapping_table_missing{impl::CHARLS_MAPPING_TABLE_MISSING}; + } // namespace charls CHARLS_EXPORT @@ -929,6 +960,28 @@ struct charls_jpegls_pc_parameters CHARLS_FINAL }; +/// +/// Defines the information that describes a mapping table. +/// +struct charls_table_info CHARLS_FINAL +{ + /// + /// Identifier of the mapping table, range [1, 255]. + /// + int32_t table_id; + + /// + /// Width of a table entry in bytes, range [1, 255]. + /// + int32_t entry_size; + + /// + /// Size of the table in bytes, range [1, 16711680] + /// + uint32_t data_size; +}; + + #ifdef __cplusplus /// @@ -962,6 +1015,7 @@ namespace charls { using spiff_header = charls_spiff_header; using frame_info = charls_frame_info; using jpegls_pc_parameters = charls_jpegls_pc_parameters; +using table_info = charls_table_info; using at_comment_handler = charls_at_comment_handler; using at_application_data_handler = charls_at_application_data_handler; @@ -981,5 +1035,6 @@ typedef int32_t(CHARLS_API_CALLING_CONVENTION* charls_at_application_data_handle typedef struct charls_spiff_header charls_spiff_header; typedef struct charls_frame_info charls_frame_info; typedef struct charls_jpegls_pc_parameters charls_jpegls_pc_parameters; +typedef struct charls_table_info charls_table_info; #endif diff --git a/spelling.dic b/spelling.dic index 88be8ae5..543c63af 100644 --- a/spelling.dic +++ b/spelling.dic @@ -12,3 +12,6 @@ misc bugprone countl cppcoreguidelines +mrfx +subspan +cppcoreguidelines diff --git a/src/charls_jpegls_decoder.cpp b/src/charls_jpegls_decoder.cpp index 1935bb8b..041a3b85 100644 --- a/src/charls_jpegls_decoder.cpp +++ b/src/charls_jpegls_decoder.cpp @@ -47,7 +47,7 @@ struct charls_jpegls_decoder final reader_.read_header(); } - state_ = state::header_read; + state_ = reader_.end_of_image() ? state::completed : state::header_read; } [[nodiscard]] @@ -129,6 +129,44 @@ struct charls_jpegls_decoder final reader_.at_application_data(at_application_data_callback); } + [[nodiscard]] + int32_t mapping_table_id(const size_t component_index) const + { + check_state_completed(); + check_argument(component_index < reader_.component_count()); + return reader_.mapping_table_id(component_index); + } + + [[nodiscard]] + std::optional mapping_table_index(const int32_t table_id) const + { + check_state_completed(); + check_argument_range(minimum_table_id, maximum_table_id, table_id); + return reader_.mapping_table_index(static_cast(table_id)); + } + + [[nodiscard]] + int32_t mapping_table_count() const + { + check_state_completed(); + return reader_.mapping_table_count(); + } + + [[nodiscard]] + table_info mapping_table_info(const int32_t index) const + { + check_table_index(index); + return reader_.mapping_table_info(index); + } + + void mapping_table(const int32_t index, const span table) const + { + check_table_index(index); + check_argument(table.data() || table.empty()); + + reader_.get_mapping_table(index, table); + } + void decode(span destination, size_t stride) { check_argument(destination.data() || destination.empty()); @@ -157,8 +195,8 @@ struct charls_jpegls_decoder final for (size_t plane{};;) { - const auto decoder{make_scan_codec( - frame_info(), reader_.get_validated_preset_coding_parameters(), reader_.parameters())}; + const auto decoder{make_scan_codec(frame_info(), reader_.get_validated_preset_coding_parameters(), + reader_.parameters())}; const size_t bytes_read{decoder->decode_scan(reader_.remaining_source(), destination.data(), stride)}; reader_.advance_position(bytes_read); @@ -196,6 +234,16 @@ struct charls_jpegls_decoder final check_operation(state_ >= state::header_read); } + void check_state_completed() const + { + check_operation(state_ == state::completed); + } + + void check_table_index(const int32_t index) const + { + check_argument_range(0, mapping_table_count() - 1, index); + } + void check_parameter_coherent() const { switch (frame_info().component_count) @@ -256,7 +304,7 @@ catch (...) } -USE_DECL_ANNOTATIONS charls_jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_read_spiff_header( +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_read_spiff_header( charls_jpegls_decoder* const decoder, charls_spiff_header* spiff_header, int32_t* header_found) noexcept try { @@ -295,7 +343,7 @@ catch (...) } -USE_DECL_ANNOTATIONS charls_jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_get_near_lossless( +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_get_near_lossless( const charls_jpegls_decoder* decoder, const int32_t component, int32_t* near_lossless) noexcept try { @@ -308,7 +356,7 @@ catch (...) } -USE_DECL_ANNOTATIONS charls_jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_get_interleave_mode( +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_get_interleave_mode( const charls_jpegls_decoder* decoder, charls_interleave_mode* interleave_mode) noexcept try { @@ -321,7 +369,7 @@ catch (...) } -USE_DECL_ANNOTATIONS charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_get_preset_coding_parameters(const charls_jpegls_decoder* decoder, const int32_t /*reserved*/, charls_jpegls_pc_parameters* preset_coding_parameters) noexcept try @@ -335,7 +383,7 @@ catch (...) } -USE_DECL_ANNOTATIONS charls_jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_get_color_transformation( +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_decoder_get_color_transformation( const charls_jpegls_decoder* decoder, charls_color_transformation* color_transformation) noexcept try { @@ -400,4 +448,69 @@ catch (...) return to_jpegls_errc(); } + +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_decoder_get_mapping_table_id( + const charls_jpegls_decoder* decoder, const int32_t component_index, int32_t* table_id) noexcept +try +{ + *check_pointer(table_id) = check_pointer(decoder)->mapping_table_id(component_index); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_decoder_get_mapping_table_index(const charls_jpegls_decoder* decoder, const int32_t table_id, int32_t* index) noexcept +try +{ + *check_pointer(index) = check_pointer(decoder)->mapping_table_index(table_id).value_or(mapping_table_missing); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_decoder_get_mapping_table_count(const charls_jpegls_decoder* decoder, int32_t* count) noexcept +try +{ + *check_pointer(count) = check_pointer(decoder)->mapping_table_count(); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_decoder_get_mapping_table_info( + const charls_jpegls_decoder* decoder, const int32_t index, table_info* table_info) noexcept +try +{ + *check_pointer(table_info) = check_pointer(decoder)->mapping_table_info(index); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_decoder_get_mapping_table( + const charls_jpegls_decoder* decoder, const int32_t index, void* table_data, const size_t table_size_bytes) noexcept +try +{ + check_pointer(decoder)->mapping_table(index, {static_cast(table_data), table_size_bytes}); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + } // extern "C" diff --git a/src/charls_jpegls_encoder.cpp b/src/charls_jpegls_encoder.cpp index eb64c40e..5cfb0f8e 100644 --- a/src/charls_jpegls_encoder.cpp +++ b/src/charls_jpegls_encoder.cpp @@ -86,13 +86,20 @@ struct charls_jpegls_encoder final void color_transformation(const color_transformation color_transformation) { - check_argument(color_transformation >= color_transformation::none && - color_transformation <= color_transformation::hp3, - jpegls_errc::invalid_argument_color_transformation); + check_argument_range(color_transformation::none, color_transformation::hp3, color_transformation, + jpegls_errc::invalid_argument_color_transformation); color_transformation_ = color_transformation; } + void set_table_id(const int32_t component_index, const int32_t table_id) + { + check_argument_range(minimum_component_index, maximum_component_index, component_index); + check_argument_range(0, maximum_table_id, table_id); + + writer_.set_table_id(static_cast(component_index), table_id); + } + [[nodiscard]] size_t estimated_destination_size() const { @@ -150,8 +157,7 @@ struct charls_jpegls_encoder final void write_application_data(const int32_t application_data_id, const span application_data) { - check_argument(application_data_id >= minimum_application_data_id && - application_data_id <= maximum_application_data_id); + check_argument_range(minimum_application_data_id, maximum_application_data_id, application_data_id); check_argument(application_data.data() || application_data.empty()); check_argument(application_data.size() <= segment_max_data_size, jpegls_errc::invalid_argument_size); check_operation(state_ >= state::destination_set && state_ < state::completed); @@ -160,6 +166,18 @@ struct charls_jpegls_encoder final writer_.write_application_data_segment(application_data_id, application_data); } + void write_table(const int32_t table_id, const int32_t entry_size, const span table_data) + { + check_argument_range(minimum_table_id, maximum_table_id, table_id); + check_argument_range(minimum_entry_size, maximum_entry_size, entry_size); + check_argument(table_data.data() || table_data.empty()); + check_argument(table_data.size() >= static_cast(entry_size), jpegls_errc::invalid_argument_size); + check_operation(state_ >= state::destination_set && state_ < state::completed); + + transition_to_tables_and_miscellaneous_state(); + writer_.write_jpegls_preset_parameters_segment(table_id, entry_size, table_data); + } + void encode(span source, size_t stride) { check_argument(source.data() || source.empty()); @@ -219,8 +237,13 @@ struct charls_jpegls_encoder final encode_scan(source.data(), stride, frame_info_.component_count); } - writer_.write_end_of_image(has_option(encoding_options::even_destination_size)); - state_ = state::completed; + write_end_of_image(); + } + + void create_tables_only() + { + check_operation(state_ == state::tables_and_miscellaneous); + write_end_of_image(); } [[nodiscard]] @@ -259,8 +282,8 @@ struct charls_jpegls_encoder final const charls::frame_info frame_info{frame_info_.width, frame_info_.height, frame_info_.bits_per_sample, component_count}; - const auto encoder{make_scan_codec( - frame_info, preset_coding_parameters_, {near_lossless_, 0, interleave_mode_, color_transformation_})}; + const auto encoder{make_scan_codec(frame_info, preset_coding_parameters_, + {near_lossless_, 0, interleave_mode_, color_transformation_})}; const size_t bytes_written{encoder->encode_scan(source, stride, writer_.remaining_destination())}; // Synchronize the destination encapsulated in the writer (encode_scan works on a local copy) @@ -341,6 +364,12 @@ struct charls_jpegls_encoder final writer_.write_color_transform_segment(color_transformation_); } + void write_end_of_image() + { + writer_.write_end_of_image(has_option(encoding_options::even_destination_size)); + state_ = state::completed; + } + [[nodiscard]] bool has_option(const charls::encoding_options option_to_test) const noexcept { @@ -466,11 +495,11 @@ catch (...) } -USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION -charls_jpegls_encoder_get_estimated_destination_size(const charls_jpegls_encoder* encoder, size_t* size_in_bytes) noexcept +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_encoder_set_table_id( + charls_jpegls_encoder* encoder, const int32_t component_index, const int32_t table_id) noexcept try { - *check_pointer(size_in_bytes) = check_pointer(encoder)->estimated_destination_size(); + check_pointer(encoder)->set_table_id(component_index, table_id); return jpegls_errc::success; } catch (...) @@ -480,24 +509,10 @@ catch (...) USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION -charls_jpegls_encoder_get_bytes_written(const charls_jpegls_encoder* encoder, size_t* bytes_written) noexcept -try -{ - *check_pointer(bytes_written) = check_pointer(encoder)->bytes_written(); - return jpegls_errc::success; -} -catch (...) -{ - return to_jpegls_errc(); -} - - -USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION -charls_jpegls_encoder_encode_from_buffer(charls_jpegls_encoder* encoder, const void* source_buffer, - const size_t source_size_bytes, const uint32_t stride) noexcept +charls_jpegls_encoder_get_estimated_destination_size(const charls_jpegls_encoder* encoder, size_t* size_in_bytes) noexcept try { - check_pointer(encoder)->encode({static_cast(source_buffer), source_size_bytes}, stride); + *check_pointer(size_in_bytes) = check_pointer(encoder)->estimated_destination_size(); return jpegls_errc::success; } catch (...) @@ -590,6 +605,60 @@ catch (...) } +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_jpegls_encoder_write_table(charls_jpegls_encoder* encoder, const int32_t table_id, const int32_t entry_size, + const void* table_data, size_t table_data_size_bytes) noexcept +try +{ + check_pointer(encoder)->write_table(table_id, entry_size, {static_cast(table_data), table_data_size_bytes}); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_jpegls_encoder_encode_from_buffer(charls_jpegls_encoder* encoder, const void* source_buffer, + const size_t source_size_bytes, const uint32_t stride) noexcept +try +{ + check_pointer(encoder)->encode({static_cast(source_buffer), source_size_bytes}, stride); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + +USE_DECL_ANNOTATIONS charls_jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_jpegls_encoder_create_tables_only(charls_jpegls_encoder* encoder) noexcept +try +{ + check_pointer(encoder)->create_tables_only(); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + +USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION +charls_jpegls_encoder_get_bytes_written(const charls_jpegls_encoder* encoder, size_t* bytes_written) noexcept +try +{ + *check_pointer(bytes_written) = check_pointer(encoder)->bytes_written(); + return jpegls_errc::success; +} +catch (...) +{ + return to_jpegls_errc(); +} + + USE_DECL_ANNOTATIONS jpegls_errc CHARLS_API_CALLING_CONVENTION charls_jpegls_encoder_rewind(charls_jpegls_encoder* encoder) noexcept try diff --git a/src/constants.h b/src/constants.h index 91a1f31d..329e2c3f 100644 --- a/src/constants.h +++ b/src/constants.h @@ -9,7 +9,7 @@ namespace charls { -// Default threshold values for JPEG-LS statistical modeling as defined in ISO/IEC 14495-1, Table C.3 +// Default threshold values for JPEG-LS statistical modeling as defined in ISO/IEC 14495-1, table C.3 // for the case MAXVAL = 255 and NEAR = 0. // Can be overridden at compression time, however this is rarely done. constexpr int default_threshold1{3}; // BASIC_T1 @@ -18,13 +18,21 @@ constexpr int default_threshold3{21}; // BASIC_T3 constexpr int default_reset_value{64}; // Default RESET value as defined in ISO/IEC 14495-1, table C.2 -constexpr int maximum_component_count{255}; -constexpr int minimum_bits_per_sample{2}; -constexpr int maximum_bits_per_sample{16}; -constexpr int maximum_near_lossless{255}; -constexpr int32_t minimum_application_data_id{0}; +constexpr int32_t maximum_component_count{255}; +constexpr int32_t minimum_component_index{}; +constexpr int32_t maximum_component_index{maximum_component_count - 1}; +constexpr int32_t minimum_bits_per_sample{2}; +constexpr int32_t maximum_bits_per_sample{16}; +constexpr int32_t maximum_near_lossless{255}; +constexpr int32_t minimum_application_data_id{}; constexpr int32_t maximum_application_data_id{15}; +// The following limits for mapping tables are defined in ISO/IEC 14495-1, C.2.4.1.2, table C.4. +constexpr int32_t minimum_table_id{1}; +constexpr int32_t maximum_table_id{255}; +constexpr int32_t minimum_entry_size{1}; +constexpr int32_t maximum_entry_size{255}; + constexpr int max_k_value{16}; // This is an implementation limit (theoretical limit is 32) constexpr int compute_maximum_near_lossless(const int maximum_sample_value) noexcept diff --git a/src/jpeg_stream_reader.cpp b/src/jpeg_stream_reader.cpp index 0434bf11..345c24cc 100644 --- a/src/jpeg_stream_reader.cpp +++ b/src/jpeg_stream_reader.cpp @@ -19,7 +19,7 @@ using impl::throw_jpegls_error; using std::array; using std::byte; using std::equal; -using std::find; +using std::optional; namespace { @@ -55,13 +55,23 @@ void jpeg_stream_reader::read_header(spiff_header* header, bool* spiff_header_fo if (UNLIKELY(read_next_marker_code() != jpeg_marker_code::start_of_image)) throw_jpegls_error(jpegls_errc::start_of_image_marker_not_found); - component_ids_.reserve(4); // expect 4 components or fewer. + scan_infos_.reserve(4); // expect 4 components or fewer. state_ = state::header_section; } for (;;) { const jpeg_marker_code marker_code{read_next_marker_code()}; + if (marker_code == jpeg_marker_code::end_of_image) + { + if (is_abbreviated_format_for_table_specification_data()) { + state_ = state::after_end_of_image; + return; + } + + throw_jpegls_error(jpegls_errc::unexpected_end_of_image_marker); + } + validate_marker_code(marker_code); read_segment_size(); @@ -201,7 +211,7 @@ void jpeg_stream_reader::validate_marker_code(const jpeg_marker_code marker_code throw_jpegls_error(jpegls_errc::duplicate_start_of_image_marker); case jpeg_marker_code::end_of_image: - throw_jpegls_error(jpegls_errc::unexpected_end_of_image_marker); + unreachable(); } if (is_restart_marker_code(marker_code)) @@ -223,6 +233,36 @@ jpegls_pc_parameters jpeg_stream_reader::get_validated_preset_coding_parameters( } +int32_t jpeg_stream_reader::mapping_table_id(const size_t component_index) const noexcept +{ + return scan_infos_[component_index].table_id; +} + + +optional jpeg_stream_reader::mapping_table_index(const uint8_t table_id) const noexcept +{ + const auto it{find_mapping_table_entry(table_id)}; + return it != mapping_tables_.cend() ? static_cast(it - mapping_tables_.cbegin()) : optional{}; +} + + +table_info jpeg_stream_reader::mapping_table_info(const size_t index) const +{ + const auto& entry{mapping_tables_[index]}; + return {entry.table_id(), entry.entry_size(), static_cast(entry.data_size())}; +} + + +void jpeg_stream_reader::get_mapping_table(const size_t index, const span table) const +{ + const auto& mapping_table{mapping_tables_[index]}; + if (mapping_table.data_size() > table.size()) + throw_jpegls_error(jpegls_errc::destination_buffer_too_small); + + mapping_table.copy(table.data()); +} + + void jpeg_stream_reader::read_marker_segment(const jpeg_marker_code marker_code, spiff_header* header, bool* spiff_header_found) { @@ -355,12 +395,16 @@ void jpeg_stream_reader::read_preset_parameters_segment() return; case jpegls_preset_parameters_type::oversize_image_dimension: - oversize_image_dimension(); + read_oversize_image_dimension(); return; case jpegls_preset_parameters_type::mapping_table_specification: + read_mapping_table_specification(); + return; + case jpegls_preset_parameters_type::mapping_table_continuation: - throw_jpegls_error(jpegls_errc::parameter_value_not_supported); + read_mapping_table_continuation(); + return; case jpegls_preset_parameters_type::coding_method_specification: case jpegls_preset_parameters_type::near_lossless_error_re_specification: @@ -390,7 +434,7 @@ void jpeg_stream_reader::read_preset_coding_parameters() } -void jpeg_stream_reader::oversize_image_dimension() +void jpeg_stream_reader::read_oversize_image_dimension() { // Note: The JPEG-LS standard supports a 2,3 or 4 bytes for the size. check_minimal_segment_size(2); @@ -427,6 +471,28 @@ void jpeg_stream_reader::oversize_image_dimension() } +void jpeg_stream_reader::read_mapping_table_specification() +{ + check_minimal_segment_size(3); + const uint8_t table_id{read_uint8()}; + const uint8_t entry_size{read_uint8()}; + + add_mapping_table(table_id, entry_size, segment_data_.subspan(3)); + skip_remaining_segment_data(); +} + + +void jpeg_stream_reader::read_mapping_table_continuation() +{ + check_minimal_segment_size(3); + const uint8_t table_id{read_uint8()}; + const uint8_t entry_size{read_uint8()}; + + extend_mapping_table(table_id, entry_size, segment_data_.subspan(3)); + skip_remaining_segment_data(); +} + + void jpeg_stream_reader::read_define_restart_interval_segment() { // Note: The JPEG-LS standard supports a 2,3 or 4 byte restart interval (see ISO/IEC 14495-1, C.2.5) @@ -469,9 +535,9 @@ void jpeg_stream_reader::read_start_of_scan_segment() for (size_t i{}; i != component_count_in_scan; ++i) { - skip_byte(); // Skip scan component selector - if (const uint8_t mapping_table_selector{read_uint8()}; UNLIKELY(mapping_table_selector != 0)) - throw_jpegls_error(jpegls_errc::parameter_value_not_supported); + const uint8_t component_id{read_uint8()}; + const uint8_t table_id{read_uint8()}; + store_table_id(component_id, table_id); } parameters_.near_lossless = read_uint8(); // Read NEAR parameter @@ -672,10 +738,12 @@ USE_DECL_ANNOTATIONS void jpeg_stream_reader::try_read_spiff_header_segment(spif void jpeg_stream_reader::add_component(const uint8_t component_id) { - if (UNLIKELY(find(component_ids_.cbegin(), component_ids_.cend(), component_id) != component_ids_.cend())) + if (UNLIKELY(find_if(scan_infos_.cbegin(), scan_infos_.cend(), [component_id](const scan_info& info) noexcept { + return info.component_id == component_id; + }) != scan_infos_.cend())) throw_jpegls_error(jpegls_errc::duplicate_component_id_in_sof_segment); - component_ids_.push_back(component_id); + scan_infos_.push_back({component_id, 0}); } @@ -754,4 +822,56 @@ void jpeg_stream_reader::call_application_data_callback(const jpeg_marker_code m throw_jpegls_error(jpegls_errc::callback_failed); } + +void jpeg_stream_reader::add_mapping_table(const uint8_t table_id, const uint8_t entry_size, + const span table_data) +{ + if (table_id == 0 || find_mapping_table_entry(table_id) != mapping_tables_.cend()) + throw_jpegls_error(jpegls_errc::invalid_parameter_mapping_table_id); + + mapping_tables_.emplace_back(table_id, entry_size, table_data); +} + + +void jpeg_stream_reader::extend_mapping_table(const uint8_t table_id, const uint8_t entry_size, + const span table_data) +{ + const auto entry{find_mapping_table_entry(table_id)}; + if (entry == mapping_tables_.cend() || entry->entry_size() != entry_size) + throw_jpegls_error(jpegls_errc::invalid_parameter_mapping_table_continuation); + + entry->add_fragment(table_data); +} + + +void jpeg_stream_reader::store_table_id(const uint8_t component_id, const uint8_t table_id) +{ + if (table_id == 0) + return; // default is already 0, no need to search and update. + + const auto it{find_if(scan_infos_.begin(), scan_infos_.end(), + [component_id](const scan_info& info) noexcept { return info.component_id == component_id; })}; + if (it == scan_infos_.end()) + throw_jpegls_error(jpegls_errc::unknown_component_id); + + it->table_id = table_id; +} + + +std::vector::const_iterator +jpeg_stream_reader::find_mapping_table_entry(uint8_t table_id) const noexcept +{ + return find_if(mapping_tables_.cbegin(), mapping_tables_.cend(), + [table_id](const mapping_table_entry& entry) noexcept { return entry.table_id() == table_id; }); +} + + +std::vector::iterator +jpeg_stream_reader::find_mapping_table_entry(uint8_t table_id) noexcept +{ + return find_if(mapping_tables_.begin(), mapping_tables_.end(), + [table_id](const mapping_table_entry& entry) noexcept { return entry.table_id() == table_id; }); +} + + } // namespace charls diff --git a/src/jpeg_stream_reader.h b/src/jpeg_stream_reader.h index fb07e6ed..7d69530d 100644 --- a/src/jpeg_stream_reader.h +++ b/src/jpeg_stream_reader.h @@ -10,6 +10,8 @@ #include "util.h" #include +#include +#include #include namespace charls { @@ -48,6 +50,12 @@ class jpeg_stream_reader final return preset_coding_parameters_; } + [[nodiscard]] + bool end_of_image() const noexcept + { + return state_ == state::after_end_of_image; + } + void at_comment(const callback_function at_comment_callback) noexcept { at_comment_callback_ = at_comment_callback; @@ -68,6 +76,7 @@ class jpeg_stream_reader final void read_header(spiff_header* header = nullptr, bool* spiff_header_found = nullptr); void read_next_start_of_scan(); void read_end_of_image(); + [[nodiscard]] jpegls_pc_parameters get_validated_preset_coding_parameters() const; @@ -77,6 +86,29 @@ class jpeg_stream_reader final position_ += count; } + [[nodiscard]] + int32_t mapping_table_id(size_t component_index) const noexcept; + + [[nodiscard]] + int32_t mapping_table_count() const noexcept + { + return static_cast(mapping_tables_.size()); + } + + [[nodiscard]] + size_t component_count() const noexcept + { + return scan_infos_.size(); + } + + [[nodiscard]] + std::optional mapping_table_index(uint8_t table_id) const noexcept; + + [[nodiscard]] + table_info mapping_table_info(size_t index) const; + + void get_mapping_table(size_t index, span table) const; + private: [[nodiscard]] std::byte read_byte_checked(); @@ -123,7 +155,9 @@ class jpeg_stream_reader final void read_application_data_segment(jpeg_marker_code marker_code); void read_preset_parameters_segment(); void read_preset_coding_parameters(); - void oversize_image_dimension(); + void read_oversize_image_dimension(); + void read_mapping_table_specification(); + void read_mapping_table_continuation(); void read_define_restart_interval_segment(); void try_read_application_data8_segment(spiff_header* header, bool* spiff_header_found); void try_read_spiff_header_segment(CHARLS_OUT spiff_header& header, CHARLS_OUT bool& spiff_header_found); @@ -140,6 +174,27 @@ class jpeg_stream_reader final void frame_info_height(uint32_t height); void frame_info_width(uint32_t width); void call_application_data_callback(jpeg_marker_code marker_code) const; + void add_mapping_table(uint8_t table_id, uint8_t entry_size, span table_data); + void extend_mapping_table(uint8_t table_id, uint8_t entry_size, span table_data); + void store_table_id(uint8_t component_id, uint8_t table_id); + + [[nodiscard]] + bool is_abbreviated_format_for_table_specification_data() const + { + if (mapping_table_count() > 0) + { + if (state_ == state::image_section) + { + impl::throw_jpegls_error(jpegls_errc::mapping_tables_and_spiff_header); + } + + // ISO/IEC 14495-1, Annex C defines 3 data formats. + // Annex C.4 defines the format that only contains mapping tables. + return state_ == state::header_section; + } + + return false; + } enum class state { @@ -153,13 +208,76 @@ class jpeg_stream_reader final after_end_of_image }; + struct scan_info final + { + uint8_t component_id; + uint8_t table_id; + }; + + class mapping_table_entry final + { + public: + mapping_table_entry(const uint8_t table_id, const uint8_t entry_size, const span table_data) : + table_id_{table_id}, entry_size_{entry_size} + { + data_fragments_.push_back(table_data); + } + + void add_fragment(const span table_data) + { + data_fragments_.push_back(table_data); + } + + [[nodiscard]] + uint8_t table_id() const noexcept + { + return table_id_; + } + + [[nodiscard]] + uint8_t entry_size() const noexcept + { + return entry_size_; + } + + [[nodiscard]] + size_t data_size() const + { + return std::accumulate(data_fragments_.cbegin(), data_fragments_.cend(), size_t{0}, + [](const size_t result, const span data_fragment) { + return result + data_fragment.size(); + }); + } + + void copy(std::byte* destination) const + { + for (const auto& data_fragment : data_fragments_) + { + std::copy(data_fragment.begin(), data_fragment.end(), destination); + destination += data_fragment.size(); + } + } + + private: + uint8_t table_id_; + uint8_t entry_size_; + std::vector> data_fragments_; + }; + + [[nodiscard]] + std::vector::const_iterator find_mapping_table_entry(uint8_t table_id) const noexcept; + + [[nodiscard]] + std::vector::iterator find_mapping_table_entry(uint8_t table_id) noexcept; + span::iterator position_{}; span::iterator end_position_{}; span segment_data_; charls::frame_info frame_info_{}; coding_parameters parameters_{}; jpegls_pc_parameters preset_coding_parameters_{}; - std::vector component_ids_; + std::vector scan_infos_; + std::vector mapping_tables_; state state_{}; callback_function at_comment_callback_{}; callback_function at_application_data_callback_{}; diff --git a/src/jpeg_stream_writer.cpp b/src/jpeg_stream_writer.cpp index d95a2b0a..50f721f5 100644 --- a/src/jpeg_stream_writer.cpp +++ b/src/jpeg_stream_writer.cpp @@ -5,7 +5,6 @@ #include "constants.h" #include "jpeg_marker_code.h" -#include "jpegls_preset_parameters_type.h" #include "util.h" #include @@ -64,8 +63,7 @@ void jpeg_stream_writer::write_spiff_header_segment(const spiff_header& header) } -void jpeg_stream_writer::write_spiff_directory_entry(const uint32_t entry_tag, - const span entry_data) +void jpeg_stream_writer::write_spiff_directory_entry(const uint32_t entry_tag, const span entry_data) { write_segment_header(jpeg_marker_code::application_data8, sizeof(uint32_t) + entry_data.size()); write_uint32(entry_tag); @@ -157,7 +155,7 @@ void jpeg_stream_writer::write_jpegls_preset_parameters_segment(const jpegls_pc_ void jpeg_stream_writer::write_jpegls_preset_parameters_segment(const uint32_t height, const uint32_t width) { // Format is defined in ISO/IEC 14495-1, C.2.4.1.4 - write_segment_header(jpeg_marker_code::jpegls_preset_parameters, sizeof(uint32_t) * 2 + 1 + 1); + write_segment_header(jpeg_marker_code::jpegls_preset_parameters, size_t{1} + 1 + 2 * sizeof(uint32_t)); write_uint8(to_underlying_type(jpegls_preset_parameters_type::oversize_image_dimension)); write_uint8(sizeof(uint32_t)); // Wxy: number of bytes used to represent Ye and Xe [2..4]. Always 4 for simplicity. write_uint32(height); // Ye: number of lines in the image. @@ -165,6 +163,28 @@ void jpeg_stream_writer::write_jpegls_preset_parameters_segment(const uint32_t h } +void jpeg_stream_writer::write_jpegls_preset_parameters_segment(const int32_t table_id, const int32_t entry_size, + const span table_data) +{ + // Write the first 65530 bytes as mapping table specification LSE segment. + constexpr size_t max_table_data_size{segment_max_data_size - 3}; + const byte* table_position{table_data.begin()}; + size_t table_size_to_write{std::min(table_data.size(), max_table_data_size)}; + write_jpegls_preset_parameters_segment(jpegls_preset_parameters_type::mapping_table_specification, table_id, entry_size, + {table_position, table_size_to_write}); + + // Write the remaining bytes as mapping table continuation LSE segments. + table_position += table_size_to_write; + while (table_position < table_data.end()) + { + table_size_to_write = std::min(static_cast(table_data.end() - table_position), max_table_data_size); + write_jpegls_preset_parameters_segment(jpegls_preset_parameters_type::mapping_table_continuation, table_id, + entry_size, {table_position, table_size_to_write}); + table_position += table_size_to_write; + } +} + + void jpeg_stream_writer::write_start_of_scan_segment(const int32_t component_count, const int32_t near_lossless, const interleave_mode interleave_mode) { @@ -179,9 +199,9 @@ void jpeg_stream_writer::write_start_of_scan_segment(const int32_t component_cou for (int32_t i{}; i != component_count; ++i) { - write_uint8(component_id_); - write_uint8(0); // Mapping table selector (0 = no table) - ++component_id_; + write_uint8(component_index_ + 1); // follow the JPEG-LS standard samples and start with component ID 1. + write_uint8(mapping_table_selector()); + ++component_index_; } write_uint8(near_lossless); // NEAR parameter @@ -190,6 +210,26 @@ void jpeg_stream_writer::write_start_of_scan_segment(const int32_t component_cou } +void jpeg_stream_writer::write_jpegls_preset_parameters_segment(const jpegls_preset_parameters_type preset_parameters_type, + const int32_t table_id, const int32_t entry_size, + const span table_data) +{ + ASSERT(preset_parameters_type == jpegls_preset_parameters_type::mapping_table_specification || + preset_parameters_type == jpegls_preset_parameters_type::mapping_table_continuation); + ASSERT(table_id > 0); + ASSERT(entry_size > 0); + ASSERT(table_data.size() >= static_cast(entry_size)); // Need to contain at least 1 entry. + ASSERT(table_data.size() <= segment_max_data_size - 3); + + // Format is defined in ISO/IEC 14495-1, C.2.4.1.2 and C.2.4.1.3 + write_segment_header(jpeg_marker_code::jpegls_preset_parameters, size_t{1} + 1 + 1 + table_data.size()); + write_uint8(to_underlying_type(preset_parameters_type)); + write_uint8(table_id); + write_uint8(entry_size); + write_bytes(table_data); +} + + void jpeg_stream_writer::write_segment_header(const jpeg_marker_code marker_code, const size_t data_size) { ASSERT(data_size <= segment_max_data_size); diff --git a/src/jpeg_stream_writer.h b/src/jpeg_stream_writer.h index 4ca714af..08155a75 100644 --- a/src/jpeg_stream_writer.h +++ b/src/jpeg_stream_writer.h @@ -5,10 +5,14 @@ #include "charls/jpegls_error.h" +#include "constants.h" #include "jpeg_marker_code.h" +#include "jpegls_preset_parameters_type.h" #include "span.h" #include "util.h" +#include + namespace charls { // Purpose: 'Writer' class that can generate JPEG-LS file streams. @@ -61,18 +65,23 @@ class jpeg_stream_writer final void write_application_data_segment(int32_t application_data_id, span application_data); /// - /// Writes a JPEG-LS preset parameters (LSE) segment. + /// Writes a JPEG-LS preset parameters (LSE) segment with JPEG-LS preset coding parameters. /// /// Parameters to write into the JPEG-LS preset segment. void write_jpegls_preset_parameters_segment(const jpegls_pc_parameters& preset_coding_parameters); /// - /// Writes a JPEG-LS preset parameters (LSE) segment for oversize image dimension. + /// Writes a JPEG-LS preset parameters (LSE) segment with oversize image dimension information. /// /// Height of the image. /// Width of the image. void write_jpegls_preset_parameters_segment(uint32_t height, uint32_t width); + /// + /// Writes JPEG-LS preset parameters (LSE) segment(s) with a mapping table. + /// + void write_jpegls_preset_parameters_segment(int32_t table_id, int32_t entry_size, span table_data); + /// /// Writes a JPEG-LS Start Of Frame (SOF-55) segment. /// @@ -120,10 +129,26 @@ class jpeg_stream_writer final void rewind() noexcept { byte_offset_ = 0; - component_id_ = 1; + component_index_ = 0; + } + + void set_table_id(const size_t component_index, const int32_t table_id) + { + ASSERT(component_index < maximum_component_count); + ASSERT(0 <= table_id && table_id <= maximum_table_id); + + // Usage of mapping tables is rare: use lazy initialization. + if (table_ids_.empty()) + { + table_ids_.resize(maximum_component_count); + } + + table_ids_[component_index] = static_cast(table_id); } private: + void write_jpegls_preset_parameters_segment(jpegls_preset_parameters_type preset_parameters_type, int32_t table_id, + int32_t entry_size, span table_data); void write_segment_header(jpeg_marker_code marker_code, size_t data_size); void write_uint8(const uint8_t value) noexcept @@ -215,9 +240,16 @@ class jpeg_stream_writer final write_bytes(data); } + [[nodiscard]] + uint8_t mapping_table_selector() const noexcept + { + return table_ids_.empty() ? 0 : table_ids_[component_index_]; + } + span destination_{}; size_t byte_offset_{}; - uint8_t component_id_{1}; + uint8_t component_index_{}; + std::vector table_ids_; }; } // namespace charls diff --git a/src/jpegls_error.cpp b/src/jpegls_error.cpp index 71b65042..55046f40 100644 --- a/src/jpegls_error.cpp +++ b/src/jpegls_error.cpp @@ -125,6 +125,12 @@ const char* CHARLS_API_CALLING_CONVENTION charls_get_error_message(const charls_ case jpegls_errc::invalid_spiff_header: return "Invalid JPEG-LS stream: invalid SPIFF header"; + case jpegls_errc::unknown_component_id: + return "Invalid JPEG-LS stream: unknown component ID in scan segment"; + + case jpegls_errc::mapping_tables_and_spiff_header: + return "Invalid JPEG-LS stream: mapping tables without SOF but with spiff header"; + case jpegls_errc::invalid_parameter_bits_per_sample: return "Invalid JPEG-LS stream: the bit per sample (sample precision) parameter is not in the range [2, 16]"; @@ -184,6 +190,12 @@ const char* CHARLS_API_CALLING_CONVENTION charls_get_error_message(const charls_ case jpegls_errc::invalid_parameter_color_transformation: return "Invalid JPEG-LS stream: Color transformation segment contains invalid values or doesn't match with frame info"; + + case jpegls_errc::invalid_parameter_mapping_table_id: + return "Invalid JPEG-LS stream: mapping table ID outside valid range or duplicate"; + + case jpegls_errc::invalid_parameter_mapping_table_continuation: + return "Invalid JPEG-LS stream: mapping table continuation without matching mapping table specification"; } return "Unknown"; diff --git a/src/util.h b/src/util.h index 88caf8fa..7766947d 100644 --- a/src/util.h +++ b/src/util.h @@ -314,6 +314,15 @@ inline void check_argument(const bool expression, const jpegls_errc error_value } +template +void check_argument_range(const T minimum, const T maximum, const T value, + const jpegls_errc error_value = jpegls_errc::invalid_argument) +{ + if (UNLIKELY(!(minimum <= value && value <= maximum))) + impl::throw_jpegls_error(error_value); +} + + inline void check_interleave_mode(const interleave_mode mode, const jpegls_errc error_value) { if (UNLIKELY(!(mode == interleave_mode::none || mode == interleave_mode::line || mode == interleave_mode::sample))) diff --git a/test/compliance.cpp b/test/compliance.cpp index b37ce957..6e974f3b 100644 --- a/test/compliance.cpp +++ b/test/compliance.cpp @@ -19,6 +19,20 @@ using namespace charls; namespace { +void compare_buffers(const byte* data1, const size_t size1, const byte* data2, const size_t size2) +{ + assert::is_true(size1 == size2); + + for (size_t i{}; i != size1; ++i) + { + if (data1[i] != data2[i]) + { + assert::is_true(false); + break; + } + } +} + void triplet2_planar(vector& buffer, const rect_size size) { vector work_buffer(buffer.size()); @@ -137,41 +151,6 @@ void decompress_file(const char* name_encoded, const char* name_raw, const int o } -////uint8_t palettisedDataH10[] = { -//// 0xFF, 0xD8, //Start of image (SOI) marker -//// 0xFF, 0xF7, //Start of JPEG-LS frame (SOF 55) marker – marker segment follows -//// 0x00, 0x0B, //Length of marker segment = 11 bytes including the length field -//// 0x02, //P = Precision = 2 bits per sample -//// 0x00, 0x04, //Y = Number of lines = 4 -//// 0x00, 0x03, //X = Number of columns = 3 -//// 0x01, //Nf = Number of components in the frame = 1 -//// 0x01, //C1 = Component ID = 1 (first and only component) -//// 0x11, //Sub-sampling: H1 = 1, V1 = 1 -//// 0x00, //Tq1 = 0 (this field is always 0) -//// -//// 0xFF, 0xF8, //LSE – JPEG-LS preset parameters marker -//// 0x00, 0x11, //Length of marker segment = 17 bytes including the length field -//// 0x02, //ID = 2, mapping table -//// 0x05, //TID = 5 Table identifier (arbitrary) -//// 0x03, //Wt = 3 Width of table entry -//// 0xFF, 0xFF, 0xFF, //Entry for index 0 -//// 0xFF, 0x00, 0x00, //Entry for index 1 -//// 0x00, 0xFF, 0x00, //Entry for index 2 -//// 0x00, 0x00, 0xFF, //Entry for index 3 -//// -//// 0xFF, 0xDA, //Start of scan (SOS) marker -//// 0x00, 0x08, //Length of marker segment = 8 bytes including the length field -//// 0x01, //Ns = Number of components for this scan = 1 -//// 0x01, //C1 = Component ID = 1 -//// 0x05, //Tm 1 = Mapping table identifier = 5 -//// 0x00, //NEAR = 0 (near-lossless max error) -//// 0x00, //ILV = 0 (interleave mode = non-interleaved) -//// 0x00, //Al = 0, Ah = 0 (no point transform) -//// 0xDB, 0x95, 0xF0, //3 bytes of compressed image data -//// 0xFF, 0xD9 //End of image (EOI) marker -////}; - - constexpr array buffer{0, 0, 90, 74, 68, 50, 43, 205, 64, 145, 145, 145, 100, 145, 145, 145}; ////const uint8_t bufferEncoded[] = { 0xFF, 0xD8, 0xFF, 0xF7, 0x00, 0x0B, 0x08, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x11, /// 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, /0xC0, 0x00, 0x00, 0x6C, 0x80, 0x20, 0x8E, /0x01, 0xC0, @@ -190,6 +169,71 @@ void test_sample_annex_h3() } +void test_sample_annex_h4_5() +{ + constexpr array palettised_data{ + byte{0xFF}, byte{0xD8}, // Start of image (SOI) marker + byte{0xFF}, byte{0xF7}, // Start of JPEG-LS frame (SOF 55) marker – marker segment follows + byte{0x00}, byte{0x0B}, // Length of marker segment = 11 bytes including the length field + byte{0x02}, // P = Precision = 2 bits per sample + byte{0x00}, byte{0x04}, // Y = Number of lines = 4 + byte{0x00}, byte{0x03}, // X = Number of columns = 3 + byte{0x01}, // Nf = Number of components in the frame = 1 + byte{0x01}, // C1 = Component ID = 1 (first and only component) + byte{0x11}, // Sub-sampling: H1 = 1, V1 = 1 + byte{0x00}, // Tq1 = 0 (this field is always 0) + + byte{0xFF}, byte{0xF8}, // LSE – JPEG-LS preset parameters marker + byte{0x00}, byte{0x11}, // Length of marker segment = 17 bytes including the length field + byte{0x02}, // ID = 2, mapping table + byte{0x05}, // TID = 5 Table identifier (arbitrary) + byte{0x03}, // Wt = 3 Width of table entry + byte{0xFF}, byte{0xFF}, byte{0xFF}, // Entry for index 0 + byte{0xFF}, byte{0x00}, byte{0x00}, // Entry for index 1 + byte{0x00}, byte{0xFF}, byte{0x00}, // Entry for index 2 + byte{0x00}, byte{0x00}, byte{0xFF}, // Entry for index 3 + + byte{0xFF}, byte{0xDA}, // Start of scan (SOS) marker + byte{0x00}, byte{0x08}, // Length of marker segment = 8 bytes including the length field + byte{0x01}, // Ns = Number of components for this scan = 1 + byte{0x01}, // C1 = Component ID = 1 + byte{0x05}, // Tm 1 = Mapping table identifier = 5 + byte{0x00}, // NEAR = 0 (near-lossless max error) + byte{0x00}, // ILV = 0 (interleave mode = non-interleaved) + byte{0x00}, // Al = 0, Ah = 0 (no point transform) + byte{0xDB}, byte{0x95}, byte{0xF0}, // 3 bytes of compressed image data + byte{0xFF}, byte{0xD9} // End of image (EOI) marker + }; + + jpegls_decoder decoder; + decoder.source(palettised_data); + decoder.read_header(); + vector destination(decoder.destination_size()); + decoder.decode(destination); + + constexpr array expected{byte{0}, byte{0}, byte{1}, byte{1}, byte{1}, byte{2}, + byte{2}, byte{2}, byte{3}, byte{3}, byte{3}, byte{3}}; + compare_buffers(expected.data(), expected.size(), destination.data(), destination.size()); + + const int32_t mapping_table_id{decoder.mapping_table_id(0)}; + assert::is_true(mapping_table_id == 5); + + const auto optional_table_index{decoder.mapping_table_index(mapping_table_id)}; + assert::is_true(optional_table_index.has_value()); + const auto table_index{optional_table_index.value_or(-1)}; + + const table_info table_info{decoder.mapping_table_info(table_index)}; + vector mapping_table(table_info.data_size); + + decoder.mapping_table(table_index, mapping_table); + + constexpr array expected_mapping_table{byte{0xFF}, byte{0xFF}, byte{0xFF}, byte{0xFF}, byte{0}, byte{0}, + byte{0}, byte{0xFF}, byte{0}, byte{0}, byte{0}, byte{0xFF}}; + compare_buffers(expected_mapping_table.data(), expected_mapping_table.size(), mapping_table.data(), + mapping_table.size()); +} + + void test_color_transforms_hp_images() { decompress_file("test/jlsimage/banny_normal.jls", "test/jlsimage/banny.ppm", 38, false); diff --git a/test/compliance.h b/test/compliance.h index 271092e3..8dabb960 100644 --- a/test/compliance.h +++ b/test/compliance.h @@ -6,3 +6,4 @@ void test_conformance(); void test_color_transforms_hp_images(); void test_sample_annex_h3(); +void test_sample_annex_h4_5(); diff --git a/test/main.cpp b/test/main.cpp index f0b51831..9eae9ac9 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -663,6 +663,9 @@ bool unit_test() cout << "Test Annex H3\n"; test_sample_annex_h3(); + cout << "Test Annex H.4.5\n"; + test_sample_annex_h4_5(); + test_noise_image(); cout << "Test robustness\n"; @@ -670,8 +673,6 @@ bool unit_test() test_decode_bit_stream_with_unsupported_encoding(); test_decode_bit_stream_with_unknown_jpeg_marker(); - cout << "Test Legacy API\n"; - return true; } catch (const unit_test_exception&) diff --git a/unittest/charls_jpegls_decoder_test.cpp b/unittest/charls_jpegls_decoder_test.cpp index 2b7113b4..24ef6cb2 100644 --- a/unittest/charls_jpegls_decoder_test.cpp +++ b/unittest/charls_jpegls_decoder_test.cpp @@ -21,6 +21,15 @@ MSVC_WARNING_SUPPRESS(6387) // '_Param_(x)' could be '0': this does not adhere t #pragma clang diagnostic ignored "-Wnonnull" #endif +namespace { + +void destroy_decoder(const charls_jpegls_decoder* decoder) noexcept +{ + charls_jpegls_decoder_destroy(decoder); +} + +} // namespace + namespace charls::test { TEST_CLASS(charls_jpegls_decoder_test) @@ -82,11 +91,9 @@ TEST_CLASS(charls_jpegls_decoder_test) auto error{charls_jpegls_decoder_get_frame_info(nullptr, &frame_info)}; Assert::AreEqual(jpegls_errc::invalid_argument, error); - const auto* decoder{get_initialized_decoder()}; - error = charls_jpegls_decoder_get_frame_info(decoder, nullptr); + const auto decoder{get_initialized_decoder()}; + error = charls_jpegls_decoder_get_frame_info(decoder.get(), nullptr); Assert::AreEqual(jpegls_errc::invalid_argument, error); - - charls_jpegls_decoder_destroy(decoder); } TEST_METHOD(get_near_lossless_nullptr) // NOLINT @@ -95,11 +102,9 @@ TEST_CLASS(charls_jpegls_decoder_test) auto error{charls_jpegls_decoder_get_near_lossless(nullptr, 0, &near_lossless)}; Assert::AreEqual(jpegls_errc::invalid_argument, error); - const auto* decoder{get_initialized_decoder()}; - error = charls_jpegls_decoder_get_near_lossless(decoder, 0, nullptr); + const auto decoder{get_initialized_decoder()}; + error = charls_jpegls_decoder_get_near_lossless(decoder.get(), 0, nullptr); Assert::AreEqual(jpegls_errc::invalid_argument, error); - - charls_jpegls_decoder_destroy(decoder); } TEST_METHOD(get_interleave_mode_nullptr) // NOLINT @@ -108,11 +113,9 @@ TEST_CLASS(charls_jpegls_decoder_test) auto error{charls_jpegls_decoder_get_interleave_mode(nullptr, &interleave_mode)}; Assert::AreEqual(jpegls_errc::invalid_argument, error); - const auto* decoder{get_initialized_decoder()}; - error = charls_jpegls_decoder_get_interleave_mode(decoder, nullptr); + const auto decoder{get_initialized_decoder()}; + error = charls_jpegls_decoder_get_interleave_mode(decoder.get(), nullptr); Assert::AreEqual(jpegls_errc::invalid_argument, error); - - charls_jpegls_decoder_destroy(decoder); } TEST_METHOD(get_preset_coding_parameters_nullptr) // NOLINT @@ -121,11 +124,9 @@ TEST_CLASS(charls_jpegls_decoder_test) auto error{charls_jpegls_decoder_get_preset_coding_parameters(nullptr, 0, &preset_coding_parameters)}; Assert::AreEqual(jpegls_errc::invalid_argument, error); - const auto* decoder{get_initialized_decoder()}; - error = charls_jpegls_decoder_get_preset_coding_parameters(decoder, 0, nullptr); + const auto decoder{get_initialized_decoder()}; + error = charls_jpegls_decoder_get_preset_coding_parameters(decoder.get(), 0, nullptr); Assert::AreEqual(jpegls_errc::invalid_argument, error); - - charls_jpegls_decoder_destroy(decoder); } TEST_METHOD(get_destination_size_nullptr) // NOLINT @@ -134,11 +135,9 @@ TEST_CLASS(charls_jpegls_decoder_test) auto error{charls_jpegls_decoder_get_destination_size(nullptr, 0, &destination_size_bytes)}; Assert::AreEqual(jpegls_errc::invalid_argument, error); - const auto* decoder{get_initialized_decoder()}; - error = charls_jpegls_decoder_get_destination_size(decoder, 0, nullptr); + const auto decoder{get_initialized_decoder()}; + error = charls_jpegls_decoder_get_destination_size(decoder.get(), 0, nullptr); Assert::AreEqual(jpegls_errc::invalid_argument, error); - - charls_jpegls_decoder_destroy(decoder); } TEST_METHOD(decode_to_buffer_nullptr) // NOLINT @@ -168,12 +167,10 @@ TEST_CLASS(charls_jpegls_decoder_test) TEST_METHOD(decode_to_zero_size_buffer) // NOLINT { - auto* decoder{get_initialized_decoder()}; + auto decoder{get_initialized_decoder()}; - const auto error{charls_jpegls_decoder_decode_to_buffer(decoder, nullptr, 0, 0)}; + const auto error{charls_jpegls_decoder_decode_to_buffer(decoder.get(), nullptr, 0, 0)}; Assert::AreEqual(jpegls_errc::destination_buffer_too_small, error); - - charls_jpegls_decoder_destroy(decoder); } TEST_METHOD(at_comment_nullptr) // NOLINT @@ -188,17 +185,38 @@ TEST_CLASS(charls_jpegls_decoder_test) Assert::AreEqual(jpegls_errc::invalid_argument, error); } + TEST_METHOD(charls_decoder_get_mapping_table_count_nullptr) // NOLINT + { + int32_t count{7}; + auto error{charls_decoder_get_mapping_table_count(nullptr, &count)}; + Assert::AreEqual(jpegls_errc::invalid_argument, error); + Assert::AreEqual(7, count); + + const auto decoder{get_initialized_decoder()}; + size_t destination_size; + error = charls_jpegls_decoder_get_destination_size(decoder.get(), 0, &destination_size); + Assert::AreEqual(jpegls_errc::success, error); + + std::vector decoded_destination(destination_size); + error = + charls_jpegls_decoder_decode_to_buffer(decoder.get(), decoded_destination.data(), decoded_destination.size(), 0); + Assert::AreEqual(jpegls_errc::success, error); + + error = charls_decoder_get_mapping_table_count(decoder.get(), nullptr); + Assert::AreEqual(jpegls_errc::invalid_argument, error); + } + private: - static charls_jpegls_decoder* get_initialized_decoder() + static std::unique_ptr get_initialized_decoder() { - const auto source{read_file("DataFiles/t8c0e0.jls")}; + static const auto source{read_file("DataFiles/t8c0e0.jls")}; auto* const decoder{charls_jpegls_decoder_create()}; auto error{charls_jpegls_decoder_set_source_buffer(decoder, source.data(), source.size())}; Assert::AreEqual(jpegls_errc::success, error); error = charls_jpegls_decoder_read_header(decoder); Assert::AreEqual(jpegls_errc::success, error); - return decoder; + return {decoder, destroy_decoder}; } }; diff --git a/unittest/charls_jpegls_encoder_test.cpp b/unittest/charls_jpegls_encoder_test.cpp index 549f5d2f..a5b41f54 100644 --- a/unittest/charls_jpegls_encoder_test.cpp +++ b/unittest/charls_jpegls_encoder_test.cpp @@ -87,6 +87,12 @@ TEST_CLASS(charls_jpegls_encoder_test) Assert::AreEqual(jpegls_errc::invalid_argument, error); } + TEST_METHOD(charls_jpegls_encoder_set_table_id_nullptr) // NOLINT + { + const auto error{charls_jpegls_encoder_set_table_id(nullptr, 0, 0)}; + Assert::AreEqual(jpegls_errc::invalid_argument, error); + } + TEST_METHOD(get_estimated_destination_size_nullptr) // NOLINT { size_t size_in_bytes{}; @@ -179,6 +185,14 @@ TEST_CLASS(charls_jpegls_encoder_test) Assert::AreEqual(jpegls_errc::invalid_argument, error); } + TEST_METHOD(write_table_data_nullptr) // NOLINT + { + constexpr table_info info{}; + constexpr array buffer{}; + const auto error{charls_jpegls_encoder_write_table(nullptr, 1, 1, buffer.data(), buffer.size())}; + Assert::AreEqual(jpegls_errc::invalid_argument, error); + } + TEST_METHOD(rewind_nullptr) // NOLINT { const auto error{charls_jpegls_encoder_rewind(nullptr)}; @@ -219,6 +233,12 @@ TEST_CLASS(charls_jpegls_encoder_test) charls_jpegls_encoder_destroy(encoder); } + + TEST_METHOD(create_tables_only_null_ptr) // NOLINT + { + const auto error{charls_jpegls_encoder_create_tables_only(nullptr)}; + Assert::AreEqual(jpegls_errc::invalid_argument, error); + } }; } // namespace charls::test diff --git a/unittest/compliance_test.cpp b/unittest/compliance_test.cpp index 5542748e..320be1b7 100644 --- a/unittest/compliance_test.cpp +++ b/unittest/compliance_test.cpp @@ -10,6 +10,28 @@ #include "../test/portable_anymap_file.h" using namespace charls_test; +using std::byte; +using std::array; +using std::vector; +using Microsoft::VisualStudio::CppUnitTestFramework::Assert; + +namespace { + +void compare_buffers(const byte* buffer1, const size_t size1, const byte* buffer2, const size_t size2) +{ + Assert::AreEqual(size1, size2); + + for (size_t i{}; i != size1; ++i) + { + if (buffer1[i] != buffer2[i]) + { + Assert::AreEqual(buffer1[i], buffer2[i]); + break; + } + } +} + +} // namespace namespace charls::test { @@ -116,6 +138,71 @@ TEST_CLASS(compliance_test) decompress_file("DataFiles/test16_rm_5.jls", "DataFiles/test16.pgm", false); } + TEST_METHOD(decompress_mapping_table_sample_annex_h4_5) // NOLINT + { + // ISO 14495-1: Sample image from appendix H.4.5 "Example of a palletised image" / Figure H.10 + constexpr array palettised_data{ + byte{0xFF}, byte{0xD8}, // Start of image (SOI) marker + byte{0xFF}, byte{0xF7}, // Start of JPEG-LS frame (SOF 55) marker - marker segment follows + byte{0x00}, byte{0x0B}, // Length of marker segment = 11 bytes including the length field + byte{0x02}, // P = Precision = 2 bits per sample + byte{0x00}, byte{0x04}, // Y = Number of lines = 4 + byte{0x00}, byte{0x03}, // X = Number of columns = 3 + byte{0x01}, // Nf = Number of components in the frame = 1 + byte{0x01}, // C1 = Component ID = 1 (first and only component) + byte{0x11}, // Sub-sampling: H1 = 1, V1 = 1 + byte{0x00}, // Tq1 = 0 (this field is always 0) + + byte{0xFF}, byte{0xF8}, // LSE - JPEG-LS preset parameters marker + byte{0x00}, byte{0x11}, // Length of marker segment = 17 bytes including the length field + byte{0x02}, // ID = 2, mapping table + byte{0x05}, // TID = 5 Table identifier (arbitrary) + byte{0x03}, // Wt = 3 Width of table entry + byte{0xFF}, byte{0xFF}, byte{0xFF}, // Entry for index 0 + byte{0xFF}, byte{0x00}, byte{0x00}, // Entry for index 1 + byte{0x00}, byte{0xFF}, byte{0x00}, // Entry for index 2 + byte{0x00}, byte{0x00}, byte{0xFF}, // Entry for index 3 + + byte{0xFF}, byte{0xDA}, // Start of scan (SOS) marker + byte{0x00}, byte{0x08}, // Length of marker segment = 8 bytes including the length field + byte{0x01}, // Ns = Number of components for this scan = 1 + byte{0x01}, // C1 = Component ID = 1 + byte{0x05}, // Tm 1 = Mapping table identifier = 5 + byte{0x00}, // NEAR = 0 (near-lossless max error) + byte{0x00}, // ILV = 0 (interleave mode = non-interleaved) + byte{0x00}, // Al = 0, Ah = 0 (no point transform) + byte{0xDB}, byte{0x95}, byte{0xF0}, // 3 bytes of compressed image data + byte{0xFF}, byte{0xD9} // End of image (EOI) marker + }; + + jpegls_decoder decoder; + decoder.source(palettised_data); + decoder.read_header(); + vector destination(decoder.destination_size()); + decoder.decode(destination); + + constexpr array expected{byte{0}, byte{0}, byte{1}, byte{1}, byte{1}, byte{2}, + byte{2}, byte{2}, byte{3}, byte{3}, byte{3}, byte{3}}; + compare_buffers(expected.data(), expected.size(), destination.data(), destination.size()); + + const int32_t mapping_table_id{decoder.mapping_table_id(0)}; + Assert::AreEqual(5, mapping_table_id); + + const auto optional_table_index{decoder.mapping_table_index(mapping_table_id)}; + Assert::IsTrue(optional_table_index.has_value()); + const auto table_index{optional_table_index.value_or(-1)}; + + const table_info table_info{decoder.mapping_table_info(table_index)}; + vector mapping_table(table_info.data_size); + + decoder.mapping_table(table_index, mapping_table); + + constexpr array expected_mapping_table{byte{0xFF}, byte{0xFF}, byte{0xFF}, byte{0xFF}, byte{0}, byte{0}, + byte{0}, byte{0xFF}, byte{0}, byte{0}, byte{0}, byte{0xFF}}; + compare_buffers(expected_mapping_table.data(), expected_mapping_table.size(), mapping_table.data(), + mapping_table.size()); + } + private: static void decompress_file(const char* encoded_filename, const char* raw_filename, const bool check_encode = true) { diff --git a/unittest/jpeg_stream_reader_test.cpp b/unittest/jpeg_stream_reader_test.cpp index dbf6d0eb..0aa505f0 100644 --- a/unittest/jpeg_stream_reader_test.cpp +++ b/unittest/jpeg_stream_reader_test.cpp @@ -685,6 +685,187 @@ TEST_CLASS(jpeg_stream_reader_test) Assert::IsFalse(called); } + TEST_METHOD(read_mapping_table) // NOLINT + { + vector source(100); + jpeg_stream_writer writer({source.data(), source.size()}); + writer.write_start_of_image(); + + constexpr array table_data_expected{byte{2}}; + + writer.write_jpegls_preset_parameters_segment(1, 1, table_data_expected); + writer.write_start_of_frame_segment({1, 1, 2, 1}); + writer.write_start_of_scan_segment(1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({source.data(), source.size()}); + + reader.read_header(); + + Assert::AreEqual(1, reader.mapping_table_count()); + Assert::AreEqual(0, reader.mapping_table_index(1).value_or(-1)); + + const auto info{reader.mapping_table_info(0)}; + Assert::AreEqual(int32_t{1}, info.table_id); + Assert::AreEqual(int32_t{1}, info.entry_size); + Assert::AreEqual(uint32_t{1}, info.data_size); + + vector table_data(1); + reader.get_mapping_table(0, {table_data.data(), table_data.size()}); + Assert::AreEqual(byte{2}, table_data[0]); + } + + TEST_METHOD(read_mapping_table_too_small_buffer_throws) // NOLINT + { + vector source(100); + jpeg_stream_writer writer({source.data(), source.size()}); + writer.write_start_of_image(); + + constexpr array table_data_expected{byte{2}, byte{3}}; + + writer.write_jpegls_preset_parameters_segment(1, 1, table_data_expected); + writer.write_start_of_frame_segment({1, 1, 2, 1}); + writer.write_start_of_scan_segment(1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({source.data(), source.size()}); + reader.read_header(); + + assert_expect_exception(jpegls_errc::destination_buffer_too_small, [&reader] { + vector table_data(1); + reader.get_mapping_table(0, {table_data.data(), table_data.size()}); + }); + } + + TEST_METHOD(mapping_table_count_is_zero_at_start) // NOLINT + { + const jpeg_stream_reader reader; + + const int32_t count{reader.mapping_table_count()}; + + Assert::AreEqual(0, count); + } + + TEST_METHOD(mapping_table_count_after_read_header) // NOLINT + { + const std::vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_start_of_frame_segment(1, 1, 8, 3); + writer.write_jpegls_preset_parameters_segment(2, 1, table_data, false); + writer.write_start_of_scan_segment(0, 1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({writer.buffer.data(), writer.buffer.size()}); + + reader.read_header(); + const int32_t count{reader.mapping_table_count()}; + + Assert::AreEqual(2, count); + } + + TEST_METHOD(mapping_table_count_after_read_header_after_frame) // NOLINT + { + const std::vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_start_of_frame_segment(1, 1, 8, 3); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_start_of_scan_segment(0, 1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({writer.buffer.data(), writer.buffer.size()}); + reader.read_header(); + const int32_t count{reader.mapping_table_count()}; + + Assert::AreEqual(1, count); + } + + TEST_METHOD(mapping_table_count_after_read_header_before_frame) // NOLINT + { + const std::vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_start_of_frame_segment(1, 1, 8, 3); + writer.write_start_of_scan_segment(0, 1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({writer.buffer.data(), writer.buffer.size()}); + reader.read_header(); + const int32_t count{reader.mapping_table_count()}; + + Assert::AreEqual(1, count); + } + + TEST_METHOD(read_mapping_table_continuation) // NOLINT + { + constexpr size_t table_size{100000}; + vector source(table_size + 100); + jpeg_stream_writer writer({source.data(), source.size()}); + writer.write_start_of_image(); + + vector table_data_expected(table_size); + table_data_expected[0] = byte{7}; + table_data_expected[table_size - 1] = byte{8}; + + writer.write_jpegls_preset_parameters_segment(1, 1, {table_data_expected.data(), table_data_expected.size()}); + writer.write_start_of_frame_segment({1, 1, 2, 1}); + writer.write_start_of_scan_segment(1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({source.data(), source.size()}); + + reader.read_header(); + + Assert::AreEqual(1, reader.mapping_table_count()); + Assert::AreEqual(0, reader.mapping_table_index(1).value_or(-1)); + + const auto info{reader.mapping_table_info(0)}; + Assert::AreEqual(int32_t{1}, info.table_id); + Assert::AreEqual(int32_t{1}, info.entry_size); + Assert::AreEqual(uint32_t{100000}, info.data_size); + + vector table_data(table_size); + reader.get_mapping_table(0, {table_data.data(), table_data.size()}); + Assert::AreEqual(byte{7}, table_data[0]); + Assert::AreEqual(byte{8}, table_data[table_size - 1]); + } + + TEST_METHOD(read_mapping_table_continuation_without_mapping_table_throws) // NOLINT + { + const std::vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, true); + writer.write_start_of_frame_segment(1, 1, 8, 3); + writer.write_start_of_scan_segment(0, 1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({writer.buffer.data(), writer.buffer.size()}); + + assert_expect_exception(jpegls_errc::invalid_parameter_mapping_table_continuation, + [&reader] { reader.read_header(); }); + } + + TEST_METHOD(read_invalid_mapping_table_continuation_throws) // NOLINT + { + const std::vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_jpegls_preset_parameters_segment(1, 2, table_data, true); + writer.write_start_of_frame_segment(1, 1, 8, 3); + writer.write_start_of_scan_segment(0, 1, 0, interleave_mode::none); + + jpeg_stream_reader reader; + reader.source({writer.buffer.data(), writer.buffer.size()}); + + assert_expect_exception(jpegls_errc::invalid_parameter_mapping_table_continuation, + [&reader] { reader.read_header(); }); + } + private: static void read_spiff_header(const uint8_t low_version) { diff --git a/unittest/jpeg_stream_writer_test.cpp b/unittest/jpeg_stream_writer_test.cpp index e7da2d58..573581fb 100644 --- a/unittest/jpeg_stream_writer_test.cpp +++ b/unittest/jpeg_stream_writer_test.cpp @@ -286,14 +286,14 @@ TEST_CLASS(jpeg_stream_writer_test) Assert::AreEqual(size_t{19}, writer.bytes_written()); Assert::AreEqual(byte{0xFF}, buffer[0]); - Assert::AreEqual(byte{0xF7}, buffer[1]); // JPEG_SOF_55 - Assert::AreEqual(byte{}, buffer[2]); // 6 + (3 * 3) + 2 (in big endian) - Assert::AreEqual(byte{17}, buffer[3]); // 6 + (3 * 3) + 2 (in big endian) + Assert::AreEqual(byte{0xF7}, buffer[1]); // JPEG_SOF_55 + Assert::AreEqual(byte{}, buffer[2]); // 6 + (3 * 3) + 2 (in big endian) + Assert::AreEqual(byte{17}, buffer[3]); // 6 + (3 * 3) + 2 (in big endian) Assert::AreEqual(static_cast(bits_per_sample), buffer[4]); - Assert::AreEqual(byte{255}, buffer[5]); // height (in big endian) - Assert::AreEqual(byte{255}, buffer[6]); // height (in big endian) - Assert::AreEqual(byte{}, buffer[7]); // width (in big endian) - Assert::AreEqual(byte{100}, buffer[8]); // width (in big endian) + Assert::AreEqual(byte{255}, buffer[5]); // height (in big endian) + Assert::AreEqual(byte{255}, buffer[6]); // height (in big endian) + Assert::AreEqual(byte{}, buffer[7]); // width (in big endian) + Assert::AreEqual(byte{100}, buffer[8]); // width (in big endian) Assert::AreEqual(static_cast(component_count), buffer[9]); Assert::AreEqual(byte{1}, buffer[10]); @@ -324,14 +324,14 @@ TEST_CLASS(jpeg_stream_writer_test) Assert::AreEqual(size_t{19}, writer.bytes_written()); Assert::AreEqual(byte{0xFF}, buffer[0]); - Assert::AreEqual(byte{0xF7}, buffer[1]); // JPEG_SOF_55 - Assert::AreEqual(byte{}, buffer[2]); // 6 + (3 * 3) + 2 (in big endian) - Assert::AreEqual(byte{17}, buffer[3]); // 6 + (3 * 3) + 2 (in big endian) + Assert::AreEqual(byte{0xF7}, buffer[1]); // JPEG_SOF_55 + Assert::AreEqual(byte{}, buffer[2]); // 6 + (3 * 3) + 2 (in big endian) + Assert::AreEqual(byte{17}, buffer[3]); // 6 + (3 * 3) + 2 (in big endian) Assert::AreEqual(static_cast(bits_per_sample), buffer[4]); - Assert::AreEqual(byte{}, buffer[5]); // height (in big endian) - Assert::AreEqual(byte{}, buffer[6]); // height (in big endian) - Assert::AreEqual(byte{}, buffer[7]); // width (in big endian) - Assert::AreEqual(byte{}, buffer[8]); // width (in big endian) + Assert::AreEqual(byte{}, buffer[5]); // height (in big endian) + Assert::AreEqual(byte{}, buffer[6]); // height (in big endian) + Assert::AreEqual(byte{}, buffer[7]); // width (in big endian) + Assert::AreEqual(byte{}, buffer[8]); // width (in big endian) Assert::AreEqual(static_cast(component_count), buffer[9]); Assert::AreEqual(byte{1}, buffer[10]); @@ -373,7 +373,8 @@ TEST_CLASS(jpeg_stream_writer_test) Assert::AreEqual(buffer.size(), writer.bytes_written()); Assert::AreEqual(byte{16}, buffer[4]); Assert::AreEqual(numeric_limits::max(), to_integer(buffer[9])); - Assert::AreEqual(numeric_limits::max(), to_integer(buffer[buffer.size() - 3])); // Last component index. + Assert::AreEqual(numeric_limits::max(), + to_integer(buffer[buffer.size() - 3])); // Last component index. } TEST_METHOD(write_color_transform_segment) // NOLINT @@ -464,12 +465,12 @@ TEST_CLASS(jpeg_stream_writer_test) writer.write_start_of_scan_segment(1, 2, interleave_mode::none); Assert::AreEqual(buffer.size(), writer.bytes_written()); - Assert::AreEqual(byte{1}, buffer[4]); // component count. - Assert::AreEqual(byte{1}, buffer[5]); // component index. - Assert::AreEqual(byte{}, buffer[6]); // table ID. - Assert::AreEqual(byte{2}, buffer[7]); // NEAR parameter. - Assert::AreEqual(byte{}, buffer[8]); // ILV parameter. - Assert::AreEqual(byte{}, buffer[9]); // transformation. + Assert::AreEqual(byte{1}, buffer[4]); // component count. + Assert::AreEqual(byte{1}, buffer[5]); // component index. + Assert::AreEqual(byte{}, buffer[6]); // table ID. + Assert::AreEqual(byte{2}, buffer[7]); // NEAR parameter. + Assert::AreEqual(byte{}, buffer[8]); // ILV parameter. + Assert::AreEqual(byte{}, buffer[9]); // transformation. } TEST_METHOD(rewind) // NOLINT @@ -485,6 +486,92 @@ TEST_CLASS(jpeg_stream_writer_test) Assert::AreEqual(buffer.size(), writer.bytes_written()); Assert::AreEqual(byte{1}, buffer[4]); // component count. } + + TEST_METHOD(write_minimal_table) // NOLINT + { + array buffer{}; + jpeg_stream_writer writer({buffer.data(), buffer.size()}); + + constexpr array table_data{byte{77}}; + writer.write_jpegls_preset_parameters_segment(100, 1, table_data); + + Assert::AreEqual(buffer.size(), writer.bytes_written()); + Assert::AreEqual(byte{0xFF}, buffer[0]); + Assert::AreEqual(byte{0xF8}, buffer[1]); // LSE + Assert::AreEqual(byte{0}, buffer[2]); + Assert::AreEqual(byte{6}, buffer[3]); + Assert::AreEqual(byte{2}, buffer[4]); // type = table + Assert::AreEqual(byte{100}, buffer[5]); // table ID + Assert::AreEqual(byte{1}, buffer[6]); // size of entry + Assert::AreEqual(byte{77}, buffer[7]); // table content + } + + TEST_METHOD(write_table_max_entry_size) // NOLINT + { + array buffer{}; + jpeg_stream_writer writer({buffer.data(), buffer.size()}); + + constexpr array table_data{}; + writer.write_jpegls_preset_parameters_segment(100, 255, table_data); + + Assert::AreEqual(buffer.size(), writer.bytes_written()); + Assert::AreEqual(byte{0xFF}, buffer[0]); + Assert::AreEqual(byte{0xF8}, buffer[1]); // LSE + Assert::AreEqual(byte{1}, buffer[2]); + Assert::AreEqual(byte{4}, buffer[3]); + Assert::AreEqual(byte{2}, buffer[4]); // type = table + Assert::AreEqual(byte{100}, buffer[5]); // table ID + Assert::AreEqual(byte{255}, buffer[6]); // size of entry + Assert::AreEqual(byte{0}, buffer[7]); // table content + } + + TEST_METHOD(write_table_fits_in_single_segment) // NOLINT + { + std::vector buffer(size_t{2} + std::numeric_limits::max()); + jpeg_stream_writer writer({buffer.data(), buffer.size()}); + + std::vector table_data(std::numeric_limits::max() - 5); + writer.write_jpegls_preset_parameters_segment(255, 1, {table_data.data(), table_data.size()}); + + Assert::AreEqual(buffer.size(), writer.bytes_written()); + Assert::AreEqual(byte{0xFF}, buffer[0]); + Assert::AreEqual(byte{0xF8}, buffer[1]); // LSE + Assert::AreEqual(byte{255}, buffer[2]); + Assert::AreEqual(byte{255}, buffer[3]); + Assert::AreEqual(byte{2}, buffer[4]); // type = table + Assert::AreEqual(byte{255}, buffer[5]); // table ID + Assert::AreEqual(byte{1}, buffer[6]); // size of entry + Assert::AreEqual(byte{0}, buffer[7]); // table content (first entry) + } + + TEST_METHOD(write_table_that_requires_two_segment) // NOLINT + { + std::vector buffer(size_t{2} + std::numeric_limits::max() + 8); + jpeg_stream_writer writer({buffer.data(), buffer.size()}); + + std::vector table_data(static_cast(std::numeric_limits::max()) - 5 + 1); + writer.write_jpegls_preset_parameters_segment(255, 1, {table_data.data(), table_data.size()}); + + Assert::AreEqual(buffer.size(), writer.bytes_written()); + Assert::AreEqual(byte{0xFF}, buffer[0]); + Assert::AreEqual(byte{0xF8}, buffer[1]); // LSE + Assert::AreEqual(byte{255}, buffer[2]); + Assert::AreEqual(byte{255}, buffer[3]); + Assert::AreEqual(byte{2}, buffer[4]); // type = table + Assert::AreEqual(byte{255}, buffer[5]); // table ID + Assert::AreEqual(byte{1}, buffer[6]); // size of entry + Assert::AreEqual(byte{0}, buffer[7]); // table content (first entry) + + // Validate second segment. + Assert::AreEqual(byte{0xFF}, buffer[65537]); + Assert::AreEqual(byte{0xF8}, buffer[65538]); // LSE + Assert::AreEqual(byte{}, buffer[65539]); + Assert::AreEqual(byte{6}, buffer[65540]); + Assert::AreEqual(byte{3}, buffer[65541]); // type = table + Assert::AreEqual(byte{255}, buffer[65542]); // table ID + Assert::AreEqual(byte{1}, buffer[65543]); // size of entry + Assert::AreEqual(byte{0}, buffer[65544]); // table content (last entry) + } }; } // namespace charls::test diff --git a/unittest/jpeg_test_stream_writer.h b/unittest/jpeg_test_stream_writer.h index f1d4d94b..13b0104c 100644 --- a/unittest/jpeg_test_stream_writer.h +++ b/unittest/jpeg_test_stream_writer.h @@ -8,6 +8,7 @@ #include "../src/util.h" #include +#include namespace charls::test { @@ -35,6 +36,45 @@ class jpeg_test_stream_writer final write_marker(jpeg_marker_code::start_of_image); } + void write_spiff_header_segment(const spiff_header& header) + { + ASSERT(header.height > 0); + ASSERT(header.width > 0); + + static constexpr std::array spiff_magic_id{std::byte{'S'}, std::byte{'P'}, std::byte{'I'}, std::byte{'F'}, std::byte{'F'}, std::byte{'\0'}}; + + // Create a JPEG APP8 segment in Still Picture Interchange File Format (SPIFF), v2.0 + write_marker(jpeg_marker_code::application_data8); + write_uint16(30 + 2); + write_bytes(spiff_magic_id.data(), spiff_magic_id.size()); + write_uint8(2); + write_uint8(0); + write_uint8(to_underlying_type(header.profile_id)); + write_uint8(header.component_count); + write_uint32(header.height); + write_uint32(header.width); + write_uint8(to_underlying_type(header.color_space)); + write_uint8(header.bits_per_sample); + write_uint8(to_underlying_type(header.compression_type)); + write_uint8(to_underlying_type(header.resolution_units)); + write_uint32(header.vertical_resolution); + write_uint32(header.horizontal_resolution); + } + + void write_spiff_end_of_directory_entry() + { + constexpr uint8_t spiff_end_of_directory_entry_type{1}; + + // Note: ISO/IEC 10918-3, Annex F.2.2.3 documents that the EOD entry segment should have a length of 8 + // but only 6 data bytes. This approach allows to wrap existing bit streams\encoders with a SPIFF header. + // In this implementation the SOI marker is added as data bytes to simplify the design. + static constexpr std::array spiff_end_of_directory{ + std::byte{0}, std::byte{0}, + std::byte{0}, std::byte{spiff_end_of_directory_entry_type}, + std::byte{0xFF}, std::byte{to_underlying_type(jpeg_marker_code::start_of_image)}}; + write_segment(jpeg_marker_code::application_data8, spiff_end_of_directory.data(), spiff_end_of_directory.size()); + } + void write_start_of_frame_segment(const int width, const int height, const int bits_per_sample, const int component_count) { @@ -80,6 +120,21 @@ class jpeg_test_stream_writer final write_segment(jpeg_marker_code::jpegls_preset_parameters, segment.data(), segment.size()); } + void write_jpegls_preset_parameters_segment(const int32_t table_id, const int32_t entry_size, + const std::vector& table_data, const bool continuation) + { + // Format is defined in ISO/IEC 14495-1, C.2.4.1.2 and C.2.4.1.3 + std::vector segment; + + segment.push_back(static_cast(continuation ? jpegls_preset_parameters_type::mapping_table_continuation + : jpegls_preset_parameters_type::mapping_table_specification)); + segment.push_back(static_cast(table_id)); + segment.push_back(static_cast(entry_size)); + segment.insert(end(segment), begin(table_data), end(table_data)); + + write_segment(jpeg_marker_code::jpegls_preset_parameters, segment.data(), segment.size()); + } + void write_oversize_image_dimension(const uint32_t number_of_bytes, const uint32_t height, const uint32_t width, const bool extra_byte = false) { @@ -181,12 +236,25 @@ class jpeg_test_stream_writer final write_byte(static_cast(marker_code)); } + void write_uint8(const uint32_t value) + { + write_byte(static_cast(value)); + } + void write_uint16(const uint16_t value) { write_byte(static_cast(value / 0x100)); write_byte(static_cast(value % 0x100)); } + void write_uint32(const uint32_t value) + { + write_byte(static_cast(value > 3)); + write_byte(static_cast(value > 2)); + write_byte(static_cast(value > 1)); + write_byte(static_cast(value)); + } + void write_byte(const std::byte value) { buffer.push_back(value); diff --git a/unittest/jpegls_decoder_test.cpp b/unittest/jpegls_decoder_test.cpp index 64a6c459..43e191f4 100644 --- a/unittest/jpegls_decoder_test.cpp +++ b/unittest/jpegls_decoder_test.cpp @@ -440,20 +440,6 @@ TEST_CLASS(jpegls_decoder_test) "DataFiles/test8.ppm"); } - TEST_METHOD(decode_reference_to_mapping_table_selector_throws) // NOLINT - { - jpeg_test_stream_writer writer; - - writer.write_start_of_image(); - writer.write_start_of_frame_segment(10, 10, 8, 3); - writer.mapping_table_selector = 1; - writer.write_start_of_scan_segment(0, 1, 0, interleave_mode::none); - - jpegls_decoder decoder{writer.buffer, false}; - - assert_expect_exception(jpegls_errc::parameter_value_not_supported, [&decoder] { decoder.read_header(); }); - } - TEST_METHOD(read_spiff_header) // NOLINT { const auto source{create_test_spiff_header()}; @@ -1026,6 +1012,247 @@ TEST_CLASS(jpegls_decoder_test) decoder.decode(data, size); } + TEST_METHOD(abbreviated_format_mapping_table_count_after_read_header) // NOLINT + { + const vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_marker(jpeg_marker_code::end_of_image); + + jpegls_decoder decoder; + decoder.source(writer.buffer); + decoder.read_header(); + const int32_t count{decoder.mapping_table_count()}; + + Assert::AreEqual(1, count); + } + + TEST_METHOD(abbreviated_format_with_spiff_header_throws) // NOLINT + { + const vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + + spiff_header header{}; + header.bits_per_sample = 8; + header.color_space = spiff_color_space::rgb; + header.component_count = 3; + header.height = 1; + header.width = 1; + + writer.write_spiff_header_segment(header); + writer.write_spiff_end_of_directory_entry(); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_marker(jpeg_marker_code::end_of_image); + + jpegls_decoder decoder; + decoder.source(writer.buffer); + decoder.read_spiff_header(); + + assert_expect_exception(jpegls_errc::mapping_tables_and_spiff_header, + [&decoder] { ignore = decoder.read_header(); }); + } + + TEST_METHOD(mapping_table_count_after_decode_table_after_first_scan) // NOLINT + { + constexpr array data_h10{ + byte{0xFF}, byte{0xD8}, // Start of image (SOI) marker + byte{0xFF}, byte{0xF7}, // Start of JPEG-LS frame (SOF 55) marker - marker segment follows + byte{0x00}, byte{0x0E}, // Length of marker segment = 14 bytes including the length field + byte{0x02}, // P = Precision = 2 bits per sample + byte{0x00}, byte{0x04}, // Y = Number of lines = 4 + byte{0x00}, byte{0x03}, // X = Number of columns = 3 + byte{0x02}, // Nf = Number of components in the frame = 2 + byte{0x01}, // C1 = Component ID = 1 (first and only component) + byte{0x11}, // Sub-sampling: H1 = 1, V1 = 1 + byte{0x00}, // Tq1 = 0 (this field is always 0) + byte{0x02}, // C2 = Component ID = 2 (first and only component) + byte{0x11}, // Sub-sampling: H1 = 1, V1 = 1 + byte{0x00}, // Tq1 = 0 (this field is always 0) + + byte{0xFF}, byte{0xF8}, // LSE - JPEG-LS preset parameters marker + byte{0x00}, byte{0x11}, // Length of marker segment = 17 bytes including the length field + byte{0x02}, // ID = 2, mapping table + byte{0x05}, // TID = 5 Table identifier (arbitrary) + byte{0x03}, // Wt = 3 Width of table entry + byte{0xFF}, byte{0xFF}, byte{0xFF}, // Entry for index 0 + byte{0xFF}, byte{0x00}, byte{0x00}, // Entry for index 1 + byte{0x00}, byte{0xFF}, byte{0x00}, // Entry for index 2 + byte{0x00}, byte{0x00}, byte{0xFF}, // Entry for index 3 + + byte{0xFF}, byte{0xDA}, // Start of scan (SOS) marker + byte{0x00}, byte{0x08}, // Length of marker segment = 8 bytes including the length field + byte{0x01}, // Ns = Number of components for this scan = 1 + byte{0x01}, // C1 = Component ID = 1 + byte{0x05}, // Tm 1 = Mapping table identifier = 5 + byte{0x00}, // NEAR = 0 (near-lossless max error) + byte{0x00}, // ILV = 0 (interleave mode = non-interleaved) + byte{0x00}, // Al = 0, Ah = 0 (no point transform) + byte{0xDB}, byte{0x95}, byte{0xF0}, // 3 bytes of compressed image data + + byte{0xFF}, byte{0xF8}, // LSE - JPEG-LS preset parameters marker + byte{0x00}, byte{0x11}, // Length of marker segment = 17 bytes including the length field + byte{0x02}, // ID = 2, mapping table + byte{0x06}, // TID = 6 Table identifier (arbitrary) + byte{0x03}, // Wt = 3 Width of table entry + byte{0xFF}, byte{0xFF}, byte{0xFF}, // Entry for index 0 + byte{0xFF}, byte{0x00}, byte{0x00}, // Entry for index 1 + byte{0x00}, byte{0xFF}, byte{0x00}, // Entry for index 2 + byte{0x00}, byte{0x00}, byte{0xFF}, // Entry for index 3 + + byte{0xFF}, byte{0xDA}, // Start of scan (SOS) marker + byte{0x00}, byte{0x08}, // Length of marker segment = 8 bytes including the length field + byte{0x01}, // Ns = Number of components for this scan = 1 + byte{0x02}, // C1 = Component ID = 2 + byte{0x06}, // Tm 1 = Mapping table identifier = 6 + byte{0x00}, // NEAR = 0 (near-lossless max error) + byte{0x00}, // ILV = 0 (interleave mode = non-interleaved) + byte{0x00}, // Al = 0, Ah = 0 (no point transform) + byte{0xDB}, byte{0x95}, byte{0xF0}, // 3 bytes of compressed image data + + byte{0xFF}, byte{0xD9} // End of image (EOI) marker + }; + + jpegls_decoder decoder; + decoder.source(data_h10); + decoder.read_header(); + + vector destination(decoder.destination_size()); + decoder.decode(destination); + + const int32_t count{decoder.mapping_table_count()}; + Assert::AreEqual(2, count); + + Assert::AreEqual(5, decoder.mapping_table_id(0)); + Assert::AreEqual(6, decoder.mapping_table_id(1)); + } + + TEST_METHOD(invalid_table_id_throws) // NOLINT + { + const std::vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_jpegls_preset_parameters_segment(0, 1, table_data, false); + + jpegls_decoder decoder; + decoder.source(writer.buffer); + assert_expect_exception(jpegls_errc::invalid_parameter_mapping_table_id, [&decoder] { decoder.read_header(); }); + } + + TEST_METHOD(duplicate_table_id_throws) // NOLINT + { + const std::vector table_data(255); + jpeg_test_stream_writer writer; + writer.write_start_of_image(); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_start_of_frame_segment(1, 1, 8, 3); + writer.write_jpegls_preset_parameters_segment(1, 1, table_data, false); + writer.write_start_of_scan_segment(0, 1, 0, interleave_mode::none); + + jpegls_decoder decoder; + decoder.source(writer.buffer); + assert_expect_exception(jpegls_errc::invalid_parameter_mapping_table_id, [&decoder] { decoder.read_header(); }); + } + + TEST_METHOD(mapping_table_id_returns_zero) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + vector decoded_destination(decoder.destination_size()); + + decoder.decode(decoded_destination); + + Assert::AreEqual(0, decoder.mapping_table_id(0)); + Assert::AreEqual(0, decoder.mapping_table_id(1)); + Assert::AreEqual(0, decoder.mapping_table_id(2)); + } + + TEST_METHOD(mapping_table_id_for_invalid_component_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + vector decoded_destination(decoder.destination_size()); + + decoder.decode(decoded_destination); + + assert_expect_exception(jpegls_errc::invalid_argument, [&decoder] { ignore = decoder.mapping_table_id(3); }); + } + + TEST_METHOD(mapping_table_id_before_decode_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + + assert_expect_exception(jpegls_errc::invalid_operation, [&decoder] { ignore = decoder.mapping_table_id(0); }); + } + + TEST_METHOD(mapping_table_index_before_decode_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + + assert_expect_exception(jpegls_errc::invalid_operation, [&decoder] { ignore = decoder.mapping_table_index(3); }); + } + + TEST_METHOD(mapping_table_index_invalid_index_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + vector decoded_destination(decoder.destination_size()); + decoder.decode(decoded_destination); + + assert_expect_exception(jpegls_errc::invalid_argument, [&decoder] { ignore = decoder.mapping_table_index(0); }); + assert_expect_exception(jpegls_errc::invalid_argument, [&decoder] { ignore = decoder.mapping_table_index(256); }); + } + + TEST_METHOD(mapping_table_count_before_decode_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + + assert_expect_exception(jpegls_errc::invalid_operation, [&decoder] { ignore = decoder.mapping_table_count(); }); + } + + TEST_METHOD(mapping_table_info_before_decode_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + + assert_expect_exception(jpegls_errc::invalid_operation, [&decoder] { ignore = decoder.mapping_table_info(0); }); + } + + TEST_METHOD(mapping_table_before_decode_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + vector table(1000); + + assert_expect_exception(jpegls_errc::invalid_operation, + [&decoder, &table] { decoder.mapping_table(0, table); }); + } + + TEST_METHOD(mapping_table_invalid_index_throws) // NOLINT + { + const auto encoded_source{read_file("DataFiles/t8c0e0.jls")}; + + const jpegls_decoder decoder(encoded_source, true); + vector decoded_destination(decoder.destination_size()); + decoder.decode(decoded_destination); + vector table(1000); + + assert_expect_exception(jpegls_errc::invalid_argument, + [&decoder, &table] { decoder.mapping_table(0, table); }); + } + private: // ReSharper disable CppPassValueParameterByConstReference [[nodiscard]] diff --git a/unittest/jpegls_encoder_test.cpp b/unittest/jpegls_encoder_test.cpp index 3b41f420..52cb19e9 100644 --- a/unittest/jpegls_encoder_test.cpp +++ b/unittest/jpegls_encoder_test.cpp @@ -859,7 +859,7 @@ TEST_CLASS(jpegls_encoder_test) TEST_METHOD(write_application_data_before_encode) // NOLINT { - const vector source{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + constexpr array source{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; constexpr frame_info frame_info{3, 1, 16, 1}; jpegls_encoder encoder; @@ -873,6 +873,165 @@ TEST_CLASS(jpegls_encoder_test) test_by_decoding(encoded, frame_info, source.data(), source.size(), interleave_mode::none); } + TEST_METHOD(write_table) // NOLINT + { + jpegls_encoder encoder; + + array destination; + encoder.destination(destination); + + constexpr array table_data{byte{0}}; + encoder.write_table(1, 1, table_data); + + Assert::AreEqual(size_t{10}, encoder.bytes_written()); + + // Check that SOI marker has been written. + Assert::AreEqual(byte{0xFF}, destination[0]); + Assert::AreEqual(static_cast(jpeg_marker_code::start_of_image), destination[1]); + + // Verify that a JPEG-LS preset segment with the table has been written. + Assert::AreEqual(byte{0xFF}, destination[2]); + Assert::AreEqual(static_cast(jpeg_marker_code::jpegls_preset_parameters), destination[3]); + Assert::AreEqual(byte{}, destination[4]); + Assert::AreEqual(byte{6}, destination[5]); + Assert::AreEqual(byte{2}, destination[6]); + Assert::AreEqual(byte{1}, destination[7]); + Assert::AreEqual(byte{1}, destination[8]); + Assert::AreEqual(byte{}, destination[9]); + } + + TEST_METHOD(write_table_before_encode) // NOLINT + { + constexpr array table_data{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + constexpr array source{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + constexpr frame_info frame_info{3, 1, 16, 1}; + + jpegls_encoder encoder; + vector encoded(100); + encoder.destination(encoded); + encoder.frame_info(frame_info); + + encoder.write_table(1, 1, table_data); + + encoded.resize(encoder.encode(source)); + test_by_decoding(encoded, frame_info, source.data(), source.size(), interleave_mode::none); + } + + TEST_METHOD(write_table_with_bad_table_id_throws) // NOLINT + { + constexpr array table_data{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + jpegls_encoder encoder; + + vector destination(100); + encoder.destination(destination); + + assert_expect_exception(jpegls_errc::invalid_argument, + [&encoder, &table_data] { ignore = encoder.write_table(0, 1, table_data); }); + + assert_expect_exception(jpegls_errc::invalid_argument, + [&encoder, &table_data] { ignore = encoder.write_table(256, 1, table_data); }); + } + + TEST_METHOD(write_table_with_bad_entry_size_throws) // NOLINT + { + constexpr array table_data{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + jpegls_encoder encoder; + + vector destination(100); + encoder.destination(destination); + + assert_expect_exception(jpegls_errc::invalid_argument, + [&encoder, &table_data] { ignore = encoder.write_table(1, 0, table_data); }); + + assert_expect_exception(jpegls_errc::invalid_argument, + [&encoder, &table_data] { ignore = encoder.write_table(1, 256, table_data); }); + } + + TEST_METHOD(write_table_with_too_small_table_throws) // NOLINT + { + constexpr array table_data{byte{0}}; + jpegls_encoder encoder; + + vector destination(100); + encoder.destination(destination); + + assert_expect_exception(jpegls_errc::invalid_argument_size, [&encoder, &table_data] { + ignore = encoder.write_table(1, 2, table_data); + }); + } + + TEST_METHOD(write_table_null_pointer_with_size_throws) // NOLINT + { + jpegls_encoder encoder; + + vector destination(100); + encoder.destination(destination); + + assert_expect_exception(jpegls_errc::invalid_argument, [&encoder] { + MSVC_WARNING_SUPPRESS_NEXT_LINE(6387) + ignore = encoder.write_table(1, 1, nullptr, 1); + }); + } + + TEST_METHOD(write_table_after_encode_throws) // NOLINT + { + constexpr array table_data{byte{0}}; + const vector source{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + + jpegls_encoder encoder; + + vector destination(100); + encoder.destination(destination); + encoder.frame_info({3, 1, 16, 1}); + ignore = encoder.encode(source); + + assert_expect_exception(jpegls_errc::invalid_operation, [&encoder, &table_data] { + ignore = encoder.write_table(1, 1, table_data); + }); + } + + TEST_METHOD(create_tables_only) // NOLINT + { + jpegls_encoder encoder; + + array destination; + encoder.destination(destination); + + constexpr array table_data{byte{0}}; + encoder.write_table(1, 1, table_data); + const size_t bytes_written{encoder.create_tables_only()}; + + Assert::AreEqual(size_t{12}, bytes_written); + + // Check that SOI marker has been written. + Assert::AreEqual(byte{0xFF}, destination[0]); + Assert::AreEqual(static_cast(jpeg_marker_code::start_of_image), destination[1]); + + // Verify that a JPEG-LS preset segment with the table has been written. + Assert::AreEqual(byte{0xFF}, destination[2]); + Assert::AreEqual(static_cast(jpeg_marker_code::jpegls_preset_parameters), destination[3]); + Assert::AreEqual(byte{}, destination[4]); + Assert::AreEqual(byte{6}, destination[5]); + Assert::AreEqual(byte{2}, destination[6]); + Assert::AreEqual(byte{1}, destination[7]); + Assert::AreEqual(byte{1}, destination[8]); + Assert::AreEqual(byte{}, destination[9]); + + // Check that SOI marker has been written. + Assert::AreEqual(byte{0xFF}, destination[10]); + Assert::AreEqual(static_cast(jpeg_marker_code::end_of_image), destination[11]); + } + + TEST_METHOD(create_tables_only_with_no_tables_throws) // NOLINT + { + jpegls_encoder encoder; + + array destination; + encoder.destination(destination); + + assert_expect_exception(jpegls_errc::invalid_operation, [&encoder] { ignore = encoder.create_tables_only(); }); + } + TEST_METHOD(set_preset_coding_parameters) // NOLINT { jpegls_encoder encoder; @@ -919,6 +1078,59 @@ TEST_CLASS(jpegls_encoder_test) [&encoder] { encoder.color_transformation(static_cast(100)); }); } + TEST_METHOD(set_table_id) // NOLINT + { + constexpr array source{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + constexpr frame_info frame_info{3, 1, 16, 1}; + jpegls_encoder encoder; + encoder.frame_info(frame_info); + vector destination(encoder.estimated_destination_size()); + encoder.destination(destination); + + encoder.set_table_id(0, 1); + + const size_t bytes_written{encoder.encode(source)}; + destination.resize(bytes_written); + const jpegls_decoder decoder(destination, true); + vector destination_decoded(decoder.destination_size()); + decoder.decode(destination_decoded); + Assert::AreEqual(1, decoder.mapping_table_id(0)); + } + + TEST_METHOD(set_table_id_clear_id) // NOLINT + { + constexpr array source{byte{0}, byte{1}, byte{2}, byte{3}, byte{4}, byte{5}}; + constexpr frame_info frame_info{3, 1, 16, 1}; + jpegls_encoder encoder; + encoder.frame_info(frame_info); + vector destination(encoder.estimated_destination_size()); + encoder.destination(destination); + + encoder.set_table_id(0, 1); + encoder.set_table_id(0, 0); + + const size_t bytes_written{encoder.encode(source)}; + destination.resize(bytes_written); + const jpegls_decoder decoder(destination, true); + vector destination_decoded(decoder.destination_size()); + decoder.decode(destination_decoded); + Assert::AreEqual(0, decoder.mapping_table_id(0)); + } + + TEST_METHOD(set_table_id_bad_component_index_throws) // NOLINT + { + jpegls_encoder encoder; + + assert_expect_exception(jpegls_errc::invalid_argument, [&encoder] { encoder.set_table_id(-1, 0); }); + } + + TEST_METHOD(set_table_id_bad_id_throws) // NOLINT + { + jpegls_encoder encoder; + + assert_expect_exception(jpegls_errc::invalid_argument, [&encoder] { encoder.set_table_id(0, -1); }); + } + TEST_METHOD(encode_without_destination_throws) // NOLINT { jpegls_encoder encoder; diff --git a/unittest/pch.h b/unittest/pch.h index 1777d6c3..2ec171bc 100644 --- a/unittest/pch.h +++ b/unittest/pch.h @@ -26,6 +26,7 @@ #include #include #include +#include #ifdef _MSC_VER #define MSVC_WARNING_SUPPRESS(x) \