#pragma once #include "Utils.h" #include "PaletteList.h" #include "VariationList.h" #include "Ember.h" /// /// EmberToXml class. /// namespace EmberNs { /// /// Class for converting ember objects to Xml documents. /// Support for saving one or more to a single file. /// Template argument expected to be float or double. /// template class EMBER_API EmberToXml : public EmberReport { public: /// /// Empty constructor. /// EmberToXml() { } /// /// Save the ember to the specified file. /// /// Full path and filename /// The ember to save /// How deep the edit depth goes /// If true included edit tags, else don't. /// If true use integers instead of floating point numbers when embedding a non-hex formatted palette, else use floating point numbers. /// If true, embed a hexadecimal palette instead of Xml Color tags, else use Xml color tags. /// If true, append to the file if it already exists, else create a new file. /// Whether a new file is to be started /// Whether an existing file is to be ended /// True if successful, else false bool Save(const string& filename, Ember& ember, size_t printEditDepth, bool doEdits, bool intPalette, bool hexPalette, bool append = false, bool start = false, bool finish = false) { vector> vec; vec.push_back(ember); return Save(filename, vec, printEditDepth, doEdits, intPalette, hexPalette, append, start, finish); } /// /// Save a vector of embers to the specified file. /// /// Full path and filename /// The vector of embers to save /// How deep the edit depth goes /// If true included edit tags, else don't. /// If true use integers instead of floating point numbers when embedding a non-hex formatted palette, else use floating point numbers. /// If true, embed a hexadecimal palette instead of Xml Color tags, else use Xml color tags. /// If true, append to the file if it already exists, else create a new file. /// Whether a new file is to be started /// Whether an existing file is to be ended /// True if successful, else false bool Save(const string& filename, vector>& embers, size_t printEditDepth, bool doEdits, bool intPalette, bool hexPalette, bool append = false, bool start = false, bool finish = false) { bool b = false; bool hasTimes = false; T t = 0; string temp; ofstream f; try { if (append) f.open(filename, std::ofstream::out | std::ofstream::app);//Appending allows us to write multiple embers to a single file. else f.open(filename); if (f.is_open()) { //Check to see if there are valid times by checking if any differed. //If so, assume they were intentionally entered times. for (size_t i = 1; i < embers.size(); i++) { if (embers[i].m_Time != embers[i - 1].m_Time) { hasTimes = true; break; } } if (!hasTimes) for (auto& ember : embers) ember.m_Time = t++; if ((append && start) || !append) { temp = "\n"; //temp = "\n\n"; f.write(temp.c_str(), temp.size()); } for (auto& ember : embers) { string s = ToString(ember, "", printEditDepth, doEdits, intPalette, hexPalette); f.write(s.c_str(), s.size()); } if ((append && finish) || !append) { temp = "\n"; f.write(temp.c_str(), temp.size()); } f.close(); b = true; } else { cout << "Error: Writing flame " << filename << " failed." << endl; b = false; } } catch (const std::exception& e) { cout << "Error: Writing flame " << filename << " failed: " << e.what() << endl; b = false; } catch (...) { cout << "Error: Writing flame " << filename << " failed." << endl; b = false; } if (f.is_open()) f.close(); return b; } /// /// Return the Xml string representation of an ember. /// /// The ember to create the Xml with /// If true, add extra attributes, else don't /// How deep the edit depth goes /// If true included edit tags, else don't. /// If true use integers instead of floating point numbers when embedding a non-hex formatted palette, else use floating point numbers. /// If true, embed a hexadecimal palette instead of Xml Color tags, else use Xml color tags. /// The Xml string representation of the passed in ember string ToString(Ember& ember, const string& extraAttributes, size_t printEditDepth, bool doEdits, bool intPalette, bool hexPalette = true) { size_t i, j; string s; ostringstream os; vector*> variations; os << "(1, ember.m_Supersample) << "\""; os << " filter=\"" << ember.m_SpatialFilterRadius << "\""; os << " filter_shape=\"" << ToLower(SpatialFilterCreator::ToString(ember.m_SpatialFilterType)) << "\""; os << " temporal_filter_type=\"" << ToLower(TemporalFilterCreator::ToString(ember.m_TemporalFilterType)) << "\""; if (ember.m_TemporalFilterType == eTemporalFilterType::EXP_TEMPORAL_FILTER) os << " temporal_filter_exp=\"" << ember.m_TemporalFilterExp << "\""; os << " temporal_filter_width=\"" << ember.m_TemporalFilterWidth << "\""; os << " quality=\"" << ember.m_Quality << "\""; os << " temporal_samples=\"" << ember.m_TemporalSamples << "\""; os << " sub_batch_size=\"" << ember.m_SubBatchSize << "\""; os << " fuse=\"" << ember.m_FuseCount << "\""; os << " background=\"" << ember.m_Background.r << " " << ember.m_Background.g << " " << ember.m_Background.b << "\""; os << " brightness=\"" << ember.m_Brightness << "\""; os << " gamma=\"" << ember.m_Gamma << "\""; os << " highlight_power=\"" << ember.m_HighlightPower << "\""; os << " vibrancy=\"" << ember.m_Vibrancy << "\""; os << " estimator_radius=\"" << ember.m_MaxRadDE << "\""; os << " estimator_minimum=\"" << ember.m_MinRadDE << "\""; os << " estimator_curve=\"" << ember.m_CurveDE << "\""; os << " gamma_threshold=\"" << ember.m_GammaThresh << "\""; os << " cam_zpos=\"" << ember.m_CamZPos << "\""; os << " cam_persp=\"" << ember.m_CamPerspective << "\""; os << " cam_yaw=\"" << ember.m_CamYaw << "\""; os << " cam_pitch=\"" << ember.m_CamPitch << "\""; os << " cam_dof=\"" << ember.m_CamDepthBlur << "\""; if (ember.m_PaletteMode == ePaletteMode::PALETTE_STEP) os << " palette_mode=\"step\""; else if (ember.m_PaletteMode == ePaletteMode::PALETTE_LINEAR) os << " palette_mode=\"linear\""; if (ember.m_Interp == eInterp::EMBER_INTERP_LINEAR) os << " interpolation=\"linear\""; else if (ember.m_Interp == eInterp::EMBER_INTERP_SMOOTH) os << " interpolation=\"smooth\""; if (ember.m_AffineInterp == eAffineInterp::AFFINE_INTERP_LINEAR) os << " interpolation_type=\"linear\""; else if (ember.m_AffineInterp == eAffineInterp::AFFINE_INTERP_LOG) os << " interpolation_type=\"log\""; else if (ember.m_AffineInterp == eAffineInterp::AFFINE_INTERP_COMPAT) os << " interpolation_type=\"old\""; else if (ember.m_AffineInterp == eAffineInterp::AFFINE_INTERP_OLDER) os << " interpolation_type=\"older\""; if (ember.m_PaletteInterp == ePaletteInterp::INTERP_SWEEP) os << " palette_interpolation=\"sweep\""; if (!extraAttributes.empty()) os << " " << extraAttributes; os << " plugins=\""; ember.GetPresentVariations(variations, false); if (!variations.empty()) for (auto var : variations) os << var->Name() << (var != variations.back() ? " " : "\""); else os << "\""; os << " new_linear=\"1\""; os << " curves=\""; for (glm::length_t ci = 0; ci < 4; ci++) { for (glm::length_t cj = 0; cj < 4; cj++) { os << ember.m_Curves.m_Points[ci][cj].x << " "; os << ember.m_Curves.m_Points[ci][cj].y << " "; os << ember.m_Curves.m_Weights[ci][cj] << " "; } } os << "\">\n"; for (i = 0; i < ember.m_EmberMotionElements.size(); ++i) os << " " << ToString(ember.m_EmberMotionElements[i]); //This is a grey area, what to do about symmetry to avoid duplicating the symmetry xforms when reading back?//TODO//BUG. //if (ember.m_Symmetry) // os << " \n"; for (i = 0; i < ember.XformCount(); i++) os << ToString(*ember.GetXform(i), ember.XformCount(), false, false);//Not final, don't do motion. if (ember.UseFinalXform()) os << ToString(*ember.NonConstFinalXform(), ember.XformCount(), true, false);//Final, don't do motion. //Note that only embedded palettes are saved. The old style of specifying a palette index to look up in a default palette file //is no longer supported, as it makes no sense when using multiple palette files. The only way it could work is if the index was //always meant to refer to the default file, or if the filename was embedded as well. It's easier, more straightforward and //less error prone to just embed the palette. if (hexPalette) { os << " \n"; for (i = 0; i < 32; i++) { os << " "; for (j = 0; j < 8; j++) { size_t idx = 8 * i + j; os << hex << setw(2) << setfill('0') << int(std::rint(ember.m_Palette[idx][0] * 255)); os << hex << setw(2) << setfill('0') << int(std::rint(ember.m_Palette[idx][1] * 255)); os << hex << setw(2) << setfill('0') << int(std::rint(ember.m_Palette[idx][2] * 255)); } os << endl; } os << " \n"; } else { for (i = 0; i < 256; i++) { double r = ember.m_Palette[i][0] * 255; double g = ember.m_Palette[i][1] * 255; double b = ember.m_Palette[i][2] * 255; double a = ember.m_Palette[i][3] * 255; os << " "; //The original used a precision of 6 which is totally unnecessary, use 2. if (IsClose(a, 255.0)) { if (intPalette) os << ""; else os << ""; } else { if (intPalette) os << " "; else os << " "; } os << "\n"; } } if (doEdits && ember.m_Edits) os << ToString(xmlDocGetRootElement(ember.m_Edits), 1, true, printEditDepth); os << "\n"; return os.str(); } /// /// Create a new editdoc optionally based on parents passed in. /// This is used when an ember is made out of some mutation or edit from one or two existing embers and /// the user wants to capture the genetic lineage history information in the edit doc of the new ember. /// /// The first parent, optionally nullptr. /// The second parent, optionally nullptr. /// The action that was taken to create the new ember /// The nickname of the author /// The Url of the author /// The id of the author /// The comment to include /// The sheep generation used if > 0. Default: 0. /// The sheep id used if > 0. Default: 0. /// xmlDocPtr CreateNewEditdoc(Ember* parent0, Ember* parent1, const string& action, const string& nick, const string& url, const string& id, const string& comment, intmax_t sheepGen = 0, intmax_t sheepId = 0) { char timeString[128]; time_t myTime; string s; xmlDocPtr commentDoc = nullptr; xmlDocPtr doc = xmlNewDoc(XC("1.0")); xmlNodePtr rootNode = nullptr, node = nullptr, nodeCopy = nullptr; xmlNodePtr rootComment = nullptr; ostringstream os; //Create the root node, called "edit". rootNode = xmlNewNode(nullptr, XC("edit")); xmlDocSetRootElement(doc, rootNode); //Add the edit attributes. //Date. myTime = time(nullptr); #ifdef WIN32 tm localt; localtime_s(&localt, &myTime); strftime(timeString, 128, "%a %b %d %H:%M:%S %z %Y", &localt);//XXX use standard time format including timezone. #else tm* localt; localt = localtime(&myTime); strftime(timeString, 128, "%a %b %d %H:%M:%S %z %Y", localt);//XXX use standard time format including timezone. #endif xmlNewProp(rootNode, XC("date"), XC(timeString)); //Nick. if (nick != "") xmlNewProp(rootNode, XC("nick"), XC(nick.c_str())); //Url. if (url != "") xmlNewProp(rootNode, XC("url"), XC(url.c_str())); if (id != "") xmlNewProp(rootNode, XC("id"), XC(id.c_str())); //Action. xmlNewProp(rootNode, XC("action"), XC(action.c_str())); //Sheep info. if (sheepGen > 0 && sheepId > 0) { //Create a child node of the root node called sheep. node = xmlNewChild(rootNode, nullptr, XC("sheep"), nullptr); //Create the sheep attributes. os << sheepGen; s = os.str(); xmlNewProp(node, XC("generation"), XC(s.c_str())); os.str(""); os << sheepId; s = os.str(); xmlNewProp(node, XC("id"), XC(s.c_str())); os.str(""); } //Check for the parents. //If parent 0 not specified, this is a randomly generated genome. if (parent0) { os << parent0->m_Index; s = os.str(); if (parent0->m_Edits) { //Copy the node from the parent. node = xmlDocGetRootElement(parent0->m_Edits); nodeCopy = xmlCopyNode(node, 1); AddFilenameWithoutAmpersand(nodeCopy, parent0->m_ParentFilename); xmlNewProp(nodeCopy, XC("index"), XC(s.c_str())); xmlAddChild(rootNode, nodeCopy); } else { //Insert a (parent has no edit) message. nodeCopy = xmlNewChild(rootNode, nullptr, XC("edit"), nullptr); AddFilenameWithoutAmpersand(nodeCopy, parent0->m_ParentFilename); xmlNewProp(nodeCopy, XC("index"), XC(s.c_str())); } os.str(""); } if (parent1) { os << parent1->m_Index; s = os.str(); if (parent1->m_Edits) { //Copy the node from the parent. node = xmlDocGetRootElement(parent1->m_Edits); nodeCopy = xmlCopyNode(node, 1); AddFilenameWithoutAmpersand(nodeCopy, parent1->m_ParentFilename); xmlNewProp(nodeCopy, XC("index"), XC(s.c_str())); xmlAddChild(rootNode, nodeCopy); } else { //Insert a (parent has no edit) message. nodeCopy = xmlNewChild(rootNode, nullptr, XC("edit"), nullptr); AddFilenameWithoutAmpersand(nodeCopy, parent1->m_ParentFilename); xmlNewProp(nodeCopy, XC("index"), XC(s.c_str())); } os.str(""); } //Comment string: //This one's hard, since the comment string must be treated as //a valid XML document. Create a new document using the comment //string as the in-memory document, and then copy all children of //the root node into the edit structure //Parsing the comment string should be done once and then copied //for each new edit doc, but that's for later. if (comment != "") { os << "" << comment << ""; s = os.str(); commentDoc = xmlReadMemory(s.c_str(), int(s.length()), "comment.env", nullptr, XML_PARSE_NONET); os.str(""); //Check for errors. if (commentDoc) { //Loop through the children of the new document and copy them into the rootNode. rootComment = xmlDocGetRootElement(commentDoc); for (node = rootComment->children; node; node = node->next) { nodeCopy = xmlCopyNode(node, 1); xmlAddChild(rootNode, nodeCopy); } //Free the created document. xmlFreeDoc(commentDoc); } else { cout << "Failed to parse comment into Xml." << endl; } } //Return the Xml doc. return doc; } private: /// /// Return the Xml string representation of an xform. /// /// The xform to create the Xml with /// The number of non-final xforms in the ember to which this xform belongs. Used for xaos. /// True if the xform is the final xform in the ember, else false. /// If true, include motion elements in the Xml string, else omit. /// The Xml string representation of the passed in xform string ToString(Xform& xform, size_t xformCount, bool isFinal, bool doMotion) { size_t i, j; ostringstream os; if (doMotion) { os << " * var = xform.GetVariation(i); ParametricVariation* parVar = dynamic_cast*>(var); if (var->m_Weight != 0) { os << var->Name() << "=\"" << var->m_Weight << "\" "; if (parVar) { auto params = parVar->Params(); for (j = 0; j < parVar->ParamCount(); j++) { if ((!doMotion || (doMotion && (params[j].ParamVal() != 0))) && !params[j].IsPrecalc()) os << params[j].Name() << "=\"" << params[j].ParamVal() << "\" "; } } } } if (!doMotion || (doMotion && !xform.m_Affine.IsZero() && !xform.m_Affine.IsEmpty())) { os << "coefs=\"" << xform.m_Affine.A() << " " << xform.m_Affine.D() << " " << xform.m_Affine.B() << " " << xform.m_Affine.E() << " " << xform.m_Affine.C() << " " << xform.m_Affine.F() << "\""; } if ((!doMotion && !xform.m_Post.IsID()) || (doMotion && !xform.m_Post.IsZero() && !xform.m_Post.IsEmpty())) { os << " post=\"" << xform.m_Post.A() << " " << xform.m_Post.D() << " " << xform.m_Post.B() << " " << xform.m_Post.E() << " " << xform.m_Post.C() << " " << xform.m_Post.F() << "\""; } //Original only printed xaos values that were not 1. Here, print them all out if any are present. if (!isFinal && !doMotion && xform.XaosPresent())//Applying motion to xaos not supported. { os << " chaos=\""; for (i = 0; i < xformCount; i++) os << xform.Xaos(i) << " "; os << "\""; } if (!doMotion || xform.m_Opacity != EMPTYFIELD) os << " opacity=\"" << xform.m_Opacity << "\""; if (!doMotion && !xform.m_Motion.empty()) { os << ">\n"; for (i = 0; i < xform.m_Motion.size(); i++) os << ToString(xform.m_Motion[i], 0, false, true); if (isFinal)//Fixed to properly close final.//SMOULDER os << " \n"; else os << " \n"; } else os << "/>\n"; return os.str(); } /// /// Return an edit node Xml string. /// /// The edit node to get the string for /// How many tabs to use /// If true, include newlines and tabs, else don't. /// How deep the edit depth goes /// The edit node Xml string string ToString(xmlNodePtr editNode, size_t tabs, bool formatting, size_t printEditDepth) { bool indentPrinted = false; const char* tabString = " ", *attStr; const char* editString = "edit"; const char* sheepString = "sheep"; size_t ti;//, editOrSheep = 0; xmlAttrPtr attPtr = nullptr, curAtt = nullptr; xmlNodePtr childPtr = nullptr, curChild = nullptr; ostringstream os; if (printEditDepth > 0 && tabs > printEditDepth) return ""; //If this node is an XML_ELEMENT_NODE, print it and its attributes. if (editNode->type == XML_ELEMENT_NODE) { //Print the node at the tab specified. if (formatting) for (ti = 0; ti < tabs; ti++) os << tabString; os << "<" << editNode->name; //This can either be an edit node or a sheep node. //If it's an edit node, add one to the tab. if (!Compare(editNode->name, editString)) { //editOrSheep = 1; tabs++; } else if (!Compare(editNode->name, sheepString)) { } //editOrSheep = 2; else { } //editOrSheep = 0; //Print the attributes. attPtr = editNode->properties; for (curAtt = attPtr; curAtt; curAtt = curAtt->next) { attStr = CX(xmlGetProp(editNode, curAtt->name)); os << " " << curAtt->name << "=\"" << attStr << "\""; xmlFree(reinterpret_cast(const_cast(attStr))); } //Does this node have children? if (!editNode->children || (printEditDepth > 0 && tabs > printEditDepth)) { //Close the tag and subtract the tab. os << "/>"; if (formatting) os << "\n"; tabs--; } else { //Close the tag. os << ">"; if (formatting) os << "\n"; //Loop through the children and print them. childPtr = editNode->children; indentPrinted = false; for (curChild = childPtr; curChild; curChild = curChild->next) { //If child is an element, indent first and then print it. if (curChild->type == XML_ELEMENT_NODE && (!Compare(curChild->name, editString) || !Compare(curChild->name, sheepString))) { if (indentPrinted) { indentPrinted = false; os << "\n"; } os << ToString(curChild, tabs, true, printEditDepth); } else { //Child is a text node, don't want to indent more than once. if (xmlIsBlankNode(curChild)) continue; if (!indentPrinted && formatting) { for (ti = 0; ti < tabs; ti++) os << tabString; indentPrinted = true; } //Print nodes without formatting. os << ToString(curChild, tabs, false, printEditDepth); } } if (indentPrinted && formatting) os << "\n"; tabs--;//Tab out. if (formatting) for (ti = 0; ti < tabs; ti++) os << tabString; os << "name << ">";//Close the tag. if (formatting) os << "\n"; } } else if (editNode->type == XML_TEXT_NODE) { string s(reinterpret_cast(xmlNodeGetContent(editNode))); os << Trim(s); } return os.str(); } /// /// Convert a FlameMotion element to an xml string /// /// The FlameMotion object to convert to XML string ToString(const EmberMotion& motion) { ostringstream os; os << " 0) os << "motion_offset=\"" << motion.m_MotionOffset << "\" "; os << "motion_func="; switch (motion.m_MotionFunc) { case eMotion::MOTION_SIN: os << "\"sin\""; break; case eMotion::MOTION_HILL: os << "\"hill\""; break; case eMotion::MOTION_TRIANGLE: os << "\"triangle\""; break; case eMotion::MOTION_SAW: default: os << "\"saw\""; break; } T r = 0.0; T g = 0.0; T b = 0.0; T cx = 0.0; T cy = 0.0; for (size_t i = 0; i < motion.m_MotionParams.size(); ++i) { switch (motion.m_MotionParams[i].first) { case eEmberMotionParam::FLAME_MOTION_ZOOM: os << " zoom=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_ZPOS: os << " cam_zpos=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_PERSPECTIVE: os << " cam_persp=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_YAW: os << " cam_yaw=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_PITCH: os << " cam_pitch=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_DEPTH_BLUR: os << " cam_dof=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_CENTER_X: cx = motion.m_MotionParams[i].second; break; case eEmberMotionParam::FLAME_MOTION_CENTER_Y: cy = motion.m_MotionParams[i].second; break; case eEmberMotionParam::FLAME_MOTION_ROTATE: os << " rotate=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_BRIGHTNESS: os << " brightness=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_GAMMA: os << " gamma=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_GAMMA_THRESH: os << " gamma_threshold=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_HIGHLIGHT_POWER: os << " highlight_power=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_BACKGROUND_R: r = motion.m_MotionParams[i].second; break; case eEmberMotionParam::FLAME_MOTION_BACKGROUND_G: g = motion.m_MotionParams[i].second; break; case eEmberMotionParam::FLAME_MOTION_BACKGROUND_B: b = motion.m_MotionParams[i].second; break; case eEmberMotionParam::FLAME_MOTION_VIBRANCY: os << " vibrancy=\"" << motion.m_MotionParams[i].second << "\""; break; case eEmberMotionParam::FLAME_MOTION_NONE: default: break; } } if (r != 0.0 || g != 0.0 || b != 0.0) os << " background=\"" << r << " " << g << " " << b << "\""; if (cx != 0.0 || cy != 0.0) os << " center=\"" << cx << " " << cy << "\""; os << "/>\n"; return os.str(); } void AddFilenameWithoutAmpersand(xmlNodePtr node, string& filename) { if (filename.find_first_of('&') != std::string::npos) { string filenameWithoutAmpersands = filename; FindAndReplace(filenameWithoutAmpersands, "&", "&"); xmlNewProp(node, XC("filename"), XC(filenameWithoutAmpersands.c_str())); } else { xmlNewProp(node, XC("filename"), XC(filename.c_str())); } } }; }