#include "fgc/CameraSettingsXml.h" #include #include #include #include #include #include namespace fgc { namespace { // The input is machine-generated by the VimbaX Viewer and has a fixed, flat // shape: elements, some nested in // . A targeted scanner is enough for that // and keeps the project free of an XML dependency - but it validates what it // finds and throws rather than guessing, because a silently mis-parsed settings // file would be worse than a missing one. // Value of `attr` in `tag`, or "" when absent. std::string attrValue(const std::string& tag, const std::string& attr) { const std::string key = attr + "=\""; const auto pos = tag.find(key); if (pos == std::string::npos) return ""; const auto start = pos + key.size(); const auto end = tag.find('"', start); if (end == std::string::npos) return ""; return tag.substr(start, end - start); } std::string slurp(const std::string& path) { std::ifstream in(path, std::ios::binary); if (!in) throw std::runtime_error("cannot read camera settings XML: " + path); std::ostringstream ss; ss << in.rdbuf(); return ss.str(); } std::string nowIso8601() { const std::time_t t = std::time(nullptr); std::tm tm{}; localtime_r(&t, &tm); char buf[32]; std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tm); return buf; } } // namespace CameraSettings parseCameraSettingsXml(const std::string& path) { const std::string xml = slurp(path); CameraSettings out; const auto info = xml.find("', info); const std::string tag = xml.substr(info, close - info); out.model = attrValue(tag, "Model"); out.device_id = attrValue(tag, "Id"); } // Only : and the stream sections describe the // host's transport layer, which is per-machine and must not be replayed. const auto begin = xml.find(""); if (begin == std::string::npos) throw std::runtime_error("not a VimbaX settings export (no ): " + path); const auto end = xml.find("", begin); if (end == std::string::npos) throw std::runtime_error("malformed VimbaX settings export (unterminated " "): " + path); const std::string body = xml.substr(begin, end - begin); // Walk the block in order, tracking the enclosing SelectorGroup so a repeated // feature name (BalanceRatio for Red and Blue, say) stays distinguishable. std::string selector; size_t i = 0; while (i < body.size()) { const auto lt = body.find('<', i); if (lt == std::string::npos) break; const auto gt = body.find('>', lt); if (gt == std::string::npos) break; const std::string tag = body.substr(lt, gt - lt + 1); i = gt + 1; if (tag.rfind(" diffSettings(const std::vector& before, const std::vector& after) { // Key by selector + name + occurrence index. A real Viewer export repeats the // same SelectorGroup name AND value (FileOperationSelector=Open appears once // per FileSelector group), so selector+name alone is not unique - keying on it // collapsed the duplicates and reported every one after the first as "new", // burying a one-line change under hundreds of phantom ones. auto keyed = [](const std::vector& v) { std::map m; std::map seen; for (const auto& e : v) { const std::string base = e.selector + "|" + e.name; m[base + "|" + std::to_string(seen[base]++)] = &e; } return m; }; auto label = [](const SettingEntry& e) { return e.selector.empty() ? e.name : e.name + " [" + e.selector + "]"; }; auto old_by_key = keyed(before); auto new_by_key = keyed(after); std::vector out; for (const auto& [k, e] : new_by_key) { auto it = old_by_key.find(k); if (it == old_by_key.end()) { out.push_back("+ " + label(*e) + " " + e->value + " (new)"); } else if (it->second->value != e->value) { out.push_back(label(*e) + " " + it->second->value + " -> " + e->value); } } for (const auto& [k, e] : old_by_key) if (new_by_key.find(k) == new_by_key.end()) out.push_back("- " + label(*e) + " (removed)"); return out; } } // namespace fgc