Exif parsing for PNG files to support Exif orientation tag. Moved decoder specific...
authorRachel A <aldridge.r.a@gmail.com>
Mon, 1 Feb 2021 20:16:44 +0000 (12:16 -0800)
committerRachel A <aldridge.r.a@gmail.com>
Tue, 9 Feb 2021 18:35:27 +0000 (10:35 -0800)
modules/imgcodecs/src/exif.cpp
modules/imgcodecs/src/exif.hpp
modules/imgcodecs/src/grfmt_base.cpp
modules/imgcodecs/src/grfmt_base.hpp
modules/imgcodecs/src/grfmt_jpeg.cpp
modules/imgcodecs/src/grfmt_jpeg.hpp
modules/imgcodecs/src/grfmt_png.cpp
modules/imgcodecs/src/loadsave.cpp
modules/imgcodecs/test/test_png.cpp

index 051999c0fa2c7dce1c90a1651ad7db614b6b5855..28d52047d828a351482925e8a1602030e9860935 100644 (file)
@@ -62,7 +62,7 @@ ExifEntry_t::ExifEntry_t() :
 /**
  * @brief ExifReader constructor
  */
-ExifReader::ExifReader(std::istream& stream) : m_stream(stream), m_format(NONE)
+ExifReader::ExifReader() : m_format(NONE)
 {
 }
 
@@ -73,25 +73,6 @@ ExifReader::~ExifReader()
 {
 }
 
-/**
- * @brief Parsing the file and prepare (internally) exif directory structure
- * @return  true if parsing was successful and exif information exists in JpegReader object
- *          false in case of unsuccessful parsing
- */
-bool ExifReader::parse()
-{
-    try {
-        m_exif = getExif();
-        if( !m_exif.empty() )
-        {
-            return true;
-        }
-        return false;
-    } catch (ExifParsingError&) {
-        return false;
-    }
-}
-
 
 /**
  *  @brief Get tag value by tag number
@@ -101,10 +82,10 @@ bool ExifReader::parse()
  *  @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 ExifReader::getTag(const ExifTagName tag)
+ExifEntry_t ExifReader::getTag(const ExifTagName tag) const
 {
     ExifEntry_t entry;
-    std::map<int, ExifEntry_t>::iterator it = m_exif.find(tag);
+    std::map<int, ExifEntry_t>::const_iterator it = m_exif.find(tag);
 
     if( it != m_exif.end() )
     {
@@ -115,100 +96,37 @@ ExifEntry_t ExifReader::getTag(const ExifTagName tag)
 
 
 /**
- * @brief Get exif directory structure contained in file (if any)
- *          This is internal function and is not exposed to client
+ * @brief Parsing the exif data buffer and prepare (internal) exif directory
+ *
+ * @param [in] data The data buffer to read EXIF data starting with endianness
+ * @param [in] size The size of the data buffer
  *
- *  @return Map where key is tag number and value is ExifEntry_t structure
+ * @return  true if parsing was successful
+ *          false in case of unsuccessful parsing
  */
-std::map<int, ExifEntry_t > ExifReader::getExif()
+bool ExifReader::parseExif(unsigned char* data, const size_t size)
 {
-    const std::streamsize markerSize = 2;
-    const std::streamsize offsetToTiffHeader = 6; //bytes from Exif size field to the first TIFF header
-    unsigned char appMarker[markerSize];
-    m_exif.erase( m_exif.begin(), m_exif.end() );
-
-    std::streamsize count;
-
-    bool exifFound = false, stopSearch = false;
-    while( ( !m_stream.eof() ) && !exifFound && !stopSearch )
+    // Populate m_data, then call parseExif() (private)
+    if( data && size > 0 )
     {
-        m_stream.read( reinterpret_cast<char*>(appMarker), markerSize );
-        count = m_stream.gcount();
-        if( count < markerSize )
-        {
-            break;
-        }
-        unsigned char marker = appMarker[1];
-        size_t bytesToSkip;
-        size_t exifSize;
-        switch( marker )
-        {
-            //For all the markers just skip bytes in file pointed by followed two bytes (field size)
-            case SOF0: case SOF2: case DHT: case DQT: case DRI: case SOS:
-            case RST0: case RST1: case RST2: case RST3: case RST4: case RST5: case RST6: case RST7:
-            case APP0: case APP2: case APP3: case APP4: case APP5: case APP6: case APP7: case APP8:
-            case APP9: case APP10: case APP11: case APP12: case APP13: case APP14: case APP15:
-            case COM:
-                bytesToSkip = getFieldSize();
-                if (bytesToSkip < markerSize) {
-                    throw ExifParsingError();
-                }
-                m_stream.seekg( static_cast<long>( bytesToSkip - markerSize ), m_stream.cur );
-                if ( m_stream.fail() ) {
-                    throw ExifParsingError();
-                }
-                break;
-
-            //SOI and EOI don't have the size field after the marker
-            case SOI: case EOI:
-                break;
-
-            case APP1: //actual Exif Marker
-                exifSize = getFieldSize();
-                if (exifSize <= offsetToTiffHeader) {
-                    throw ExifParsingError();
-                }
-                m_data.resize( exifSize - offsetToTiffHeader );
-                m_stream.seekg( static_cast<long>( offsetToTiffHeader ), m_stream.cur );
-                if ( m_stream.fail() ) {
-                    throw ExifParsingError();
-                }
-                m_stream.read( reinterpret_cast<char*>(&m_data[0]), exifSize - offsetToTiffHeader );
-                exifFound = true;
-                break;
-
-            default: //No other markers are expected according to standard. May be a signal of error
-                stopSearch = true;
-                break;
-        }
+        m_data.assign(data, data + size);
     }
-
-    if( !exifFound )
+    else
     {
-        return m_exif;
+        return false;
     }
 
-    parseExif();
-
-    return m_exif;
-}
-
-/**
- * @brief Get the size of exif field (required to properly ready whole exif from the file)
- *          This is internal function and is not exposed to client
- *
- *  @return size of exif field in the file
- */
-size_t ExifReader::getFieldSize ()
-{
-    unsigned char fieldSize[2];
-    m_stream.read( reinterpret_cast<char*>(fieldSize), 2 );
-    std::streamsize count = m_stream.gcount();
-    if (count < 2)
-    {
-        return 0;
+    try {
+        parseExif();
+        if( !m_exif.empty() )
+        {
+            return true;
+        }
+        return false;
+    }
+    catch( ExifParsingError& ) {
+        return false;
     }
-    return ( fieldSize[0] << 8 ) + fieldSize[1];
 }
 
 /**
index dc9a58ab0b3167b5923e73e78018097ab825cbcd..6cc95afb1abb8f454caa945b7c93bb707376014c 100644 (file)
 
 namespace cv
 {
-/**
- * @brief Jpeg markers that can encounter in Jpeg file
- */
-enum AppMarkerTypes
-{
-    SOI   = 0xD8, SOF0  = 0xC0, SOF2  = 0xC2, DHT   = 0xC4,
-    DQT   = 0xDB, DRI   = 0xDD, SOS   = 0xDA,
-
-    RST0  = 0xD0, RST1  = 0xD1, RST2  = 0xD2, RST3  = 0xD3,
-    RST4  = 0xD4, RST5  = 0xD5, RST6  = 0xD6, RST7  = 0xD7,
-
-    APP0  = 0xE0, APP1  = 0xE1, APP2  = 0xE2, APP3  = 0xE3,
-    APP4  = 0xE4, APP5  = 0xE5, APP6  = 0xE6, APP7  = 0xE7,
-    APP8  = 0xE8, APP9  = 0xE9, APP10 = 0xEA, APP11 = 0xEB,
-    APP12 = 0xEC, APP13 = 0xED, APP14 = 0xEE, APP15 = 0xEF,
-
-    COM   = 0xFE, EOI   = 0xD9
-};
 
 /**
  * @brief Base Exif tags used by IFD0 (main image)
@@ -168,19 +150,22 @@ class ExifReader
 public:
     /**
      * @brief ExifReader constructor. Constructs an object of exif reader
-     *
-     * @param [in]stream An istream to look for EXIF bytes from
      */
-    explicit ExifReader( std::istream& stream );
+    ExifReader();
     ~ExifReader();
 
 
     /**
      * @brief Parse the file with exif info
      *
-     * @return true if parsing was successful and exif information exists in JpegReader object
+     * @param [in] data The data buffer to read EXIF data starting with endianness
+     * @param [in] size The size of the data buffer
+     *
+     * @return true if successful parsing
+     *         false if parsing error
      */
-    bool parse();
+
+    bool parseExif(unsigned char* data, const size_t size);
 
     /**
      * @brief Get tag info by tag number
@@ -188,10 +173,10 @@ public:
      * @param [in] tag The tag number
      * @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 );
+    ExifEntry_t getTag( const ExifTagName tag ) const;
+
 
 private:
-    std::istream& m_stream;
     std::vector<unsigned char> m_data;
     std::map<int, ExifEntry_t > m_exif;
     Endianess_t m_format;
@@ -199,7 +184,6 @@ private:
     void parseExif();
     bool checkTagMark() const;
 
-    size_t getFieldSize ();
     size_t getNumDirEntry( const size_t offsetNumDir ) const;
     uint32_t getStartOffset() const;
     uint16_t getExifTag( const size_t offset ) const;
@@ -215,7 +199,6 @@ private:
 
     u_rational_t getURational( const size_t offset ) const;
 
-    std::map<int, ExifEntry_t > getExif();
     std::string getString( const size_t offset ) const;
     std::vector<u_rational_t> getResolution( const size_t offset ) const;
     std::vector<u_rational_t> getWhitePoint( const size_t offset ) const;
index b7032c17237545f4aeea60b20f29853698b07a6d..c1a8854a427319ff326738c11c66e22c88055fc3 100644 (file)
@@ -55,6 +55,11 @@ BaseImageDecoder::BaseImageDecoder()
     m_scale_denom = 1;
 }
 
+
+ExifEntry_t BaseImageDecoder::getExifTag(const ExifTagName tag) const
+{
+    return m_exif.getTag(tag);
+}
 bool BaseImageDecoder::setSource( const String& filename )
 {
     m_filename = filename;
index 7d75636cf59e2cfa2ac41dea1f3131e44cbd0353..816bef98fb0a37c69c6897abf6c65f71f407bc9e 100644 (file)
@@ -45,6 +45,7 @@
 
 #include "utils.hpp"
 #include "bitstrm.hpp"
+#include "exif.hpp"
 
 namespace cv
 {
@@ -65,6 +66,7 @@ public:
     int height() const { return m_height; }
     virtual int type() const { return m_type; }
 
+    ExifEntry_t getExifTag(const ExifTagName tag) const;
     virtual bool setSource( const String& filename );
     virtual bool setSource( const Mat& buf );
     virtual int setScale( const int& scale_denom );
@@ -87,6 +89,7 @@ protected:
     String m_signature;
     Mat m_buf;
     bool m_buf_supported;
+    ExifReader m_exif;
 };
 
 
index ba5dab41dfd2cb76f2a6f713fa8a786c32fdac7b..758ac512e85d136f47ea8ec5a8b58b63ee68af47 100644 (file)
@@ -244,6 +244,7 @@ bool  JpegDecoder::readHeader()
 
         if (state->cinfo.src != 0)
         {
+            jpeg_save_markers(&state->cinfo, APP1, 0xffff);
             jpeg_read_header( &state->cinfo, TRUE );
 
             state->cinfo.scale_num=1;
@@ -456,6 +457,29 @@ 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 );
 
             buffer = (*cinfo->mem->alloc_sarray)((j_common_ptr)cinfo,
index 90d80b4b59eeb52ff72047559648e55869628178..e7c8c25457ab9e83e523704b07d56ec4713c62e3 100644 (file)
 
 namespace cv
 {
+/**
+* @brief Jpeg markers that can be encountered in a Jpeg file
+*/
+enum AppMarkerTypes
+{
+    SOI = 0xD8, SOF0 = 0xC0, SOF2 = 0xC2, DHT = 0xC4,
+    DQT = 0xDB, DRI = 0xDD, SOS = 0xDA,
+
+    RST0 = 0xD0, RST1 = 0xD1, RST2 = 0xD2, RST3 = 0xD3,
+    RST4 = 0xD4, RST5 = 0xD5, RST6 = 0xD6, RST7 = 0xD7,
+
+    APP0 = 0xE0, APP1 = 0xE1, APP2 = 0xE2, APP3 = 0xE3,
+    APP4 = 0xE4, APP5 = 0xE5, APP6 = 0xE6, APP7 = 0xE7,
+    APP8 = 0xE8, APP9 = 0xE9, APP10 = 0xEA, APP11 = 0xEB,
+    APP12 = 0xEC, APP13 = 0xED, APP14 = 0xEE, APP15 = 0xEF,
+
+    COM = 0xFE, EOI = 0xD9
+};
+
 
 class JpegDecoder CV_FINAL : public BaseImageDecoder
 {
index b533cd849f98b8ac4d18e20543568bf32015f812..9e1a2d4c71238dcc42124ce26b8287cefa049416 100644 (file)
@@ -284,6 +284,22 @@ bool  PngDecoder::readData( Mat& img )
             png_read_image( png_ptr, buffer );
             png_read_end( png_ptr, end_info );
 
+#ifdef PNG_eXIf_SUPPORTED
+            png_uint_32 num_exif = 0;
+            png_bytep exif = 0;
+
+            // Exif info could be in info_ptr (intro_info) or end_info per specification
+            if( png_get_valid(png_ptr, info_ptr, PNG_INFO_eXIf) )
+                png_get_eXIf_1(png_ptr, info_ptr, &num_exif, &exif);
+            else if( png_get_valid(png_ptr, end_info, PNG_INFO_eXIf) )
+                png_get_eXIf_1(png_ptr, end_info, &num_exif, &exif);
+
+            if( exif && num_exif > 0 )
+            {
+                m_exif.parseExif(exif, num_exif);
+            }
+#endif
+
             result = true;
         }
     }
index 44c458c7273ce05a26eba34f10894b48c3be9c99..c8fcbea7ee1e19110f9c43362cd443e847969b27 100644 (file)
@@ -354,48 +354,15 @@ static void ExifTransform(int orientation, Mat& img)
     }
 }
 
-static void ApplyExifOrientation(const String& filename, Mat& img)
+static void ApplyExifOrientation(ExifEntry_t orientationTag, Mat& img)
 {
     int orientation = IMAGE_ORIENTATION_TL;
 
-    if (filename.size() > 0)
+    if (orientationTag.tag != INVALID_TAG)
     {
-        std::ifstream stream( filename.c_str(), std::ios_base::in | std::ios_base::binary );
-        ExifReader reader( stream );
-        if( reader.parse() )
-        {
-            ExifEntry_t entry = reader.getTag( ORIENTATION );
-            if (entry.tag != INVALID_TAG)
-            {
-                orientation = entry.field_u16; //orientation is unsigned short, so check field_u16
-            }
-        }
-        stream.close();
-    }
-
-    ExifTransform(orientation, img);
-}
-
-static void ApplyExifOrientation(const Mat& buf, Mat& img)
-{
-    int orientation = IMAGE_ORIENTATION_TL;
-
-    if( buf.isContinuous() )
-    {
-        ByteStreamBuffer bsb( reinterpret_cast<char*>(buf.data), buf.total() * buf.elemSize() );
-        std::istream stream( &bsb );
-        ExifReader reader( stream );
-        if( reader.parse() )
-        {
-            ExifEntry_t entry = reader.getTag( ORIENTATION );
-            if (entry.tag != INVALID_TAG)
-            {
-                orientation = entry.field_u16; //orientation is unsigned short, so check field_u16
-            }
-        }
+        orientation = orientationTag.field_u16; //orientation is unsigned short, so check field_u16
+        ExifTransform(orientation, img);
     }
-
-    ExifTransform(orientation, img);
 }
 
 /**
@@ -537,6 +504,12 @@ imread_( const String& filename, int flags, int hdrtype, Mat* mat=0 )
         resize( *mat, *mat, Size( size.width / scale_denom, size.height / scale_denom ), 0, 0, INTER_LINEAR_EXACT);
     }
 
+    /// optionally rotate the data if EXIF orientation flag says so
+    if( mat && !mat->empty() && (flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED )
+    {
+        ApplyExifOrientation(decoder->getExifTag(ORIENTATION), *mat);
+    }
+
     return hdrtype == LOAD_CVMAT ? (void*)matrix :
         hdrtype == LOAD_IMAGE ? (void*)image : (void*)mat;
 }
@@ -634,7 +607,7 @@ imreadmulti_(const String& filename, int flags, std::vector<Mat>& mats)
         // optionally rotate the data if EXIF' orientation flag says so
         if( (flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED )
         {
-            ApplyExifOrientation(filename, mat);
+            ApplyExifOrientation(decoder->getExifTag(ORIENTATION), mat);
         }
 
         mats.push_back(mat);
@@ -665,12 +638,6 @@ Mat imread( const String& filename, int flags )
     /// load the data
     imread_( filename, flags, LOAD_MAT, &img );
 
-    /// optionally rotate the data if EXIF' orientation flag says so
-    if( !img.empty() && (flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED )
-    {
-        ApplyExifOrientation(filename, img);
-    }
-
     /// return a reference to the data
     return img;
 }
@@ -932,6 +899,12 @@ imdecode_( const Mat& buf, int flags, int hdrtype, Mat* mat=0 )
         resize( *mat, *mat, Size( size.width / scale_denom, size.height / scale_denom ), 0, 0, INTER_LINEAR_EXACT);
     }
 
+    /// optionally rotate the data if EXIF' orientation flag says so
+    if (!mat->empty() && (flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED)
+    {
+        ApplyExifOrientation(decoder->getExifTag(ORIENTATION), *mat);
+    }
+
     decoder.release();
 
     return hdrtype == LOAD_CVMAT ? (void*)matrix :
@@ -946,12 +919,6 @@ Mat imdecode( InputArray _buf, int flags )
     Mat buf = _buf.getMat(), img;
     imdecode_( buf, flags, LOAD_MAT, &img );
 
-    /// optionally rotate the data if EXIF' orientation flag says so
-    if( !img.empty() && (flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED )
-    {
-        ApplyExifOrientation(buf, img);
-    }
-
     return img;
 }
 
@@ -963,12 +930,6 @@ Mat imdecode( InputArray _buf, int flags, Mat* dst )
     dst = dst ? dst : &img;
     imdecode_( buf, flags, LOAD_MAT, dst );
 
-    /// optionally rotate the data if EXIF' orientation flag says so
-    if( !dst->empty() && (flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED )
-    {
-        ApplyExifOrientation(buf, *dst);
-    }
-
     return *dst;
 }
 
index 051cbf69e6d6c4988e1912dec75e9d379c6c03ce..74920ee9ae5290c98825c17bb98ed446b84d7cf7 100644 (file)
@@ -7,6 +7,12 @@ namespace opencv_test { namespace {
 
 #ifdef HAVE_PNG
 
+#ifdef HAVE_LIBPNG_PNG_H
+#include <libpng/png.h>
+#else
+#include <png.h>
+#endif
+
 TEST(Imgcodecs_Png, write_big)
 {
     const string root = cvtest::TS::ptr()->get_data_path();
@@ -93,6 +99,97 @@ TEST(Imgcodecs_Png, read_color_palette_with_alpha)
     EXPECT_EQ(img.at<Vec3b>(0, 1), Vec3b(0, 0, 255));
 }
 
+#ifdef PNG_eXIf_SUPPORTED
+/**
+ * Test for check whether reading exif orientation tag was processed successfully or not
+ * The test info is the set of 8 images named testExifRotate_{1 to 8}.png
+ * The test image is the square 10x10 points divided by four sub-squares:
+ * (R corresponds to Red, G to Green, B to Blue, W to white)
+ * ---------             ---------
+ * | R | G |             | G | R |
+ * |-------| - (tag 1)   |-------| - (tag 2)
+ * | B | W |             | W | B |
+ * ---------             ---------
+ *
+ * ---------             ---------
+ * | W | B |             | B | W |
+ * |-------| - (tag 3)   |-------| - (tag 4)
+ * | G | R |             | R | G |
+ * ---------             ---------
+ *
+ * ---------             ---------
+ * | R | B |             | G | W |
+ * |-------| - (tag 5)   |-------| - (tag 6)
+ * | G | W |             | R | B |
+ * ---------             ---------
+ *
+ * ---------             ---------
+ * | W | G |             | B | R |
+ * |-------| - (tag 7)   |-------| - (tag 8)
+ * | B | R |             | W | G |
+ * ---------             ---------
+ *
+ *
+ * Every image contains exif field with orientation tag (0x112)
+ * After reading each image and applying the orientation tag,
+ * the resulting image should be:
+ * ---------
+ * | R | G |
+ * |-------|
+ * | B | W |
+ * ---------
+ *
+ */
+
+typedef testing::TestWithParam<string> Imgcodecs_PNG_Exif;
+
+// Solution to issue 16579: PNG read doesn't support Exif orientation data
+TEST_P(Imgcodecs_PNG_Exif, exif_orientation)
+{
+    const string root = cvtest::TS::ptr()->get_data_path();
+    const string filename = root + GetParam();
+    const int colorThresholdHigh = 250;
+    const int colorThresholdLow = 5;
+
+    Mat m_img = imread(filename);
+    ASSERT_FALSE(m_img.empty());
+    Vec3b vec;
+
+    //Checking the first quadrant (with supposed red)
+    vec = m_img.at<Vec3b>(2, 2); //some point inside the square
+    EXPECT_LE(vec.val[0], colorThresholdLow);
+    EXPECT_LE(vec.val[1], colorThresholdLow);
+    EXPECT_GE(vec.val[2], colorThresholdHigh);
+
+    //Checking the second quadrant (with supposed green)
+    vec = m_img.at<Vec3b>(2, 7);  //some point inside the square
+    EXPECT_LE(vec.val[0], colorThresholdLow);
+    EXPECT_GE(vec.val[1], colorThresholdHigh);
+    EXPECT_LE(vec.val[2], colorThresholdLow);
+
+    //Checking the third quadrant (with supposed blue)
+    vec = m_img.at<Vec3b>(7, 2);  //some point inside the square
+    EXPECT_GE(vec.val[0], colorThresholdHigh);
+    EXPECT_LE(vec.val[1], colorThresholdLow);
+    EXPECT_LE(vec.val[2], colorThresholdLow);
+}
+
+const string exif_files[] =
+{
+    "readwrite/testExifOrientation_1.png",
+    "readwrite/testExifOrientation_2.png",
+    "readwrite/testExifOrientation_3.png",
+    "readwrite/testExifOrientation_4.png",
+    "readwrite/testExifOrientation_5.png",
+    "readwrite/testExifOrientation_6.png",
+    "readwrite/testExifOrientation_7.png",
+    "readwrite/testExifOrientation_8.png"
+};
+
+INSTANTIATE_TEST_CASE_P(ExifFiles, Imgcodecs_PNG_Exif,
+    testing::ValuesIn(exif_files));
+#endif // PNG_eXIf_SUPPORTED
+
 #endif // HAVE_PNG
 
 }} // namespace