From df6ec66f4e31d73553b6018d41fb2f4b3f8d7d7b Mon Sep 17 00:00:00 2001 From: Vadim Pisarevskiy Date: Tue, 1 Jul 2025 01:03:01 +0300 Subject: [PATCH 1/9] added metadata muxer/demuxer with the corresponding API. For now: [tested] * AVIF encoder can write exif, xmp, icc * AVIF decoder can read exif * JPEG encoder can write exif * JPEG decoder can read exif [/tested] * (?) probably PNG, WebP etc. decoders can also read exif, need to check. --- .../imgcodecs/include/opencv2/imgcodecs.hpp | 68 +++ modules/imgcodecs/src/exif.cpp | 4 + modules/imgcodecs/src/exif.hpp | 6 +- modules/imgcodecs/src/grfmt_avif.cpp | 24 +- modules/imgcodecs/src/grfmt_base.cpp | 33 ++ modules/imgcodecs/src/grfmt_base.hpp | 22 + modules/imgcodecs/src/grfmt_jpeg.cpp | 16 + modules/imgcodecs/src/loadsave.cpp | 147 ++++- modules/imgcodecs/test/test_exif.cpp | 503 ++++++++++++++++++ 9 files changed, 808 insertions(+), 15 deletions(-) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index c610802b1012..d0f6ee61d69e 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -251,6 +251,15 @@ enum ImwriteGIFCompressionFlags { IMWRITE_GIF_COLORTABLE_SIZE_256 = 8 }; +enum ImageMetadataType +{ + IMAGE_METADATA_UNKNOWN = -1, + IMAGE_METADATA_EXIF = 0, + IMAGE_METADATA_XMP = 1, + IMAGE_METADATA_ICCP = 2, + IMAGE_METADATA_MAX = 2 +}; + //! @} imgcodecs_flags /** @brief Represents an animation with multiple frames. @@ -360,6 +369,17 @@ The image passing through the img parameter can be pre-allocated. The memory is */ CV_EXPORTS_W void imread( const String& filename, OutputArray dst, int flags = IMREAD_COLOR_BGR ); +/** @brief Reads an image from a file together with associated metadata. + +The function imreadWithMetadata reads image from the specified file. It does the same thing as imread, but additionally reads metadata if the corresponding file contains any. +@param filename Name of the file to be loaded. +@param metadataTypes Output vector with types of metadata chucks returned in metadata, see ImageMetadataType. +@param metadata Output vector of vectors or vector of matrices to store the retrieved metadata +@param flags Flag that can take values of cv::ImreadModes +*/ +CV_EXPORTS_W Mat imreadWithMetadata( const String& filename, CV_OUT std::vector& metadataTypes, + OutputArrayOfArrays metadata, int flags = IMREAD_ANYCOLOR); + /** @brief Loads a multi-page image from a file. The function imreadmulti loads a multi-page image from the specified file into a vector of Mat objects. @@ -508,6 +528,20 @@ It also demonstrates how to save multiple images in a TIFF file: CV_EXPORTS_W bool imwrite( const String& filename, InputArray img, const std::vector& params = std::vector()); +/** @brief Saves an image to a specified file with metadata + +The function imwriteWithMetadata saves the image to the specified file. It does the same thing as imwrite, but additionally writes metadata if the corresponding format supports it. +@param filename Name of the file. As with imwrite, image format is determined by the file extension. +@param img (Mat or vector of Mat) Image or Images to be saved. +@param metadataTypes Vector with types of metadata chucks stored in metadata to write, see ImageMetadataType. +@param metadata Vector of vectors or vector of matrices with chunks of metadata to store into the file +@param params Format-specific parameters encoded as pairs (paramId_1, paramValue_1, paramId_2, paramValue_2, ... .) see cv::ImwriteFlags +*/ +CV_EXPORTS_W bool imwriteWithMetadata( const String& filename, InputArray img, + const std::vector& metadataTypes, + InputArrayOfArrays& metadata, + const std::vector& params = std::vector()); + //! @brief multi-image overload for bindings CV_WRAP static inline bool imwritemulti(const String& filename, InputArrayOfArrays img, @@ -529,6 +563,22 @@ See cv::imread for the list of supported formats and flags description. */ CV_EXPORTS_W Mat imdecode( InputArray buf, int flags ); +/** @brief Reads an image from a buffer in memory together with associated metadata. + +The function imdecode reads an image from the specified buffer in the memory. If the buffer is too short or +contains invalid data, the function returns an empty matrix ( Mat::data==NULL ). + +See cv::imread for the list of supported formats and flags description. + +@note In the case of color images, the decoded images will have the channels stored in **B G R** order. +@param buf Input array or vector of bytes. +@param metadataTypes Output vector with types of metadata chucks returned in metadata, see ImageMetadataType. +@param metadata Output vector of vectors or vector of matrices to store the retrieved metadata +@param flags The same flags as in cv::imread, see cv::ImreadModes. +*/ +CV_EXPORTS_W Mat imdecodeWithMetadata( InputArray buf, CV_OUT std::vector& metadataTypes, + OutputArrayOfArrays metadata, int flags = IMREAD_ANYCOLOR ); + /** @overload @param buf Input array or vector of bytes. @param flags The same flags as in cv::imread, see cv::ImreadModes. @@ -567,6 +617,24 @@ CV_EXPORTS_W bool imencode( const String& ext, InputArray img, CV_OUT std::vector& buf, const std::vector& params = std::vector()); +/** @brief Encodes an image into a memory buffer. + +The function imencode compresses the image and stores it in the memory buffer that is resized to fit the +result. See cv::imwrite for the list of supported formats and flags description. + +@param ext File extension that defines the output format. Must include a leading period. +@param img Image to be compressed. +@param metadataTypes Vector with types of metadata chucks stored in metadata to write, see ImageMetadataType. +@param metadata Vector of vectors or vector of matrices with chunks of metadata to store into the file +@param buf Output buffer resized to fit the compressed image. +@param params Format-specific parameters. See cv::imwrite and cv::ImwriteFlags. +*/ +CV_EXPORTS_W bool imencodeWithMetadata( const String& ext, InputArray img, + const std::vector& metadataTypes, + InputArrayOfArrays metadata, + CV_OUT std::vector& buf, + const std::vector& params = std::vector()); + /** @brief Encodes array of images into a memory buffer. The function is analog to cv::imencode for in-memory multi-page image compression. diff --git a/modules/imgcodecs/src/exif.cpp b/modules/imgcodecs/src/exif.cpp index 8ed976055646..3f1bbdbe18e7 100644 --- a/modules/imgcodecs/src/exif.cpp +++ b/modules/imgcodecs/src/exif.cpp @@ -94,6 +94,10 @@ ExifEntry_t ExifReader::getTag(const ExifTagName tag) const return entry; } +const std::vector& ExifReader::getData() const +{ + return m_data; +} /** * @brief Parsing the exif data buffer and prepare (internal) exif directory diff --git a/modules/imgcodecs/src/exif.hpp b/modules/imgcodecs/src/exif.hpp index a8914bec039f..85cadfac228b 100644 --- a/modules/imgcodecs/src/exif.hpp +++ b/modules/imgcodecs/src/exif.hpp @@ -174,7 +174,11 @@ class ExifReader * @return ExifEntru_t structure. Caller has to know what tag it calls in order to extract proper field from the structure ExifEntry_t */ ExifEntry_t getTag( const ExifTagName tag ) const; - + + /** + * @brief Get the whole exif buffer + */ + const std::vector& getData() const; private: std::vector m_data; diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index c35eb5030620..acc8a8014473 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -68,8 +68,8 @@ avifResult CopyToMat(const avifImage *image, int channels, bool useRGB , Mat *ma return avifImageYUVToRGB(image, &rgba); } -AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, - int bit_depth) { +AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, int bit_depth, + const std::vector >& metadata) { CV_Assert(img.depth() == CV_8U || img.depth() == CV_16U); const int width = img.cols; @@ -111,6 +111,20 @@ AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, result->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; result->yuvRange = AVIF_RANGE_FULL; } + + avifResult status = AVIF_RESULT_OK; + + if (!metadata.empty()) { + const std::vector& metadata_exif = metadata[IMAGE_METADATA_EXIF]; + const std::vector& metadata_xmp = metadata[IMAGE_METADATA_XMP]; + const std::vector& metadata_iccp = metadata[IMAGE_METADATA_ICCP]; + if (!metadata_exif.empty()) + status = avifImageSetMetadataExif(result, (const uint8_t*)metadata_exif.data(), metadata_exif.size()); + if (!metadata_exif.empty() && status == AVIF_RESULT_OK) + status = avifImageSetMetadataXMP(result, (const uint8_t*)metadata_xmp.data(), metadata_xmp.size()); + if (!metadata_iccp.empty() && status == AVIF_RESULT_OK) + status = avifImageSetProfileICC(result, (const uint8_t*)metadata_iccp.data(), metadata_iccp.size()); + } avifRGBImage rgba; avifRGBImageSetDefaults(&rgba, result); @@ -120,12 +134,12 @@ AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, CV_Assert(img.channels() == 4); rgba.format = AVIF_RGB_FORMAT_BGRA; } - rgba.rowBytes = img.step[0]; + rgba.rowBytes = (uint32_t)img.step[0]; rgba.depth = bit_depth; rgba.pixels = const_cast(reinterpret_cast(img.data)); - if (avifImageRGBToYUV(result, &rgba) != AVIF_RESULT_OK) { + if (status != AVIF_RESULT_OK || avifImageRGBToYUV(result, &rgba) != AVIF_RESULT_OK) { avifImageDestroy(result); return nullptr; } @@ -349,7 +363,7 @@ bool AvifEncoder::writeanimation(const Animation& animation, img.channels() == 1 || img.channels() == 3 || img.channels() == 4, "AVIF only supports 1, 3, 4 channels"); - images.emplace_back(ConvertToAvif(img, do_lossless, bit_depth)); + images.emplace_back(ConvertToAvif(img, do_lossless, bit_depth, m_metadata)); } for (size_t i = 0; i < images.size(); i++) diff --git a/modules/imgcodecs/src/grfmt_base.cpp b/modules/imgcodecs/src/grfmt_base.cpp index dc3d07ab789f..d3e75ba792fa 100644 --- a/modules/imgcodecs/src/grfmt_base.cpp +++ b/modules/imgcodecs/src/grfmt_base.cpp @@ -58,11 +58,30 @@ BaseImageDecoder::BaseImageDecoder() m_frame_count = 1; } +bool BaseImageDecoder::haveMetadata(ImageMetadataType type) const +{ + if (type == IMAGE_METADATA_EXIF) + return !m_exif.getData().empty(); + return false; +} + +Mat BaseImageDecoder::getMetadata(ImageMetadataType type) const +{ + if (type == IMAGE_METADATA_EXIF) { + const std::vector& exif = m_exif.getData(); + if (!exif.empty()) { + Mat exifmat(1, (int)exif.size(), CV_8U, (void*)exif.data()); + return exifmat; + } + } + return Mat(); +} ExifEntry_t BaseImageDecoder::getExifTag(const ExifTagName tag) const { return m_exif.getTag(tag); } + bool BaseImageDecoder::setSource( const String& filename ) { m_filename = filename; @@ -140,6 +159,20 @@ bool BaseImageEncoder::setDestination( std::vector& buf ) return true; } +bool BaseImageEncoder::addMetadata(ImageMetadataType type, const Mat& metadata) +{ + CV_Assert_N(type >= IMAGE_METADATA_EXIF, type <= IMAGE_METADATA_MAX); + if (metadata.empty()) + return true; + if (m_metadata.empty()) + m_metadata.resize((int)IMAGE_METADATA_MAX+1); + CV_Assert(metadata.elemSize() == 1); + CV_Assert(metadata.isContinuous()); + const unsigned char* data = metadata.ptr(); + m_metadata[(int)type].assign(data, data + metadata.total()); + return true; +} + bool BaseImageEncoder::write(const Mat &img, const std::vector ¶ms) { std::vector img_vec(1, img); return writemulti(img_vec, params); diff --git a/modules/imgcodecs/src/grfmt_base.hpp b/modules/imgcodecs/src/grfmt_base.hpp index 6d98bd373584..ff7d14cb142c 100644 --- a/modules/imgcodecs/src/grfmt_base.hpp +++ b/modules/imgcodecs/src/grfmt_base.hpp @@ -68,6 +68,20 @@ class BaseImageDecoder { * @return The type of the image. */ virtual int type() const { return m_type; } + + /** + * @brief Checks whether file contains metadata of the certain type. + * @param type The type of metadata to look for + */ + virtual bool haveMetadata(ImageMetadataType type) const; + + /** + * @brief Retrieves metadata (if any) of the certain kind. + * If there is no such metadata, the method returns empty array. + * + * @param type The type of metadata to look for + */ + virtual Mat getMetadata(ImageMetadataType type) const; /** * @brief Fetch a specific EXIF tag from the image's metadata. @@ -204,6 +218,13 @@ class BaseImageEncoder { * @return true if the destination was successfully set, false otherwise. */ virtual bool setDestination(std::vector& buf); + + /** + * @brief Sets the metadata to write together with the image data + * @param type The type of metadata to add + * @param metadata The packed metadata (Exif, XMP, ...) + */ + virtual bool addMetadata(ImageMetadataType type, const Mat& metadata); /** * @brief Encode and write the image data. @@ -243,6 +264,7 @@ class BaseImageEncoder { virtual void throwOnError() const; protected: + std::vector > m_metadata; // see IMAGE_METADATA_... String m_description; ///< Description of the encoder (e.g., format name, capabilities). String m_filename; ///< Destination file name for encoded data. std::vector* m_buf; ///< Pointer to the buffer for encoded data if using memory-based destination. diff --git a/modules/imgcodecs/src/grfmt_jpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg.cpp index a3a7f70c3cb6..9a0e8193bdd6 100644 --- a/modules/imgcodecs/src/grfmt_jpeg.cpp +++ b/modules/imgcodecs/src/grfmt_jpeg.cpp @@ -814,6 +814,22 @@ bool JpegEncoder::write( const Mat& img, const std::vector& params ) } jpeg_start_compress( &cinfo, TRUE ); + + if (!m_metadata.empty()) { + const std::vector& metadata_exif = m_metadata[IMAGE_METADATA_EXIF]; + size_t exif_size = metadata_exif.size(); + if (exif_size > 0u) { + const char app1_exif_prefix[] = {'E', 'x', 'i', 'f', '\0', '\0'}; + size_t app1_exif_prefix_size = sizeof(app1_exif_prefix); + size_t data_size = exif_size + app1_exif_prefix_size; + + std::vector metadata_app1(data_size); + uchar* data = metadata_app1.data(); + memcpy(data, app1_exif_prefix, app1_exif_prefix_size); + memcpy(data + app1_exif_prefix_size, metadata_exif.data(), exif_size); + jpeg_write_marker(&cinfo, JPEG_APP0 + 1, data, (unsigned)data_size); + } + } if( doDirectWrite ) { diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index dfbf118fb9b1..0190bf9f16a2 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -410,6 +410,62 @@ static void ApplyExifOrientation(ExifEntry_t orientationTag, OutputArray img) } } +static void readMetadata(ImageDecoder& decoder, + std::vector* metadata_types, + OutputArrayOfArrays metadata) +{ + if (!metadata_types) + return; + int kind = metadata.kind(); + void* obj = metadata.getObj(); + std::vector* matvector = nullptr; + std::vector >* vecvector = nullptr; + if (kind == _InputArray::STD_VECTOR_MAT) { + matvector = (std::vector*)obj; + } else if (kind == _InputArray::STD_VECTOR_VECTOR) { + int elemtype = metadata.type(0); + CV_Assert(elemtype == CV_8UC1 || elemtype == CV_8SC1); + vecvector = (std::vector >*)obj; + } else { + CV_Error(Error::StsBadArg, + "unsupported metadata type, should be a vector of matrices or vector of byte vectors"); + } + std::vector src_metadata; + for (int m = (int)IMAGE_METADATA_EXIF; m <= (int)IMAGE_METADATA_MAX; m++) { + Mat mm = decoder->getMetadata((ImageMetadataType)m); + if (!mm.empty()) { + CV_Assert(mm.isContinuous()); + CV_Assert(mm.elemSize() == 1u); + metadata_types->push_back(m); + src_metadata.push_back(mm); + } + } + size_t nmetadata = metadata_types->size(); + if (matvector) { + matvector->resize(nmetadata); + for (size_t m = 0; m < nmetadata; m++) + matvector->at(m) = src_metadata[m]; + } else { + vecvector->resize(nmetadata); + for (size_t m = 0; m < nmetadata; m++) { + const Mat& mm = src_metadata[m]; + const uchar* data = (uchar*)mm.data; + vecvector->at(m).assign(data, data + mm.total()); + } + } +} + +static void addMetadata(ImageEncoder& encoder, + const std::vector& metadata_types, + InputArrayOfArrays metadata) +{ + size_t nmetadata_chunks = metadata_types.size(); + for (size_t i = 0; i < nmetadata_chunks; i++) { + ImageMetadataType metadata_type = (ImageMetadataType)metadata_types[i]; + encoder->addMetadata(metadata_type, metadata.getMat((int)i)); + } +} + /** * Read an image into memory and return the information * @@ -419,10 +475,14 @@ static void ApplyExifOrientation(ExifEntry_t orientationTag, OutputArray img) * */ static bool -imread_( const String& filename, int flags, OutputArray mat ) +imread_( const String& filename, int flags, OutputArray mat, + std::vector* metadata_types, OutputArrayOfArrays metadata) { /// Search for the relevant decoder to handle the imagery ImageDecoder decoder; + + if (metadata_types) + metadata_types->clear(); #ifdef HAVE_GDAL if(flags != IMREAD_UNCHANGED && (flags & IMREAD_LOAD_GDAL) == IMREAD_LOAD_GDAL ){ @@ -509,6 +569,8 @@ imread_( const String& filename, int flags, OutputArray mat ) CV_CheckTrue(original_ptr == real_mat.data, "Internal imread issue"); success = true; } + + readMetadata(decoder, metadata_types, metadata); } catch (const cv::Exception& e) { @@ -662,7 +724,24 @@ Mat imread( const String& filename, int flags ) Mat img; /// load the data - imread_( filename, flags, img ); + imread_( filename, flags, img, nullptr, noArray() ); + + /// return a reference to the data + return img; +} + +Mat imreadWithMetadata( const String& filename, + std::vector& metadata_types, + OutputArrayOfArrays metadata, + int flags ) +{ + CV_TRACE_FUNCTION(); + + /// create the basic container + Mat img; + + /// load the data + imread_( filename, flags, img, &metadata_types, metadata ); /// return a reference to the data return img; @@ -673,7 +752,7 @@ void imread( const String& filename, OutputArray dst, int flags ) CV_TRACE_FUNCTION(); /// load the data - imread_(filename, flags, dst); + imread_(filename, flags, dst, nullptr, noArray()); } /** @@ -946,6 +1025,8 @@ size_t imcount(const String& filename, int flags) static bool imwrite_( const String& filename, const std::vector& img_vec, + const std::vector& metadata_types, + InputArrayOfArrays metadata, const std::vector& params_, bool flipv ) { bool isMultiImg = img_vec.size() > 1; @@ -981,6 +1062,8 @@ static bool imwrite_( const String& filename, const std::vector& img_vec, } encoder->setDestination( filename ); + addMetadata(encoder, metadata_types, metadata); + #if CV_VERSION_MAJOR < 5 && defined(HAVE_IMGCODEC_HDR) bool fixed = false; std::vector params_pair(2); @@ -1055,7 +1138,26 @@ bool imwrite( const String& filename, InputArray _img, img_vec.push_back(_img.getMat()); CV_Assert(!img_vec.empty()); - return imwrite_(filename, img_vec, params, false); + return imwrite_(filename, img_vec, {}, noArray(), params, false); +} + +bool imwriteWithMetadata( const String& filename, InputArray _img, + const std::vector& metadata_types, + InputArrayOfArrays metadata, + const std::vector& params ) +{ + CV_TRACE_FUNCTION(); + + CV_Assert(!_img.empty()); + + std::vector img_vec; + if (_img.isMatVector() || _img.isUMatVector()) + _img.getMatVector(img_vec); + else + img_vec.push_back(_img.getMat()); + + CV_Assert(!img_vec.empty()); + return imwrite_(filename, img_vec, metadata_types, metadata, params, false); } static bool imwriteanimation_(const String& filename, const Animation& animation, const std::vector& params) @@ -1140,8 +1242,13 @@ bool imencodeanimation(const String& ext, const Animation& animation, std::vecto } static bool -imdecode_( const Mat& buf, int flags, Mat& mat ) +imdecode_( const Mat& buf, int flags, Mat& mat, + std::vector* metadata_types, + OutputArrayOfArrays metadata ) { + if (metadata_types) + metadata_types->clear(); + CV_Assert(!buf.empty()); CV_Assert(buf.isContinuous()); CV_Assert(buf.checkVector(1, CV_8U) > 0); @@ -1231,6 +1338,7 @@ imdecode_( const Mat& buf, int flags, Mat& mat ) { if (decoder->readData(mat)) success = true; + readMetadata(decoder, metadata_types, metadata); } catch (const cv::Exception& e) { @@ -1274,7 +1382,7 @@ Mat imdecode( InputArray _buf, int flags ) CV_TRACE_FUNCTION(); Mat buf = _buf.getMat(), img; - if (!imdecode_(buf, flags, img)) + if (!imdecode_(buf, flags, img, nullptr, noArray())) img.release(); return img; @@ -1286,12 +1394,24 @@ Mat imdecode( InputArray _buf, int flags, Mat* dst ) Mat buf = _buf.getMat(), img; dst = dst ? dst : &img; - if (imdecode_(buf, flags, *dst)) + if (imdecode_(buf, flags, *dst, nullptr, noArray())) return *dst; else return cv::Mat(); } +Mat imdecodeWithMetadata( InputArray _buf, std::vector& metadata_types, + OutputArrayOfArrays metadata, int flags ) +{ + CV_TRACE_FUNCTION(); + + Mat buf = _buf.getMat(), img; + if (!imdecode_(buf, flags, img, &metadata_types, metadata)) + img.release(); + + return img; +} + static bool imdecodemulti_(const Mat& buf, int flags, std::vector& mats, int start, int count) { @@ -1447,8 +1567,10 @@ bool imdecodemulti(InputArray _buf, int flags, CV_OUT std::vector& mats, co } } -bool imencode( const String& ext, InputArray _img, - std::vector& buf, const std::vector& params_ ) +bool imencodeWithMetadata( const String& ext, InputArray _img, + const std::vector& metadata_types, + InputArrayOfArrays metadata, + std::vector& buf, const std::vector& params_ ) { CV_TRACE_FUNCTION(); @@ -1517,6 +1639,7 @@ bool imencode( const String& ext, InputArray _img, code = encoder->setDestination(filename); CV_Assert( code ); } + addMetadata(encoder, metadata_types, metadata); try { if (!isMultiImg) @@ -1553,6 +1676,12 @@ bool imencode( const String& ext, InputArray _img, return code; } +bool imencode( const String& ext, InputArray img, + std::vector& buf, const std::vector& params_ ) +{ + return imencodeWithMetadata(ext, img, {}, noArray(), buf, params_); +} + bool imencodemulti( const String& ext, InputArrayOfArrays imgs, std::vector& buf, const std::vector& params) { diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index d1a9e720a967..1311f85a60f5 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -150,5 +150,508 @@ const std::vector exif_files INSTANTIATE_TEST_CASE_P(Imgcodecs, Exif, testing::ValuesIn(exif_files)); +enum ExifTagId +{ + TAG_EMPTY = 0, + TAG_SUB_FILETYPE = 254, + TAG_IMAGE_WIDTH = 256, + TAG_IMAGE_LENGTH = 257, + TAG_BITS_PER_SAMPLE = 258, + TAG_COMPRESSION = 259, + TAG_PHOTOMETRIC = 262, + TAG_IMAGE_DESCRIPTION = 270, + TAG_MAKE = 271, + TAG_MODEL = 272, + TAG_STRIP_OFFSET = 273, + TAG_SAMPLES_PER_PIXEL = 277, + TAG_ROWS_PER_STRIP = 278, + TAG_STRIP_BYTE_COUNTS = 279, + TAG_PLANAR_CONFIG = 284, + TAG_ORIENTATION = 274, + + TAG_XRESOLUTION = 282, + TAG_YRESOLUTION = 283, + TAG_RESOLUTION_UNIT = 296, + + TAG_SOFTWARE = 305, + TAG_MODIFY_DATE = 306, + + TAG_SAMPLE_FORMAT = 339, + + // DNG extension + TAG_CFA_REPEAT_PATTERN_DIM = 33421, + TAG_CFA_PATTERN = 33422, + + TAG_COPYRIGHT = 33432, + TAG_EXPOSURE_TIME = 33434, + TAG_FNUMBER = 33437, + + TAG_EXIF_TAGS = 34665, + TAG_ISOSPEED = 34855, + + TAG_EXIF_VERSION = 36864, + TAG_DATETIME_ORIGINAL = 36867, + TAG_DATETIME_CREATE = 36868, + + TAG_SHUTTER_SPEED = 37377, + TAG_APERTURE_VALUE = 37378, + TAG_FLASH = 37385, + TAG_FOCALLENGTH = 37386, + TAG_EP_STANDARD_ID = 37398, + + TAG_SUBSECTIME = 37520, + TAG_SUBSECTIME_ORIGINAL = 37521, + TAG_SUBSECTIME_DIGITIZED = 37522, + + TAG_EXIF_IMAGE_WIDTH = 40962, + TAG_EXIF_IMAGE_HEIGHT = 40963, + TAG_WHITE_BALANCE = 41987, +}; + +enum ExifTagType +{ + TAG_TYPE_NOTYPE = 0, + TAG_TYPE_BYTE = 1, + TAG_TYPE_ASCII = 2, // null-terminated string + TAG_TYPE_SHORT = 3, + TAG_TYPE_LONG = 4, + TAG_TYPE_RATIONAL = 5, // 64-bit unsigned fraction + TAG_TYPE_SBYTE = 6, + TAG_TYPE_UNDEFINED = 7, // 8-bit untyped data */ + TAG_TYPE_SSHORT = 8, + TAG_TYPE_SLONG = 9, + TAG_TYPE_SRATIONAL = 10, // 64-bit signed fraction + TAG_TYPE_FLOAT = 11, + TAG_TYPE_DOUBLE = 12, + TAG_TYPE_IFD = 13, // 32-bit unsigned integer (offset) + TAG_TYPE_LONG8 = 16, // BigTIFF 64-bit unsigned + TAG_TYPE_SLONG8 = 17, // BigTIFF 64-bit signed + TAG_TYPE_IFD8 = 18 // BigTIFF 64-bit unsigned integer (offset) +}; + +struct rational64_t +{ + int64_t num, denom; +}; + +struct ExifTag +{ + int id=0; + ExifTagType type=TAG_TYPE_NOTYPE; + std::string str; + rational64_t n={0, 1}; + std::vector v; + + bool empty() const { return id == 0; } + size_t nvalues() const; +}; + +constexpr size_t EXIF_HDR_SIZE = 8; // ('II' or 'MM'), (0x2A 0x00), (IFD0 offset: 4 bytes) +constexpr size_t IFD_ENTRY_SIZE = 12; +constexpr size_t IFD_MAX_INLINE_SIZE = 4; +constexpr size_t IFD_HDR_SIZE = 6; + +size_t tagTypeSize(ExifTagType type) +{ + return + type == TAG_TYPE_NOTYPE ? 0 : + type == TAG_TYPE_BYTE ? 1 : + type == TAG_TYPE_ASCII ? 1 : + type == TAG_TYPE_SHORT ? 2 : + type == TAG_TYPE_LONG ? 4 : + type == TAG_TYPE_RATIONAL ? 8 : + type == TAG_TYPE_SBYTE ? 1 : + type == TAG_TYPE_UNDEFINED ? 1 : + type == TAG_TYPE_SSHORT ? 2 : + type == TAG_TYPE_SLONG ? 4 : + type == TAG_TYPE_SRATIONAL ? 8 : + type == TAG_TYPE_FLOAT ? 4 : + type == TAG_TYPE_DOUBLE ? 8 : + type == TAG_TYPE_IFD ? 0 : + type == TAG_TYPE_LONG8 ? 8 : + type == TAG_TYPE_SLONG8 ? 8 : + type == TAG_TYPE_IFD8 ? 0 : 0; +} + +size_t ExifTag::nvalues() const +{ + return empty() ? 0u : + type == TAG_TYPE_ASCII || type == TAG_TYPE_UNDEFINED ? str.size() + (type == TAG_TYPE_ASCII) : + !v.empty() ? v.size() : 1u; +} + +size_t tagValueSize(ExifTagType type, size_t nvalues) +{ + size_t size = tagTypeSize(type)*nvalues; + return (size + 1u) & ~1u; +} + +static void pack1(std::vector& data, size_t& offset, uint8_t value) +{ + data.resize(std::max(data.size(), offset+1)); + data[offset++] = (char)value; +} + +static void pack2(std::vector& data, size_t& offset, + uint16_t value, bool bigendian_) +{ + size_t ofs = offset, bigendian = (size_t)bigendian_; + data.resize(std::max(data.size(), ofs+sizeof(uint16_t))); + uchar* ptr = data.data(); + ptr[ofs + bigendian] = (uchar)value; + ptr[ofs + 1 - bigendian] = (uchar)(value >> 8); + offset = ofs + sizeof(uint16_t); +} + +static void pack4(std::vector& data, size_t& offset, + uint32_t value, bool bigendian_) +{ + size_t ofs = offset, bigendian = (size_t)bigendian_; + data.resize(std::max(data.size(), ofs+sizeof(uint32_t))); + uchar* ptr = data.data(); + ptr[ofs+bigendian*3] = (uchar)value; + ptr[ofs+1+bigendian] = (uchar)(value >> 8); + ptr[ofs+2-bigendian] = (uchar)(value >> 16); + ptr[ofs+3-bigendian*3] = (uchar)(value >> 24); + offset = ofs + sizeof(uint32_t); +} + +static size_t computeIFDSize(const std::vector* ifds, + size_t nifds, size_t idx, size_t& values_size) +{ + CV_Assert(idx < nifds); + const std::vector& ifd = ifds[idx]; + size_t i, ntags = ifd.size(), size = IFD_HDR_SIZE + IFD_ENTRY_SIZE*ntags; + for (i = 0; i < ntags; i++) { + const ExifTag& tag = ifd[i]; + if (tag.type == TAG_TYPE_IFD) { + int64_t subifd_idx = tag.n.num; + CV_Assert_N(0 <= subifd_idx, (size_t)subifd_idx < nifds); + size += computeIFDSize(ifds, nifds, (size_t)subifd_idx, values_size); + } else { + size_t tag_values_size = tagValueSize(tag.type, tag.nvalues()); + if (tag_values_size > IFD_MAX_INLINE_SIZE) + values_size += tag_values_size; + } + } + return size; +} + +static void packIFD(const std::vector* ifds, size_t nifds, size_t idx, + std::vector& data, size_t& offset, + size_t& values_offset, bool bigendian) +{ + CV_Assert(idx < nifds); + const std::vector& ifd = ifds[idx]; + std::vector > subifds; + size_t ntags = ifd.size(); + + size_t subifd_offset0 = offset + IFD_HDR_SIZE + ntags*IFD_ENTRY_SIZE; + size_t subifd_offset = subifd_offset0; + pack2(data, offset, (uint16_t)ntags, bigendian); + + // first, pack the specified (by idx) IFD without subdirectories + for (const ExifTag& tag: ifd) { + pack2(data, offset, (uint16_t)tag.id, bigendian); + + ExifTagType type = tag.type == TAG_TYPE_IFD ? TAG_TYPE_LONG : tag.type; + pack2(data, offset, (uint16_t)type, bigendian); + size_t nvalues = tag.nvalues(); + + pack4(data, offset, (uint32_t)nvalues, bigendian); + if (tag.type == TAG_TYPE_IFD) { + int64_t sub_idx = tag.n.num; + CV_Assert_N(sub_idx >= 0, (size_t)sub_idx < nifds); + subifds.push_back({(size_t)sub_idx, subifd_offset}); + pack4(data, offset, (uint32_t)subifd_offset, bigendian); + const std::vector& subifd = ifds[sub_idx]; + size_t subifd_ntags = subifd.size(); + subifd_offset += IFD_HDR_SIZE + subifd_ntags*IFD_ENTRY_SIZE; + continue; + } + size_t tag_values_size = tagValueSize(type, nvalues); + int inline_values = tag_values_size <= 4u; + size_t tag_values_offset = inline_values ? offset : values_offset; + if (!inline_values) { + pack4(data, offset, (uint32_t)values_offset, bigendian); + data.resize(std::max(data.size(), tag_values_offset + tag_values_size)); + } else { + pack4(data, offset, 0u, bigendian); + } + + if (type == TAG_TYPE_ASCII || type == TAG_TYPE_UNDEFINED) { + size_t v_size = tag.str.size(); + memcpy(&data[tag_values_offset], tag.str.c_str(), v_size); + if (type == TAG_TYPE_ASCII) { + data[tag_values_offset + v_size] = '\0'; + v_size++; + } + if ((v_size & 1u) != 0) { + data[tag_values_offset + v_size] = '\0'; + v_size++; + } + tag_values_offset += v_size; + } else if (type == TAG_TYPE_RATIONAL || type == TAG_TYPE_SRATIONAL || + type == TAG_TYPE_BYTE || type == TAG_TYPE_SBYTE || + type == TAG_TYPE_SHORT || type == TAG_TYPE_SSHORT || + type == TAG_TYPE_LONG || type == TAG_TYPE_SLONG) { + const rational64_t* nptr = tag.v.empty() ? &tag.n : tag.v.data(); + int64_t minval = + type == TAG_TYPE_SBYTE ? INT8_MIN : + type == TAG_TYPE_SSHORT ? INT16_MIN : + type == TAG_TYPE_SLONG || type == TAG_TYPE_SRATIONAL ? INT32_MIN : 0; + int64_t maxval = + type == TAG_TYPE_BYTE || type == TAG_TYPE_UNDEFINED ? UINT8_MAX : + type == TAG_TYPE_SBYTE ? INT8_MAX : + type == TAG_TYPE_SHORT ? UINT16_MAX : + type == TAG_TYPE_SSHORT ? INT16_MAX : + type == TAG_TYPE_LONG ? UINT32_MAX : + type == TAG_TYPE_SLONG || type == TAG_TYPE_SRATIONAL ? INT32_MAX : INT64_MAX; + for (size_t i = 0; i < nvalues; i++) { + int64_t n = std::min(std::max(nptr[i].num, minval), maxval); + if (type == TAG_TYPE_RATIONAL || type == TAG_TYPE_SRATIONAL) { + int64_t d = std::min(std::max(nptr[i].denom, minval), maxval); + pack4(data, tag_values_offset, (uint32_t)n, bigendian); + pack4(data, tag_values_offset, (uint32_t)d, bigendian); + } + else if (type == TAG_TYPE_LONG || type == TAG_TYPE_SLONG) + pack4(data, tag_values_offset, (uint32_t)n, bigendian); + else if (type == TAG_TYPE_SHORT || type == TAG_TYPE_SSHORT) + pack2(data, tag_values_offset, (uint16_t)n, bigendian); + else + pack1(data, tag_values_offset, (uint8_t)n); + } + if ((type == TAG_TYPE_BYTE || type == TAG_TYPE_SBYTE) && (nvalues & 1) != 0) + pack1(data, tag_values_offset, (uint8_t)0); + } else { + CV_Error_(Error::StsBadArg, ("unsupported tag type %d", tag.type)); + } + + if (!inline_values) + values_offset = tag_values_offset; + } + + pack4(data, offset, 0u, bigendian); + + // now pack all sub-IFDs and the next one, if any + for (auto sub: subifds) { + size_t subofs = sub.second; + packIFD(ifds, nifds, sub.first, data, subofs, values_offset, bigendian); + } +} + +static bool packExif(const std::vector >& exif, + std::vector& data, bool bigendian) +{ + data.clear(); + size_t values_size = 0; + size_t ifd_size = computeIFDSize(exif.data(), exif.size(), 0u, values_size) + EXIF_HDR_SIZE; + data.resize(ifd_size + values_size); + + char signature = bigendian ? 'M' : 'I'; + size_t offset = 0; + pack1(data, offset, (uint8_t)signature); + pack1(data, offset, (uint8_t)signature); + pack2(data, offset, 42u, bigendian); + pack4(data, offset, 8u, bigendian); + + packIFD(exif.data(), exif.size(), 0u, data, offset, ifd_size, bigendian); + return true; +} + +static ExifTag exifInt(int id, ExifTagType type, int64_t v) +{ + ExifTag tag; + tag.id = id; + tag.type = type; + CV_Assert(type == TAG_TYPE_LONG || type == TAG_TYPE_SLONG || + type == TAG_TYPE_SHORT || type == TAG_TYPE_SSHORT || + type == TAG_TYPE_BYTE || type == TAG_TYPE_SBYTE); + tag.n.num = v; + tag.n.denom = 1; + return tag; +} + +static ExifTag exifStr(int id, ExifTagType type, const std::string& str) +{ + ExifTag tag; + tag.id = id; + CV_Assert(type == TAG_TYPE_ASCII || type == TAG_TYPE_UNDEFINED); + tag.type = type; + tag.str = str; + return tag; +} + +static rational64_t doubleToRational(double v, int maxbits) +{ + rational64_t r = {1, 0}; + if (std::isfinite(v)) { + int e = 0; + frexp(v, &e); + if (e >= maxbits) + return r; + + double iv = round(v); + if (iv == v) { + r.denom = 1; + r.num = (int64_t)iv; + } else { + r.denom = (int64_t)1 << (maxbits - std::max(e, 0)); + r.num = (int64_t)round(v*r.denom); + while ((r.denom & 1) == 0 && (r.num & 1) == 0) { + r.num >>= 1; + r.denom >>= 1; + } + } + } + return r; +} + +static ExifTag exifRatio(int id, ExifTagType type, double v) +{ + ExifTag tag; + tag.id = id; + CV_Assert(type == TAG_TYPE_RATIONAL || type == TAG_TYPE_SRATIONAL); + tag.type = type; + tag.n = doubleToRational(v, (type == TAG_TYPE_RATIONAL ? 31 : 30)); + return tag; +} + +static ExifTag exifIDF(int id, int idx) +{ + ExifTag tag; + tag.id = id; + tag.type = TAG_TYPE_IFD; + tag.n.num = idx; + tag.n.denom = 1; + return tag; +} + +static Mat makeCirclesImage(Size size, int type, int nbits) +{ + Mat img(size, type); + img.setTo(Scalar::all(0)); + RNG& rng = theRNG(); + int maxval = (int)(1 << nbits); + for (int i = 0; i < 100; i++) { + int x = rng.uniform(0, img.cols); + int y = rng.uniform(0, img.rows); + int radius = rng.uniform(5, std::min(img.cols, img.rows)/5); + int b = rng.uniform(0, maxval); + int g = rng.uniform(0, maxval); + int r = rng.uniform(0, maxval); + circle(img, Point(x, y), radius, Scalar(b, g, r), -1, LINE_AA); + } + return img; +} + +static std::vector > makeTestExif(Size imgsize, int nbits, int orientation=1) +{ + std::vector > exif = + { + { + exifInt(TAG_IMAGE_WIDTH, TAG_TYPE_LONG, imgsize.width), + exifInt(TAG_IMAGE_LENGTH, TAG_TYPE_LONG, imgsize.height), + exifInt(TAG_BITS_PER_SAMPLE, TAG_TYPE_SHORT, nbits), + exifInt(TAG_ORIENTATION, TAG_TYPE_SHORT, orientation), + exifStr(TAG_IMAGE_DESCRIPTION, TAG_TYPE_ASCII, format("Sample %d-bit image with metadata", nbits)), + exifStr(TAG_SOFTWARE, TAG_TYPE_ASCII, "OpenCV"), + exifRatio(TAG_XRESOLUTION, TAG_TYPE_RATIONAL, 72.), + exifRatio(TAG_YRESOLUTION, TAG_TYPE_RATIONAL, 72.), + exifInt(TAG_RESOLUTION_UNIT, TAG_TYPE_SHORT, 2), + exifIDF(TAG_EXIF_TAGS, 1) + }, + { + exifStr(TAG_EXIF_VERSION, TAG_TYPE_UNDEFINED, "0221"), + exifInt(TAG_EXIF_IMAGE_WIDTH, TAG_TYPE_LONG, imgsize.width), + exifInt(TAG_EXIF_IMAGE_HEIGHT, TAG_TYPE_LONG, imgsize.height) + } + }; + return exif; +} + +TEST(Imgcodecs_Avif, ReadWriteWithExif) +{ + int avif_nbits = 10; + int avif_speed = 10; + int avif_quality = 85; + int imgdepth = avif_nbits > 8 ? CV_16U : CV_8U; + int imgtype = CV_MAKETYPE(imgdepth, 3); + const string outputname = cv::tempfile(".avif"); + Mat img = makeCirclesImage(Size(1280, 720), imgtype, avif_nbits); + + std::vector metadata_types = {IMAGE_METADATA_EXIF}; + std::vector > metadata(1); + std::vector > exif = makeTestExif(img.size(), avif_nbits); + packExif(exif, metadata[0], true); + + std::vector write_params = { + IMWRITE_AVIF_DEPTH, avif_nbits, + IMWRITE_AVIF_SPEED, avif_speed, + IMWRITE_AVIF_QUALITY, avif_quality + }; + + imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params); + std::vector compressed; + imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params); + + std::vector read_metadata_types, read_metadata_types2; + std::vector > read_metadata, read_metadata2; + Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED); + Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, IMREAD_UNCHANGED); + EXPECT_EQ(img2.cols, img.cols); + EXPECT_EQ(img2.rows, img.rows); + EXPECT_EQ(img2.type(), imgtype); + EXPECT_EQ(read_metadata_types, read_metadata_types2); + EXPECT_GE(read_metadata_types.size(), 1u); + EXPECT_EQ(read_metadata, read_metadata2); + EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_EXIF); + EXPECT_EQ(read_metadata_types.size(), read_metadata.size()); + EXPECT_EQ(read_metadata[0], metadata[0]); + EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.); + double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols); + EXPECT_LT(mse, 1500); + remove(outputname.c_str()); +} + +TEST(Imgcodecs_Jpeg, ReadWriteWithExif) +{ + int jpeg_quality = 95; + int imgtype = CV_MAKETYPE(CV_8U, 3); + const string outputname = cv::tempfile(".jpeg"); + Mat img = makeCirclesImage(Size(1280, 720), imgtype, 8); + + std::vector metadata_types = {IMAGE_METADATA_EXIF}; + std::vector > metadata(1); + std::vector > exif = makeTestExif(img.size(), 8); + packExif(exif, metadata[0], true); + + std::vector write_params = { + IMWRITE_JPEG_QUALITY, jpeg_quality + }; + + imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params); + std::vector compressed; + imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params); + + std::vector read_metadata_types, read_metadata_types2; + std::vector > read_metadata, read_metadata2; + Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED); + Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, IMREAD_UNCHANGED); + EXPECT_EQ(img2.cols, img.cols); + EXPECT_EQ(img2.rows, img.rows); + EXPECT_EQ(img2.type(), imgtype); + EXPECT_EQ(read_metadata_types, read_metadata_types2); + EXPECT_GE(read_metadata_types.size(), 1u); + EXPECT_EQ(read_metadata, read_metadata2); + EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_EXIF); + EXPECT_EQ(read_metadata_types.size(), read_metadata.size()); + EXPECT_EQ(read_metadata[0], metadata[0]); + EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.); + double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols); + EXPECT_LT(mse, 80); + remove(outputname.c_str()); +} + } } From 032bcd519420c79ae50e47e1ff4db6673cc28778 Mon Sep 17 00:00:00 2001 From: Vadim Pisarevskiy Date: Tue, 1 Jul 2025 01:24:32 +0300 Subject: [PATCH 2/9] 1. fixed compile errors with old versions of libavif 2. fixed potential compile errors when opencv is built without avif --- modules/imgcodecs/src/grfmt_avif.cpp | 14 ++++++-------- modules/imgcodecs/test/test_exif.cpp | 2 ++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index acc8a8014473..d4b3def136f7 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -112,18 +112,16 @@ AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, int bit_dept result->yuvRange = AVIF_RANGE_FULL; } - avifResult status = AVIF_RESULT_OK; - if (!metadata.empty()) { const std::vector& metadata_exif = metadata[IMAGE_METADATA_EXIF]; const std::vector& metadata_xmp = metadata[IMAGE_METADATA_XMP]; const std::vector& metadata_iccp = metadata[IMAGE_METADATA_ICCP]; if (!metadata_exif.empty()) - status = avifImageSetMetadataExif(result, (const uint8_t*)metadata_exif.data(), metadata_exif.size()); - if (!metadata_exif.empty() && status == AVIF_RESULT_OK) - status = avifImageSetMetadataXMP(result, (const uint8_t*)metadata_xmp.data(), metadata_xmp.size()); - if (!metadata_iccp.empty() && status == AVIF_RESULT_OK) - status = avifImageSetProfileICC(result, (const uint8_t*)metadata_iccp.data(), metadata_iccp.size()); + avifImageSetMetadataExif(result, (const uint8_t*)metadata_exif.data(), metadata_exif.size()); + if (!metadata_exif.empty()) + avifImageSetMetadataXMP(result, (const uint8_t*)metadata_xmp.data(), metadata_xmp.size()); + if (!metadata_iccp.empty()) + avifImageSetProfileICC(result, (const uint8_t*)metadata_iccp.data(), metadata_iccp.size()); } avifRGBImage rgba; @@ -139,7 +137,7 @@ AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, int bit_dept rgba.pixels = const_cast(reinterpret_cast(img.data)); - if (status != AVIF_RESULT_OK || avifImageRGBToYUV(result, &rgba) != AVIF_RESULT_OK) { + if (avifImageRGBToYUV(result, &rgba) != AVIF_RESULT_OK) { avifImageDestroy(result); return nullptr; } diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index 1311f85a60f5..513a7b3f8ff5 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -570,6 +570,7 @@ static std::vector > makeTestExif(Size imgsize, int nbits, return exif; } +#ifdef HAVE_AVIF TEST(Imgcodecs_Avif, ReadWriteWithExif) { int avif_nbits = 10; @@ -613,6 +614,7 @@ TEST(Imgcodecs_Avif, ReadWriteWithExif) EXPECT_LT(mse, 1500); remove(outputname.c_str()); } +#endif // HAVE_AVIF TEST(Imgcodecs_Jpeg, ReadWriteWithExif) { From 923c5ea16f3d01ddaa95a801ca50563f69d11b33 Mon Sep 17 00:00:00 2001 From: Vadim Pisarevskiy Date: Tue, 1 Jul 2025 02:23:17 +0300 Subject: [PATCH 3/9] removed trailing whitespaces --- modules/imgcodecs/src/exif.hpp | 2 +- modules/imgcodecs/src/grfmt_avif.cpp | 2 +- modules/imgcodecs/src/grfmt_base.hpp | 6 ++--- modules/imgcodecs/src/grfmt_jpeg.cpp | 4 ++-- modules/imgcodecs/src/loadsave.cpp | 8 +++---- modules/imgcodecs/test/test_exif.cpp | 36 ++++++++++++++-------------- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/modules/imgcodecs/src/exif.hpp b/modules/imgcodecs/src/exif.hpp index 85cadfac228b..3c5fbc7fe88b 100644 --- a/modules/imgcodecs/src/exif.hpp +++ b/modules/imgcodecs/src/exif.hpp @@ -174,7 +174,7 @@ class ExifReader * @return ExifEntru_t structure. Caller has to know what tag it calls in order to extract proper field from the structure ExifEntry_t */ ExifEntry_t getTag( const ExifTagName tag ) const; - + /** * @brief Get the whole exif buffer */ diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index d4b3def136f7..8aed9b92740a 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -111,7 +111,7 @@ AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, int bit_dept result->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; result->yuvRange = AVIF_RANGE_FULL; } - + if (!metadata.empty()) { const std::vector& metadata_exif = metadata[IMAGE_METADATA_EXIF]; const std::vector& metadata_xmp = metadata[IMAGE_METADATA_XMP]; diff --git a/modules/imgcodecs/src/grfmt_base.hpp b/modules/imgcodecs/src/grfmt_base.hpp index ff7d14cb142c..369906b54952 100644 --- a/modules/imgcodecs/src/grfmt_base.hpp +++ b/modules/imgcodecs/src/grfmt_base.hpp @@ -68,13 +68,13 @@ class BaseImageDecoder { * @return The type of the image. */ virtual int type() const { return m_type; } - + /** * @brief Checks whether file contains metadata of the certain type. * @param type The type of metadata to look for */ virtual bool haveMetadata(ImageMetadataType type) const; - + /** * @brief Retrieves metadata (if any) of the certain kind. * If there is no such metadata, the method returns empty array. @@ -218,7 +218,7 @@ class BaseImageEncoder { * @return true if the destination was successfully set, false otherwise. */ virtual bool setDestination(std::vector& buf); - + /** * @brief Sets the metadata to write together with the image data * @param type The type of metadata to add diff --git a/modules/imgcodecs/src/grfmt_jpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg.cpp index 9a0e8193bdd6..be3b2cbd6124 100644 --- a/modules/imgcodecs/src/grfmt_jpeg.cpp +++ b/modules/imgcodecs/src/grfmt_jpeg.cpp @@ -814,7 +814,7 @@ bool JpegEncoder::write( const Mat& img, const std::vector& params ) } jpeg_start_compress( &cinfo, TRUE ); - + if (!m_metadata.empty()) { const std::vector& metadata_exif = m_metadata[IMAGE_METADATA_EXIF]; size_t exif_size = metadata_exif.size(); @@ -822,7 +822,7 @@ bool JpegEncoder::write( const Mat& img, const std::vector& params ) const char app1_exif_prefix[] = {'E', 'x', 'i', 'f', '\0', '\0'}; size_t app1_exif_prefix_size = sizeof(app1_exif_prefix); size_t data_size = exif_size + app1_exif_prefix_size; - + std::vector metadata_app1(data_size); uchar* data = metadata_app1.data(); memcpy(data, app1_exif_prefix, app1_exif_prefix_size); diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 0190bf9f16a2..20d38eb1c17e 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -480,7 +480,7 @@ imread_( const String& filename, int flags, OutputArray mat, { /// Search for the relevant decoder to handle the imagery ImageDecoder decoder; - + if (metadata_types) metadata_types->clear(); @@ -569,7 +569,7 @@ imread_( const String& filename, int flags, OutputArray mat, CV_CheckTrue(original_ptr == real_mat.data, "Internal imread issue"); success = true; } - + readMetadata(decoder, metadata_types, metadata); } catch (const cv::Exception& e) @@ -1063,7 +1063,7 @@ static bool imwrite_( const String& filename, const std::vector& img_vec, encoder->setDestination( filename ); addMetadata(encoder, metadata_types, metadata); - + #if CV_VERSION_MAJOR < 5 && defined(HAVE_IMGCODEC_HDR) bool fixed = false; std::vector params_pair(2); @@ -1248,7 +1248,7 @@ imdecode_( const Mat& buf, int flags, Mat& mat, { if (metadata_types) metadata_types->clear(); - + CV_Assert(!buf.empty()); CV_Assert(buf.isContinuous()); CV_Assert(buf.checkVector(1, CV_8U) > 0); diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index 513a7b3f8ff5..2546a4773c0d 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -241,7 +241,7 @@ struct ExifTag std::string str; rational64_t n={0, 1}; std::vector v; - + bool empty() const { return id == 0; } size_t nvalues() const; }; @@ -345,19 +345,19 @@ static void packIFD(const std::vector* ifds, size_t nifds, size_t idx, const std::vector& ifd = ifds[idx]; std::vector > subifds; size_t ntags = ifd.size(); - + size_t subifd_offset0 = offset + IFD_HDR_SIZE + ntags*IFD_ENTRY_SIZE; size_t subifd_offset = subifd_offset0; pack2(data, offset, (uint16_t)ntags, bigendian); - + // first, pack the specified (by idx) IFD without subdirectories for (const ExifTag& tag: ifd) { pack2(data, offset, (uint16_t)tag.id, bigendian); - + ExifTagType type = tag.type == TAG_TYPE_IFD ? TAG_TYPE_LONG : tag.type; pack2(data, offset, (uint16_t)type, bigendian); size_t nvalues = tag.nvalues(); - + pack4(data, offset, (uint32_t)nvalues, bigendian); if (tag.type == TAG_TYPE_IFD) { int64_t sub_idx = tag.n.num; @@ -378,7 +378,7 @@ static void packIFD(const std::vector* ifds, size_t nifds, size_t idx, } else { pack4(data, offset, 0u, bigendian); } - + if (type == TAG_TYPE_ASCII || type == TAG_TYPE_UNDEFINED) { size_t v_size = tag.str.size(); memcpy(&data[tag_values_offset], tag.str.c_str(), v_size); @@ -426,13 +426,13 @@ static void packIFD(const std::vector* ifds, size_t nifds, size_t idx, } else { CV_Error_(Error::StsBadArg, ("unsupported tag type %d", tag.type)); } - + if (!inline_values) values_offset = tag_values_offset; } - + pack4(data, offset, 0u, bigendian); - + // now pack all sub-IFDs and the next one, if any for (auto sub: subifds) { size_t subofs = sub.second; @@ -447,14 +447,14 @@ static bool packExif(const std::vector >& exif, size_t values_size = 0; size_t ifd_size = computeIFDSize(exif.data(), exif.size(), 0u, values_size) + EXIF_HDR_SIZE; data.resize(ifd_size + values_size); - + char signature = bigendian ? 'M' : 'I'; size_t offset = 0; pack1(data, offset, (uint8_t)signature); pack1(data, offset, (uint8_t)signature); pack2(data, offset, 42u, bigendian); pack4(data, offset, 8u, bigendian); - + packIFD(exif.data(), exif.size(), 0u, data, offset, ifd_size, bigendian); return true; } @@ -580,22 +580,22 @@ TEST(Imgcodecs_Avif, ReadWriteWithExif) int imgtype = CV_MAKETYPE(imgdepth, 3); const string outputname = cv::tempfile(".avif"); Mat img = makeCirclesImage(Size(1280, 720), imgtype, avif_nbits); - + std::vector metadata_types = {IMAGE_METADATA_EXIF}; std::vector > metadata(1); std::vector > exif = makeTestExif(img.size(), avif_nbits); packExif(exif, metadata[0], true); - + std::vector write_params = { IMWRITE_AVIF_DEPTH, avif_nbits, IMWRITE_AVIF_SPEED, avif_speed, IMWRITE_AVIF_QUALITY, avif_quality }; - + imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params); std::vector compressed; imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params); - + std::vector read_metadata_types, read_metadata_types2; std::vector > read_metadata, read_metadata2; Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED); @@ -622,16 +622,16 @@ TEST(Imgcodecs_Jpeg, ReadWriteWithExif) int imgtype = CV_MAKETYPE(CV_8U, 3); const string outputname = cv::tempfile(".jpeg"); Mat img = makeCirclesImage(Size(1280, 720), imgtype, 8); - + std::vector metadata_types = {IMAGE_METADATA_EXIF}; std::vector > metadata(1); std::vector > exif = makeTestExif(img.size(), 8); packExif(exif, metadata[0], true); - + std::vector write_params = { IMWRITE_JPEG_QUALITY, jpeg_quality }; - + imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params); std::vector compressed; imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params); From 44786718aa1f49b10640e184b90cbba0057ff6e2 Mon Sep 17 00:00:00 2001 From: Vadim Pisarevskiy Date: Tue, 1 Jul 2025 12:50:45 +0300 Subject: [PATCH 4/9] 1. removed unnecessary exif packing functionality from the tests (to be added later into opencv itself) 2. print warning if certain encoders don't support metadata ('yet' or 'at all'). 3. added sanity check for jpg, png and avif that exif is read properly. --- modules/imgcodecs/src/grfmt_avif.cpp | 4 + modules/imgcodecs/src/grfmt_base.cpp | 7 +- modules/imgcodecs/src/grfmt_base.hpp | 1 + modules/imgcodecs/src/grfmt_jpeg.cpp | 2 + modules/imgcodecs/src/loadsave.cpp | 16 +- modules/imgcodecs/test/test_exif.cpp | 490 +++++---------------------- 6 files changed, 110 insertions(+), 410 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index 8aed9b92740a..600f673fb4bd 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -299,6 +299,10 @@ bool AvifDecoder::nextPage() { AvifEncoder::AvifEncoder() { m_description = "AVIF files (*.avif)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX + 1, false); + m_support_metadata[(size_t)IMAGE_METADATA_EXIF] = true; + m_support_metadata[(size_t)IMAGE_METADATA_XMP] = true; + m_support_metadata[(size_t)IMAGE_METADATA_ICCP] = true; encoder_ = avifEncoderCreate(); } diff --git a/modules/imgcodecs/src/grfmt_base.cpp b/modules/imgcodecs/src/grfmt_base.cpp index d3e75ba792fa..1241edb0774e 100644 --- a/modules/imgcodecs/src/grfmt_base.cpp +++ b/modules/imgcodecs/src/grfmt_base.cpp @@ -164,12 +164,15 @@ bool BaseImageEncoder::addMetadata(ImageMetadataType type, const Mat& metadata) CV_Assert_N(type >= IMAGE_METADATA_EXIF, type <= IMAGE_METADATA_MAX); if (metadata.empty()) return true; + size_t itype = (size_t)type; + if (itype >= m_support_metadata.size() || !m_support_metadata[itype]) + return false; if (m_metadata.empty()) - m_metadata.resize((int)IMAGE_METADATA_MAX+1); + m_metadata.resize((size_t)IMAGE_METADATA_MAX+1); CV_Assert(metadata.elemSize() == 1); CV_Assert(metadata.isContinuous()); const unsigned char* data = metadata.ptr(); - m_metadata[(int)type].assign(data, data + metadata.total()); + m_metadata[itype].assign(data, data + metadata.total()); return true; } diff --git a/modules/imgcodecs/src/grfmt_base.hpp b/modules/imgcodecs/src/grfmt_base.hpp index 369906b54952..2eeb2fc13091 100644 --- a/modules/imgcodecs/src/grfmt_base.hpp +++ b/modules/imgcodecs/src/grfmt_base.hpp @@ -265,6 +265,7 @@ class BaseImageEncoder { protected: std::vector > m_metadata; // see IMAGE_METADATA_... + std::vector m_support_metadata; String m_description; ///< Description of the encoder (e.g., format name, capabilities). String m_filename; ///< Destination file name for encoded data. std::vector* m_buf; ///< Pointer to the buffer for encoded data if using memory-based destination. diff --git a/modules/imgcodecs/src/grfmt_jpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg.cpp index be3b2cbd6124..9b2ab59b2b83 100644 --- a/modules/imgcodecs/src/grfmt_jpeg.cpp +++ b/modules/imgcodecs/src/grfmt_jpeg.cpp @@ -600,6 +600,8 @@ JpegEncoder::JpegEncoder() { m_description = "JPEG files (*.jpeg;*.jpg;*.jpe)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX + 1, false); + m_support_metadata[(size_t)IMAGE_METADATA_EXIF] = true; } diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 20d38eb1c17e..0c849b7356f0 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -455,6 +455,13 @@ static void readMetadata(ImageDecoder& decoder, } } +static const char* metadataTypeToString(ImageMetadataType type) +{ + return type == IMAGE_METADATA_EXIF ? "Exif" : + type == IMAGE_METADATA_XMP ? "XMP" : + type == IMAGE_METADATA_ICCP ? "ICC Profile" : "???"; +} + static void addMetadata(ImageEncoder& encoder, const std::vector& metadata_types, InputArrayOfArrays metadata) @@ -462,7 +469,14 @@ static void addMetadata(ImageEncoder& encoder, size_t nmetadata_chunks = metadata_types.size(); for (size_t i = 0; i < nmetadata_chunks; i++) { ImageMetadataType metadata_type = (ImageMetadataType)metadata_types[i]; - encoder->addMetadata(metadata_type, metadata.getMat((int)i)); + bool ok = encoder->addMetadata(metadata_type, metadata.getMat((int)i)); + if (!ok) { + std::string desc = encoder->getDescription(); + CV_LOG_WARNING(NULL, "Imgcodecs: metadata of type '" + << metadataTypeToString(metadata_type) + << "' is not supported when encoding '" + << desc << "'"); + } } } diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index 2546a4773c0d..b4f607ce571c 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -148,384 +148,7 @@ const std::vector exif_files }; INSTANTIATE_TEST_CASE_P(Imgcodecs, Exif, - testing::ValuesIn(exif_files)); - -enum ExifTagId -{ - TAG_EMPTY = 0, - TAG_SUB_FILETYPE = 254, - TAG_IMAGE_WIDTH = 256, - TAG_IMAGE_LENGTH = 257, - TAG_BITS_PER_SAMPLE = 258, - TAG_COMPRESSION = 259, - TAG_PHOTOMETRIC = 262, - TAG_IMAGE_DESCRIPTION = 270, - TAG_MAKE = 271, - TAG_MODEL = 272, - TAG_STRIP_OFFSET = 273, - TAG_SAMPLES_PER_PIXEL = 277, - TAG_ROWS_PER_STRIP = 278, - TAG_STRIP_BYTE_COUNTS = 279, - TAG_PLANAR_CONFIG = 284, - TAG_ORIENTATION = 274, - - TAG_XRESOLUTION = 282, - TAG_YRESOLUTION = 283, - TAG_RESOLUTION_UNIT = 296, - - TAG_SOFTWARE = 305, - TAG_MODIFY_DATE = 306, - - TAG_SAMPLE_FORMAT = 339, - - // DNG extension - TAG_CFA_REPEAT_PATTERN_DIM = 33421, - TAG_CFA_PATTERN = 33422, - - TAG_COPYRIGHT = 33432, - TAG_EXPOSURE_TIME = 33434, - TAG_FNUMBER = 33437, - - TAG_EXIF_TAGS = 34665, - TAG_ISOSPEED = 34855, - - TAG_EXIF_VERSION = 36864, - TAG_DATETIME_ORIGINAL = 36867, - TAG_DATETIME_CREATE = 36868, - - TAG_SHUTTER_SPEED = 37377, - TAG_APERTURE_VALUE = 37378, - TAG_FLASH = 37385, - TAG_FOCALLENGTH = 37386, - TAG_EP_STANDARD_ID = 37398, - - TAG_SUBSECTIME = 37520, - TAG_SUBSECTIME_ORIGINAL = 37521, - TAG_SUBSECTIME_DIGITIZED = 37522, - - TAG_EXIF_IMAGE_WIDTH = 40962, - TAG_EXIF_IMAGE_HEIGHT = 40963, - TAG_WHITE_BALANCE = 41987, -}; - -enum ExifTagType -{ - TAG_TYPE_NOTYPE = 0, - TAG_TYPE_BYTE = 1, - TAG_TYPE_ASCII = 2, // null-terminated string - TAG_TYPE_SHORT = 3, - TAG_TYPE_LONG = 4, - TAG_TYPE_RATIONAL = 5, // 64-bit unsigned fraction - TAG_TYPE_SBYTE = 6, - TAG_TYPE_UNDEFINED = 7, // 8-bit untyped data */ - TAG_TYPE_SSHORT = 8, - TAG_TYPE_SLONG = 9, - TAG_TYPE_SRATIONAL = 10, // 64-bit signed fraction - TAG_TYPE_FLOAT = 11, - TAG_TYPE_DOUBLE = 12, - TAG_TYPE_IFD = 13, // 32-bit unsigned integer (offset) - TAG_TYPE_LONG8 = 16, // BigTIFF 64-bit unsigned - TAG_TYPE_SLONG8 = 17, // BigTIFF 64-bit signed - TAG_TYPE_IFD8 = 18 // BigTIFF 64-bit unsigned integer (offset) -}; - -struct rational64_t -{ - int64_t num, denom; -}; - -struct ExifTag -{ - int id=0; - ExifTagType type=TAG_TYPE_NOTYPE; - std::string str; - rational64_t n={0, 1}; - std::vector v; - - bool empty() const { return id == 0; } - size_t nvalues() const; -}; - -constexpr size_t EXIF_HDR_SIZE = 8; // ('II' or 'MM'), (0x2A 0x00), (IFD0 offset: 4 bytes) -constexpr size_t IFD_ENTRY_SIZE = 12; -constexpr size_t IFD_MAX_INLINE_SIZE = 4; -constexpr size_t IFD_HDR_SIZE = 6; - -size_t tagTypeSize(ExifTagType type) -{ - return - type == TAG_TYPE_NOTYPE ? 0 : - type == TAG_TYPE_BYTE ? 1 : - type == TAG_TYPE_ASCII ? 1 : - type == TAG_TYPE_SHORT ? 2 : - type == TAG_TYPE_LONG ? 4 : - type == TAG_TYPE_RATIONAL ? 8 : - type == TAG_TYPE_SBYTE ? 1 : - type == TAG_TYPE_UNDEFINED ? 1 : - type == TAG_TYPE_SSHORT ? 2 : - type == TAG_TYPE_SLONG ? 4 : - type == TAG_TYPE_SRATIONAL ? 8 : - type == TAG_TYPE_FLOAT ? 4 : - type == TAG_TYPE_DOUBLE ? 8 : - type == TAG_TYPE_IFD ? 0 : - type == TAG_TYPE_LONG8 ? 8 : - type == TAG_TYPE_SLONG8 ? 8 : - type == TAG_TYPE_IFD8 ? 0 : 0; -} - -size_t ExifTag::nvalues() const -{ - return empty() ? 0u : - type == TAG_TYPE_ASCII || type == TAG_TYPE_UNDEFINED ? str.size() + (type == TAG_TYPE_ASCII) : - !v.empty() ? v.size() : 1u; -} - -size_t tagValueSize(ExifTagType type, size_t nvalues) -{ - size_t size = tagTypeSize(type)*nvalues; - return (size + 1u) & ~1u; -} - -static void pack1(std::vector& data, size_t& offset, uint8_t value) -{ - data.resize(std::max(data.size(), offset+1)); - data[offset++] = (char)value; -} - -static void pack2(std::vector& data, size_t& offset, - uint16_t value, bool bigendian_) -{ - size_t ofs = offset, bigendian = (size_t)bigendian_; - data.resize(std::max(data.size(), ofs+sizeof(uint16_t))); - uchar* ptr = data.data(); - ptr[ofs + bigendian] = (uchar)value; - ptr[ofs + 1 - bigendian] = (uchar)(value >> 8); - offset = ofs + sizeof(uint16_t); -} - -static void pack4(std::vector& data, size_t& offset, - uint32_t value, bool bigendian_) -{ - size_t ofs = offset, bigendian = (size_t)bigendian_; - data.resize(std::max(data.size(), ofs+sizeof(uint32_t))); - uchar* ptr = data.data(); - ptr[ofs+bigendian*3] = (uchar)value; - ptr[ofs+1+bigendian] = (uchar)(value >> 8); - ptr[ofs+2-bigendian] = (uchar)(value >> 16); - ptr[ofs+3-bigendian*3] = (uchar)(value >> 24); - offset = ofs + sizeof(uint32_t); -} - -static size_t computeIFDSize(const std::vector* ifds, - size_t nifds, size_t idx, size_t& values_size) -{ - CV_Assert(idx < nifds); - const std::vector& ifd = ifds[idx]; - size_t i, ntags = ifd.size(), size = IFD_HDR_SIZE + IFD_ENTRY_SIZE*ntags; - for (i = 0; i < ntags; i++) { - const ExifTag& tag = ifd[i]; - if (tag.type == TAG_TYPE_IFD) { - int64_t subifd_idx = tag.n.num; - CV_Assert_N(0 <= subifd_idx, (size_t)subifd_idx < nifds); - size += computeIFDSize(ifds, nifds, (size_t)subifd_idx, values_size); - } else { - size_t tag_values_size = tagValueSize(tag.type, tag.nvalues()); - if (tag_values_size > IFD_MAX_INLINE_SIZE) - values_size += tag_values_size; - } - } - return size; -} - -static void packIFD(const std::vector* ifds, size_t nifds, size_t idx, - std::vector& data, size_t& offset, - size_t& values_offset, bool bigendian) -{ - CV_Assert(idx < nifds); - const std::vector& ifd = ifds[idx]; - std::vector > subifds; - size_t ntags = ifd.size(); - - size_t subifd_offset0 = offset + IFD_HDR_SIZE + ntags*IFD_ENTRY_SIZE; - size_t subifd_offset = subifd_offset0; - pack2(data, offset, (uint16_t)ntags, bigendian); - - // first, pack the specified (by idx) IFD without subdirectories - for (const ExifTag& tag: ifd) { - pack2(data, offset, (uint16_t)tag.id, bigendian); - - ExifTagType type = tag.type == TAG_TYPE_IFD ? TAG_TYPE_LONG : tag.type; - pack2(data, offset, (uint16_t)type, bigendian); - size_t nvalues = tag.nvalues(); - - pack4(data, offset, (uint32_t)nvalues, bigendian); - if (tag.type == TAG_TYPE_IFD) { - int64_t sub_idx = tag.n.num; - CV_Assert_N(sub_idx >= 0, (size_t)sub_idx < nifds); - subifds.push_back({(size_t)sub_idx, subifd_offset}); - pack4(data, offset, (uint32_t)subifd_offset, bigendian); - const std::vector& subifd = ifds[sub_idx]; - size_t subifd_ntags = subifd.size(); - subifd_offset += IFD_HDR_SIZE + subifd_ntags*IFD_ENTRY_SIZE; - continue; - } - size_t tag_values_size = tagValueSize(type, nvalues); - int inline_values = tag_values_size <= 4u; - size_t tag_values_offset = inline_values ? offset : values_offset; - if (!inline_values) { - pack4(data, offset, (uint32_t)values_offset, bigendian); - data.resize(std::max(data.size(), tag_values_offset + tag_values_size)); - } else { - pack4(data, offset, 0u, bigendian); - } - - if (type == TAG_TYPE_ASCII || type == TAG_TYPE_UNDEFINED) { - size_t v_size = tag.str.size(); - memcpy(&data[tag_values_offset], tag.str.c_str(), v_size); - if (type == TAG_TYPE_ASCII) { - data[tag_values_offset + v_size] = '\0'; - v_size++; - } - if ((v_size & 1u) != 0) { - data[tag_values_offset + v_size] = '\0'; - v_size++; - } - tag_values_offset += v_size; - } else if (type == TAG_TYPE_RATIONAL || type == TAG_TYPE_SRATIONAL || - type == TAG_TYPE_BYTE || type == TAG_TYPE_SBYTE || - type == TAG_TYPE_SHORT || type == TAG_TYPE_SSHORT || - type == TAG_TYPE_LONG || type == TAG_TYPE_SLONG) { - const rational64_t* nptr = tag.v.empty() ? &tag.n : tag.v.data(); - int64_t minval = - type == TAG_TYPE_SBYTE ? INT8_MIN : - type == TAG_TYPE_SSHORT ? INT16_MIN : - type == TAG_TYPE_SLONG || type == TAG_TYPE_SRATIONAL ? INT32_MIN : 0; - int64_t maxval = - type == TAG_TYPE_BYTE || type == TAG_TYPE_UNDEFINED ? UINT8_MAX : - type == TAG_TYPE_SBYTE ? INT8_MAX : - type == TAG_TYPE_SHORT ? UINT16_MAX : - type == TAG_TYPE_SSHORT ? INT16_MAX : - type == TAG_TYPE_LONG ? UINT32_MAX : - type == TAG_TYPE_SLONG || type == TAG_TYPE_SRATIONAL ? INT32_MAX : INT64_MAX; - for (size_t i = 0; i < nvalues; i++) { - int64_t n = std::min(std::max(nptr[i].num, minval), maxval); - if (type == TAG_TYPE_RATIONAL || type == TAG_TYPE_SRATIONAL) { - int64_t d = std::min(std::max(nptr[i].denom, minval), maxval); - pack4(data, tag_values_offset, (uint32_t)n, bigendian); - pack4(data, tag_values_offset, (uint32_t)d, bigendian); - } - else if (type == TAG_TYPE_LONG || type == TAG_TYPE_SLONG) - pack4(data, tag_values_offset, (uint32_t)n, bigendian); - else if (type == TAG_TYPE_SHORT || type == TAG_TYPE_SSHORT) - pack2(data, tag_values_offset, (uint16_t)n, bigendian); - else - pack1(data, tag_values_offset, (uint8_t)n); - } - if ((type == TAG_TYPE_BYTE || type == TAG_TYPE_SBYTE) && (nvalues & 1) != 0) - pack1(data, tag_values_offset, (uint8_t)0); - } else { - CV_Error_(Error::StsBadArg, ("unsupported tag type %d", tag.type)); - } - - if (!inline_values) - values_offset = tag_values_offset; - } - - pack4(data, offset, 0u, bigendian); - - // now pack all sub-IFDs and the next one, if any - for (auto sub: subifds) { - size_t subofs = sub.second; - packIFD(ifds, nifds, sub.first, data, subofs, values_offset, bigendian); - } -} - -static bool packExif(const std::vector >& exif, - std::vector& data, bool bigendian) -{ - data.clear(); - size_t values_size = 0; - size_t ifd_size = computeIFDSize(exif.data(), exif.size(), 0u, values_size) + EXIF_HDR_SIZE; - data.resize(ifd_size + values_size); - - char signature = bigendian ? 'M' : 'I'; - size_t offset = 0; - pack1(data, offset, (uint8_t)signature); - pack1(data, offset, (uint8_t)signature); - pack2(data, offset, 42u, bigendian); - pack4(data, offset, 8u, bigendian); - - packIFD(exif.data(), exif.size(), 0u, data, offset, ifd_size, bigendian); - return true; -} - -static ExifTag exifInt(int id, ExifTagType type, int64_t v) -{ - ExifTag tag; - tag.id = id; - tag.type = type; - CV_Assert(type == TAG_TYPE_LONG || type == TAG_TYPE_SLONG || - type == TAG_TYPE_SHORT || type == TAG_TYPE_SSHORT || - type == TAG_TYPE_BYTE || type == TAG_TYPE_SBYTE); - tag.n.num = v; - tag.n.denom = 1; - return tag; -} - -static ExifTag exifStr(int id, ExifTagType type, const std::string& str) -{ - ExifTag tag; - tag.id = id; - CV_Assert(type == TAG_TYPE_ASCII || type == TAG_TYPE_UNDEFINED); - tag.type = type; - tag.str = str; - return tag; -} - -static rational64_t doubleToRational(double v, int maxbits) -{ - rational64_t r = {1, 0}; - if (std::isfinite(v)) { - int e = 0; - frexp(v, &e); - if (e >= maxbits) - return r; - - double iv = round(v); - if (iv == v) { - r.denom = 1; - r.num = (int64_t)iv; - } else { - r.denom = (int64_t)1 << (maxbits - std::max(e, 0)); - r.num = (int64_t)round(v*r.denom); - while ((r.denom & 1) == 0 && (r.num & 1) == 0) { - r.num >>= 1; - r.denom >>= 1; - } - } - } - return r; -} - -static ExifTag exifRatio(int id, ExifTagType type, double v) -{ - ExifTag tag; - tag.id = id; - CV_Assert(type == TAG_TYPE_RATIONAL || type == TAG_TYPE_SRATIONAL); - tag.type = type; - tag.n = doubleToRational(v, (type == TAG_TYPE_RATIONAL ? 31 : 30)); - return tag; -} - -static ExifTag exifIDF(int id, int idx) -{ - ExifTag tag; - tag.id = id; - tag.type = TAG_TYPE_IFD; - tag.n.num = idx; - tag.n.denom = 1; - return tag; -} + testing::ValuesIn(exif_files)); static Mat makeCirclesImage(Size size, int type, int nbits) { @@ -545,34 +168,24 @@ static Mat makeCirclesImage(Size size, int type, int nbits) return img; } -static std::vector > makeTestExif(Size imgsize, int nbits, int orientation=1) -{ - std::vector > exif = - { - { - exifInt(TAG_IMAGE_WIDTH, TAG_TYPE_LONG, imgsize.width), - exifInt(TAG_IMAGE_LENGTH, TAG_TYPE_LONG, imgsize.height), - exifInt(TAG_BITS_PER_SAMPLE, TAG_TYPE_SHORT, nbits), - exifInt(TAG_ORIENTATION, TAG_TYPE_SHORT, orientation), - exifStr(TAG_IMAGE_DESCRIPTION, TAG_TYPE_ASCII, format("Sample %d-bit image with metadata", nbits)), - exifStr(TAG_SOFTWARE, TAG_TYPE_ASCII, "OpenCV"), - exifRatio(TAG_XRESOLUTION, TAG_TYPE_RATIONAL, 72.), - exifRatio(TAG_YRESOLUTION, TAG_TYPE_RATIONAL, 72.), - exifInt(TAG_RESOLUTION_UNIT, TAG_TYPE_SHORT, 2), - exifIDF(TAG_EXIF_TAGS, 1) - }, - { - exifStr(TAG_EXIF_VERSION, TAG_TYPE_UNDEFINED, "0221"), - exifInt(TAG_EXIF_IMAGE_WIDTH, TAG_TYPE_LONG, imgsize.width), - exifInt(TAG_EXIF_IMAGE_HEIGHT, TAG_TYPE_LONG, imgsize.height) - } - }; - return exif; -} - #ifdef HAVE_AVIF TEST(Imgcodecs_Avif, ReadWriteWithExif) { + static const uchar exif_data[] = { + 'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5, + 0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1, + 0, 10, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0, + 0, '"', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26, + 0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, + 226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0, + 1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2', + '2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0, + 0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '1', '0', + '-', 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', + 'm', 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0, + 0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1 + }; + int avif_nbits = 10; int avif_speed = 10; int avif_quality = 85; @@ -583,8 +196,7 @@ TEST(Imgcodecs_Avif, ReadWriteWithExif) std::vector metadata_types = {IMAGE_METADATA_EXIF}; std::vector > metadata(1); - std::vector > exif = makeTestExif(img.size(), avif_nbits); - packExif(exif, metadata[0], true); + metadata[0].assign(exif_data, exif_data + sizeof(exif_data)); std::vector write_params = { IMWRITE_AVIF_DEPTH, avif_nbits, @@ -618,6 +230,21 @@ TEST(Imgcodecs_Avif, ReadWriteWithExif) TEST(Imgcodecs_Jpeg, ReadWriteWithExif) { + static const uchar exif_data[] = { + 'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5, + 0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1, + 0, 8, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0, + 0, '!', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26, + 0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, + 226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0, + 1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2', + '2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0, + 0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '8', '-', + 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'm', + 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0, + 0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1 + }; + int jpeg_quality = 95; int imgtype = CV_MAKETYPE(CV_8U, 3); const string outputname = cv::tempfile(".jpeg"); @@ -625,8 +252,7 @@ TEST(Imgcodecs_Jpeg, ReadWriteWithExif) std::vector metadata_types = {IMAGE_METADATA_EXIF}; std::vector > metadata(1); - std::vector > exif = makeTestExif(img.size(), 8); - packExif(exif, metadata[0], true); + metadata[0].assign(exif_data, exif_data + sizeof(exif_data)); std::vector write_params = { IMWRITE_JPEG_QUALITY, jpeg_quality @@ -655,5 +281,55 @@ TEST(Imgcodecs_Jpeg, ReadWriteWithExif) remove(outputname.c_str()); } +static size_t locateString(const std::vector& exif, const std::string& pattern) +{ + size_t plen = pattern.size(), size = exif.size(); + for (size_t i = 0; i + plen <= size; i++) { + if (exif[i] == pattern[0] && memcmp(&exif[i], pattern.c_str(), plen) == 0) + return i; + } + return 0xFFFFFFFFu; } + +typedef std::tuple ReadExif_Sanity_Params; +typedef testing::TestWithParam ReadExif_Sanity; + +TEST_P(ReadExif_Sanity, Check) +{ + std::string filename = get<0>(GetParam()); + size_t exif_size = get<1>(GetParam()); + std::string pattern = get<2>(GetParam()); + size_t ploc = get<3>(GetParam()); + + const string root = cvtest::TS::ptr()->get_data_path(); + filename = root + filename; + + std::vector metadata_types; + std::vector > metadata; + Mat img = imreadWithMetadata(filename, metadata_types, metadata, 1); + + EXPECT_EQ(img.type(), CV_8UC3); + ASSERT_GE(metadata_types.size(), 1u); + EXPECT_EQ(metadata_types.size(), metadata.size()); + const std::vector& exif = metadata[IMAGE_METADATA_EXIF]; + EXPECT_EQ(exif.size(), exif_size); + EXPECT_EQ(locateString(exif, pattern), ploc); } + +static const std::vector exif_sanity_params +{ +#ifdef HAVE_JPEG + {"readwrite/testExifOrientation_3.jpg", 916, "Photoshop", 120}, +#endif +#ifdef OPENCV_IMGCODECS_PNG_WITH_EXIF + {"readwrite/testExifOrientation_5.png", 112, "ExifTool", 102}, +#endif +#ifdef HAVE_AVIF + {"readwrite/testExifOrientation_7.avif", 913, "Photoshop", 120}, +#endif +}; + +INSTANTIATE_TEST_CASE_P(Imgcodecs, ReadExif_Sanity, + testing::ValuesIn(exif_sanity_params)); + +}} From b8b4633a9d9c0e45078caa5a1cc088d81539cc3f Mon Sep 17 00:00:00 2001 From: Vadim Pisarevskiy Date: Tue, 1 Jul 2025 12:54:02 +0300 Subject: [PATCH 5/9] one more little exif check --- modules/imgcodecs/test/test_exif.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index b4f607ce571c..78b4b559ba58 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -313,6 +313,10 @@ TEST_P(ReadExif_Sanity, Check) EXPECT_EQ(metadata_types.size(), metadata.size()); const std::vector& exif = metadata[IMAGE_METADATA_EXIF]; EXPECT_EQ(exif.size(), exif_size); + ASSERT_GE(exif.size(), 26u); // minimal exif should take at least 26 bytes + // (the header + IDF0 with at least 1 entry). + EXPECT_TRUE(exif[0] == 'I' || exif[0] == 'M'); + EXPECT_EQ(exif[0], exif[1]); EXPECT_EQ(locateString(exif, pattern), ploc); } From ab6b932be07568ac025c3764610bd0f33383e15b Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Tue, 1 Jul 2025 13:12:03 +0300 Subject: [PATCH 6/9] Smoke test for cv.imreadWithMetadata in Python. --- modules/python/test/test_imread.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/python/test/test_imread.py b/modules/python/test/test_imread.py index b5f286d42696..da8f514f8f4f 100644 --- a/modules/python/test/test_imread.py +++ b/modules/python/test/test_imread.py @@ -22,6 +22,17 @@ def test_imread_to_buffer(self): cv.imread(path, img) self.assertEqual(cv.norm(ref, img, cv.NORM_INF), 0.0) + def test_imread_with_meta(self): + path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.jpg' + img, meta_types, meta_data = cv.imreadWithMetadata(path) + self.assertTrue(meta_types is not None) + self.assertTrue(meta_data is not None) + + path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.png' + img, meta_types, meta_data = cv.imreadWithMetadata(path) + self.assertTrue(meta_types is not None) + self.assertTrue(meta_data is not None) + if __name__ == '__main__': NewOpenCVTests.bootstrap() From 855bef997a3baae3cc8ac3a49cb8cee39eb0ae2b Mon Sep 17 00:00:00 2001 From: Vadim Pisarevskiy Date: Tue, 1 Jul 2025 14:00:45 +0300 Subject: [PATCH 7/9] fixed the case when user retrieves metadata as vector instead of vector>. --- modules/imgcodecs/src/loadsave.cpp | 2 +- modules/imgcodecs/test/test_exif.cpp | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 0c849b7356f0..8f811f908584 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -444,7 +444,7 @@ static void readMetadata(ImageDecoder& decoder, if (matvector) { matvector->resize(nmetadata); for (size_t m = 0; m < nmetadata; m++) - matvector->at(m) = src_metadata[m]; + src_metadata[m].copyTo(matvector->at(m)); } else { vecvector->resize(nmetadata); for (size_t m = 0; m < nmetadata; m++) { diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index 78b4b559ba58..1e72e1cd87fd 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -281,10 +281,10 @@ TEST(Imgcodecs_Jpeg, ReadWriteWithExif) remove(outputname.c_str()); } -static size_t locateString(const std::vector& exif, const std::string& pattern) +static size_t locateString(const uchar* exif, size_t exif_size, const std::string& pattern) { - size_t plen = pattern.size(), size = exif.size(); - for (size_t i = 0; i + plen <= size; i++) { + size_t plen = pattern.size(); + for (size_t i = 0; i + plen <= exif_size; i++) { if (exif[i] == pattern[0] && memcmp(&exif[i], pattern.c_str(), plen) == 0) return i; } @@ -305,19 +305,20 @@ TEST_P(ReadExif_Sanity, Check) filename = root + filename; std::vector metadata_types; - std::vector > metadata; + std::vector metadata; Mat img = imreadWithMetadata(filename, metadata_types, metadata, 1); EXPECT_EQ(img.type(), CV_8UC3); ASSERT_GE(metadata_types.size(), 1u); EXPECT_EQ(metadata_types.size(), metadata.size()); - const std::vector& exif = metadata[IMAGE_METADATA_EXIF]; - EXPECT_EQ(exif.size(), exif_size); - ASSERT_GE(exif.size(), 26u); // minimal exif should take at least 26 bytes + const Mat& exif = metadata[IMAGE_METADATA_EXIF]; + EXPECT_EQ(exif.type(), CV_8U); + EXPECT_EQ(exif.total(), exif_size); + ASSERT_GE(exif_size, 26u); // minimal exif should take at least 26 bytes // (the header + IDF0 with at least 1 entry). - EXPECT_TRUE(exif[0] == 'I' || exif[0] == 'M'); - EXPECT_EQ(exif[0], exif[1]); - EXPECT_EQ(locateString(exif, pattern), ploc); + EXPECT_TRUE(exif.data[0] == 'I' || exif.data[0] == 'M'); + EXPECT_EQ(exif.data[0], exif.data[1]); + EXPECT_EQ(locateString(exif.data, exif_size, pattern), ploc); } static const std::vector exif_sanity_params From a6c6e6695f7eea983fb3015c604c06a0bdd144ff Mon Sep 17 00:00:00 2001 From: Vadim Pisarevskiy Date: Tue, 1 Jul 2025 14:32:35 +0300 Subject: [PATCH 8/9] added exif metadata support in PNG encoder + the corresponding test --- modules/imgcodecs/src/grfmt_png.cpp | 12 +++++++ modules/imgcodecs/test/test_exif.cpp | 53 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index a47db5aa2a9c..f0f656bd2561 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -858,6 +858,8 @@ PngEncoder::PngEncoder() { m_description = "Portable Network Graphics files (*.png;*.apng)"; m_buf_supported = true; + m_support_metadata.assign((size_t)IMAGE_METADATA_MAX+1, false); + m_support_metadata[IMAGE_METADATA_EXIF] = true; op_zstream1.zalloc = NULL; op_zstream2.zalloc = NULL; next_seq_num = 0; @@ -989,6 +991,16 @@ bool PngEncoder::write( const Mat& img, const std::vector& params ) for( y = 0; y < height; y++ ) buffer[y] = img.data + y*img.step; + if (!m_metadata.empty()) { + std::vector& exif = m_metadata[IMAGE_METADATA_EXIF]; + if (!exif.empty()) { + writeChunk(f, "eXIf", exif.data(), (uint32_t)exif.size()); + } + // [TODO] add xmp and icc. They need special handling, + // see https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files and + // https://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html. + } + png_write_image( png_ptr, buffer.data() ); png_write_end( png_ptr, info_ptr ); diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index 1e72e1cd87fd..792c38514f65 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -281,6 +281,59 @@ TEST(Imgcodecs_Jpeg, ReadWriteWithExif) remove(outputname.c_str()); } +TEST(Imgcodecs_Png, ReadWriteWithExif) +{ + static const uchar exif_data[] = { + 'M', 'M', 0, '*', 0, 0, 0, 8, 0, 10, 1, 0, 0, 4, 0, 0, 0, 1, 0, 0, 5, + 0, 1, 1, 0, 4, 0, 0, 0, 1, 0, 0, 2, 208, 1, 2, 0, 3, 0, 0, 0, 1, + 0, 8, 0, 0, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 14, 0, 2, 0, 0, + 0, '!', 0, 0, 0, 176, 1, '1', 0, 2, 0, 0, 0, 7, 0, 0, 0, 210, 1, 26, + 0, 5, 0, 0, 0, 1, 0, 0, 0, 218, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, + 226, 1, '(', 0, 3, 0, 0, 0, 1, 0, 2, 0, 0, 135, 'i', 0, 4, 0, 0, 0, + 1, 0, 0, 0, 134, 0, 0, 0, 0, 0, 3, 144, 0, 0, 7, 0, 0, 0, 4, '0', '2', + '2', '1', 160, 2, 0, 4, 0, 0, 0, 1, 0, 0, 5, 0, 160, 3, 0, 4, 0, 0, + 0, 1, 0, 0, 2, 208, 0, 0, 0, 0, 'S', 'a', 'm', 'p', 'l', 'e', ' ', '8', '-', + 'b', 'i', 't', ' ', 'i', 'm', 'a', 'g', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'm', + 'e', 't', 'a', 'd', 'a', 't', 'a', 0, 0, 'O', 'p', 'e', 'n', 'C', 'V', 0, 0, + 0, 0, 0, 'H', 0, 0, 0, 1, 0, 0, 0, 'H', 0, 0, 0, 1 + }; + + int png_compression = 3; + int imgtype = CV_MAKETYPE(CV_8U, 3); + const string outputname = cv::tempfile(".png"); + Mat img = makeCirclesImage(Size(1280, 720), imgtype, 8); + + std::vector metadata_types = {IMAGE_METADATA_EXIF}; + std::vector > metadata(1); + metadata[0].assign(exif_data, exif_data + sizeof(exif_data)); + + std::vector write_params = { + IMWRITE_PNG_COMPRESSION, png_compression + }; + + imwriteWithMetadata(outputname, img, metadata_types, metadata, write_params); + std::vector compressed; + imencodeWithMetadata(outputname, img, metadata_types, metadata, compressed, write_params); + + std::vector read_metadata_types, read_metadata_types2; + std::vector > read_metadata, read_metadata2; + Mat img2 = imreadWithMetadata(outputname, read_metadata_types, read_metadata, IMREAD_UNCHANGED); + Mat img3 = imdecodeWithMetadata(compressed, read_metadata_types2, read_metadata2, IMREAD_UNCHANGED); + EXPECT_EQ(img2.cols, img.cols); + EXPECT_EQ(img2.rows, img.rows); + EXPECT_EQ(img2.type(), imgtype); + EXPECT_EQ(read_metadata_types, read_metadata_types2); + EXPECT_GE(read_metadata_types.size(), 1u); + EXPECT_EQ(read_metadata, read_metadata2); + EXPECT_EQ(read_metadata_types[0], IMAGE_METADATA_EXIF); + EXPECT_EQ(read_metadata_types.size(), read_metadata.size()); + EXPECT_EQ(read_metadata[0], metadata[0]); + EXPECT_EQ(cv::norm(img2, img3, NORM_INF), 0.); + double mse = cv::norm(img, img2, NORM_L2SQR)/(img.rows*img.cols); + EXPECT_EQ(mse, 0); // png is lossless + remove(outputname.c_str()); +} + static size_t locateString(const uchar* exif, size_t exif_size, const std::string& pattern) { size_t plen = pattern.size(); From 8444a1eada5dd8bd9113084b979516e1fe6c4c09 Mon Sep 17 00:00:00 2001 From: Alexander Smorkalov Date: Tue, 1 Jul 2025 16:57:29 +0300 Subject: [PATCH 9/9] Pylint warning fix. --- modules/python/test/test_imread.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/python/test/test_imread.py b/modules/python/test/test_imread.py index da8f514f8f4f..471c786acc91 100644 --- a/modules/python/test/test_imread.py +++ b/modules/python/test/test_imread.py @@ -25,14 +25,15 @@ def test_imread_to_buffer(self): def test_imread_with_meta(self): path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.jpg' img, meta_types, meta_data = cv.imreadWithMetadata(path) + self.assertTrue(img is not None) self.assertTrue(meta_types is not None) self.assertTrue(meta_data is not None) path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.png' img, meta_types, meta_data = cv.imreadWithMetadata(path) + self.assertTrue(img is not None) self.assertTrue(meta_types is not None) self.assertTrue(meta_data is not None) - if __name__ == '__main__': NewOpenCVTests.bootstrap() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy