summaryrefslogtreecommitdiffstats
path: root/image/decoders/EXIF.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--image/decoders/EXIF.cpp519
1 files changed, 519 insertions, 0 deletions
diff --git a/image/decoders/EXIF.cpp b/image/decoders/EXIF.cpp
new file mode 100644
index 0000000000..97563248c7
--- /dev/null
+++ b/image/decoders/EXIF.cpp
@@ -0,0 +1,519 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EXIF.h"
+
+#include "mozilla/EndianUtils.h"
+#include "mozilla/StaticPrefs_image.h"
+
+namespace mozilla::image {
+
+// Section references in this file refer to the EXIF v2.3 standard, also known
+// as CIPA DC-008-Translation-2010.
+
+// See Section 4.6.4, Table 4.
+// Typesafe enums are intentionally not used here since we're comparing to raw
+// integers produced by parsing.
+enum class EXIFTag : uint16_t {
+ Orientation = 0x112,
+ XResolution = 0x11a,
+ YResolution = 0x11b,
+ PixelXDimension = 0xa002,
+ PixelYDimension = 0xa003,
+ ResolutionUnit = 0x128,
+ IFDPointer = 0x8769,
+};
+
+// See Section 4.6.2.
+enum EXIFType {
+ ByteType = 1,
+ ASCIIType = 2,
+ ShortType = 3,
+ LongType = 4,
+ RationalType = 5,
+ UndefinedType = 7,
+ SignedLongType = 9,
+ SignedRational = 10,
+};
+
+static const char* EXIFHeader = "Exif\0\0";
+static const uint32_t EXIFHeaderLength = 6;
+static const uint32_t TIFFHeaderStart = EXIFHeaderLength;
+
+struct ParsedEXIFData {
+ Orientation orientation;
+ Maybe<float> resolutionX;
+ Maybe<float> resolutionY;
+ Maybe<uint32_t> pixelXDimension;
+ Maybe<uint32_t> pixelYDimension;
+ Maybe<ResolutionUnit> resolutionUnit;
+};
+
+static float ToDppx(float aResolution, ResolutionUnit aUnit) {
+ constexpr float kPointsPerInch = 72.0f;
+ constexpr float kPointsPerCm = 1.0f / 2.54f;
+ switch (aUnit) {
+ case ResolutionUnit::Dpi:
+ return aResolution / kPointsPerInch;
+ case ResolutionUnit::Dpcm:
+ return aResolution / kPointsPerCm;
+ }
+ MOZ_CRASH("Unknown resolution unit?");
+}
+
+static Resolution ResolutionFromParsedData(const ParsedEXIFData& aData,
+ const gfx::IntSize& aRealImageSize) {
+ if (!aData.resolutionUnit || !aData.resolutionX || !aData.resolutionY) {
+ return {};
+ }
+
+ Resolution resolution{ToDppx(*aData.resolutionX, *aData.resolutionUnit),
+ ToDppx(*aData.resolutionY, *aData.resolutionUnit)};
+
+ if (StaticPrefs::image_exif_density_correction_sanity_check_enabled()) {
+ if (!aData.pixelXDimension || !aData.pixelYDimension) {
+ return {};
+ }
+
+ const gfx::IntSize exifSize(*aData.pixelXDimension, *aData.pixelYDimension);
+
+ gfx::IntSize scaledSize = aRealImageSize;
+ resolution.ApplyTo(scaledSize.width, scaledSize.height);
+
+ if (exifSize != scaledSize) {
+ return {};
+ }
+ }
+
+ return resolution;
+}
+
+/////////////////////////////////////////////////////////////
+// Parse EXIF data, typically found in a JPEG's APP1 segment.
+/////////////////////////////////////////////////////////////
+EXIFData EXIFParser::ParseEXIF(const uint8_t* aData, const uint32_t aLength,
+ const gfx::IntSize& aRealImageSize) {
+ if (!Initialize(aData, aLength)) {
+ return EXIFData();
+ }
+
+ if (!ParseEXIFHeader()) {
+ return EXIFData();
+ }
+
+ uint32_t offsetIFD;
+ if (!ParseTIFFHeader(offsetIFD)) {
+ return EXIFData();
+ }
+
+ JumpTo(offsetIFD);
+
+ ParsedEXIFData data;
+ ParseIFD(data);
+
+ return EXIFData{data.orientation,
+ ResolutionFromParsedData(data, aRealImageSize)};
+}
+
+/////////////////////////////////////////////////////////
+// Parse the EXIF header. (Section 4.7.2, Figure 30)
+/////////////////////////////////////////////////////////
+bool EXIFParser::ParseEXIFHeader() {
+ return MatchString(EXIFHeader, EXIFHeaderLength);
+}
+
+/////////////////////////////////////////////////////////
+// Parse the TIFF header. (Section 4.5.2, Table 1)
+/////////////////////////////////////////////////////////
+bool EXIFParser::ParseTIFFHeader(uint32_t& aIFD0OffsetOut) {
+ // Determine byte order.
+ if (MatchString("MM\0*", 4)) {
+ mByteOrder = ByteOrder::BigEndian;
+ } else if (MatchString("II*\0", 4)) {
+ mByteOrder = ByteOrder::LittleEndian;
+ } else {
+ return false;
+ }
+
+ // Determine offset of the 0th IFD. (It shouldn't be greater than 64k, which
+ // is the maximum size of the entry APP1 segment.)
+ uint32_t ifd0Offset;
+ if (!ReadUInt32(ifd0Offset) || ifd0Offset > 64 * 1024) {
+ return false;
+ }
+
+ // The IFD offset is relative to the beginning of the TIFF header, which
+ // begins after the EXIF header, so we need to increase the offset
+ // appropriately.
+ aIFD0OffsetOut = ifd0Offset + TIFFHeaderStart;
+ return true;
+}
+
+// An arbitrary limit on the amount of pointers that we'll chase, to prevent bad
+// inputs getting us stuck.
+constexpr uint32_t kMaxEXIFDepth = 16;
+
+/////////////////////////////////////////////////////////
+// Parse the entries in IFD0. (Section 4.6.2)
+/////////////////////////////////////////////////////////
+void EXIFParser::ParseIFD(ParsedEXIFData& aData, uint32_t aDepth) {
+ if (NS_WARN_IF(aDepth > kMaxEXIFDepth)) {
+ return;
+ }
+
+ uint16_t entryCount;
+ if (!ReadUInt16(entryCount)) {
+ return;
+ }
+
+ for (uint16_t entry = 0; entry < entryCount; ++entry) {
+ // Read the fields of the 12-byte entry.
+ uint16_t tag;
+ if (!ReadUInt16(tag)) {
+ return;
+ }
+
+ uint16_t type;
+ if (!ReadUInt16(type)) {
+ return;
+ }
+
+ uint32_t count;
+ if (!ReadUInt32(count)) {
+ return;
+ }
+
+ switch (EXIFTag(tag)) {
+ case EXIFTag::Orientation:
+ // We should have an orientation value here; go ahead and parse it.
+ if (!ParseOrientation(type, count, aData.orientation)) {
+ return;
+ }
+ break;
+ case EXIFTag::ResolutionUnit:
+ if (!ParseResolutionUnit(type, count, aData.resolutionUnit)) {
+ return;
+ }
+ break;
+ case EXIFTag::XResolution:
+ if (!ParseResolution(type, count, aData.resolutionX)) {
+ return;
+ }
+ break;
+ case EXIFTag::YResolution:
+ if (!ParseResolution(type, count, aData.resolutionY)) {
+ return;
+ }
+ break;
+ case EXIFTag::PixelXDimension:
+ if (!ParseDimension(type, count, aData.pixelXDimension)) {
+ return;
+ }
+ break;
+ case EXIFTag::PixelYDimension:
+ if (!ParseDimension(type, count, aData.pixelYDimension)) {
+ return;
+ }
+ break;
+ case EXIFTag::IFDPointer: {
+ uint32_t offset;
+ if (!ReadUInt32(offset)) {
+ return;
+ }
+
+ ScopedJump jump(*this, offset + TIFFHeaderStart);
+ ParseIFD(aData, aDepth + 1);
+ break;
+ }
+
+ default:
+ Advance(4);
+ break;
+ }
+ }
+}
+
+bool EXIFParser::ReadRational(float& aOut) {
+ // Values larger than 4 bytes (like rationals) are specified as an offset into
+ // the TIFF header.
+ uint32_t valueOffset;
+ if (!ReadUInt32(valueOffset)) {
+ return false;
+ }
+ ScopedJump jumpToHeader(*this, valueOffset + TIFFHeaderStart);
+ uint32_t numerator;
+ if (!ReadUInt32(numerator)) {
+ return false;
+ }
+ uint32_t denominator;
+ if (!ReadUInt32(denominator)) {
+ return false;
+ }
+ if (denominator == 0) {
+ return false;
+ }
+ aOut = float(numerator) / float(denominator);
+ return true;
+}
+
+bool EXIFParser::ParseResolution(uint16_t aType, uint32_t aCount,
+ Maybe<float>& aOut) {
+ if (!StaticPrefs::image_exif_density_correction_enabled()) {
+ Advance(4);
+ return true;
+ }
+ if (aType != RationalType || aCount != 1) {
+ return false;
+ }
+ float value;
+ if (!ReadRational(value)) {
+ return false;
+ }
+ if (value == 0.0f) {
+ return false;
+ }
+ aOut = Some(value);
+ return true;
+}
+
+bool EXIFParser::ParseDimension(uint16_t aType, uint32_t aCount,
+ Maybe<uint32_t>& aOut) {
+ if (!StaticPrefs::image_exif_density_correction_enabled()) {
+ Advance(4);
+ return true;
+ }
+
+ if (aCount != 1) {
+ return false;
+ }
+
+ switch (aType) {
+ case ShortType: {
+ uint16_t value;
+ if (!ReadUInt16(value)) {
+ return false;
+ }
+ aOut = Some(value);
+ Advance(2);
+ break;
+ }
+ case LongType: {
+ uint32_t value;
+ if (!ReadUInt32(value)) {
+ return false;
+ }
+ aOut = Some(value);
+ break;
+ }
+ default:
+ return false;
+ }
+ return true;
+}
+
+bool EXIFParser::ParseResolutionUnit(uint16_t aType, uint32_t aCount,
+ Maybe<ResolutionUnit>& aOut) {
+ if (!StaticPrefs::image_exif_density_correction_enabled()) {
+ Advance(4);
+ return true;
+ }
+ if (aType != ShortType || aCount != 1) {
+ return false;
+ }
+ uint16_t value;
+ if (!ReadUInt16(value)) {
+ return false;
+ }
+ switch (value) {
+ case 2:
+ aOut = Some(ResolutionUnit::Dpi);
+ break;
+ case 3:
+ aOut = Some(ResolutionUnit::Dpcm);
+ break;
+ default:
+ return false;
+ }
+
+ // This is a 32-bit field, but the unit value only occupies the first 16 bits.
+ // We need to advance another 16 bits to consume the entire field.
+ Advance(2);
+ return true;
+}
+
+bool EXIFParser::ParseOrientation(uint16_t aType, uint32_t aCount,
+ Orientation& aOut) {
+ // Sanity check the type and count.
+ if (aType != ShortType || aCount != 1) {
+ return false;
+ }
+
+ uint16_t value;
+ if (!ReadUInt16(value)) {
+ return false;
+ }
+
+ switch (value) {
+ case 1:
+ aOut = Orientation(Angle::D0, Flip::Unflipped);
+ break;
+ case 2:
+ aOut = Orientation(Angle::D0, Flip::Horizontal);
+ break;
+ case 3:
+ aOut = Orientation(Angle::D180, Flip::Unflipped);
+ break;
+ case 4:
+ aOut = Orientation(Angle::D180, Flip::Horizontal);
+ break;
+ case 5:
+ aOut = Orientation(Angle::D90, Flip::Horizontal);
+ break;
+ case 6:
+ aOut = Orientation(Angle::D90, Flip::Unflipped);
+ break;
+ case 7:
+ aOut = Orientation(Angle::D270, Flip::Horizontal);
+ break;
+ case 8:
+ aOut = Orientation(Angle::D270, Flip::Unflipped);
+ break;
+ default:
+ return false;
+ }
+
+ // This is a 32-bit field, but the orientation value only occupies the first
+ // 16 bits. We need to advance another 16 bits to consume the entire field.
+ Advance(2);
+ return true;
+}
+
+bool EXIFParser::Initialize(const uint8_t* aData, const uint32_t aLength) {
+ if (aData == nullptr) {
+ return false;
+ }
+
+ // An APP1 segment larger than 64k violates the JPEG standard.
+ if (aLength > 64 * 1024) {
+ return false;
+ }
+
+ mStart = mCurrent = aData;
+ mLength = mRemainingLength = aLength;
+ mByteOrder = ByteOrder::Unknown;
+ return true;
+}
+
+void EXIFParser::Advance(const uint32_t aDistance) {
+ if (mRemainingLength >= aDistance) {
+ mCurrent += aDistance;
+ mRemainingLength -= aDistance;
+ } else {
+ mCurrent = mStart;
+ mRemainingLength = 0;
+ }
+}
+
+void EXIFParser::JumpTo(const uint32_t aOffset) {
+ if (mLength >= aOffset) {
+ mCurrent = mStart + aOffset;
+ mRemainingLength = mLength - aOffset;
+ } else {
+ mCurrent = mStart;
+ mRemainingLength = 0;
+ }
+}
+
+bool EXIFParser::MatchString(const char* aString, const uint32_t aLength) {
+ if (mRemainingLength < aLength) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < aLength; ++i) {
+ if (mCurrent[i] != aString[i]) {
+ return false;
+ }
+ }
+
+ Advance(aLength);
+ return true;
+}
+
+bool EXIFParser::MatchUInt16(const uint16_t aValue) {
+ if (mRemainingLength < 2) {
+ return false;
+ }
+
+ bool matched;
+ switch (mByteOrder) {
+ case ByteOrder::LittleEndian:
+ matched = LittleEndian::readUint16(mCurrent) == aValue;
+ break;
+ case ByteOrder::BigEndian:
+ matched = BigEndian::readUint16(mCurrent) == aValue;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Should know the byte order by now");
+ matched = false;
+ }
+
+ if (matched) {
+ Advance(2);
+ }
+
+ return matched;
+}
+
+bool EXIFParser::ReadUInt16(uint16_t& aValue) {
+ if (mRemainingLength < 2) {
+ return false;
+ }
+
+ bool matched = true;
+ switch (mByteOrder) {
+ case ByteOrder::LittleEndian:
+ aValue = LittleEndian::readUint16(mCurrent);
+ break;
+ case ByteOrder::BigEndian:
+ aValue = BigEndian::readUint16(mCurrent);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Should know the byte order by now");
+ matched = false;
+ }
+
+ if (matched) {
+ Advance(2);
+ }
+
+ return matched;
+}
+
+bool EXIFParser::ReadUInt32(uint32_t& aValue) {
+ if (mRemainingLength < 4) {
+ return false;
+ }
+
+ bool matched = true;
+ switch (mByteOrder) {
+ case ByteOrder::LittleEndian:
+ aValue = LittleEndian::readUint32(mCurrent);
+ break;
+ case ByteOrder::BigEndian:
+ aValue = BigEndian::readUint32(mCurrent);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Should know the byte order by now");
+ matched = false;
+ }
+
+ if (matched) {
+ Advance(4);
+ }
+
+ return matched;
+}
+
+} // namespace mozilla::image