// SPDX-License-Identifier: GPL-2.0-or-later // included header files #include "bmffimage.hpp" #include "basicio.hpp" #include "config.h" #include "enforce.hpp" #include "error.hpp" #include "futils.hpp" #include "image.hpp" #include "image_int.hpp" #include "safe_op.hpp" #include "tiffimage.hpp" #include "tiffimage_int.hpp" #include "types.hpp" #include "utils.hpp" #ifdef EXV_HAVE_BROTLI #include // for JXL brob #endif // + standard includes #include #include #include #include #include enum { TAG_ftyp = 0x66747970U, ///< "ftyp" File type box */ TAG_avif = 0x61766966U, ///< "avif" AVIF */ TAG_avio = 0x6176696fU, ///< "avio" AVIF */ TAG_avis = 0x61766973U, ///< "avis" AVIF */ TAG_heic = 0x68656963U, ///< "heic" HEIC */ TAG_heif = 0x68656966U, ///< "heif" HEIF */ TAG_heim = 0x6865696dU, ///< "heim" HEIC */ TAG_heis = 0x68656973U, ///< "heis" HEIC */ TAG_heix = 0x68656978U, ///< "heix" HEIC */ TAG_mif1 = 0x6d696631U, ///< "mif1" HEIF */ TAG_crx = 0x63727820U, ///< "crx " Canon CR3 */ TAG_jxl = 0x6a786c20U, ///< "jxl " JPEG XL file type */ TAG_moov = 0x6d6f6f76U, ///< "moov" Movie */ TAG_meta = 0x6d657461U, ///< "meta" Metadata */ TAG_mdat = 0x6d646174U, ///< "mdat" Media data */ TAG_uuid = 0x75756964U, ///< "uuid" UUID */ TAG_dinf = 0x64696e66U, ///< "dinf" Data information */ TAG_iprp = 0x69707270U, ///< "iprp" Item properties */ TAG_ipco = 0x6970636fU, ///< "ipco" Item property container */ TAG_iinf = 0x69696e66U, ///< "iinf" Item info */ TAG_iloc = 0x696c6f63U, ///< "iloc" Item location */ TAG_ispe = 0x69737065U, ///< "ispe" Image spatial extents */ TAG_infe = 0x696e6665U, ///< "infe" Item Info Extension */ TAG_ipma = 0x69706d61U, ///< "ipma" Item Property Association */ TAG_cmt1 = 0x434d5431U, ///< "CMT1" ifd0Id */ TAG_cmt2 = 0x434D5432U, ///< "CMD2" exifID */ TAG_cmt3 = 0x434D5433U, ///< "CMT3" canonID */ TAG_cmt4 = 0x434D5434U, ///< "CMT4" gpsID */ TAG_colr = 0x636f6c72U, ///< "colr" Colour information */ TAG_exif = 0x45786966U, ///< "Exif" Used by JXL */ TAG_xml = 0x786d6c20U, ///< "xml " Used by JXL */ TAG_brob = 0x62726f62U, ///< "brob" Used by JXL (brotli box) */ TAG_thmb = 0x54484d42U, ///< "THMB" Canon thumbnail */ TAG_prvw = 0x50525657U, ///< "PRVW" Canon preview image */ }; // ***************************************************************************** // class member definitions #ifndef EXV_ENABLE_BMFF namespace Exiv2 { bool enableBMFF(bool) { return false; } } // namespace Exiv2 #else namespace Exiv2 { static bool enabled = false; bool enableBMFF(bool enable) { enabled = enable; return true; } std::string Iloc::toString() const { return Internal::stringFormat("ID = %u from,length = %u,%u", ID_, start_, length_); } BmffImage::BmffImage(BasicIo::UniquePtr io, bool /* create */) : Image(ImageType::bmff, mdExif | mdIptc | mdXmp, std::move(io)) { } // BmffImage::BmffImage std::string BmffImage::toAscii(uint32_t n) { const auto p = reinterpret_cast(&n); std::string result(p, p + 4); if (!isBigEndianPlatform()) std::reverse(result.begin(), result.end()); // show 0 as _ std::replace(result.begin(), result.end(), '\0', '_'); // show non 7-bit printable ascii as . std::replace_if( result.begin(), result.end(), [](char c) { return c < 32 || c > 126; }, '.'); return result; } bool BmffImage::superBox(uint32_t box) { return box == TAG_moov || box == TAG_dinf || box == TAG_iprp || box == TAG_ipco || box == TAG_meta || box == TAG_iinf || box == TAG_iloc; } bool BmffImage::fullBox(uint32_t box) { return box == TAG_meta || box == TAG_iinf || box == TAG_iloc || box == TAG_thmb || box == TAG_prvw; } static bool skipBox(uint32_t box) { // Allows boxHandler() to optimise the reading of files by identifying // box types that we're not interested in. Box types listed here must // not appear in the cases in switch (box_type) in boxHandler(). return box == 0 || box == TAG_mdat; // mdat is where the main image lives and can be huge } std::string BmffImage::mimeType() const { switch (fileType_) { case TAG_avif: case TAG_avio: case TAG_avis: return "image/avif"; case TAG_heic: case TAG_heim: case TAG_heis: case TAG_heix: return "image/heic"; case TAG_heif: case TAG_mif1: return "image/heif"; case TAG_crx: return "image/x-canon-cr3"; case TAG_jxl: return "image/jxl"; // https://github.com/novomesk/qt-jpegxl-image-plugin/issues/1 default: return "image/generic"; } } uint32_t BmffImage::pixelWidth() const { auto imageWidth = exifData_.findKey(Exiv2::ExifKey("Exif.Photo.PixelXDimension")); if (imageWidth == exifData_.end() || imageWidth->count() == 0) return pixelWidth_; return imageWidth->toUint32(); } uint32_t BmffImage::pixelHeight() const { auto imageHeight = exifData_.findKey(Exiv2::ExifKey("Exif.Photo.PixelYDimension")); if (imageHeight == exifData_.end() || imageHeight->count() == 0) return pixelHeight_; return imageHeight->toUint32(); } std::string BmffImage::uuidName(const Exiv2::DataBuf& uuid) { const char* uuidCano = "\x85\xC0\xB6\x87\x82\xF\x11\xE0\x81\x11\xF4\xCE\x46\x2B\x6A\x48"; const char* uuidXmp = "\xBE\x7A\xCF\xCB\x97\xA9\x42\xE8\x9C\x71\x99\x94\x91\xE3\xAF\xAC"; const char* uuidCanp = "\xEA\xF4\x2B\x5E\x1C\x98\x4B\x88\xB9\xFB\xB7\xDC\x40\x6E\x4D\x16"; if (uuid.cmpBytes(0, uuidCano, 16) == 0) return "cano"; if (uuid.cmpBytes(0, uuidXmp, 16) == 0) return "xmp"; if (uuid.cmpBytes(0, uuidCanp, 16) == 0) return "canp"; return ""; } #ifdef EXV_HAVE_BROTLI // Wrapper class for BrotliDecoderState that automatically calls // BrotliDecoderDestroyInstance in its destructor. class BrotliDecoderWrapper { BrotliDecoderState* decoder_; public: BrotliDecoderWrapper() : decoder_(BrotliDecoderCreateInstance(nullptr, nullptr, nullptr)) { if (!decoder_) { throw Error(ErrorCode::kerMallocFailed); } } ~BrotliDecoderWrapper() { BrotliDecoderDestroyInstance(decoder_); } BrotliDecoderWrapper(const BrotliDecoderWrapper&) = delete; BrotliDecoderWrapper& operator=(const BrotliDecoderWrapper&) = delete; [[nodiscard]] BrotliDecoderState* get() const { return decoder_; } }; void BmffImage::brotliUncompress(const byte* compressedBuf, size_t compressedBufSize, DataBuf& arr) { BrotliDecoderWrapper decoder; size_t uncompressedLen = compressedBufSize * 2; // just a starting point BrotliDecoderResult result; int dos = 0; size_t available_in = compressedBufSize; const byte* next_in = compressedBuf; size_t available_out; byte* next_out; size_t total_out = 0; do { arr.alloc(uncompressedLen); available_out = uncompressedLen - total_out; next_out = arr.data() + total_out; result = BrotliDecoderDecompressStream(decoder.get(), &available_in, &next_in, &available_out, &next_out, &total_out); if (result == BROTLI_DECODER_RESULT_SUCCESS) { arr.resize(total_out); } else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { uncompressedLen *= 2; // DoS protection - can't be bigger than 128k if (uncompressedLen > 131072) { if (++dos > 1) break; uncompressedLen = 131072; } } else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) { // compressed input buffer in incomplete throw Error(ErrorCode::kerFailedToReadImageData); } else { // something bad happened throw Error(ErrorCode::kerErrorMessage, BrotliDecoderErrorString(BrotliDecoderGetErrorCode(decoder.get()))); } } while (result != BROTLI_DECODER_RESULT_SUCCESS); if (result != BROTLI_DECODER_RESULT_SUCCESS) { throw Error(ErrorCode::kerFailedToReadImageData); } } #endif uint64_t BmffImage::boxHandler(std::ostream& out /* = std::cout*/, Exiv2::PrintStructureOption option /* = kpsNone */, uint64_t pbox_end, size_t depth) { const size_t address = io_->tell(); // never visit a box twice! if (depth == 0) visits_.clear(); if (visits_.find(address) != visits_.end() || visits_.size() > visits_max_) { throw Error(ErrorCode::kerCorruptedMetadata); } visits_.insert(address); #ifdef EXIV2_DEBUG_MESSAGES bool bTrace = true; #else bool bTrace = option == kpsBasic || option == kpsRecursive; #endif // 8-byte buffer for parsing the box length and type. byte hdrbuf[2 * sizeof(uint32_t)]; size_t hdrsize = sizeof(hdrbuf); Internal::enforce(hdrsize <= static_cast(pbox_end - address), Exiv2::ErrorCode::kerCorruptedMetadata); if (io_->read(hdrbuf, sizeof(hdrbuf)) != sizeof(hdrbuf)) return pbox_end; // The box length is encoded as a uint32_t by default, but the special value 1 means // that it's a uint64_t. uint64_t box_length = getULong(&hdrbuf[0], endian_); uint32_t box_type = getULong(&hdrbuf[sizeof(uint32_t)], endian_); bool bLF = true; if (bTrace) { bLF = true; out << Internal::indent(depth) << "Exiv2::BmffImage::boxHandler: " << toAscii(box_type) << Internal::stringFormat(" %8zd->%" PRIu64 " ", address, box_length); } if (box_length == 1) { // The box size is encoded as a uint64_t, so we need to read another 8 bytes. hdrsize += 8; Internal::enforce(hdrsize <= static_cast(pbox_end - address), Exiv2::ErrorCode::kerCorruptedMetadata); DataBuf data(8); io_->read(data.data(), data.size()); box_length = data.read_uint64(0, endian_); } if (box_length == 0) { // Zero length is also valid and indicates box extends to the end of file. box_length = pbox_end - address; } // read data in box and restore file position const size_t restore = io_->tell(); Internal::enforce(box_length >= hdrsize, Exiv2::ErrorCode::kerCorruptedMetadata); Internal::enforce(box_length - hdrsize <= pbox_end - restore, Exiv2::ErrorCode::kerCorruptedMetadata); const auto buffer_size = box_length - hdrsize; if (skipBox(box_type)) { if (bTrace) { out << std::endl; } // The enforce() above checks that restore + buffer_size won't // exceed pbox_end, and by implication, won't exceed LONG_MAX return restore + buffer_size; } DataBuf data(static_cast(buffer_size)); const size_t box_end = restore + data.size(); io_->read(data.data(), data.size()); io_->seek(restore, BasicIo::beg); size_t skip = 0; // read position in data.pData_ uint8_t version = 0; uint32_t flags = 0; if (fullBox(box_type)) { Internal::enforce(data.size() - skip >= 4, Exiv2::ErrorCode::kerCorruptedMetadata); flags = data.read_uint32(skip, endian_); // version/flags version = static_cast(flags >> 24); flags &= 0x00ffffff; skip += 4; } switch (box_type) { // See notes in skipBox() case TAG_ftyp: { Internal::enforce(data.size() >= 4, Exiv2::ErrorCode::kerCorruptedMetadata); fileType_ = data.read_uint32(0, endian_); if (bTrace) { out << "brand: " << toAscii(fileType_); } } break; // 8.11.6.1 case TAG_iinf: { if (bTrace) { out << std::endl; bLF = false; } Internal::enforce(data.size() - skip >= 2, Exiv2::ErrorCode::kerCorruptedMetadata); uint16_t n = data.read_uint16(skip, endian_); skip += 2; io_->seek(skip, BasicIo::cur); while (n-- > 0) { io_->seek(boxHandler(out, option, box_end, depth + 1), BasicIo::beg); } } break; // 8.11.6.2 case TAG_infe: { // .__._.__hvc1_ 2 0 0 1 0 1 0 0 104 118 99 49 0 Internal::enforce(data.size() - skip >= 8, Exiv2::ErrorCode::kerCorruptedMetadata); /* getULong (data.pData_+skip,endian_) ; */ skip += 4; uint16_t ID = data.read_uint16(skip, endian_); skip += 2; /* getShort(data.pData_+skip,endian_) ; */ skip += 2; // protection std::string id; // Check that the string has a '\0' terminator. const char* str = data.c_str(skip); const size_t maxlen = data.size() - skip; Internal::enforce(maxlen > 0 && strnlen(str, maxlen) < maxlen, Exiv2::ErrorCode::kerCorruptedMetadata); std::string name(str); if (Internal::contains(name, "Exif")) { // "Exif" or "ExifExif" exifID_ = ID; id = " *** Exif ***"; } else if (Internal::contains(name, "mime\0xmp") || Internal::contains(name, "mime\0application/rdf+xml")) { xmpID_ = ID; id = " *** XMP ***"; } if (bTrace) { out << Internal::stringFormat("ID = %3d ", ID) << name << " " << id; } } break; case TAG_moov: case TAG_iprp: case TAG_ipco: case TAG_meta: { if (bTrace) { out << std::endl; bLF = false; } io_->seek(skip, BasicIo::cur); while (io_->tell() < box_end) { io_->seek(boxHandler(out, option, box_end, depth + 1), BasicIo::beg); } // post-process meta box to recover Exif and XMP if (box_type == TAG_meta) { if (ilocs_.find(exifID_) != ilocs_.end()) { const Iloc& iloc = ilocs_.find(exifID_)->second; if (bTrace) { out << Internal::indent(depth) << "Exiv2::BMFF Exif: " << iloc.toString() << std::endl; } parseTiff(Internal::Tag::root, iloc.length_, iloc.start_); } if (ilocs_.find(xmpID_) != ilocs_.end()) { const Iloc& iloc = ilocs_.find(xmpID_)->second; if (bTrace) { out << Internal::indent(depth) << "Exiv2::BMFF XMP: " << iloc.toString() << std::endl; } parseXmp(iloc.length_, iloc.start_); } ilocs_.clear(); } } break; // 8.11.3.1 case TAG_iloc: { Internal::enforce(data.size() - skip >= 2, Exiv2::ErrorCode::kerCorruptedMetadata); uint8_t u = data.read_uint8(skip++); uint16_t offsetSize = u >> 4; uint16_t lengthSize = u & 0xF; #if 0 uint16_t indexSize = 0 ; u = data.read_uint8(skip++); if ( version == 1 || version == 2 ) { indexSize = u & 0xF ; } #else skip++; #endif Internal::enforce(data.size() - skip >= (version < 2u ? 2u : 4u), Exiv2::ErrorCode::kerCorruptedMetadata); uint32_t itemCount = version < 2 ? data.read_uint16(skip, endian_) : data.read_uint32(skip, endian_); skip += version < 2 ? 2 : 4; if (itemCount && itemCount < box_length / 14 && offsetSize == 4 && lengthSize == 4 && ((box_length - 16) % itemCount) == 0) { if (bTrace) { out << std::endl; bLF = false; } auto step = (static_cast(box_length) - 16) / itemCount; // length of data per item. size_t base = skip; for (uint32_t i = 0; i < itemCount; i++) { skip = base + i * step; // move in 14, 16 or 18 byte steps Internal::enforce(data.size() - skip >= (version > 2u ? 4u : 2u), Exiv2::ErrorCode::kerCorruptedMetadata); Internal::enforce(data.size() - skip >= step, Exiv2::ErrorCode::kerCorruptedMetadata); uint32_t ID = version > 2 ? data.read_uint32(skip, endian_) : data.read_uint16(skip, endian_); auto offset = [&data, skip, step] { if (step == 14 || step == 16) return data.read_uint32(skip + step - 8, endian_); if (step == 18) return data.read_uint32(skip + 4, endian_); return 0u; }(); uint32_t ldata = data.read_uint32(skip + step - 4, endian_); if (bTrace) { out << Internal::indent(depth) << Internal::stringFormat("%8zd | %8zd | ID | %4u | %6u,%6u", address + skip, step, ID, offset, ldata) << std::endl; } // save data for post-processing in meta box if (offset && ldata && ID != unknownID_) { ilocs_[ID] = Iloc{ID, offset, ldata}; } } } } break; case TAG_ispe: { Internal::enforce(data.size() - skip >= 12, Exiv2::ErrorCode::kerCorruptedMetadata); skip += 4; uint32_t width = data.read_uint32(skip, endian_); skip += 4; uint32_t height = data.read_uint32(skip, endian_); skip += 4; if (bTrace) { out << "pixelWidth_, pixelHeight_ = " << Internal::stringFormat("%d, %d", width, height); } // HEIC files can have multiple ispe records // Store largest width/height if (width > pixelWidth_ && height > pixelHeight_) { pixelWidth_ = width; pixelHeight_ = height; } } break; // 12.1.5.2 case TAG_colr: { if (data.size() >= (skip + 4 + 8)) { // .____.HLino..__mntrR 2 0 0 0 0 12 72 76 105 110 111 2 16 ... // https://www.ics.uci.edu/~dan/class/267/papers/jpeg2000.pdf uint8_t meth = data.read_uint8(skip + 0); uint8_t prec = data.read_uint8(skip + 1); uint8_t approx = data.read_uint8(skip + 2); auto colour_type = std::string(data.c_str(), 4); skip += 4; if (colour_type == "rICC" || colour_type == "prof") { DataBuf profile(data.c_data(skip), data.size() - skip); setIccProfile(std::move(profile)); } else if (meth == 2 && prec == 0 && approx == 0) { // JP2000 files have a 3 byte head // 2 0 0 icc...... skip -= 1; DataBuf profile(data.c_data(skip), data.size() - skip); setIccProfile(std::move(profile)); } } } break; case TAG_uuid: { DataBuf uuid(16); io_->read(uuid.data(), uuid.size()); std::string name = uuidName(uuid); if (bTrace) { out << " uuidName " << name << std::endl; bLF = false; } 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); } } else if (name == "xmp") { parseXmp(box_length, io_->tell()); } } break; case TAG_cmt1: parseTiff(Internal::Tag::root, box_length); break; case TAG_cmt2: parseTiff(Internal::Tag::cmt2, box_length); break; case TAG_cmt3: parseTiff(Internal::Tag::cmt3, box_length); break; case TAG_cmt4: parseTiff(Internal::Tag::cmt4, box_length); break; case TAG_exif: parseTiff(Internal::Tag::root, buffer_size, io_->tell()); break; case TAG_xml: parseXmp(buffer_size, io_->tell()); break; case TAG_brob: { Internal::enforce(data.size() >= 4, Exiv2::ErrorCode::kerCorruptedMetadata); uint32_t realType = data.read_uint32(0, endian_); if (bTrace) { out << "type: " << toAscii(realType); } #ifdef EXV_HAVE_BROTLI DataBuf arr; brotliUncompress(data.c_data(4), data.size() - 4, arr); if (realType == TAG_exif) { uint32_t offset = Safe::add(arr.read_uint32(0, endian_), 4u); Internal::enforce(Safe::add(offset, 4u) < arr.size(), Exiv2::ErrorCode::kerCorruptedMetadata); Internal::TiffParserWorker::decode(exifData(), iptcData(), xmpData(), arr.c_data(offset), arr.size() - offset, Internal::Tag::root, Internal::TiffMapping::findDecoder); } else if (realType == TAG_xml) { try { Exiv2::XmpParser::decode(xmpData(), std::string(arr.c_str(), arr.size())); } catch (...) { throw Error(ErrorCode::kerFailedToReadImageData); } } #endif } break; case TAG_thmb: switch (version) { case 0: // JPEG parseCr3Preview(data, out, bTrace, version, skip, skip + 2, skip + 4, skip + 12); break; case 1: // HDR parseCr3Preview(data, out, bTrace, version, skip + 2, skip + 4, skip + 8, skip + 12); break; default: break; } break; case TAG_prvw: switch (version) { case 0: // JPEG case 1: // HDR parseCr3Preview(data, out, bTrace, version, skip + 2, skip + 4, skip + 8, skip + 12); break; default: break; } break; default: break; /* do nothing */ } if (bLF && bTrace) out << std::endl; // return address of next box return box_end; } void BmffImage::parseTiff(uint32_t root_tag, uint64_t length, uint64_t start) { Internal::enforce(start <= io_->size(), ErrorCode::kerCorruptedMetadata); Internal::enforce(length <= io_->size() - start, ErrorCode::kerCorruptedMetadata); Internal::enforce(start <= static_cast(std::numeric_limits::max()), ErrorCode::kerCorruptedMetadata); Internal::enforce(length <= std::numeric_limits::max(), ErrorCode::kerCorruptedMetadata); // read and parse exif data const size_t restore = io_->tell(); DataBuf exif(static_cast(length)); io_->seek(static_cast(start), BasicIo::beg); if (exif.size() > 8 && io_->read(exif.data(), exif.size()) == exif.size()) { // hunt for "II" or "MM" const size_t eof = std::numeric_limits::max(); // impossible value for punt size_t punt = eof; for (size_t i = 0; i < exif.size() - 9 && punt == eof; ++i) { auto charCurrent = exif.read_uint8(i); auto charNext = exif.read_uint8(i + 1); if (charCurrent == charNext && (charCurrent == 'I' || charCurrent == 'M')) punt = i; } if (punt != eof) { Internal::TiffParserWorker::decode(exifData(), iptcData(), xmpData(), exif.c_data(punt), exif.size() - punt, root_tag, Internal::TiffMapping::findDecoder); } } io_->seek(restore, BasicIo::beg); } void BmffImage::parseTiff(uint32_t root_tag, uint64_t length) { if (length > 8) { Internal::enforce(length - 8 <= io_->size() - io_->tell(), ErrorCode::kerCorruptedMetadata); Internal::enforce(length - 8 <= std::numeric_limits::max(), ErrorCode::kerCorruptedMetadata); DataBuf data(static_cast(length - 8u)); const size_t bufRead = io_->read(data.data(), data.size()); if (io_->error()) throw Error(ErrorCode::kerFailedToReadImageData); if (bufRead != data.size()) throw Error(ErrorCode::kerInputDataReadFailed); Internal::TiffParserWorker::decode(exifData(), iptcData(), xmpData(), data.c_data(), data.size(), root_tag, Internal::TiffMapping::findDecoder); } } void BmffImage::parseXmp(uint64_t length, uint64_t start) { Internal::enforce(start <= io_->size(), ErrorCode::kerCorruptedMetadata); Internal::enforce(length <= io_->size() - start, ErrorCode::kerCorruptedMetadata); const size_t restore = io_->tell(); io_->seek(static_cast(start), BasicIo::beg); auto lengthSizeT = static_cast(length); DataBuf xmp(lengthSizeT + 1); xmp.write_uint8(lengthSizeT, 0); // ensure xmp is null terminated! if (io_->read(xmp.data(), lengthSizeT) != lengthSizeT) throw Error(ErrorCode::kerInputDataReadFailed); if (io_->error()) throw Error(ErrorCode::kerFailedToReadImageData); try { Exiv2::XmpParser::decode(xmpData(), std::string(xmp.c_str())); } catch (...) { throw Error(ErrorCode::kerFailedToReadImageData); } io_->seek(restore, BasicIo::beg); } /// \todo instead of passing the last 4 parameters, pass just one and build the different offsets inside void BmffImage::parseCr3Preview(const DataBuf& data, std::ostream& out, bool bTrace, uint8_t version, size_t width_offset, size_t height_offset, size_t size_offset, size_t relative_position) { // Derived from https://github.com/lclevy/canon_cr3 const size_t here = io_->tell(); Internal::enforce(here <= std::numeric_limits::max() - relative_position, ErrorCode::kerCorruptedMetadata); NativePreview nativePreview; 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_ = [version] { if (version == 0) return "image/jpeg"; return "application/octet-stream"; }(); if (bTrace) { out << Internal::stringFormat("width,height,size = %zu,%zu,%zu", nativePreview.width_, nativePreview.height_, nativePreview.size_); } nativePreviews_.push_back(std::move(nativePreview)); } void BmffImage::setExifData(const ExifData& /*exifData*/) { throw(Error(ErrorCode::kerInvalidSettingForImage, "Exif metadata", "BMFF")); } void BmffImage::setIptcData(const IptcData& /*iptcData*/) { throw(Error(ErrorCode::kerInvalidSettingForImage, "IPTC metadata", "BMFF")); } void BmffImage::setXmpData(const XmpData& /*xmpData*/) { throw(Error(ErrorCode::kerInvalidSettingForImage, "XMP metadata", "BMFF")); } void BmffImage::setComment(const std::string&) { // bmff files are read-only throw(Error(ErrorCode::kerInvalidSettingForImage, "Image comment", "BMFF")); } void BmffImage::openOrThrow() const { if (io_->open() != 0) { throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError()); } // Ensure that this is the correct image type if (!isBmffType(*io_, false)) { if (io_->error() || io_->eof()) throw Error(ErrorCode::kerFailedToReadImageData); throw Error(ErrorCode::kerNotAnImage, "BMFF"); } } void BmffImage::readMetadata() { openOrThrow(); IoCloser closer(*io_); clearMetadata(); ilocs_.clear(); visits_max_ = io_->size() / 16; unknownID_ = 0xffff; exifID_ = unknownID_; xmpID_ = unknownID_; uint64_t address = 0; const auto file_end = io_->size(); while (address < file_end) { io_->seek(address, BasicIo::beg); address = boxHandler(std::cout, kpsNone, file_end, 0); } bReadMetadata_ = true; } // BmffImage::readMetadata void BmffImage::printStructure(std::ostream& out, Exiv2::PrintStructureOption option, size_t depth) { if (!bReadMetadata_) readMetadata(); switch (option) { default: break; // do nothing case kpsIccProfile: { out.write(iccProfile_.c_str(), iccProfile_.size()); } break; #ifdef EXV_HAVE_XMP_TOOLKIT case kpsXMP: { std::string xmp; if (Exiv2::XmpParser::encode(xmp, xmpData())) { throw Exiv2::Error(Exiv2::ErrorCode::kerErrorMessage, "Failed to serialize XMP data"); } out << xmp; } break; #endif case kpsBasic: // drop case kpsRecursive: { openOrThrow(); IoCloser closer(*io_); uint64_t address = 0; const auto file_end = io_->size(); while (address < file_end) { io_->seek(address, BasicIo::beg); address = boxHandler(out, option, file_end, depth); } } break; } } void BmffImage::writeMetadata() { // bmff files are read-only throw(Error(ErrorCode::kerWritingImageFormatUnsupported, "BMFF")); } // BmffImage::writeMetadata // ************************************************************************* // free functions Image::UniquePtr newBmffInstance(BasicIo::UniquePtr io, bool create) { auto image = std::make_unique(std::move(io), create); if (!image->good()) { return nullptr; } return image; } bool isBmffType(BasicIo& iIo, bool advance) { if (!enabled) { return false; } const int32_t len = 12; byte buf[len]; iIo.read(buf, len); if (iIo.error() || iIo.eof()) { return false; } // bmff should start with "ftyp" bool const is_ftyp = (buf[4] == 'f' && buf[5] == 't' && buf[6] == 'y' && buf[7] == 'p'); // jxl files have a special start indicator of "JXL " bool const is_jxl = (buf[4] == 'J' && buf[5] == 'X' && buf[6] == 'L' && buf[7] == ' '); bool matched = is_jxl || is_ftyp; if (!advance || !matched) { iIo.seek(0, BasicIo::beg); } return matched; } } // namespace Exiv2 #endif