Exif parsing for PNG files to support Exif orientation tag. Moved decoder specific Exif parsing to JPEG and PNG decoders, respectively. Issue 16579

pull/19439/head
Rachel A 4 years ago
parent 2d2d72afbb
commit 650836d700
  1. 132
      modules/imgcodecs/src/exif.cpp
  2. 37
      modules/imgcodecs/src/exif.hpp
  3. 5
      modules/imgcodecs/src/grfmt_base.cpp
  4. 3
      modules/imgcodecs/src/grfmt_base.hpp
  5. 24
      modules/imgcodecs/src/grfmt_jpeg.cpp
  6. 19
      modules/imgcodecs/src/grfmt_jpeg.hpp
  7. 16
      modules/imgcodecs/src/grfmt_png.cpp
  8. 73
      modules/imgcodecs/src/loadsave.cpp
  9. 97
      modules/imgcodecs/test/test_png.cpp

@ -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];
}
/**

@ -54,24 +54,6 @@
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;

@ -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;

@ -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;
};

@ -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,

@ -52,6 +52,25 @@
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
{

@ -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;
}
}

@ -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;
}

@ -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

Loading…
Cancel
Save