aboutsummaryrefslogtreecommitdiff
path: root/src/piex_cr3.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/piex_cr3.cc')
-rwxr-xr-xsrc/piex_cr3.cc559
1 files changed, 559 insertions, 0 deletions
diff --git a/src/piex_cr3.cc b/src/piex_cr3.cc
new file mode 100755
index 0000000..4fa82b7
--- /dev/null
+++ b/src/piex_cr3.cc
@@ -0,0 +1,559 @@
+// 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 <array>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <unordered_set>
+
+#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<std::uint8_t, kUuidSize>;
+// 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<char, kTagSize>;
+
+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<std::uint8_t*>(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<BoxTag> 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<uint8_t, 3> kJpegMagicNumber = {0xFF, 0xD8, 0xFF};
+ std::array<uint8_t, 3> 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