/* * 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 https://mozilla.org/MPL/2.0/. */ #include "ExifBuilder.hpp" #include "Extensions/FmtFormatter.hpp" #include "Math/Math.hpp" #include #include #include #include #include #include #include namespace { constexpr int EXIF_HEADER_SIZE = 6; constexpr std::array EXIF_HEADER_AND_PADDING = { 'E', 'x', 'i', 'f', 0, 0 }; constexpr int TIFF_HEADER_SIZE = 4; constexpr std::array TIFF_HEADER = { 0x4d, 0x4d, 0, 0x2a }; constexpr bool IS_LITTLE_ENDIAN = std::endian::native == std::endian::little; enum class IFDTag : uint16_t { END = 0, MAKE = 0x010f, MODEL = 0x0110, ORIENTATION = 0x0112, X_RESOLUTION = 0x011a, Y_RESOLUTION = 0x011b, RESOLUTION_UNIT = 0x0128, SOFTWARE_USED = 0x0131, DATE_TAKEN = 0x0132, EXPOSURE_TIME = 0x829a, F_NUMBER = 0x829d, FOCAL_LENGTH = 0x920a, GPS_INFO_OFFSET = 0x8825, }; enum class IFDGPSTag : uint16_t { LATITUDE_REF = 1, LATITUDE, LONGITUDE_REF, LONGITUDE, ALTITUDE_REF, ALTITUDE, TRACK_REF = 14, TRACK, }; enum class IFDValueType { BYTE = 1, ASCII = 2, SHORT = 3, LONG_ = 4, RATIONAL = 5, }; template T EndianSwap(T value) { T result; char* ptr = reinterpret_cast(&value); std::reverse(ptr, ptr + sizeof(T)); result = value; return result; } template int Append(std::vector& array, T value) { int offset = array.size(); const int bytes = sizeof(T); char *c = (char *) &value; for (int i = 0; i < bytes; i++) { array.push_back(c[i]); } return offset; } int AppendU8(std::vector& array, uint8_t value) { return Append(array, value); } int AppendU16(std::vector& array, uint16_t value) { if constexpr (IS_LITTLE_ENDIAN) { value = ::EndianSwap(value); } return Append(array, value); } // no endian swap int AppendU32NES(std::vector& array, uint32_t value) { return Append(array, value); } int AppendU32(std::vector& array, uint32_t value) { if constexpr (IS_LITTLE_ENDIAN) { value = ::EndianSwap(value); } return Append(array, value); } template int AppendVector(std::vector& array, const std::array& values) { int offset = array.size(); for (auto value: values) { array.push_back(value); } return offset; } int AppendVector(std::vector& array, const std::vector& values) { int offset = array.size(); for (auto value: values) { array.push_back(value); } return offset; } int AppendVector(std::vector& array, char* values, int count) { int offset = array.size(); for (int i = 0; i < count; ++i) { array.push_back(values[i]); } return offset; } int AppendRational(std::vector& array, const OpenVulkano::Image::RationalValue& rational) { int offset = array.size(); AppendU32(array, rational.nominator); AppendU32(array, rational.denominator); return offset; } int AppendGPSCoords(std::vector& array, const OpenVulkano::Image::GPSCoords& coords) { int offset = array.size(); AppendU32(array, coords.degrees); AppendU32(array, 1); AppendU32(array, coords.minutes); AppendU32(array, 1); AppendU32(array, coords.seconds); AppendU32(array, 1); return offset; } void AppendTagAndValueType(std::vector& array, uint16_t tag, uint16_t valueType) { AppendU16(array, tag); AppendU16(array, valueType); } void AddValueToU32AndEndianSwap(uint8_t *data, int valueToAdd) { uint32_t *ptr = (uint32_t *) data; *ptr += valueToAdd; *ptr = EndianSwap(*ptr); } int AppendTagValueTypeAndString(std::vector& array, IFDTag tag, std::vector &otherArray, const std::string& str) { AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) IFDValueType::ASCII); AppendU32(array, str.size() + 1); int offset = AppendU32(array, otherArray.size() + 1); int offsetInData = AppendVector(otherArray, (char*) str.c_str(), str.size() + 1); uint32_t* ptr = (uint32_t*) (array.data() + offset); *ptr = offsetInData; return offset; } int AppendTagValueTypeAndRational(std::vector& array, IFDTag tag, std::vector &otherArray, const OpenVulkano::Image::RationalValue& value) { AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) IFDValueType::RATIONAL); AppendU32(array, 1); // number of components int offset = AppendU32(array, otherArray.size()); int offsetInData = AppendRational(otherArray, value); uint32_t* ptr = (uint32_t*) (array.data() + offset); *ptr = offsetInData; return offset; } void AppendTagValueTypeAndShort(std::vector& array, IFDTag tag, uint16_t value) { AppendTagAndValueType(array, (uint16_t ) tag, (uint16_t) IFDValueType::SHORT); AppendU32(array, 1); AppendU16(array, value); AppendU16(array, 0); // padding } void AppendTagValueTypeAndByte(std::vector& array, IFDGPSTag tag, IFDValueType valueType, uint16_t byte) { AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) valueType); AppendU32(array, (valueType == IFDValueType::BYTE) ? 1 : 2); // 2 for N/S/E/W + \0, 1 for a single byte AppendU8(array, byte); AppendU8(array, 0); // padding AppendU8(array, 0); // padding AppendU8(array, 0); // padding } int AppendTagValueTypeAndGPSRational(std::vector& array, IFDGPSTag tag, int numberOfComponents, int initialOffsetValue) { AppendTagAndValueType(array, (uint16_t) tag, (uint16_t) IFDValueType::RATIONAL); AppendU32(array, numberOfComponents); // number of components int offset = AppendU32NES(array, initialOffsetValue); return offset; } char CoordRefToChar(const std::variant& ref) { char c = 0; if (std::holds_alternative(ref)) { auto lat = std::get(ref); c = (lat == OpenVulkano::Image::LatitudeRef::NORTH ? 'N' : 'S'); } else if (std::holds_alternative(ref)) { auto lon = std::get(ref); c = (lon == OpenVulkano::Image::LongitudeRef::EAST ? 'E' : 'W'); } else { throw std::runtime_error("An alternative does not contain neither LatitudeRef nor LongitudeRef!"); } return c; } } namespace OpenVulkano::Image { GPSCoords::GPSCoords(float decimalDegrees, bool isLatitude) { degrees = static_cast(decimalDegrees); float fractionalDegrees = decimalDegrees - degrees; minutes = static_cast(std::abs(fractionalDegrees) * 60); float fractionalMinutes = (std::abs(fractionalDegrees) * 60) - minutes; seconds = static_cast(fractionalMinutes * 60); if (isLatitude) { ref = (decimalDegrees < 0) ? LatitudeRef::SOUTH : LatitudeRef::NORTH; } else { ref = (decimalDegrees < 0) ? LongitudeRef::WEST : LongitudeRef::EAST; } degrees = std::abs(degrees); } GPSCoords::GPSCoords(int32_t degrees, int32_t minutes, int32_t seconds, LatitudeRef ref) : degrees(degrees), minutes(minutes), seconds(seconds), ref(ref) { } GPSCoords::GPSCoords(int32_t degrees, int32_t minutes, int32_t seconds, LongitudeRef ref) : degrees(degrees), minutes(minutes), seconds(seconds), ref(ref) { } void ExifBuilder::SetOrientation(float orientationRad) { orientationRad = Math::Utils::NormalizeAngleRad(orientationRad); if (orientationRad > 0.78f && orientationRad < 2.35f) orientation = 8; else if (orientationRad >= 2.35f && orientationRad < 3.93f) orientation = 3; else if (orientationRad >= 3.93f && orientationRad < 5.5f) orientation = 6; else orientation = 1; } void GPSInfo::SetAltitude(const float level) { altitudeIsAboveSeaLevel = level >= 0; altitude = std::abs(level); } std::vector ExifBuilder::Build() { std::vector result; std::vector data; // the data that has ascii and rational values if (dateTaken.empty()) { dateTaken = GetCurrentTimestamp(); } AppendVector(result, EXIF_HEADER_AND_PADDING); AppendVector(result, TIFF_HEADER); int numberOfMainTags = 0; // 1 is for GPS Info tag numberOfMainTags += orientation != 0; numberOfMainTags += make != ""; numberOfMainTags += model != ""; numberOfMainTags += (bool)xResolution; numberOfMainTags += (bool)yResolution; numberOfMainTags += resolutionUnit != 0; numberOfMainTags += (bool)exposureTime; numberOfMainTags += softwareUsed != ""; numberOfMainTags += dateTaken != ""; numberOfMainTags += gpsInfo.has_value(); numberOfMainTags += (bool)fNumber; numberOfMainTags += (bool)focalLength; AppendU32(result, 8); // Append offset to the ifd AppendU16(result, numberOfMainTags); std::vector offsets; int gpsInfoOffset = 0; if (!make.empty()) { offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::MAKE, data, make)); } if (!model.empty()) { offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::MODEL, data, model)); } if (orientation) { AppendTagValueTypeAndShort(result, IFDTag::ORIENTATION, orientation); } if (xResolution) { offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::X_RESOLUTION, data, xResolution)); } if (yResolution) { offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::Y_RESOLUTION, data, yResolution)); } if (exposureTime) { offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::EXPOSURE_TIME, data, exposureTime)); } if (fNumber) { offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::F_NUMBER, data, fNumber)); } if (focalLength) { offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::FOCAL_LENGTH, data, focalLength)); } if (resolutionUnit != 0) { AppendTagValueTypeAndShort(result, IFDTag::RESOLUTION_UNIT, resolutionUnit); } if (!softwareUsed.empty()) { offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::SOFTWARE_USED, data, softwareUsed)); } // NOTE(vb): For some reason windows file properties doesn't print date taken field! // Even though other software does provide this information without a problem offsets.push_back(AppendTagValueTypeAndString(result, IFDTag::DATE_TAKEN, data, dateTaken)); if (gpsInfo) { // GPS Info offset AppendTagAndValueType(result, (uint16_t) IFDTag::GPS_INFO_OFFSET, (uint16_t) IFDValueType::LONG_); AppendU32(result, 1); // num components gpsInfoOffset = AppendU32(result, 0); } // next ifd offset AppendU32(result, 0); int resultSize = result.size(); AppendVector(result, data); // Resolve offsets { const int valueToAdd = resultSize - EXIF_HEADER_SIZE; for (const auto& offset: offsets) { AddValueToU32AndEndianSwap(result.data() + offset, valueToAdd); } } if (gpsInfo) { // Resolve GPS info offset { int ifdAndSubdataSize = result.size(); uint32_t *ptr = (uint32_t *) (result.data() + gpsInfoOffset); *ptr = EndianSwap((uint32_t)(ifdAndSubdataSize - EXIF_HEADER_SIZE)); } // Writing GPS Info structure constexpr int numberOfGPSInfoTags = 8; AppendU16(result, numberOfGPSInfoTags); offsets.resize(0); // Latitude Ref char latitudeRef = CoordRefToChar(gpsInfo->latitude.ref); AppendTagValueTypeAndByte(result, IFDGPSTag::LATITUDE_REF, IFDValueType::ASCII, latitudeRef); // Latitude offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::LATITUDE, 3, 0)); // 0 * sizeof(RationalValue) // Longitude Ref char longitudeRef = CoordRefToChar(gpsInfo->longitude.ref); AppendTagValueTypeAndByte(result, IFDGPSTag::LONGITUDE_REF, IFDValueType::ASCII, longitudeRef); // Longitude offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::LONGITUDE, 3, 24)); // 3 * sizeof(RationalValue) // Altitude Ref AppendTagValueTypeAndByte(result, IFDGPSTag::ALTITUDE_REF, IFDValueType::BYTE, gpsInfo->altitudeIsAboveSeaLevel ? 0 : 1); // Altitude offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::ALTITUDE, 1, 48)); // 6 * sizeof(RationalValue) // Track Ref AppendTagValueTypeAndByte(result, IFDGPSTag::TRACK_REF, IFDValueType::ASCII, (gpsInfo->trackRef == GPSTrackRef::TRUE_NORTH) ? 'T' : 'M'); // Track offsets.push_back(AppendTagValueTypeAndGPSRational(result, IFDGPSTag::TRACK, 1, 56)); // 7 * sizeof(RationalValue) int sizeOfResultSoFar = result.size(); const int valueToAdd = sizeOfResultSoFar - EXIF_HEADER_SIZE; for(const auto &offset : offsets) { AddValueToU32AndEndianSwap(result.data() + offset, valueToAdd); } AppendGPSCoords(result, gpsInfo->latitude); AppendGPSCoords(result, gpsInfo->longitude); AppendU32(result, gpsInfo->altitude); AppendU32(result, 1); // denominator for altitude constexpr int TRACK_PRECISION = 10000; AppendU32(result, gpsInfo->track * TRACK_PRECISION); AppendU32(result, TRACK_PRECISION); } return result; } std::string ExifBuilder::GetCurrentTimestamp() { auto now = std::chrono::system_clock::now(); return fmt::format("{:%Y:%m:%d %H:%M:%S}", now); // TODO convert to local time } }