// Copyright 2020 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// #include "src/piex_cr3.h" #include #include #include #include #include #include "src/binary_parse/range_checked_byte_ptr.h" #include "src/piex_types.h" #include "src/tiff_directory/tiff_directory.h" #include "src/tiff_parser.h" namespace piex { namespace { constexpr size_t kUuidSize = 16; using Uuid = std::array; // Uuid of uuid box under the moov box. constexpr Uuid kUuidMoov = {0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, 0x48}; // Uuid of uuid box containing PRVW box. constexpr Uuid kUuidPrvw = {0xea, 0xf4, 0x2b, 0x5e, 0x1c, 0x98, 0x4b, 0x88, 0xb9, 0xfb, 0xb7, 0xdc, 0x40, 0x6e, 0x4d, 0x16}; constexpr size_t kTagSize = 4; using BoxTag = std::array; constexpr BoxTag NewTag(const char s[kTagSize + 1]) { return BoxTag{s[0], s[1], s[2], s[3]}; } constexpr BoxTag kUuidTag = NewTag("uuid"); constexpr BoxTag kPrvwTag = NewTag("PRVW"); constexpr BoxTag kThmbTag = NewTag("THMB"); constexpr BoxTag kCmt1Tag = NewTag("CMT1"); constexpr BoxTag kCmt2Tag = NewTag("CMT2"); constexpr BoxTag kStblTag = NewTag("stbl"); constexpr BoxTag kStsdTag = NewTag("stsd"); constexpr BoxTag kCrawTag = NewTag("CRAW"); constexpr BoxTag kStszTag = NewTag("stsz"); constexpr BoxTag kCo64Tag = NewTag("co64"); constexpr BoxTag kMdatTag = NewTag("mdat"); // Convenience class for a box. class Box { public: Box() : is_valid_(false), tag_(BoxTag()), offset_(0), header_offset_(0), next_box_offset_(0) {} Box(const BoxTag& tag, size_t offset, size_t header_length, size_t length) : is_valid_(true), tag_(tag), offset_(offset), header_offset_(offset + header_length), next_box_offset_(offset + length) {} bool IsValid() const { return is_valid_ && next_box_offset_ > offset_; } const BoxTag& tag() const { return tag_; } // Returns offset from start of file. size_t offset() const { return offset_; } // Returns offset from start of file, including box's header. size_t header_offset() const { return header_offset_; } // Returns offset from start of file of the next box, accounting for size of // this box. size_t next_box_offset() const { return next_box_offset_; } private: bool is_valid_; BoxTag tag_; size_t offset_; size_t header_offset_; size_t next_box_offset_; }; struct ProcessData { PreviewImageData* preview_image_data = nullptr; Image mdat_image; Image prvw_image; }; // Wraps Get16u w/ assumption that CR3 is always big endian, based on // ISO/IEC 14496-12 specification that all box fields are big endian. bool Get16u(StreamInterface* stream, size_t offset, std::uint16_t* value) { return Get16u(stream, offset, tiff_directory::kBigEndian, value); } // Wraps Get32u w/ assumption that CR3 is always big endian, based on // ISO/IEC 14496-12 specification that all box fields are big endian. bool Get32u(StreamInterface* stream, size_t offset, std::uint32_t* value) { return Get32u(stream, offset, tiff_directory::kBigEndian, value); } // Always big endian, based on ISO/IEC 14496-12 specification that all box // fields are big endian. bool Get64u(StreamInterface* stream, size_t offset, std::uint64_t* value) { std::uint8_t data[8]; if (stream->GetData(offset, 8, data) == kOk) { *value = (data[0] * 0x1000000u) | (data[1] * 0x10000u) | (data[2] * 0x100u) | data[3]; *value <<= 32; *value = (data[4] * 0x1000000u) | (data[5] * 0x10000u) | (data[6] * 0x100u) | data[7]; return true; } else { return false; } } // Jpeg box offsets based on the box tag. The expected layout is as follows: // Byte Offset Type Meaning // 0 [long] size of box // 4 [char[]] box tag // offset.width [short] width of jpeg // offset.height [short] height of jpeg // offset.jpeg_size [long] number of bytes in jpeg // offset.jpeg_data [byte[]] start of jpeg data struct JpegBoxOffset { size_t width = 0; size_t height = 0; size_t jpeg_size = 0; size_t jpeg_data = 0; }; // Processes box w/ JPEG data. Box must be PRVW and THMB boxes. bool ProcessJpegBox(StreamInterface* stream, const Box& box, Image* image) { static constexpr JpegBoxOffset kPrvwJpegOffsets{14, 16, 20, 24}; static constexpr JpegBoxOffset kThmbJpegOffsets{12, 14, 16, 24}; if (box.tag() != kPrvwTag && box.tag() != kThmbTag) { return false; } const JpegBoxOffset& offsets = box.tag() == kPrvwTag ? kPrvwJpegOffsets : kThmbJpegOffsets; uint16_t width, height; uint32_t jpeg_size; if (!Get16u(stream, box.offset() + offsets.width, &width)) { return false; } if (!Get16u(stream, box.offset() + offsets.height, &height)) { return false; } if (!Get32u(stream, box.offset() + offsets.jpeg_size, &jpeg_size)) { return false; } image->format = Image::kJpegCompressed; image->width = width; image->height = height; image->offset = box.offset() + offsets.jpeg_data; image->length = jpeg_size; return true; } // Parses the Exif IFD0 tags at tiff_offset. bool ParseExifIfd0(StreamInterface* stream, size_t tiff_offset, PreviewImageData* preview_image_data) { static const TagSet kIfd0TagSet = {kTiffTagModel, kTiffTagMake, kTiffTagOrientation, kTiffTagImageWidth, kTiffTagImageLength}; TiffContent content; TiffParser(stream, tiff_offset).Parse(kIfd0TagSet, 1, &content); if (content.tiff_directory.size() != 1) { return false; } content.tiff_directory[0].Get(kTiffTagModel, &preview_image_data->model); content.tiff_directory[0].Get(kTiffTagMake, &preview_image_data->maker); content.tiff_directory[0].Get(kTiffTagOrientation, &preview_image_data->exif_orientation); content.tiff_directory[0].Get(kTiffTagImageWidth, &preview_image_data->full_width); content.tiff_directory[0].Get(kTiffTagImageLength, &preview_image_data->full_height); return true; } // Parses the Exif Exif IFD tags at tiff_offset. bool ParseExifExifIfd(StreamInterface* stream, size_t tiff_offset, PreviewImageData* preview_image_data) { static const TagSet kExifIfdTagSet = {kExifTagDateTimeOriginal, kExifTagExposureTime, kExifTagFnumber, kExifTagFocalLength, kExifTagIsoSpeed}; TiffContent content; TiffParser(stream, tiff_offset).Parse(kExifIfdTagSet, 1, &content); if (content.tiff_directory.size() != 1) { return false; } content.tiff_directory[0].Get(kExifTagDateTimeOriginal, &preview_image_data->date_time); GetRational(kExifTagExposureTime, content.tiff_directory[0], 1, &preview_image_data->exposure_time); GetRational(kExifTagFnumber, content.tiff_directory[0], 1, &preview_image_data->fnumber); GetRational(kExifTagFocalLength, content.tiff_directory[0], 1, &preview_image_data->focal_length); content.tiff_directory[0].Get(kExifTagIsoSpeed, &preview_image_data->iso); return true; } // Returns the next box or an invalid box. // // Based on ISO/IEC 14496-12: boxes start with a header: size and type. The size // can be compact (32-bits) or extended (64-bit, e.g. mdat box). // The type can be compact (32 bits) or extended (full UUID, e.g. uuid boxes). // values are stored after the compact size/type. // // Fields in a box are big-endian. Box GetNextBox(StreamInterface* stream, size_t offset) { uint32_t length_32; if (!Get32u(stream, offset, &length_32)) { return Box(); } BoxTag tag; Error status = stream->GetData(offset + sizeof(length_32), kTagSize, reinterpret_cast(tag.data())); if (status != kOk) { return Box(); } size_t length; size_t header_offset = sizeof(length_32) + sizeof(tag); if (length_32 == 1) { // Magic number of 1 implies extended size. uint64_t length_64 = 0; if (!Get64u(stream, offset + header_offset, &length_64)) { return Box(); } length = length_64; header_offset += sizeof(length_64); } else { // Compact size. length = length_32; } return Box(tag, offset, header_offset, length); } // Searches for the next box with the given tag. Box GetNextBoxWithTag(StreamInterface* stream, size_t offset, const BoxTag& expected_tag) { while (true) { Box box = GetNextBox(stream, offset); if (!box.IsValid() || box.tag() == expected_tag) { return box; } offset = box.next_box_offset(); } } // Returns the width, height, and content type from the CRAW box. bool ProcessCrawBox(StreamInterface* stream, const Box& craw_box, uint16_t* width, uint16_t* height, uint16_t* content_type) { constexpr size_t kWidthOffset = 32; if (!Get16u(stream, craw_box.offset() + kWidthOffset, width)) { return false; } constexpr size_t kHeightOffset = 34; if (!Get16u(stream, craw_box.offset() + kHeightOffset, height)) { return false; } constexpr size_t kTypeOffset = 86; if (!Get16u(stream, craw_box.offset() + kTypeOffset, content_type)) { return false; } return true; } // stsz box offset: // Byte Offset Type Meaning // 0 [long] size of box // 4 [char[]] box tag // 8 [long] version/flags // 12 [long] sample size // 16 [long] number of entries in sample table // 20 [long[]] sample table if samples size is 0 bool ProcessStszBox(StreamInterface* stream, const Box& stsz_box, uint32_t* image_size) { uint32_t sample_size; if (!Get32u(stream, stsz_box.offset() + 12, &sample_size)) { return false; } if (sample_size > 0) { *image_size = sample_size; return true; } // sample_size of 0 implies the data is in the sample table. We expect only // one entry. This is true of Canon EOS RP Cr3 files. uint32_t count; if (!Get32u(stream, stsz_box.offset() + 16, &count)) { return false; } if (count != 1) { // Expect at most one entry in the table. return false; } return Get32u(stream, stsz_box.offset() + 20, image_size); } // co64 box offsets: // Byte Offset Type Meaning // 0 [long] size of box // 4 [char[]] box tag // 8 [long] version // 12 [long] count (expect to be value 1) // 16 [long] offset of image data in mdat bool ProcessCo64(StreamInterface* stream, const Box& co64_box, uint32_t* image_offset) { uint32_t count = 0; if (!Get32u(stream, co64_box.header_offset() + 4, &count)) { return false; } if (count != 1) { return false; } return Get32u(stream, co64_box.header_offset() + 8, image_offset); } // Process the stbl box. Expected box layout: // stbl // stsd // CRAW (embedded image (JPEG) information) // (0 or more skipped boxes) // stsz (embedded image byte size) // (0 or more skipped boxes) // co64 (offset of embedded image, relative to mdat box) bool ProcessStblBox(StreamInterface* stream, const Box& stbl_box, ProcessData* data) { Box stsd_box = GetNextBoxWithTag(stream, stbl_box.header_offset(), kStsdTag); if (!stsd_box.IsValid()) { return false; } // This is either CRAW or CTMD. Skip when CTMD. Box craw_box = GetNextBox(stream, stsd_box.header_offset() + 8); if (!craw_box.IsValid()) { return false; } if (craw_box.tag() != kCrawTag) { return true; } // CRAW contains info about the full-size image embedded in the mdat box. // The image is either JPEG or HEVC. uint16_t image_width = 0; uint16_t image_height = 0; uint16_t content_type = 0; if (!ProcessCrawBox(stream, craw_box, &image_width, &image_height, &content_type)) { return false; } // Only continue if JPEG or HEVC content. constexpr uint16_t kJpegContentType = 3; constexpr uint16_t kHevcContentType = 4; if (content_type != kJpegContentType && content_type != kHevcContentType) { return true; } // Skip until we find stsz, contains the size (# of bytes) of image data. Box stsz_box = GetNextBoxWithTag(stream, stsd_box.next_box_offset(), kStszTag); if (!stsz_box.IsValid()) { return false; } uint32_t image_size; if (!ProcessStszBox(stream, stsz_box, &image_size)) { return false; } // Skip until we find co64, contains the offset of image data. Box co64_box = GetNextBoxWithTag(stream, stsz_box.next_box_offset(), kCo64Tag); if (!co64_box.IsValid()) { return false; } uint32_t image_offset = 0; if (!ProcessCo64(stream, co64_box, &image_offset)) { return false; } data->mdat_image.format = content_type == kJpegContentType ? Image::kJpegCompressed : Image::kHevcCompressed; data->mdat_image.width = image_width; data->mdat_image.height = image_height; data->mdat_image.length = image_size; // This offset is relative to the position of the mdat box. The value will // be updated once mdat's offset is found. data->mdat_image.offset = image_offset; return true; } // Returns true if we should parse the children of the box. bool DoProcessChildren(const BoxTag& tag) { static const std::set kTags = {NewTag("trak"), NewTag("moov"), NewTag("mdia"), NewTag("minf")}; return kTags.find(tag) != kTags.end(); } // Processes box and returns offset of the next box to process. // A return value of 0 indicates an error. // // Outline of hierarchy and important boxes: // ftyp // moov // uuid (id is kUuidMoov) // ... boxes we skip ... // CMT1 (EXIF data) // CMT2 (EXIF data) // ... boxes we skip ... // THMB (160x120 JPEG thumbnail, embedded in this box) // trak // tkhd // mdia // ... boxes we skip ... // minf // ... boxes we skip ... // stbl // stsd // CRAW (Full image preview, type (JPEG or HEVC), width, height. The // image data is found in mdat box, below.) // ... boxes we skip ... // stsz (Size of preview, in bytes) // ... boxes we skip ... // co64 (Location/offset of full preview data in mdat) // .. boxes we skip ... // uuid (id is kUuidPrvw) // PRVW (1620x1080 JPEG preview, embedded in this box) // mdat // Full image preview (JPEG or HEVC) // ... RAW image data ... size_t ProcessBox(StreamInterface* stream, const Box& box, ProcessData* data) { // Parse child boxes. if (box.tag() == kUuidTag) { // Uuid box have extended box types. Uuid uuid; if (stream->GetData(box.header_offset(), uuid.size(), uuid.data()) != kOk) { return 0; } if (uuid == kUuidPrvw) { return box.header_offset() + uuid.size() + 8; } else if (uuid == kUuidMoov) { return box.header_offset() + uuid.size(); } // else skip the box, below. } else if (DoProcessChildren(box.tag())) { return box.header_offset(); } // Potentially process the data contained in the box. bool success; if (box.tag() == kMdatTag) { // mdat_image.offset is relative to mdat's header, update it to be absolute // offset to the image data. data->mdat_image.offset += box.header_offset(); success = true; } else if (box.tag() == kStblTag) { success = ProcessStblBox(stream, box, data); } else if (box.tag() == kPrvwTag) { // Preview jpeg. 1620x1080 for EOS R. success = ProcessJpegBox(stream, box, &data->prvw_image); } else if (box.tag() == kThmbTag) { // Thumbnail jpeg. 160x120 for EOS R. success = ProcessJpegBox(stream, box, &data->preview_image_data->thumbnail); } else if (box.tag() == kCmt1Tag) { success = ParseExifIfd0(stream, box.header_offset(), data->preview_image_data); } else if (box.tag() == kCmt2Tag) { success = ParseExifExifIfd(stream, box.header_offset(), data->preview_image_data); } else { // This box isn't interesting, skip it. success = true; } return success ? box.next_box_offset() : 0; } bool ProcessStream(StreamInterface* stream, const BoxTag& last_chunk, ProcessData* data) { size_t offset = 0; while (true) { Box box = GetNextBox(stream, offset); if (!box.IsValid()) { return false; } size_t new_offset = ProcessBox(stream, box, data); if (new_offset <= offset) { return false; } if (box.tag() == last_chunk) { return true; } offset = new_offset; } } bool IsImage(StreamInterface* stream, const Image& image) { if (image.format != Image::kJpegCompressed) { // Pass responsibility to the caller. return true; } // Check for JPEG magic number at start. This could be HEVC data. constexpr std::array kJpegMagicNumber = {0xFF, 0xD8, 0xFF}; std::array magic_number; if (stream->GetData(image.offset, magic_number.size(), magic_number.data()) != kOk) { return false; } return magic_number == kJpegMagicNumber; } } // namespace Error Cr3GetPreviewData(StreamInterface* stream, PreviewImageData* preview_image_data) { ProcessData data{.preview_image_data = preview_image_data}; if (!ProcessStream(stream, kMdatTag, &data)) { return kFail; } // Prefer image in mdata box, as spec ensures it is the largest image. if (data.mdat_image.length > 0 && IsImage(stream, data.mdat_image)) { preview_image_data->preview = data.mdat_image; } else if (data.prvw_image.length > 0 && IsImage(stream, data.prvw_image)) { preview_image_data->preview = data.prvw_image; } else { return kFail; } return kOk; } bool Cr3GetOrientation(StreamInterface* stream, std::uint32_t* orientation) { PreviewImageData preview_image_data; ProcessData data{.preview_image_data = &preview_image_data}; if (ProcessStream(stream, kCmt1Tag, &data)) { *orientation = preview_image_data.exif_orientation; return true; } return false; } } // namespace piex