diff options
Diffstat (limited to 'internal/xmpmeta/xmp_writer.cc')
-rw-r--r-- | internal/xmpmeta/xmp_writer.cc | 350 |
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, §ions)) { + 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, §ions)) { + return false; + } + + WriteSections(sections, output_jpeg_stream); + return true; +} + +} // namespace photos_editing_formats |