From a1b85d566521a4a03c7c715d2851c4272da4e301 Mon Sep 17 00:00:00 2001 From: Nick Chusid Date: Fri, 6 Nov 2020 13:34:55 -0800 Subject: [PATCH] Add support for CR3s with embedded heif thumbnails --- LICENSE | 0 README | 0 src/binary_parse/cached_paged_byte_array.cc | 0 src/binary_parse/cached_paged_byte_array.h | 0 src/binary_parse/range_checked_byte_ptr.cc | 0 src/binary_parse/range_checked_byte_ptr.h | 0 .../image_type_recognition_lite.cc | 21 + .../image_type_recognition_lite.h | 1 + src/piex.cc | 28 +- src/piex.h | 8 +- src/piex_cr3.cc | 559 ++++++++++++++++++ src/piex_cr3.h | 43 ++ src/piex_types.h | 1 + src/tiff_directory/tiff_directory.cc | 0 src/tiff_directory/tiff_directory.h | 0 src/tiff_parser.cc | 28 +- src/tiff_parser.h | 6 + 17 files changed, 672 insertions(+), 23 deletions(-) mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README mode change 100644 => 100755 src/binary_parse/cached_paged_byte_array.cc mode change 100644 => 100755 src/binary_parse/cached_paged_byte_array.h mode change 100644 => 100755 src/binary_parse/range_checked_byte_ptr.cc mode change 100644 => 100755 src/binary_parse/range_checked_byte_ptr.h mode change 100644 => 100755 src/image_type_recognition/image_type_recognition_lite.cc mode change 100644 => 100755 src/image_type_recognition/image_type_recognition_lite.h mode change 100644 => 100755 src/piex.cc mode change 100644 => 100755 src/piex.h create mode 100755 src/piex_cr3.cc create mode 100755 src/piex_cr3.h mode change 100644 => 100755 src/piex_types.h mode change 100644 => 100755 src/tiff_directory/tiff_directory.cc mode change 100644 => 100755 src/tiff_directory/tiff_directory.h mode change 100644 => 100755 src/tiff_parser.cc mode change 100644 => 100755 src/tiff_parser.h diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README b/README old mode 100644 new mode 100755 diff --git a/src/binary_parse/cached_paged_byte_array.cc b/src/binary_parse/cached_paged_byte_array.cc old mode 100644 new mode 100755 diff --git a/src/binary_parse/cached_paged_byte_array.h b/src/binary_parse/cached_paged_byte_array.h old mode 100644 new mode 100755 diff --git a/src/binary_parse/range_checked_byte_ptr.cc b/src/binary_parse/range_checked_byte_ptr.cc old mode 100644 new mode 100755 diff --git a/src/binary_parse/range_checked_byte_ptr.h b/src/binary_parse/range_checked_byte_ptr.h old mode 100644 new mode 100755 diff --git a/src/image_type_recognition/image_type_recognition_lite.cc b/src/image_type_recognition/image_type_recognition_lite.cc old mode 100644 new mode 100755 index c56c1c0..cb32e1c --- a/src/image_type_recognition/image_type_recognition_lite.cc +++ b/src/image_type_recognition/image_type_recognition_lite.cc @@ -201,6 +201,25 @@ class ArwTypeChecker : public TypeChecker { } }; +// Canon RAW (CR3 extension). +class Cr3TypeChecker : public TypeChecker { + public: + static constexpr size_t kSignatureOffset = 4; + static constexpr const char* kSignature = "ftypcrx "; + + virtual RawImageTypes Type() const { return kCr3Image; } + + virtual size_t RequestedSize() const { + return kSignatureOffset + strlen(kSignature); + } + + // Checks for the ftyp box w/ brand 'crx '. + virtual bool IsMyType(const RangeCheckedBytePtr& source) const { + RangeCheckedBytePtr limited_source = LimitSource(source); + return IsSignatureMatched(limited_source, kSignatureOffset, kSignature); + } +}; + // Canon RAW (CR2 extension). class Cr2TypeChecker : public TypeChecker { public: @@ -749,6 +768,7 @@ class TypeCheckerList { TypeCheckerList() { // Add all supported RAW type checkers here. checkers_.push_back(new ArwTypeChecker()); + checkers_.push_back(new Cr3TypeChecker()); checkers_.push_back(new Cr2TypeChecker()); checkers_.push_back(new CrwTypeChecker()); checkers_.push_back(new DcrTypeChecker()); @@ -841,6 +861,7 @@ bool IsRaw(const RawImageTypes type) { // Raw image types case kArwImage: + case kCr3Image: case kCr2Image: case kCrwImage: case kDcrImage: diff --git a/src/image_type_recognition/image_type_recognition_lite.h b/src/image_type_recognition/image_type_recognition_lite.h old mode 100644 new mode 100755 index a7e212d..30db915 --- a/src/image_type_recognition/image_type_recognition_lite.h +++ b/src/image_type_recognition/image_type_recognition_lite.h @@ -40,6 +40,7 @@ enum RawImageTypes { // raw image types kArwImage, kCr2Image, + kCr3Image, kCrwImage, kDcrImage, kDngImage, diff --git a/src/piex.cc b/src/piex.cc old mode 100644 new mode 100755 index 338e581..4b868d9 --- a/src/piex.cc +++ b/src/piex.cc @@ -23,6 +23,7 @@ #include "src/binary_parse/range_checked_byte_ptr.h" #include "src/image_type_recognition/image_type_recognition_lite.h" +#include "src/piex_cr3.h" #include "src/tiff_parser.h" namespace piex { @@ -649,7 +650,8 @@ bool IsRaw(StreamInterface* data) { } Error GetPreviewImageData(StreamInterface* data, - PreviewImageData* preview_image_data) { + PreviewImageData* preview_image_data, + RawImageTypes* output_type) { const size_t bytes = BytesRequiredForIsRaw(); if (data == nullptr || bytes == 0) { return kFail; @@ -662,11 +664,15 @@ Error GetPreviewImageData(StreamInterface* data, } RangeCheckedBytePtr header_buffer(file_header.data(), file_header.size()); - switch (RecognizeRawImageTypeLite(header_buffer)) { + RawImageTypes type = RecognizeRawImageTypeLite(header_buffer); + if (output_type != nullptr) *output_type = type; + switch (type) { case image_type_recognition::kArwImage: return ArwGetPreviewData(data, preview_image_data); case image_type_recognition::kCr2Image: return Cr2GetPreviewData(data, preview_image_data); + case image_type_recognition::kCr3Image: + return Cr3GetPreviewData(data, preview_image_data); case image_type_recognition::kDngImage: return DngGetPreviewData(data, preview_image_data); case image_type_recognition::kNefImage: @@ -703,24 +709,32 @@ bool GetOrientation(StreamInterface* data, std::uint32_t* orientation) { using image_type_recognition::GetNumberOfBytesForIsOfType; using image_type_recognition::IsOfType; - std::vector file_header( - GetNumberOfBytesForIsOfType(image_type_recognition::kRafImage)); + size_t min_header_bytes = + std::max(GetNumberOfBytesForIsOfType(image_type_recognition::kRafImage), + GetNumberOfBytesForIsOfType(image_type_recognition::kCr3Image)); + + std::vector file_header(min_header_bytes); if (data->GetData(0, file_header.size(), file_header.data()) != kOk) { return false; } - // For RAF files a special routine is necessary to get orientation. For others - // the general approach is sufficient. + // For RAF and CR# files a special routine is necessary to get orientation. + // For others the general approach is sufficient. if (IsOfType(RangeCheckedBytePtr(file_header.data(), file_header.size()), image_type_recognition::kRafImage)) { return RafGetOrientation(data, orientation); + } else if (IsOfType( + RangeCheckedBytePtr(file_header.data(), file_header.size()), + image_type_recognition::kCr3Image)) { + return Cr3GetOrientation(data, orientation); } else { return GetExifOrientation(data, 0 /* offset */, orientation); } } std::vector SupportedExtensions() { - return {"ARW", "CR2", "DNG", "NEF", "NRW", "ORF", "PEF", "RAF", "RW2", "SRW"}; + return {"ARW", "CR2", "CR3", "DNG", "NEF", "NRW", + "ORF", "PEF", "RAF", "RW2", "SRW"}; } } // namespace piex diff --git a/src/piex.h b/src/piex.h old mode 100644 new mode 100755 index 3225421..8d74ca0 --- a/src/piex.h +++ b/src/piex.h @@ -49,6 +49,7 @@ #include #include +#include "src/image_type_recognition/image_type_recognition_lite.h" #include "src/piex_types.h" namespace piex { @@ -70,8 +71,11 @@ bool IsRaw(StreamInterface* data); // // One could check the "preview_image_data->preview_length != 0" for the // existance of a preview image. -Error GetPreviewImageData(StreamInterface* data, - PreviewImageData* preview_image_data); +// +// Updates output_type based on data, if output_type is non-null. +Error GetPreviewImageData( + StreamInterface* data, PreviewImageData* preview_image_data, + image_type_recognition::RawImageTypes* output_type = nullptr); // Returns true if the full width and height and the mosaic pattern dimension of // a DNG image could be obtained. False otherwise. diff --git a/src/piex_cr3.cc b/src/piex_cr3.cc new file mode 100755 index 0000000..1e9a50a --- /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 +#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, 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}; + 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}; + if (ProcessStream(stream, kCmt1Tag, &data)) { + *orientation = preview_image_data.exif_orientation; + return true; + } + return false; +} + +} // namespace piex diff --git a/src/piex_cr3.h b/src/piex_cr3.h new file mode 100755 index 0000000..3108503 --- /dev/null +++ b/src/piex_cr3.h @@ -0,0 +1,43 @@ +// 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_types.h" + +#ifndef PIEX_PIEX_CR3_H_ +#define PIEX_PIEX_CR3_H_ + +namespace piex { + +// Gets the EXIF orientation of a CR3 stream, returning true on success. +bool Cr3GetOrientation(StreamInterface* stream, std::uint32_t* orientation); + +// Gets preview images of a CR3 stream, returning kOk on success. Assumes the +// stream is a CR3 stream. +// +// Canon's CR3 is based on ISO/IEC 14496-12: ISO base media file format. (CR2 is +// TIFF based.) A Canon CR3 contains multiple embedded images. Most cameras +// output CR3 files that contain a full-size JPEG, a 1620x1080 preview JPEG, and +// a 160x120 thumbnail JPEG. +// The Canon EOS 1D X Mark III, though, contains a full-size HEVC image, a +// 1620x1080 preview JPEG, and a 160x120 thumbnail JPEG. +// Until support for HEVC is added, this method returns the largest embedded +// JPEG in preview_image_data->preview. +// +Error Cr3GetPreviewData(StreamInterface* stream, + PreviewImageData* preview_image_data); +} // namespace piex + +#endif // PIEX_PIEX_CR3_H_ diff --git a/src/piex_types.h b/src/piex_types.h old mode 100644 new mode 100755 index 4fdb7c2..2062136 --- a/src/piex_types.h +++ b/src/piex_types.h @@ -37,6 +37,7 @@ struct Image { enum Format { kJpegCompressed, kUncompressedRgb, + kHevcCompressed, }; std::uint16_t width = 0; diff --git a/src/tiff_directory/tiff_directory.cc b/src/tiff_directory/tiff_directory.cc old mode 100644 new mode 100755 diff --git a/src/tiff_directory/tiff_directory.h b/src/tiff_directory/tiff_directory.h old mode 100644 new mode 100755 diff --git a/src/tiff_parser.cc b/src/tiff_parser.cc old mode 100644 new mode 100755 index f36c5ba..fc63461 --- a/src/tiff_parser.cc +++ b/src/tiff_parser.cc @@ -54,20 +54,6 @@ bool GetFullDimension16(const TiffDirectory& tiff_directory, return true; } -bool GetRational(const TiffDirectory::Tag& tag, const TiffDirectory& directory, - const int data_size, PreviewImageData::Rational* data) { - std::vector value; - if (directory.Get(tag, &value) && - value.size() == static_cast(data_size)) { - for (size_t i = 0; i < value.size(); ++i) { - data[i].numerator = value[i].numerator; - data[i].denominator = value[i].denominator; - } - return true; - } - return false; -} - void FillGpsPreviewImageData(const TiffDirectory& gps_directory, PreviewImageData* preview_image_data) { if (gps_directory.Has(kGpsTagLatitudeRef) && @@ -463,6 +449,20 @@ bool GetJpegDimensions(const std::uint32_t jpeg_offset, StreamInterface* stream, return false; } +bool GetRational(const TiffDirectory::Tag& tag, const TiffDirectory& directory, + const int data_size, PreviewImageData::Rational* data) { + std::vector value; + if (directory.Get(tag, &value) && + value.size() == static_cast(data_size)) { + for (size_t i = 0; i < value.size(); ++i) { + data[i].numerator = value[i].numerator; + data[i].denominator = value[i].denominator; + } + return true; + } + return false; +} + bool IsThumbnail(const Image& image, const int max_dimension) { return image.width <= max_dimension && image.height <= max_dimension; } diff --git a/src/tiff_parser.h b/src/tiff_parser.h old mode 100644 new mode 100755 index e809274..f89c319 --- a/src/tiff_parser.h +++ b/src/tiff_parser.h @@ -167,6 +167,12 @@ bool GetFullDimension32(const tiff_directory::TiffDirectory& tiff_directory, bool GetFullCropDimension(const tiff_directory::TiffDirectory& tiff_directory, std::uint32_t* width, std::uint32_t* height); +// Reads 1 or more rational values for a tag and stores results into data. +// Returns false if an error occurred. +bool GetRational(const tiff_directory::TiffDirectory::Tag& tag, + const tiff_directory::TiffDirectory& directory, + const int data_size, PreviewImageData::Rational* data); + // Enables us to parse through data that complies to the Tiff/EP specification. class TiffParser { public: