blob: fc78ad13b829634ebe6a6714a595712d5f194774 [file] [log] [blame]
#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, &sections)) {
return false;
}
WriteSections(sections, output_jpeg_stream);
return true;
}
} // namespace xmpmeta
} // namespace dynamic_depth