// SPDX-License-Identifier: GPL-2.0-or-later // Sample program to print metadata in JSON format #include #include #include #include #include #include #include "Jzon.h" #if defined(__MINGW32__) || defined(__MINGW64__) #ifndef __MINGW__ #define __MINGW__ #endif #endif struct Token { std::string n; // the name eg "History" bool a; // name is an array eg History[] int i; // index (indexed from 1) eg History[1]/stEvt:action }; using Tokens = std::vector; // "XMP.xmp.MP.RegionInfo/MPRI:Regions[1]/MPReg:Rectangle" bool getToken(std::string& in, Token& token, std::set* pNS = nullptr) { bool result = false; bool ns = false; token.n = ""; token.a = false; token.i = 0; while (!result && in.length()) { std::string c = in.substr(0, 1); char C = c.at(0); in = in.substr(1, std::string::npos); if (in.length() == 0 && C != ']') token.n += c; if (C == '/' || C == '[' || C == ':' || C == '.' || C == ']' || in.length() == 0) { ns |= C == '/'; token.a = C == '['; if (C == ']') token.i = std::atoi(token.n.c_str()); // encoded string first index == 1 result = token.n.length() > 0; } else { token.n += c; } } if (ns && pNS) pNS->insert(token.n); return result; } Jzon::Node& addToTree(Jzon::Node& r1, const Token& token) { Jzon::Object object; Jzon::Array array; std::string key = token.n; size_t index = token.i - 1; // array Eg: "History[1]" indexed from 1. Jzon expects 0 based index. auto& empty = token.a ? static_cast(array) : static_cast(object); if (r1.IsObject()) { Jzon::Object& o1 = r1.AsObject(); if (!o1.Has(key)) o1.Add(key, empty); return o1.Get(key); } if (r1.IsArray()) { Jzon::Array& a1 = r1.AsArray(); while (a1.GetCount() <= index) a1.Add(empty); return a1.Get(index); } return r1; } Jzon::Node& recursivelyBuildTree(Jzon::Node& root, Tokens& tokens, size_t k) { return addToTree(k == 0 ? root : recursivelyBuildTree(root, tokens, k - 1), tokens.at(k)); } // build the json tree for this key. return location and discover the name Jzon::Node& objectForKey(const std::string& Key, Jzon::Object& root, std::string& name, std::set* pNS = nullptr) { // Parse the key Tokens tokens; Token token; std::string input = Key; // Example: "XMP.xmp.MP.RegionInfo/MPRI:Regions[1]/MPReg:Rectangle" while (getToken(input, token, pNS)) tokens.push_back(token); size_t l = tokens.size() - 1; // leave leaf name to push() name = tokens.at(l).n; // The second token. For example: XMP.dc is a namespace if (pNS && tokens.size() > 1) pNS->insert(tokens[1].n); return recursivelyBuildTree(root, tokens, l - 1); #if 0 // recursivelyBuildTree: // Go to the root. Climb out adding objects or arrays to create the tree // The leaf is pushed on the top by the caller of objectForKey() // The recursion could be expressed by these if statements: if ( l == 1 ) return addToTree(root,tokens[0]); if ( l == 2 ) return addToTree(addToTree(root,tokens[0]),tokens[1]); if ( l == 3 ) return addToTree(addToTree(addToTree(root,tokens[0]),tokens[1]),tokens[2]); if ( l == 4 ) return addToTree(addToTree(addToTree(addToTree(root,tokens[0]),tokens[1]),tokens[2]),tokens[3]); ... #endif } bool isObject(std::string& value) { return value == std::string("type=\"Struct\""); } bool isArray(std::string& value) { return value == "type=\"Seq\"" || value == "type=\"Bag\"" || value == "type=\"Alt\""; } #define STORE(node, key, value) \ if (node.IsObject()) \ node.AsObject().Add(key, value); \ else \ node.AsArray().Add(value) template void push(Jzon::Node& node, const std::string& key, T i) { #define ABORT_IF_I_EMPTY \ if (i->value().size() == 0) { \ return; \ } std::string value = i->value().toString(); switch (i->typeId()) { case Exiv2::xmpText: if (::isObject(value)) { Jzon::Object v; STORE(node, key, v); } else if (::isArray(value)) { Jzon::Array v; STORE(node, key, v); } else { STORE(node, key, value); } break; case Exiv2::unsignedByte: case Exiv2::unsignedShort: case Exiv2::unsignedLong: case Exiv2::signedByte: case Exiv2::signedShort: case Exiv2::signedLong: STORE(node, key, std::atoi(value.c_str())); break; case Exiv2::tiffFloat: case Exiv2::tiffDouble: STORE(node, key, std::atof(value.c_str())); break; case Exiv2::unsignedRational: case Exiv2::signedRational: { ABORT_IF_I_EMPTY Jzon::Array arr; Exiv2::Rational rat = i->value().toRational(); arr.Add(rat.first); arr.Add(rat.second); STORE(node, key, arr); } break; case Exiv2::langAlt: { ABORT_IF_I_EMPTY Jzon::Object l; const auto& langs = dynamic_cast(i->value()); for (auto&& lang : langs.value_) { l.Add(lang.first, lang.second); } Jzon::Object o; o.Add("lang", l); STORE(node, key, o); } break; default: case Exiv2::date: case Exiv2::time: case Exiv2::asciiString: case Exiv2::string: case Exiv2::comment: case Exiv2::undefined: case Exiv2::tiffIfd: case Exiv2::directory: case Exiv2::xmpAlt: case Exiv2::xmpBag: case Exiv2::xmpSeq: // http://dev.exiv2.org/boards/3/topics/1367#message-1373 if (key == "UserComment") { size_t pos = value.find('\0'); if (pos != std::string::npos) value = value.substr(0, pos); } if (key == "MakerNote") return; STORE(node, key, value); break; } } void fileSystemPush(const char* path, Jzon::Node& nfs) { auto& fs = dynamic_cast(nfs); fs.Add("path", path); fs.Add("realpath", std::filesystem::absolute(std::filesystem::path(path)).string()); struct stat buf; memset(&buf, 0, sizeof(buf)); stat(path, &buf); fs.Add("st_dev", static_cast(buf.st_dev)); /* ID of device containing file */ fs.Add("st_ino", static_cast(buf.st_ino)); /* inode number */ fs.Add("st_mode", static_cast(buf.st_mode)); /* protection */ fs.Add("st_nlink", static_cast(buf.st_nlink)); /* number of hard links */ fs.Add("st_uid", static_cast(buf.st_uid)); /* user ID of owner */ fs.Add("st_gid", static_cast(buf.st_gid)); /* group ID of owner */ fs.Add("st_rdev", static_cast(buf.st_rdev)); /* device ID (if special file) */ fs.Add("st_size", static_cast(buf.st_size)); /* total size, in bytes */ fs.Add("st_atime", static_cast(buf.st_atime)); /* time of last access */ fs.Add("st_mtime", static_cast(buf.st_mtime)); /* time of last modification */ fs.Add("st_ctime", static_cast(buf.st_ctime)); /* time of last status change */ #if defined(_MSC_VER) || defined(__MINGW__) size_t blksize = 1024; size_t blocks = (buf.st_size + blksize - 1) / blksize; #else size_t blksize = buf.st_blksize; size_t blocks = buf.st_blocks; #endif fs.Add("st_blksize", static_cast(blksize)); /* blocksize for file system I/O */ fs.Add("st_blocks", static_cast(blocks)); /* number of 512B blocks allocated */ } int main(int argc, char* const argv[]) { Exiv2::XmpParser::initialize(); ::atexit(Exiv2::XmpParser::terminate); #ifdef EXV_ENABLE_BMFF Exiv2::enableBMFF(); #endif try { if (argc < 2 || argc > 3) { std::cout << "Usage: " << argv[0] << " [-option] file" << std::endl; std::cout << "Option: all | exif | iptc | xmp | filesystem" << std::endl; return 1; } const char* path = argv[argc - 1]; const char* opt = argc == 3 ? argv[1] : "-all"; while (opt[0] == '-') opt++; // skip past leading -'s char option = opt[0]; Exiv2::Image::UniquePtr image = Exiv2::ImageFactory::open(path); assert(image.get() != 0); image->readMetadata(); Jzon::Object root; if (option == 'f') { // only report filesystem when requested const char* Fs = "FS"; Jzon::Object fs; root.Add(Fs, fs); fileSystemPush(path, root.Get(Fs)); } if (option == 'a' || option == 'e') { Exiv2::ExifData& exifData = image->exifData(); for (auto i = exifData.begin(); i != exifData.end(); ++i) { std::string name; Jzon::Node& object = objectForKey(i->key(), root, name); push(object, name, i); } } if (option == 'a' || option == 'i') { Exiv2::IptcData& iptcData = image->iptcData(); for (auto i = iptcData.begin(); i != iptcData.end(); ++i) { std::string name; Jzon::Node& object = objectForKey(i->key(), root, name); push(object, name, i); } } #ifdef EXV_HAVE_XMP_TOOLKIT if (option == 'a' || option == 'x') { Exiv2::XmpData& xmpData = image->xmpData(); if (!xmpData.empty()) { // get the xmpData and recursively parse into a Jzon Object std::set namespaces; for (auto i = xmpData.begin(); i != xmpData.end(); ++i) { std::string name; Jzon::Node& object = objectForKey(i->key(), root, name, &namespaces); push(object, name, i); } // get the namespace dictionary from XMP Exiv2::Dictionary nsDict; Exiv2::XmpProperties::registeredNamespaces(nsDict); // create and populate a Jzon::Object for the namespaces Jzon::Object xmlns; for (auto&& ns : namespaces) { xmlns.Add(ns, nsDict[ns]); } // add xmlns as Xmp.xmlns root.Get("Xmp").AsObject().Add("xmlns", xmlns); } } #endif Jzon::Writer writer(root, Jzon::StandardFormat); writer.Write(); std::cout << writer.GetResult() << std::endl; return EXIT_SUCCESS; } catch (Exiv2::Error& e) { std::cout << "Caught Exiv2 exception '" << e.what() << "'\n"; return EXIT_FAILURE; } }