diff --git a/include/exiv2/bmffimage.hpp b/include/exiv2/bmffimage.hpp index d2a9ff5e..0a4553b4 100644 --- a/include/exiv2/bmffimage.hpp +++ b/include/exiv2/bmffimage.hpp @@ -106,6 +106,28 @@ namespace Exiv2 void parseXmp(uint64_t length,uint64_t start); //@} + //@{ + /*! + @brief Parse a Canon PRVW or THMB box and add an entry to the set + of native previews. + @param data Buffer containing the box + @param out Logging stream + @param bTrace Controls logging + @param width_offset Index of image width field in data + @param height_offset Index of image height field in data + @param size_offset Index of image size field in data + @param relative_position Location of the start of image data in the file, + relative to the current file position indicator. + */ + void parseCr3Preview(DataBuf &data, + std::ostream &out, + bool bTrace, + uint16_t width_offset, + uint16_t height_offset, + uint32_t size_offset, + uint16_t relative_position); + //@} + //! @name Manipulators //@{ void readMetadata() override /* override */; diff --git a/src/bmffimage.cpp b/src/bmffimage.cpp index 9e70c8db..b58fa44a 100644 --- a/src/bmffimage.cpp +++ b/src/bmffimage.cpp @@ -75,7 +75,9 @@ #define TAG_cmt4 0x434D5434 /**< "CMT4" gpsID */ #define TAG_colr 0x636f6c72 /**< "colr" */ #define TAG_exif 0x45786966 /**< "Exif" Used by JXL*/ -#define TAG_xml 0x786d6c20 /**< "xml" Used by JXL*/ +#define TAG_xml 0x786d6c20 /**< "xml " Used by JXL*/ +#define TAG_thmb 0x54484d42 /**< "THMB" Canon thumbnail */ +#define TAG_prvw 0x50525657 /**< "PRVW" Canon preview image */ // ***************************************************************************** // class member definitions @@ -429,7 +431,12 @@ namespace Exiv2 out << " uuidName " << name << std::endl; bLF = false; } - if (name == "cano") { + if (name == "cano" || name == "canp" ) { + if (name == "canp") { + // based on + // https://github.com/lclevy/canon_cr3/blob/7be75d6/parse_cr3.py#L271 + io_->seek(8, BasicIo::cur); + } while (io_->tell() < box_end) { io_->seek(boxHandler(out,option,box_end,depth + 1), BasicIo::beg); } @@ -456,10 +463,16 @@ namespace Exiv2 case TAG_xml: parseXmp(box_length,io_->tell()); break; + case TAG_thmb: + parseCr3Preview(data, out, bTrace, 4, 6, 8, 16); + break; + case TAG_prvw: + parseCr3Preview(data, out, bTrace, 6, 8, 12, 16); + break; default: break ; /* do nothing */ } - if ( bLF&& bTrace) out << std::endl; + if (bLF && bTrace) out << std::endl; // return address of next box return box_end; @@ -540,6 +553,36 @@ namespace Exiv2 } } + void BmffImage::parseCr3Preview(DataBuf &data, + std::ostream& out, + bool bTrace, + uint16_t width_offset, + uint16_t height_offset, + uint32_t size_offset, + uint16_t relative_position) + { + // Derived from https://github.com/lclevy/canon_cr3 + NativePreview nativePreview; + long here = io_->tell(); + enforce(here >= 0 && + here <= std::numeric_limits::max() - relative_position, + kerCorruptedMetadata); + nativePreview.position_ = here + relative_position; + nativePreview.width_ = data.read_uint16(width_offset, endian_); + nativePreview.height_ = data.read_uint16(height_offset, endian_); + nativePreview.size_ = data.read_uint32(size_offset, endian_); + nativePreview.filter_ = ""; + nativePreview.mimeType_ = "image/jpeg"; + nativePreviews_.push_back(nativePreview); + + if (bTrace) { + out << Internal::stringFormat("width,height,size = %u,%u,%u", + nativePreview.width_, + nativePreview.height_, + nativePreview.size_); + } + } + void BmffImage::setComment(const std::string& /*comment*/) { // bmff files are read-only diff --git a/test/data/Canon-R6-pruned.CR3 b/test/data/Canon-R6-pruned.CR3 new file mode 100644 index 00000000..6aa5e23f Binary files /dev/null and b/test/data/Canon-R6-pruned.CR3 differ diff --git a/tests/bugfixes/github/test_issue_1893.py b/tests/bugfixes/github/test_issue_1893.py new file mode 100644 index 00000000..d888cae7 --- /dev/null +++ b/tests/bugfixes/github/test_issue_1893.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +import system_tests +import unittest +from tempfile import TemporaryDirectory +import shutil +import hashlib +import os + +# test needs system_tests.BT.vv['enable_bmff']=1 +bSkip=system_tests.BT.verbose_version().get('enable_bmff')!='1' + +if bSkip: + raise unittest.SkipTest('*** requires enable_bmff=1 ***') + +file_basename = 'Canon-R6-pruned.CR3' +previews_expected = ( + ('Canon-R6-pruned-preview1.jpg', 'a182ef12ac883309b4dfc66b87eac1891286d3ae'), + ('Canon-R6-pruned-preview2.jpg', '524a07f1797854e349ae140e2682ba37147fa6b2') +) + +class issue_1893_cr3_preview(metaclass=system_tests.CaseMeta): + """ + Check that THMB and PRVW images are extracted from Canon CR3 files + """ + url = "https://github.com/Exiv2/exiv2/issues/1893" + filename = "$data_path/" + file_basename + commands=[] # see setUp() + + if bSkip: + retval=[] + stdin=[] + stderr=[] + stdout=[] + print("*** test skipped. requires enable_bmff=1***") + else: + retval = [ 0, 0] + stderr = [ "",""] + stdin = [ "", ""] + stdout = ["""Preview 1: image/jpeg, 160x120 pixels, 16005 bytes +Preview 2: image/jpeg, 1620x1080 pixels, 389450 bytes +""", ""] + + def post_tests_hook(self): + if self.commands: + for j, sha1 in previews_expected: + p = os.path.join(self.preview_image_tmp_dir.name, j) + self.assertTrue(os.path.isfile(p)) + h = hashlib.sha1(open(p, 'rb').read()).hexdigest() + self.assertEqual(h, sha1) + + def setUp(self): + if bSkip: + return + # Avoid polluting the test data directory with extracted previews + self.preview_image_tmp_dir = TemporaryDirectory() + shutil.copy(self.expand_variables(self.filename), + self.preview_image_tmp_dir.name) + p = os.path.join( + self.preview_image_tmp_dir.name, + file_basename) + self.commands = [ + self.expand_variables("$exiv2 -pp ") + p, + self.expand_variables("$exiv2 -ep ") + p + ]