601 lines
20 KiB
C++
601 lines
20 KiB
C++
/*
|
|
* Copyright 2022 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include <ultrahdr/jpegrutils.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include <image_io/xml/xml_reader.h>
|
|
#include <image_io/xml/xml_writer.h>
|
|
#include <image_io/base/message_handler.h>
|
|
#include <image_io/xml/xml_element_rules.h>
|
|
#include <image_io/xml/xml_handler.h>
|
|
#include <image_io/xml/xml_rule.h>
|
|
#include <utils/Log.h>
|
|
|
|
using namespace photos_editing_formats::image_io;
|
|
using namespace std;
|
|
|
|
namespace android::ultrahdr {
|
|
/*
|
|
* Helper function used for generating XMP metadata.
|
|
*
|
|
* @param prefix The prefix part of the name.
|
|
* @param suffix The suffix part of the name.
|
|
* @return A name of the form "prefix:suffix".
|
|
*/
|
|
static inline string Name(const string &prefix, const string &suffix) {
|
|
std::stringstream ss;
|
|
ss << prefix << ":" << suffix;
|
|
return ss.str();
|
|
}
|
|
|
|
DataStruct::DataStruct(int s) {
|
|
data = malloc(s);
|
|
length = s;
|
|
memset(data, 0, s);
|
|
writePos = 0;
|
|
}
|
|
|
|
DataStruct::~DataStruct() {
|
|
if (data != nullptr) {
|
|
free(data);
|
|
}
|
|
}
|
|
|
|
void* DataStruct::getData() {
|
|
return data;
|
|
}
|
|
|
|
int DataStruct::getLength() {
|
|
return length;
|
|
}
|
|
|
|
int DataStruct::getBytesWritten() {
|
|
return writePos;
|
|
}
|
|
|
|
bool DataStruct::write8(uint8_t value) {
|
|
uint8_t v = value;
|
|
return write(&v, 1);
|
|
}
|
|
|
|
bool DataStruct::write16(uint16_t value) {
|
|
uint16_t v = value;
|
|
return write(&v, 2);
|
|
}
|
|
bool DataStruct::write32(uint32_t value) {
|
|
uint32_t v = value;
|
|
return write(&v, 4);
|
|
}
|
|
|
|
bool DataStruct::write(const void* src, int size) {
|
|
if (writePos + size > length) {
|
|
ALOGE("Writing out of boundary: write position: %d, size: %d, capacity: %d",
|
|
writePos, size, length);
|
|
return false;
|
|
}
|
|
memcpy((uint8_t*) data + writePos, src, size);
|
|
writePos += size;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Helper function used for writing data to destination.
|
|
*/
|
|
status_t Write(jr_compressed_ptr destination, const void* source, size_t length, int &position) {
|
|
if (position + length > destination->maxLength) {
|
|
return ERROR_JPEGR_BUFFER_TOO_SMALL;
|
|
}
|
|
|
|
memcpy((uint8_t*)destination->data + sizeof(uint8_t) * position, source, length);
|
|
position += length;
|
|
return NO_ERROR;
|
|
}
|
|
|
|
// Extremely simple XML Handler - just searches for interesting elements
|
|
class XMPXmlHandler : public XmlHandler {
|
|
public:
|
|
|
|
XMPXmlHandler() : XmlHandler() {
|
|
state = NotStrarted;
|
|
versionFound = false;
|
|
minContentBoostFound = false;
|
|
maxContentBoostFound = false;
|
|
gammaFound = false;
|
|
offsetSdrFound = false;
|
|
offsetHdrFound = false;
|
|
hdrCapacityMinFound = false;
|
|
hdrCapacityMaxFound = false;
|
|
baseRenditionIsHdrFound = false;
|
|
}
|
|
|
|
enum ParseState {
|
|
NotStrarted,
|
|
Started,
|
|
Done
|
|
};
|
|
|
|
virtual DataMatchResult StartElement(const XmlTokenContext& context) {
|
|
string val;
|
|
if (context.BuildTokenValue(&val)) {
|
|
if (!val.compare(containerName)) {
|
|
state = Started;
|
|
} else {
|
|
if (state != Done) {
|
|
state = NotStrarted;
|
|
}
|
|
}
|
|
}
|
|
return context.GetResult();
|
|
}
|
|
|
|
virtual DataMatchResult FinishElement(const XmlTokenContext& context) {
|
|
if (state == Started) {
|
|
state = Done;
|
|
lastAttributeName = "";
|
|
}
|
|
return context.GetResult();
|
|
}
|
|
|
|
virtual DataMatchResult AttributeName(const XmlTokenContext& context) {
|
|
string val;
|
|
if (state == Started) {
|
|
if (context.BuildTokenValue(&val)) {
|
|
if (!val.compare(versionAttrName)) {
|
|
lastAttributeName = versionAttrName;
|
|
} else if (!val.compare(maxContentBoostAttrName)) {
|
|
lastAttributeName = maxContentBoostAttrName;
|
|
} else if (!val.compare(minContentBoostAttrName)) {
|
|
lastAttributeName = minContentBoostAttrName;
|
|
} else if (!val.compare(gammaAttrName)) {
|
|
lastAttributeName = gammaAttrName;
|
|
} else if (!val.compare(offsetSdrAttrName)) {
|
|
lastAttributeName = offsetSdrAttrName;
|
|
} else if (!val.compare(offsetHdrAttrName)) {
|
|
lastAttributeName = offsetHdrAttrName;
|
|
} else if (!val.compare(hdrCapacityMinAttrName)) {
|
|
lastAttributeName = hdrCapacityMinAttrName;
|
|
} else if (!val.compare(hdrCapacityMaxAttrName)) {
|
|
lastAttributeName = hdrCapacityMaxAttrName;
|
|
} else if (!val.compare(baseRenditionIsHdrAttrName)) {
|
|
lastAttributeName = baseRenditionIsHdrAttrName;
|
|
} else {
|
|
lastAttributeName = "";
|
|
}
|
|
}
|
|
}
|
|
return context.GetResult();
|
|
}
|
|
|
|
virtual DataMatchResult AttributeValue(const XmlTokenContext& context) {
|
|
string val;
|
|
if (state == Started) {
|
|
if (context.BuildTokenValue(&val, true)) {
|
|
if (!lastAttributeName.compare(versionAttrName)) {
|
|
versionStr = val;
|
|
versionFound = true;
|
|
} else if (!lastAttributeName.compare(maxContentBoostAttrName)) {
|
|
maxContentBoostStr = val;
|
|
maxContentBoostFound = true;
|
|
} else if (!lastAttributeName.compare(minContentBoostAttrName)) {
|
|
minContentBoostStr = val;
|
|
minContentBoostFound = true;
|
|
} else if (!lastAttributeName.compare(gammaAttrName)) {
|
|
gammaStr = val;
|
|
gammaFound = true;
|
|
} else if (!lastAttributeName.compare(offsetSdrAttrName)) {
|
|
offsetSdrStr = val;
|
|
offsetSdrFound = true;
|
|
} else if (!lastAttributeName.compare(offsetHdrAttrName)) {
|
|
offsetHdrStr = val;
|
|
offsetHdrFound = true;
|
|
} else if (!lastAttributeName.compare(hdrCapacityMinAttrName)) {
|
|
hdrCapacityMinStr = val;
|
|
hdrCapacityMinFound = true;
|
|
} else if (!lastAttributeName.compare(hdrCapacityMaxAttrName)) {
|
|
hdrCapacityMaxStr = val;
|
|
hdrCapacityMaxFound = true;
|
|
} else if (!lastAttributeName.compare(baseRenditionIsHdrAttrName)) {
|
|
baseRenditionIsHdrStr = val;
|
|
baseRenditionIsHdrFound = true;
|
|
}
|
|
}
|
|
}
|
|
return context.GetResult();
|
|
}
|
|
|
|
bool getVersion(string* version, bool* present) {
|
|
if (state == Done) {
|
|
*version = versionStr;
|
|
*present = versionFound;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool getMaxContentBoost(float* max_content_boost, bool* present) {
|
|
if (state == Done) {
|
|
*present = maxContentBoostFound;
|
|
stringstream ss(maxContentBoostStr);
|
|
float val;
|
|
if (ss >> val) {
|
|
*max_content_boost = exp2(val);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool getMinContentBoost(float* min_content_boost, bool* present) {
|
|
if (state == Done) {
|
|
*present = minContentBoostFound;
|
|
stringstream ss(minContentBoostStr);
|
|
float val;
|
|
if (ss >> val) {
|
|
*min_content_boost = exp2(val);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool getGamma(float* gamma, bool* present) {
|
|
if (state == Done) {
|
|
*present = gammaFound;
|
|
stringstream ss(gammaStr);
|
|
float val;
|
|
if (ss >> val) {
|
|
*gamma = val;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
bool getOffsetSdr(float* offset_sdr, bool* present) {
|
|
if (state == Done) {
|
|
*present = offsetSdrFound;
|
|
stringstream ss(offsetSdrStr);
|
|
float val;
|
|
if (ss >> val) {
|
|
*offset_sdr = val;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
bool getOffsetHdr(float* offset_hdr, bool* present) {
|
|
if (state == Done) {
|
|
*present = offsetHdrFound;
|
|
stringstream ss(offsetHdrStr);
|
|
float val;
|
|
if (ss >> val) {
|
|
*offset_hdr = val;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
bool getHdrCapacityMin(float* hdr_capacity_min, bool* present) {
|
|
if (state == Done) {
|
|
*present = hdrCapacityMinFound;
|
|
stringstream ss(hdrCapacityMinStr);
|
|
float val;
|
|
if (ss >> val) {
|
|
*hdr_capacity_min = exp2(val);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
bool getHdrCapacityMax(float* hdr_capacity_max, bool* present) {
|
|
if (state == Done) {
|
|
*present = hdrCapacityMaxFound;
|
|
stringstream ss(hdrCapacityMaxStr);
|
|
float val;
|
|
if (ss >> val) {
|
|
*hdr_capacity_max = exp2(val);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
bool getBaseRenditionIsHdr(bool* base_rendition_is_hdr, bool* present) {
|
|
if (state == Done) {
|
|
*present = baseRenditionIsHdrFound;
|
|
if (!baseRenditionIsHdrStr.compare("False")) {
|
|
*base_rendition_is_hdr = false;
|
|
return true;
|
|
} else if (!baseRenditionIsHdrStr.compare("True")) {
|
|
*base_rendition_is_hdr = true;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
static const string containerName;
|
|
|
|
static const string versionAttrName;
|
|
string versionStr;
|
|
bool versionFound;
|
|
static const string maxContentBoostAttrName;
|
|
string maxContentBoostStr;
|
|
bool maxContentBoostFound;
|
|
static const string minContentBoostAttrName;
|
|
string minContentBoostStr;
|
|
bool minContentBoostFound;
|
|
static const string gammaAttrName;
|
|
string gammaStr;
|
|
bool gammaFound;
|
|
static const string offsetSdrAttrName;
|
|
string offsetSdrStr;
|
|
bool offsetSdrFound;
|
|
static const string offsetHdrAttrName;
|
|
string offsetHdrStr;
|
|
bool offsetHdrFound;
|
|
static const string hdrCapacityMinAttrName;
|
|
string hdrCapacityMinStr;
|
|
bool hdrCapacityMinFound;
|
|
static const string hdrCapacityMaxAttrName;
|
|
string hdrCapacityMaxStr;
|
|
bool hdrCapacityMaxFound;
|
|
static const string baseRenditionIsHdrAttrName;
|
|
string baseRenditionIsHdrStr;
|
|
bool baseRenditionIsHdrFound;
|
|
|
|
string lastAttributeName;
|
|
ParseState state;
|
|
};
|
|
|
|
// GContainer XMP constants - URI and namespace prefix
|
|
const string kContainerUri = "http://ns.google.com/photos/1.0/container/";
|
|
const string kContainerPrefix = "Container";
|
|
|
|
// GContainer XMP constants - element and attribute names
|
|
const string kConDirectory = Name(kContainerPrefix, "Directory");
|
|
const string kConItem = Name(kContainerPrefix, "Item");
|
|
|
|
// GContainer XMP constants - names for XMP handlers
|
|
const string XMPXmlHandler::containerName = "rdf:Description";
|
|
// Item XMP constants - URI and namespace prefix
|
|
const string kItemUri = "http://ns.google.com/photos/1.0/container/item/";
|
|
const string kItemPrefix = "Item";
|
|
|
|
// Item XMP constants - element and attribute names
|
|
const string kItemLength = Name(kItemPrefix, "Length");
|
|
const string kItemMime = Name(kItemPrefix, "Mime");
|
|
const string kItemSemantic = Name(kItemPrefix, "Semantic");
|
|
|
|
// Item XMP constants - element and attribute values
|
|
const string kSemanticPrimary = "Primary";
|
|
const string kSemanticGainMap = "GainMap";
|
|
const string kMimeImageJpeg = "image/jpeg";
|
|
|
|
// GainMap XMP constants - URI and namespace prefix
|
|
const string kGainMapUri = "http://ns.adobe.com/hdr-gain-map/1.0/";
|
|
const string kGainMapPrefix = "hdrgm";
|
|
|
|
// GainMap XMP constants - element and attribute names
|
|
const string kMapVersion = Name(kGainMapPrefix, "Version");
|
|
const string kMapGainMapMin = Name(kGainMapPrefix, "GainMapMin");
|
|
const string kMapGainMapMax = Name(kGainMapPrefix, "GainMapMax");
|
|
const string kMapGamma = Name(kGainMapPrefix, "Gamma");
|
|
const string kMapOffsetSdr = Name(kGainMapPrefix, "OffsetSDR");
|
|
const string kMapOffsetHdr = Name(kGainMapPrefix, "OffsetHDR");
|
|
const string kMapHDRCapacityMin = Name(kGainMapPrefix, "HDRCapacityMin");
|
|
const string kMapHDRCapacityMax = Name(kGainMapPrefix, "HDRCapacityMax");
|
|
const string kMapBaseRenditionIsHDR = Name(kGainMapPrefix, "BaseRenditionIsHDR");
|
|
|
|
// GainMap XMP constants - names for XMP handlers
|
|
const string XMPXmlHandler::versionAttrName = kMapVersion;
|
|
const string XMPXmlHandler::minContentBoostAttrName = kMapGainMapMin;
|
|
const string XMPXmlHandler::maxContentBoostAttrName = kMapGainMapMax;
|
|
const string XMPXmlHandler::gammaAttrName = kMapGamma;
|
|
const string XMPXmlHandler::offsetSdrAttrName = kMapOffsetSdr;
|
|
const string XMPXmlHandler::offsetHdrAttrName = kMapOffsetHdr;
|
|
const string XMPXmlHandler::hdrCapacityMinAttrName = kMapHDRCapacityMin;
|
|
const string XMPXmlHandler::hdrCapacityMaxAttrName = kMapHDRCapacityMax;
|
|
const string XMPXmlHandler::baseRenditionIsHdrAttrName = kMapBaseRenditionIsHDR;
|
|
|
|
bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, ultrahdr_metadata_struct* metadata) {
|
|
string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
|
|
|
|
if (xmp_size < nameSpace.size()+2) {
|
|
// Data too short
|
|
return false;
|
|
}
|
|
|
|
if (strncmp(reinterpret_cast<char*>(xmp_data), nameSpace.c_str(), nameSpace.size())) {
|
|
// Not correct namespace
|
|
return false;
|
|
}
|
|
|
|
// Position the pointers to the start of XMP XML portion
|
|
xmp_data += nameSpace.size()+1;
|
|
xmp_size -= nameSpace.size()+1;
|
|
XMPXmlHandler handler;
|
|
|
|
// We need to remove tail data until the closing tag. Otherwise parser will throw an error.
|
|
while(xmp_data[xmp_size-1]!='>' && xmp_size > 1) {
|
|
xmp_size--;
|
|
}
|
|
|
|
string str(reinterpret_cast<const char*>(xmp_data), xmp_size);
|
|
MessageHandler msg_handler;
|
|
unique_ptr<XmlRule> rule(new XmlElementRule);
|
|
XmlReader reader(&handler, &msg_handler);
|
|
reader.StartParse(std::move(rule));
|
|
reader.Parse(str);
|
|
reader.FinishParse();
|
|
if (reader.HasErrors()) {
|
|
// Parse error
|
|
return false;
|
|
}
|
|
|
|
// Apply default values to any not-present fields, except for Version,
|
|
// maxContentBoost, and hdrCapacityMax, which are required. Return false if
|
|
// we encounter a present field that couldn't be parsed, since this
|
|
// indicates it is invalid (eg. string where there should be a float).
|
|
bool present = false;
|
|
if (!handler.getVersion(&metadata->version, &present) || !present) {
|
|
return false;
|
|
}
|
|
if (!handler.getMaxContentBoost(&metadata->maxContentBoost, &present) || !present) {
|
|
return false;
|
|
}
|
|
if (!handler.getHdrCapacityMax(&metadata->hdrCapacityMax, &present) || !present) {
|
|
return false;
|
|
}
|
|
if (!handler.getMinContentBoost(&metadata->minContentBoost, &present)) {
|
|
if (present) return false;
|
|
metadata->minContentBoost = 1.0f;
|
|
}
|
|
if (!handler.getGamma(&metadata->gamma, &present)) {
|
|
if (present) return false;
|
|
metadata->gamma = 1.0f;
|
|
}
|
|
if (!handler.getOffsetSdr(&metadata->offsetSdr, &present)) {
|
|
if (present) return false;
|
|
metadata->offsetSdr = 1.0f / 64.0f;
|
|
}
|
|
if (!handler.getOffsetHdr(&metadata->offsetHdr, &present)) {
|
|
if (present) return false;
|
|
metadata->offsetHdr = 1.0f / 64.0f;
|
|
}
|
|
if (!handler.getHdrCapacityMin(&metadata->hdrCapacityMin, &present)) {
|
|
if (present) return false;
|
|
metadata->hdrCapacityMin = 1.0f;
|
|
}
|
|
|
|
bool base_rendition_is_hdr;
|
|
if (!handler.getBaseRenditionIsHdr(&base_rendition_is_hdr, &present)) {
|
|
if (present) return false;
|
|
base_rendition_is_hdr = false;
|
|
}
|
|
if (base_rendition_is_hdr) {
|
|
ALOGE("Base rendition of HDR is not supported!");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
string generateXmpForPrimaryImage(int secondary_image_length, ultrahdr_metadata_struct& metadata) {
|
|
const vector<string> kConDirSeq({kConDirectory, string("rdf:Seq")});
|
|
const vector<string> kLiItem({string("rdf:li"), kConItem});
|
|
|
|
std::stringstream ss;
|
|
photos_editing_formats::image_io::XmlWriter writer(ss);
|
|
writer.StartWritingElement("x:xmpmeta");
|
|
writer.WriteXmlns("x", "adobe:ns:meta/");
|
|
writer.WriteAttributeNameAndValue("x:xmptk", "Adobe XMP Core 5.1.2");
|
|
writer.StartWritingElement("rdf:RDF");
|
|
writer.WriteXmlns("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
|
|
writer.StartWritingElement("rdf:Description");
|
|
writer.WriteXmlns(kContainerPrefix, kContainerUri);
|
|
writer.WriteXmlns(kItemPrefix, kItemUri);
|
|
writer.WriteXmlns(kGainMapPrefix, kGainMapUri);
|
|
writer.WriteAttributeNameAndValue(kMapVersion, metadata.version);
|
|
|
|
writer.StartWritingElements(kConDirSeq);
|
|
|
|
size_t item_depth = writer.StartWritingElement("rdf:li");
|
|
writer.WriteAttributeNameAndValue("rdf:parseType", "Resource");
|
|
writer.StartWritingElement(kConItem);
|
|
writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticPrimary);
|
|
writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg);
|
|
writer.FinishWritingElementsToDepth(item_depth);
|
|
|
|
writer.StartWritingElement("rdf:li");
|
|
writer.WriteAttributeNameAndValue("rdf:parseType", "Resource");
|
|
writer.StartWritingElement(kConItem);
|
|
writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticGainMap);
|
|
writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg);
|
|
writer.WriteAttributeNameAndValue(kItemLength, secondary_image_length);
|
|
|
|
writer.FinishWriting();
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
string generateXmpForSecondaryImage(ultrahdr_metadata_struct& metadata) {
|
|
const vector<string> kConDirSeq({kConDirectory, string("rdf:Seq")});
|
|
|
|
std::stringstream ss;
|
|
photos_editing_formats::image_io::XmlWriter writer(ss);
|
|
writer.StartWritingElement("x:xmpmeta");
|
|
writer.WriteXmlns("x", "adobe:ns:meta/");
|
|
writer.WriteAttributeNameAndValue("x:xmptk", "Adobe XMP Core 5.1.2");
|
|
writer.StartWritingElement("rdf:RDF");
|
|
writer.WriteXmlns("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
|
|
writer.StartWritingElement("rdf:Description");
|
|
writer.WriteXmlns(kGainMapPrefix, kGainMapUri);
|
|
writer.WriteAttributeNameAndValue(kMapVersion, metadata.version);
|
|
writer.WriteAttributeNameAndValue(kMapGainMapMin, log2(metadata.minContentBoost));
|
|
writer.WriteAttributeNameAndValue(kMapGainMapMax, log2(metadata.maxContentBoost));
|
|
writer.WriteAttributeNameAndValue(kMapGamma, metadata.gamma);
|
|
writer.WriteAttributeNameAndValue(kMapOffsetSdr, metadata.offsetSdr);
|
|
writer.WriteAttributeNameAndValue(kMapOffsetHdr, metadata.offsetHdr);
|
|
writer.WriteAttributeNameAndValue(kMapHDRCapacityMin, log2(metadata.hdrCapacityMin));
|
|
writer.WriteAttributeNameAndValue(kMapHDRCapacityMax, log2(metadata.hdrCapacityMax));
|
|
writer.WriteAttributeNameAndValue(kMapBaseRenditionIsHDR, "False");
|
|
writer.FinishWriting();
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
} // namespace android::ultrahdr
|