summaryrefslogtreecommitdiff
path: root/internal/xmpmeta/jpeg_io.cc
diff options
context:
space:
mode:
Diffstat (limited to 'internal/xmpmeta/jpeg_io.cc')
-rw-r--r--internal/xmpmeta/jpeg_io.cc194
1 files changed, 194 insertions, 0 deletions
diff --git a/internal/xmpmeta/jpeg_io.cc b/internal/xmpmeta/jpeg_io.cc
new file mode 100644
index 0000000..20fc7a6
--- /dev/null
+++ b/internal/xmpmeta/jpeg_io.cc
@@ -0,0 +1,194 @@
+#include "xmpmeta/jpeg_io.h"
+
+#include <fstream>
+#include <sstream>
+
+#include "android-base/logging.h"
+
+namespace photos_editing_formats {
+namespace {
+
+// File markers.
+// See: http://www.fileformat.info/format/jpeg/egff.htm or
+// https://en.wikipedia.org/wiki/JPEG
+const int kSoi = 0xd8; // Start of image marker.
+const int kApp1 = 0xe1; // Start of EXIF section.
+const int kSos = 0xda; // Start of scan.
+
+// Number of bytes used to store a section's length in a JPEG file.
+const int kSectionLengthByteSize = 2;
+
+// Returns the number of bytes available to be read. Sets the seek position
+// to the place it was in before calling this function.
+size_t GetBytesAvailable(std::istream* input_stream) {
+ const std::streamoff pos = input_stream->tellg();
+ if (pos == -1) {
+ return 0;
+ }
+
+ input_stream->seekg(0, std::ios::end);
+ if (!input_stream->good()) {
+ return 0;
+ }
+
+ const std::streamoff end = input_stream->tellg();
+ if (end == -1) {
+ return 0;
+ }
+ input_stream->seekg(pos);
+
+ if (end <= pos) {
+ return 0;
+ }
+ return end - pos;
+}
+
+// Returns the first byte in the stream cast to an integer.
+int ReadByteAsInt(std::istream* input_stream) {
+ unsigned char byte;
+ input_stream->read(reinterpret_cast<char*>(&byte), 1);
+ if (!input_stream->good()) {
+ // Return an invalid value - no byte can be read as -1.
+ return -1;
+ }
+ return static_cast<int>(byte);
+}
+
+// Reads the length of a section from 2 bytes.
+size_t Read2ByteLength(std::istream* input_stream, bool* error) {
+ const int length_high = ReadByteAsInt(input_stream);
+ const int length_low = ReadByteAsInt(input_stream);
+ if (length_high == -1 || length_low == -1) {
+ *error = true;
+ return 0;
+ }
+ *error = false;
+ return length_high << 8 | length_low;
+}
+
+bool HasPrefixString(const string& to_check, const string& prefix) {
+ if (to_check.size() < prefix.size()) {
+ return false;
+ }
+ return std::equal(prefix.begin(), prefix.end(), to_check.begin());
+}
+
+} // namespace
+
+Section::Section(const string& buffer) {
+ marker = kApp1;
+ is_image_section = false;
+ data = buffer;
+}
+
+bool Section::IsMarkerApp1() { return marker == kApp1; }
+
+std::vector<Section> Parse(const ParseOptions& options,
+ std::istream* input_stream) {
+ std::vector<Section> sections;
+ // Return early if this is not the start of a JPEG section.
+ if (ReadByteAsInt(input_stream) != 0xff ||
+ ReadByteAsInt(input_stream) != kSoi) {
+ LOG(WARNING) << "File's first two bytes does not match the sequence \xff"
+ << kSoi;
+ return std::vector<Section>();
+ }
+
+ int chr; // Short for character.
+ while ((chr = ReadByteAsInt(input_stream)) != -1) {
+ if (chr != 0xff) {
+ LOG(WARNING) << "Read non-padding byte: " << chr;
+ return sections;
+ }
+ // Skip padding bytes.
+ while ((chr = ReadByteAsInt(input_stream)) == 0xff) {
+ }
+ if (chr == -1) {
+ LOG(WARNING) << "No more bytes in file available to be read.";
+ return sections;
+ }
+
+ const int marker = chr;
+ if (marker == kSos) {
+ // kSos indicates the image data will follow and no metadata after that,
+ // so read all data at one time.
+ if (!options.read_meta_only) {
+ Section section;
+ section.marker = marker;
+ section.is_image_section = true;
+ const size_t bytes_available = GetBytesAvailable(input_stream);
+ section.data.resize(bytes_available);
+ input_stream->read(&section.data[0], bytes_available);
+ if (input_stream->good()) {
+ sections.push_back(section);
+ }
+ }
+ // All sections have been read.
+ return sections;
+ }
+
+ bool error;
+ const size_t length = Read2ByteLength(input_stream, &error);
+ if (error || length < kSectionLengthByteSize) {
+ // No sections to read.
+ LOG(WARNING) << "No sections to read; section length is " << length;
+ return sections;
+ }
+
+ const size_t bytes_left = GetBytesAvailable(input_stream);
+ if (length - kSectionLengthByteSize > bytes_left) {
+ LOG(WARNING) << "Invalid section length = " << length
+ << " total bytes available = " << bytes_left;
+ return sections;
+ }
+
+ if (!options.read_meta_only || marker == kApp1) {
+ Section section;
+ section.marker = marker;
+ section.is_image_section = false;
+ const size_t data_size = length - kSectionLengthByteSize;
+ section.data.resize(data_size);
+ if (section.data.size() != data_size) {
+ LOG(WARNING) << "Discrepancy in section data size "
+ << section.data.size() << "and data size " << data_size;
+ return sections;
+ }
+ input_stream->read(&section.data[0], section.data.size());
+ if (input_stream->good() &&
+ (options.section_header.empty() ||
+ HasPrefixString(section.data, options.section_header))) {
+ sections.push_back(section);
+ // Return if we have specified to return the 1st section with
+ // the given name.
+ if (options.section_header_return_first) {
+ return sections;
+ }
+ }
+ } else {
+ // Skip this section since all EXIF/XMP meta will be in kApp1 section.
+ input_stream->ignore(length - kSectionLengthByteSize);
+ }
+ }
+ return sections;
+}
+
+void WriteSections(const std::vector<Section>& sections,
+ std::ostream* output_stream) {
+ output_stream->put(0xff);
+ output_stream->put(kSoi);
+ for (const Section& section : sections) {
+ output_stream->put(0xff);
+ output_stream->put(section.marker);
+ if (!section.is_image_section) {
+ const int section_length = static_cast<int>(section.data.length()) + 2;
+ // It's not the image data.
+ const int lh = section_length >> 8;
+ const int ll = section_length & 0xff;
+ output_stream->put(lh);
+ output_stream->put(ll);
+ }
+ output_stream->write(section.data.c_str(), section.data.length());
+ }
+}
+
+} // namespace photos_editing_formats