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.
331 lines
10 KiB
C++
331 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 <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 = {};
|
|
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 EXIT_FAILURE;
|
|
}
|
|
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);
|
|
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;
|
|
}
|
|
}
|