diff --git a/openVulkanoCpp/Image/ExifBuilder.cpp b/openVulkanoCpp/Image/ExifBuilder.cpp new file mode 100644 index 0000000..1ec4361 --- /dev/null +++ b/openVulkanoCpp/Image/ExifBuilder.cpp @@ -0,0 +1,478 @@ +/* + * 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 +#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, + 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::SetAltitude(float level) + { + altitudeIsAboveSeaLevel = level >= 0; + altitude = std::abs(level); + } + + void ExifBuilder::SetTime(std::time_t timestamp) + { + dateTaken = StringFromTime(timestamp); + } + + 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 = 1; // 1 is for GPS Info tag + numberOfMainTags += orientation != 0; + numberOfMainTags += make != ""; + numberOfMainTags += model != ""; + numberOfMainTags += xResolution.nominator || xResolution.denominator; + numberOfMainTags += yResolution.nominator || yResolution.denominator; + numberOfMainTags += resolutionUnit != 0; + numberOfMainTags += exposureTime.nominator || exposureTime.denominator; + numberOfMainTags += softwareUsed != ""; + numberOfMainTags += dateTaken != ""; + + 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 != 0) + { + AppendTagValueTypeAndShort(result, IFDTag::ORIENTATION, orientation); + } + + if (xResolution.nominator || xResolution.denominator) + { + offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::X_RESOLUTION, data, xResolution)); + } + + if (yResolution.nominator || yResolution.denominator) + { + offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::Y_RESOLUTION, data, yResolution)); + } + + if (exposureTime.nominator || exposureTime.denominator) + { + offsets.push_back(AppendTagValueTypeAndRational(result, IFDTag::EXPOSURE_TIME, data, exposureTime)); + } + + 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)); + + // 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); + } + } + + // 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 + int numberOfGPSInfoTags = 8; + AppendU16(result, numberOfGPSInfoTags); + offsets.resize(0); + + // Latitude Ref + char latitudeRef = CoordRefToChar(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(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, 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, (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, latitude); + AppendGPSCoords(result, longitude); + + AppendU32(result, altitude); + AppendU32(result, 1); // denominator for altitude + + constexpr int TRACK_PRECISION = 10000; + AppendU32(result, track * TRACK_PRECISION); + AppendU32(result, TRACK_PRECISION); + + return result; + } + + std::string ExifBuilder::StringFromTime(std::time_t time) + { + std::tm* timeInfo = std::localtime(&time); + std::ostringstream oss; + oss << std::put_time(timeInfo, "%Y:%m:%d %H:%M:%S"); + return oss.str(); + } + + std::string ExifBuilder::GetCurrentTimestamp() + { + auto now = std::chrono::system_clock::now(); + std::time_t currentTime = std::chrono::system_clock::to_time_t(now); + return StringFromTime(currentTime); + } +} \ No newline at end of file diff --git a/openVulkanoCpp/Image/ExifBuilder.hpp b/openVulkanoCpp/Image/ExifBuilder.hpp new file mode 100644 index 0000000..2f13771 --- /dev/null +++ b/openVulkanoCpp/Image/ExifBuilder.hpp @@ -0,0 +1,78 @@ +/* + * 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/. + */ + +#pragma once +#include +#include +#include +#include +#include + +namespace OpenVulkano::Image +{ + struct RationalValue + { + uint32_t nominator; + uint32_t denominator; + }; + + enum class LatitudeRef + { + NORTH, + SOUTH, + }; + enum class LongitudeRef + { + EAST, + WEST, + }; + enum class GPSTrackRef + { + TRUE_NORTH, + MAGNETIC + }; + + struct GPSCoords + { + int32_t degrees, minutes, seconds; + std::variant ref; + + GPSCoords(float decimalDegrees, bool isLatitude); + GPSCoords(int32_t degrees, int32_t minutes, int32_t seconds, LatitudeRef ref); + GPSCoords(int32_t degrees, int32_t minutes, int32_t seconds, LongitudeRef ref); + }; + + class ExifBuilder + { + public: + int orientation = 0; + std::string make; + std::string model; + RationalValue xResolution = { 0, 0 }; + RationalValue yResolution = { 0, 0 }; + int resolutionUnit = 0; + RationalValue exposureTime = { 0, 0 }; + std::string dateTaken; // format: yyyy:mm:dd hh:mm:ss + std::string softwareUsed = "OpenVulkano"; + + GPSCoords latitude = { 0, 0, 0, LatitudeRef::NORTH }; + GPSCoords longitude = { 0, 0, 0, LongitudeRef::EAST }; + + bool altitudeIsAboveSeaLevel = true; + uint32_t altitude = 0; + + GPSTrackRef trackRef = GPSTrackRef::TRUE_NORTH; + float track = 0; // range is [0.0; 360.0) + + + void SetAltitude(float level); + void SetTime(std::time_t timestamp); + // Typical usage is -> jpeg_write_marker(cinfo, JPEG_APP0 + 1, exif_data.data(), exif_data.size()); + [[nodiscard]] std::vector Build(); + [[nodiscard]] static std::string StringFromTime(std::time_t time); + [[nodiscard]] static std::string GetCurrentTimestamp(); + }; +} \ No newline at end of file