| #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 ::dynamic_depth::xmpmeta::xml::FromXmlChar; |
| using ::dynamic_depth::xmpmeta::xml::GetFirstDescriptionElement; |
| using ::dynamic_depth::xmpmeta::xml::ToXmlChar; |
| using ::dynamic_depth::xmpmeta::xml::XmlConst; |
| |
| namespace dynamic_depth { |
| namespace xmpmeta { |
| 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 = 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(xmp_data, &input_jpeg_stream, &output_jpeg_stream); |
| output_jpeg_stream.close(); |
| return success; |
| } |
| |
| bool WriteLeftEyeAndXmpMeta(const XmpData& xmp_data, |
| std::istream* input_jpeg_stream, |
| std::ostream* 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; |
| } |
| |
| WriteSections(sections, output_jpeg_stream); |
| return true; |
| } |
| |
| } // namespace xmpmeta |
| } // namespace dynamic_depth |