You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

334 lines
10 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
// Sample program to print metadata in JSON format
#include <exiv2/exiv2.hpp>
#include <sys/stat.h>
#include <cassert>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#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<Token>;
// "XMP.xmp.MP.RegionInfo/MPRI:Regions[1]/MPReg:Rectangle"
bool getToken(std::string& in, Token& token, std::set<std::string>* 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<Jzon::Node&>(array) : static_cast<Jzon::Node&>(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<std::string>* 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 <class T>
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<const Exiv2::LangAltValue&>(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<Jzon::Object&>(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<int>(buf.st_dev)); /* ID of device containing file */
fs.Add("st_ino", static_cast<int>(buf.st_ino)); /* inode number */
fs.Add("st_mode", static_cast<int>(buf.st_mode)); /* protection */
fs.Add("st_nlink", static_cast<int>(buf.st_nlink)); /* number of hard links */
fs.Add("st_uid", static_cast<int>(buf.st_uid)); /* user ID of owner */
fs.Add("st_gid", static_cast<int>(buf.st_gid)); /* group ID of owner */
fs.Add("st_rdev", static_cast<int>(buf.st_rdev)); /* device ID (if special file) */
fs.Add("st_size", static_cast<int>(buf.st_size)); /* total size, in bytes */
fs.Add("st_atime", static_cast<int>(buf.st_atime)); /* time of last access */
fs.Add("st_mtime", static_cast<int>(buf.st_mtime)); /* time of last modification */
fs.Add("st_ctime", static_cast<int>(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<int>(blksize)); /* blocksize for file system I/O */
fs.Add("st_blocks", static_cast<int>(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<std::string> 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;
}
}