summaryrefslogtreecommitdiff
path: root/internal/xmpmeta/xmp_writer.cc
diff options
context:
space:
mode:
Diffstat (limited to 'internal/xmpmeta/xmp_writer.cc')
-rw-r--r--internal/xmpmeta/xmp_writer.cc350
1 files changed, 350 insertions, 0 deletions
diff --git a/internal/xmpmeta/xmp_writer.cc b/internal/xmpmeta/xmp_writer.cc
new file mode 100644
index 0000000..73e5a65
--- /dev/null
+++ b/internal/xmpmeta/xmp_writer.cc
@@ -0,0 +1,350 @@
+#include "xmpmeta/xmp_writer.h"
+
+#include <libxml/tree.h>
+#include <libxml/xmlIO.h>
+#include <libxml/xmlstring.h>
+
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "android-base/logging.h"
+#include "xmpmeta/jpeg_io.h"
+#include "xmpmeta/md5.h"
+#include "xmpmeta/xml/const.h"
+#include "xmpmeta/xml/utils.h"
+#include "xmpmeta/xmp_const.h"
+#include "xmpmeta/xmp_data.h"
+#include "xmpmeta/xmp_parser.h"
+
+using photos_editing_formats::xml::FromXmlChar;
+using photos_editing_formats::xml::ToXmlChar;
+using photos_editing_formats::xml::XmlConst;
+
+namespace photos_editing_formats {
+namespace {
+
+const char kXmlStartTag = '<';
+
+const char kCEmptyString[] = "\x00";
+const int kXmlDumpFormat = 1;
+const int kInvalidIndex = -1;
+
+// True if 's' starts with substring 'x'.
+bool StartsWith(const string& s, const string& x) {
+ return s.size() >= x.size() && !s.compare(0, x.size(), x);
+}
+// True if 's' ends with substring 'x'.
+bool EndsWith(const string& s, const string& x) {
+ return s.size() >= x.size() && !s.compare(s.size() - x.size(), x.size(), x);
+}
+
+// Creates the outer rdf:RDF node for XMP.
+xmlNodePtr CreateXmpRdfNode() {
+ xmlNodePtr rdf_node = xmlNewNode(nullptr, ToXmlChar(XmlConst::RdfNodeName()));
+ xmlNsPtr rdf_ns = xmlNewNs(rdf_node, ToXmlChar(XmlConst::RdfNodeNs()),
+ ToXmlChar(XmlConst::RdfPrefix()));
+ xmlSetNs(rdf_node, rdf_ns);
+ return rdf_node;
+}
+
+// Creates the root node for XMP.
+xmlNodePtr CreateXmpRootNode() {
+ xmlNodePtr root_node = xmlNewNode(nullptr, ToXmlChar(XmpConst::NodeName()));
+ xmlNsPtr root_ns = xmlNewNs(root_node, ToXmlChar(XmpConst::Namespace()),
+ ToXmlChar(XmpConst::NamespacePrefix()));
+ xmlSetNs(root_node, root_ns);
+ xmlSetNsProp(root_node, root_ns, ToXmlChar(XmpConst::AdobePropName()),
+ ToXmlChar(XmpConst::AdobePropValue()));
+ return root_node;
+}
+
+// Creates a new XMP metadata section, with an x:xmpmeta element wrapping
+// rdf:RDF and rdf:Description child elements. This is the equivalent of
+// createXMPMeta in geo/lightfield/metadata/XmpUtils.java
+xmlDocPtr CreateXmpSection() {
+ xmlDocPtr xmp_meta = xmlNewDoc(ToXmlChar(XmlConst::Version()));
+
+ xmlNodePtr root_node = CreateXmpRootNode();
+ xmlNodePtr rdf_node = CreateXmpRdfNode();
+ xmlNodePtr description_node =
+ xmlNewNode(nullptr, ToXmlChar(XmlConst::RdfDescription()));
+ xmlNsPtr rdf_prefix_ns =
+ xmlNewNs(description_node, nullptr, ToXmlChar(XmlConst::RdfPrefix()));
+ xmlSetNs(description_node, rdf_prefix_ns);
+
+ // rdf:about is mandatory.
+ xmlSetNsProp(description_node, rdf_node->ns, ToXmlChar(XmlConst::RdfAbout()),
+ ToXmlChar(""));
+
+ // Align nodes into the proper hierarchy.
+ xmlAddChild(rdf_node, description_node);
+ xmlAddChild(root_node, rdf_node);
+ xmlDocSetRootElement(xmp_meta, root_node);
+
+ return xmp_meta;
+}
+
+void WriteIntTo4Bytes(int integer, std::ostream* output_stream) {
+ output_stream->put((integer >> 24) & 0xff);
+ output_stream->put((integer >> 16) & 0xff);
+ output_stream->put((integer >> 8) & 0xff);
+ output_stream->put(integer & 0xff);
+}
+
+// Serializes an XML document to a string.
+void SerializeMeta(const xmlDocPtr parent, string* serialized_value) {
+ if (parent == nullptr || parent->children == nullptr) {
+ LOG(WARNING) << "Nothing to serialize, either XML doc is null or it has "
+ << "no elements";
+ return;
+ }
+
+ std::ostringstream serialized_stream;
+ xmlChar* xml_doc_contents;
+ int doc_size = 0;
+ xmlDocDumpFormatMemoryEnc(parent, &xml_doc_contents, &doc_size,
+ XmlConst::EncodingStr(), kXmlDumpFormat);
+ const char* xml_doc_string = FromXmlChar(xml_doc_contents);
+
+ // Find the index of the second "<" so we can discard the first element,
+ // which is <?xml version...>, so start searching after the first "<". XMP
+ // starts directly afterwards.
+ const int xmp_start_idx =
+ static_cast<int>(strchr(&xml_doc_string[2], kXmlStartTag) -
+ xml_doc_string) -
+ 1;
+ serialized_stream.write(&xml_doc_string[xmp_start_idx],
+ doc_size - xmp_start_idx);
+ xmlFree(xml_doc_contents);
+ *serialized_value = serialized_stream.str();
+}
+
+// TODO(miraleung): Switch to different library for Android if needed.
+const string GetGUID(const string& to_hash) { return MD5Hash(to_hash); }
+
+// Creates the standard XMP section.
+void CreateStandardSectionXmpString(const string& buffer, string* value) {
+ std::ostringstream data_stream;
+ data_stream.write(XmpConst::Header(), strlen(XmpConst::Header()));
+ data_stream.write(kCEmptyString, 1);
+ data_stream.write(buffer.c_str(), buffer.length());
+ *value = data_stream.str();
+}
+
+// Creates the extended XMP section.
+void CreateExtendedSections(const string& buffer,
+ std::vector<Section>* extended_sections) {
+ string guid = GetGUID(buffer);
+ // Increment by 1 for the null byte in the middle.
+ const int header_length =
+ static_cast<int>(strlen(XmpConst::ExtensionHeader()) + 1 + guid.length());
+ const int buffer_length = static_cast<int>(buffer.length());
+ const int overhead = header_length + XmpConst::ExtensionHeaderOffset();
+ const int num_sections =
+ buffer_length / (XmpConst::ExtendedMaxBufferSize() - overhead) + 1;
+ for (int i = 0, position = 0; i < num_sections; ++i) {
+ const int section_size =
+ std::min(static_cast<int>(buffer_length - position + overhead),
+ XmpConst::ExtendedMaxBufferSize());
+ const int bytes_from_buffer = section_size - overhead;
+
+ // Header and GUID.
+ std::ostringstream data_stream;
+ data_stream.write(XmpConst::ExtensionHeader(),
+ strlen(XmpConst::ExtensionHeader()));
+ data_stream.write(kCEmptyString, 1);
+ data_stream.write(guid.c_str(), guid.length());
+
+ // Total buffer length.
+ WriteIntTo4Bytes(buffer_length, &data_stream);
+ // Current position.
+ WriteIntTo4Bytes(position, &data_stream);
+ // Data
+ data_stream.write(&buffer[position], bytes_from_buffer);
+ position += bytes_from_buffer;
+
+ extended_sections->push_back(Section(data_stream.str()));
+ }
+}
+
+int InsertStandardXMPSection(const string& buffer,
+ std::vector<Section>* sections) {
+ if (buffer.length() > XmpConst::MaxBufferSize()) {
+ LOG(WARNING) << "The standard XMP section (at size " << buffer.length()
+ << ") cannot have a size larger than "
+ << XmpConst::MaxBufferSize() << " bytes";
+ return kInvalidIndex;
+ }
+ string value;
+ CreateStandardSectionXmpString(buffer, &value);
+ Section xmp_section(value);
+ // If we can find the old XMP section, replace it with the new one
+ for (int index = 0; index < sections->size(); ++index) {
+ if (sections->at(index).IsMarkerApp1() &&
+ StartsWith(sections->at(index).data, XmpConst::Header())) {
+ // Replace with the new XMP data.
+ sections->at(index) = xmp_section;
+ return index;
+ }
+ }
+ // If the first section is EXIF, insert XMP data after it.
+ // Otherwise, make XMP data the first section.
+ const int position =
+ (!sections->empty() && sections->at(0).IsMarkerApp1()) ? 1 : 0;
+ sections->emplace(sections->begin() + position, xmp_section);
+ return position;
+}
+
+// Position is the index in the Section vector where the extended sections
+// will be inserted.
+void InsertExtendedXMPSections(const string& buffer, int position,
+ std::vector<Section>* sections) {
+ std::vector<Section> extended_sections;
+ CreateExtendedSections(buffer, &extended_sections);
+ sections->insert(sections->begin() + position, extended_sections.begin(),
+ extended_sections.end());
+}
+
+// Returns true if the respective sections in xmp_data and their serialized
+// counterparts are (correspondingly) not null and not empty.
+bool XmpSectionsAndSerializedDataValid(const XmpData& xmp_data,
+ const string& main_buffer,
+ const string& extended_buffer) {
+ // Standard section and its serialized counterpart cannot be null/empty.
+ // Extended section can be null XOR the extended buffer can be empty.
+ const bool extended_is_consistent =
+ ((xmp_data.ExtendedSection() == nullptr) == extended_buffer.empty());
+ const bool is_valid = (xmp_data.StandardSection() != nullptr) &&
+ !main_buffer.empty() && extended_is_consistent;
+ if (!is_valid) {
+ LOG(ERROR) << "XMP sections Xor their serialized counterparts are empty";
+ }
+ return is_valid;
+}
+
+// Updates a list of JPEG sections with serialized XMP data.
+bool UpdateSections(const string& main_buffer, const string& extended_buffer,
+ std::vector<Section>* sections) {
+ if (main_buffer.empty()) {
+ LOG(WARNING) << "Main section was empty";
+ return false;
+ }
+
+ // Update the list of sections with the new standard XMP section.
+ const int main_index = InsertStandardXMPSection(main_buffer, sections);
+ if (main_index < 0) {
+ LOG(WARNING) << "Could not find a valid index for inserting the "
+ << "standard sections";
+ return false;
+ }
+
+ // Insert the extended section right after the main section.
+ if (!extended_buffer.empty()) {
+ InsertExtendedXMPSections(extended_buffer, main_index + 1, sections);
+ }
+ return true;
+}
+
+void LinkXmpStandardAndExtendedSections(const string& extended_buffer,
+ xmlDocPtr standard_section) {
+ xmlNodePtr description_node =
+ xml::GetFirstDescriptionElement(standard_section);
+ xmlNsPtr xmp_note_ns_ptr =
+ xmlNewNs(description_node, ToXmlChar(XmpConst::NoteNamespace()),
+ ToXmlChar(XmpConst::HasExtensionPrefix()));
+ const string extended_id = GetGUID(extended_buffer);
+ xmlSetNsProp(description_node, xmp_note_ns_ptr,
+ ToXmlChar(XmpConst::HasExtension()),
+ ToXmlChar(extended_id.c_str()));
+ xmlUnsetProp(description_node, ToXmlChar(XmpConst::HasExtension()));
+}
+
+} // namespace
+
+std::unique_ptr<XmpData> CreateXmpData(bool create_extended) {
+ std::unique_ptr<XmpData> xmp_data(new XmpData());
+ *xmp_data->MutableStandardSection() = CreateXmpSection();
+ if (create_extended) {
+ *xmp_data->MutableExtendedSection() = CreateXmpSection();
+ }
+ return xmp_data;
+}
+
+bool WriteLeftEyeAndXmpMeta(const string& left_data, const string& filename,
+ const XmpData& xmp_data) {
+ std::istringstream input_jpeg_stream(left_data);
+ std::ofstream output_jpeg_stream;
+ output_jpeg_stream.open(filename, std::ostream::out);
+ bool success = WriteLeftEyeAndXmpMeta(filename, xmp_data, &input_jpeg_stream,
+ &output_jpeg_stream);
+ output_jpeg_stream.close();
+ return success;
+}
+
+bool WriteLeftEyeAndXmpMeta(const string& filename, const XmpData& xmp_data,
+ std::istringstream* input_jpeg_stream,
+ std::ofstream* output_jpeg_stream) {
+ if (input_jpeg_stream == nullptr || output_jpeg_stream == nullptr) {
+ LOG(ERROR) << "Input and output streams must both be non-null";
+ return false;
+ }
+
+ // Get a list of sections from the input stream.
+ ParseOptions parse_options;
+ std::vector<Section> sections = Parse(parse_options, input_jpeg_stream);
+
+ string extended_buffer;
+ if (xmp_data.ExtendedSection() != nullptr) {
+ SerializeMeta(xmp_data.ExtendedSection(), &extended_buffer);
+ LinkXmpStandardAndExtendedSections(extended_buffer,
+ xmp_data.StandardSection());
+ }
+ string main_buffer;
+ SerializeMeta(xmp_data.StandardSection(), &main_buffer);
+
+ // Update the input sections with the XMP data.
+ if (!XmpSectionsAndSerializedDataValid(xmp_data, main_buffer,
+ extended_buffer) ||
+ !UpdateSections(main_buffer, extended_buffer, &sections)) {
+ return false;
+ }
+
+ // Write the sections to the output stream.
+ if (!output_jpeg_stream->is_open()) {
+ output_jpeg_stream->open(filename, std::ostream::out);
+ }
+
+ WriteSections(sections, output_jpeg_stream);
+ return true;
+}
+
+bool AddXmpMetaToJpegStream(std::istream* input_jpeg_stream,
+ const XmpData& xmp_data,
+ std::ostream* output_jpeg_stream) {
+ // Get a list of sections from the input stream.
+ ParseOptions parse_options;
+ std::vector<Section> sections = Parse(parse_options, input_jpeg_stream);
+
+ string extended_buffer;
+ if (xmp_data.ExtendedSection() != nullptr) {
+ SerializeMeta(xmp_data.ExtendedSection(), &extended_buffer);
+ LinkXmpStandardAndExtendedSections(extended_buffer,
+ xmp_data.StandardSection());
+ }
+ string main_buffer;
+ SerializeMeta(xmp_data.StandardSection(), &main_buffer);
+
+ // Update the input sections with the XMP data.
+ if (!XmpSectionsAndSerializedDataValid(xmp_data, main_buffer,
+ extended_buffer) ||
+ !UpdateSections(main_buffer, extended_buffer, &sections)) {
+ return false;
+ }
+
+ WriteSections(sections, output_jpeg_stream);
+ return true;
+}
+
+} // namespace photos_editing_formats