diff --git a/modules/imgcodecs/src/grfmt_jpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg.cpp index 9b2ab59b2b..810ae1d63b 100644 --- a/modules/imgcodecs/src/grfmt_jpeg.cpp +++ b/modules/imgcodecs/src/grfmt_jpeg.cpp @@ -247,8 +247,43 @@ bool JpegDecoder::readHeader() if (state->cinfo.src != 0) { jpeg_save_markers(&state->cinfo, APP1, 0xffff); + jpeg_save_markers(&state->cinfo, APP2, 0xffff); jpeg_read_header( &state->cinfo, TRUE ); + const std::streamsize EXIF_HEADER_SIZE = 6; // "Exif\0\0" + const std::streamsize XMP_HEADER_SIZE = 29; // "http://ns.adobe.com/xap/1.0/" + const std::streamsize ICC_HEADER_SIZE = 14; // "ICC_PROFILE\0" + seq/total + + for (jpeg_saved_marker_ptr cmarker = state->cinfo.marker_list; cmarker != nullptr; cmarker = cmarker->next) + { + // Handle APP1 marker: could be Exif or XMP + if (cmarker->marker == APP1 && cmarker->data_length > EXIF_HEADER_SIZE) + { + unsigned char* data = cmarker->data; + + // Check for Exif data + if (std::memcmp(data, "Exif\0\0", EXIF_HEADER_SIZE) == 0) + { + m_exif.parseExif(data + EXIF_HEADER_SIZE, cmarker->data_length - EXIF_HEADER_SIZE); + } + // Check for XMP metadata + else if (m_read_options && cmarker->data_length >= XMP_HEADER_SIZE && + std::memcmp(data, "http://ns.adobe.com/xap/1.0/", XMP_HEADER_SIZE) == 0) + { + std::vector& xmp = m_metadata[IMAGE_METADATA_XMP]; + xmp.insert(xmp.end(), data, data + cmarker->data_length); + } + } + + // Handle APP2 marker: typically contains ICC profile data + if (m_read_options && cmarker->marker == APP2 && cmarker->data_length > ICC_HEADER_SIZE) + { + const unsigned char* data = cmarker->data; + std::vector& iccp = m_metadata[IMAGE_METADATA_ICCP]; + iccp.insert(iccp.end(), data + ICC_HEADER_SIZE, data + cmarker->data_length); + } + } + state->cinfo.scale_num=1; state->cinfo.scale_denom = m_scale_denom; m_scale_denom=1; // trick! to know which decoder used scale_denom see imread_ @@ -469,29 +504,6 @@ bool JpegDecoder::readData( Mat& img ) } } - // Check for Exif marker APP1 - jpeg_saved_marker_ptr exif_marker = NULL; - jpeg_saved_marker_ptr cmarker = cinfo->marker_list; - while( cmarker && exif_marker == NULL ) - { - if (cmarker->marker == APP1) - exif_marker = cmarker; - - cmarker = cmarker->next; - } - - // Parse Exif data - if( exif_marker ) - { - const std::streamsize offsetToTiffHeader = 6; //bytes from Exif size field to the first TIFF header - - if (exif_marker->data_length > offsetToTiffHeader) - { - m_exif.parseExif(exif_marker->data + offsetToTiffHeader, exif_marker->data_length - offsetToTiffHeader); - } - } - - jpeg_start_decompress( cinfo ); if( doDirectRead) @@ -602,6 +614,8 @@ JpegEncoder::JpegEncoder() 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; } @@ -831,6 +845,26 @@ bool JpegEncoder::write( const Mat& img, const std::vector& params ) memcpy(data + app1_exif_prefix_size, metadata_exif.data(), exif_size); jpeg_write_marker(&cinfo, JPEG_APP0 + 1, data, (unsigned)data_size); } + + const std::vector& metadata_xmp = m_metadata[IMAGE_METADATA_XMP]; + size_t xmp_size = metadata_xmp.size(); + if (xmp_size > 0u) { + jpeg_write_marker(&cinfo, JPEG_APP0 + 1, metadata_xmp.data(), (unsigned)xmp_size); + } + + const std::vector& metadata_iccp = m_metadata[IMAGE_METADATA_ICCP]; + size_t iccp_size = metadata_iccp.size(); + if (iccp_size > 0u) { + const char app1_iccp_prefix[] = {'I','C','C','_','P','R','O','F','I','L','E','\0','\1','\1'}; + size_t app1_iccp_prefix_size = sizeof(app1_iccp_prefix); + size_t data_size = iccp_size + app1_iccp_prefix_size; + + std::vector metadata_app1(data_size); + uchar* data = metadata_app1.data(); + memcpy(data, app1_iccp_prefix, app1_iccp_prefix_size); + memcpy(data + app1_iccp_prefix_size, metadata_iccp.data(), iccp_size); + jpeg_write_marker(&cinfo, JPEG_APP0 + 2, data, (unsigned)data_size); + } } if( doDirectWrite ) diff --git a/modules/imgcodecs/test/test_exif.cpp b/modules/imgcodecs/test/test_exif.cpp index ac47e632a2..706896fedc 100644 --- a/modules/imgcodecs/test/test_exif.cpp +++ b/modules/imgcodecs/test/test_exif.cpp @@ -532,7 +532,7 @@ static size_t locateString(const uchar* exif, size_t exif_size, const std::strin return 0xFFFFFFFFu; } -typedef std::tuple ReadExif_Sanity_Params; +typedef std::tuple ReadExif_Sanity_Params; typedef testing::TestWithParam ReadExif_Sanity; TEST_P(ReadExif_Sanity, Check) @@ -541,18 +541,27 @@ TEST_P(ReadExif_Sanity, Check) size_t exif_size = get<1>(GetParam()); std::string pattern = get<2>(GetParam()); size_t ploc = get<3>(GetParam()); + size_t expected_xmp_size = get<4>(GetParam()); + size_t expected_iccp_size = get<5>(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); + std::vector metadata_types, metadata_types2; + std::vector > metadata, metadata2; + Mat img = imreadWithMetadata(filename, metadata_types, metadata); + + std::vector compressed; + imencodeWithMetadata(".jpg", img, metadata_types, metadata, compressed); + img = imdecodeWithMetadata(compressed, metadata_types2, metadata2); + + EXPECT_EQ(metadata_types, metadata_types2); + EXPECT_EQ(metadata, metadata2); EXPECT_EQ(img.type(), CV_8UC3); ASSERT_GE(metadata_types.size(), 1u); EXPECT_EQ(metadata_types.size(), metadata.size()); - const Mat& exif = metadata[IMAGE_METADATA_EXIF]; + const Mat exif = Mat(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 @@ -560,18 +569,36 @@ TEST_P(ReadExif_Sanity, Check) 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); + + if (metadata_types.size() > IMAGE_METADATA_XMP) + { + const Mat xmp = Mat(metadata[IMAGE_METADATA_XMP]); + EXPECT_EQ(xmp.type(), CV_8U); + EXPECT_GT(xmp.total(), 0u); + size_t xmp_size = xmp.total() * xmp.elemSize(); + EXPECT_EQ(expected_xmp_size, xmp_size); + } + + if (metadata_types.size() > IMAGE_METADATA_ICCP) + { + const Mat iccp = Mat(metadata[IMAGE_METADATA_ICCP]); + EXPECT_EQ(iccp.type(), CV_8U); + EXPECT_GT(iccp.total(), 0u); + size_t iccp_size = iccp.total() * iccp.elemSize(); + EXPECT_EQ(expected_iccp_size, iccp_size); + } } static const std::vector exif_sanity_params { #ifdef HAVE_JPEG - ReadExif_Sanity_Params("readwrite/testExifOrientation_3.jpg", 916, "Photoshop", 120), + ReadExif_Sanity_Params("readwrite/testExifOrientation_3.jpg", 916, "Photoshop", 120, 3597, 940), #endif #ifdef OPENCV_IMGCODECS_PNG_WITH_EXIF - ReadExif_Sanity_Params("readwrite/testExifOrientation_5.png", 112, "ExifTool", 102), + ReadExif_Sanity_Params("readwrite/testExifOrientation_5.png", 112, "ExifTool", 102, 505, 0), #endif #ifdef HAVE_AVIF - ReadExif_Sanity_Params("readwrite/testExifOrientation_7.avif", 913, "Photoshop", 120), + ReadExif_Sanity_Params("readwrite/testExifOrientation_7.avif", 913, "Photoshop", 120, 3597, 940), #endif };