Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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