220 lines
8.7 KiB
C++
220 lines
8.7 KiB
C++
#include "fgc/CameraSettingsXml.h"
|
|
|
|
#include <algorithm>
|
|
#include <ctime>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
|
|
namespace fgc {
|
|
|
|
namespace {
|
|
|
|
// The input is machine-generated by the VimbaX Viewer and has a fixed, flat
|
|
// shape: <Feature Name=".." Value=".." Type=".." /> elements, some nested in
|
|
// <SelectorGroup Name=".." Value=".." >. 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("<CameraInfo");
|
|
if (info != std::string::npos) {
|
|
const auto close = xml.find('>', info);
|
|
const std::string tag = xml.substr(info, close - info);
|
|
out.model = attrValue(tag, "Model");
|
|
out.device_id = attrValue(tag, "Id");
|
|
}
|
|
|
|
// Only <RemoteDevice>: <LocalDevice> and the stream sections describe the
|
|
// host's transport layer, which is per-machine and must not be replayed.
|
|
const auto begin = xml.find("<RemoteDevice>");
|
|
if (begin == std::string::npos)
|
|
throw std::runtime_error("not a VimbaX settings export (no <RemoteDevice>): " + path);
|
|
const auto end = xml.find("</RemoteDevice>", begin);
|
|
if (end == std::string::npos)
|
|
throw std::runtime_error("malformed VimbaX settings export (unterminated "
|
|
"<RemoteDevice>): " + 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("<SelectorGroup", 0) == 0) {
|
|
selector = attrValue(tag, "Name") + "=" + attrValue(tag, "Value");
|
|
} else if (tag.rfind("</SelectorGroup", 0) == 0) {
|
|
selector.clear();
|
|
} else if (tag.rfind("<Feature", 0) == 0) {
|
|
SettingEntry e;
|
|
e.name = attrValue(tag, "Name");
|
|
e.value = attrValue(tag, "Value");
|
|
e.selector = selector;
|
|
if (!e.name.empty()) out.entries.push_back(std::move(e));
|
|
}
|
|
}
|
|
|
|
if (out.entries.empty())
|
|
throw std::runtime_error("VimbaX settings export has no features: " + path);
|
|
return out;
|
|
}
|
|
|
|
std::string renderSettingsRecord(const CameraSettings& s, const std::string& xml_name,
|
|
const std::string& hash_label) {
|
|
std::ostringstream o;
|
|
o << "; ===========================================================================\n"
|
|
"; GENERATED - THIS FILE DOES NOT CONFIGURE THE CAMERA\n"
|
|
";\n"
|
|
"; A human-readable record of what " << xml_name << " contains, so the settings\n"
|
|
"; can be read and diffed without opening the Viewer. The program applies\n"
|
|
"; " << xml_name << " directly through the Vimba X SDK. Nothing here is ever\n"
|
|
"; written to the camera, and editing this file changes nothing.\n"
|
|
";\n"
|
|
"; To change the settings: open the VimbaX Viewer, configure the camera there\n"
|
|
"; (it validates combinations as you go), save the settings XML over\n"
|
|
"; " << xml_name << ", and restart. This file is then regenerated and the\n"
|
|
"; changes are listed in the log.\n"
|
|
"; ===========================================================================\n\n";
|
|
|
|
o << "[Source]\n"
|
|
<< "xml = " << xml_name << "\n"
|
|
<< "xml_hash = " << hash_label << "\n"
|
|
<< "generated = " << nowIso8601() << "\n";
|
|
if (!s.model.empty()) o << "model = " << s.model << "\n";
|
|
if (!s.device_id.empty()) o << "device_id = " << s.device_id << "\n";
|
|
|
|
o << "\n[Settings]\n";
|
|
std::string current_selector;
|
|
for (const auto& e : s.entries) {
|
|
// Reproduce the selector context as a comment: without it a repeated
|
|
// feature name reads as a duplicate rather than a per-selector value.
|
|
if (e.selector != current_selector) {
|
|
current_selector = e.selector;
|
|
o << "; --- " << (current_selector.empty() ? "(none)" : current_selector) << "\n";
|
|
}
|
|
o << e.name << " = " << e.value << "\n";
|
|
}
|
|
return o.str();
|
|
}
|
|
|
|
SettingsRecord readSettingsRecord(const std::string& path) {
|
|
SettingsRecord rec;
|
|
std::ifstream in(path);
|
|
if (!in) return rec; // absent: caller treats it as a mismatch
|
|
|
|
std::string line;
|
|
bool in_settings = false;
|
|
std::string selector;
|
|
while (std::getline(in, line)) {
|
|
// Selector context is written as a comment; recover it so a diff can tell
|
|
// BalanceRatio-for-Red from BalanceRatio-for-Blue.
|
|
if (line.rfind("; --- ", 0) == 0) {
|
|
selector = line.substr(6);
|
|
if (selector == "(none)") selector.clear();
|
|
continue;
|
|
}
|
|
if (!line.empty() && line[0] == ';') continue;
|
|
if (line == "[Settings]") { in_settings = true; continue; }
|
|
if (!line.empty() && line[0] == '[') { in_settings = false; continue; }
|
|
|
|
const auto eq = line.find('=');
|
|
if (eq == std::string::npos) continue;
|
|
auto trim = [](std::string v) {
|
|
const auto a = v.find_first_not_of(" \t\r");
|
|
if (a == std::string::npos) return std::string();
|
|
const auto b = v.find_last_not_of(" \t\r");
|
|
return v.substr(a, b - a + 1);
|
|
};
|
|
const std::string key = trim(line.substr(0, eq));
|
|
const std::string val = trim(line.substr(eq + 1));
|
|
if (in_settings) {
|
|
rec.entries.push_back({key, val, selector});
|
|
} else if (key == "xml_hash") {
|
|
rec.hash_label = val;
|
|
}
|
|
}
|
|
return rec;
|
|
}
|
|
|
|
std::vector<std::string> diffSettings(const std::vector<SettingEntry>& before,
|
|
const std::vector<SettingEntry>& 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<SettingEntry>& v) {
|
|
std::map<std::string, const SettingEntry*> m;
|
|
std::map<std::string, int> 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<std::string> 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
|