You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
12 KiB
321 lines
12 KiB
#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
|