diff --git a/.gitignore b/.gitignore index de358ff..5973000 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .vscode/ +test/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2676665 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "utils"] + path = utils + url = https://github.com/lifegpc/c-utils diff --git a/Cargo.lock b/Cargo.lock index da3b506..18cf73b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,30 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bindgen" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -156,12 +180,36 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "c_fixed_string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6833b1e1b632d1a4c167aa8dc2a78a774bd78e14bc14579505d209327b47cf" +dependencies = [ + "memchr", +] + [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -181,6 +229,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "clang-sys" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "2.34.0" @@ -196,6 +255,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cmake" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" +dependencies = [ + "cc", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -218,7 +286,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -318,7 +386,20 @@ version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", ] [[package]] @@ -342,7 +423,7 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "crc32fast", "libc", "miniz_oxide", @@ -459,6 +540,12 @@ dependencies = [ "encoding", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "h2" version = "0.3.11" @@ -551,6 +638,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + [[package]] name = "hyper" version = "0.14.17" @@ -628,7 +724,30 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", +] + +[[package]] +name = "int-enum" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b1428b2b1abe959e6eedb0a17d0ab12f6ba20e1106cc29fc4874e3ba393c177" +dependencies = [ + "cfg-if 0.1.10", + "int-enum-impl", +] + +[[package]] +name = "int-enum-impl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c3cecaad8ca1a5020843500c696de2b9a07b63b624ddeef91f85f9bafb3671" +dependencies = [ + "cfg-if 0.1.10", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -664,19 +783,35 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.119" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi", +] + [[package]] name = "log" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -753,6 +888,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -810,7 +955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -836,6 +981,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -901,18 +1052,23 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" name = "pixiv_downloader" version = "0.0.1" dependencies = [ + "bindgen", + "c_fixed_string", "chrono", + "cmake", "dateparser", "futures-util", "getopts", "gettext", "html_parser", + "int-enum", "json", "lazy_static", "regex", "reqwest", "spin_on", "tokio", + "utf16string", "winapi", ] @@ -922,6 +1078,15 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -955,6 +1120,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.15" @@ -1058,6 +1229,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustls" version = "0.20.4" @@ -1183,6 +1360,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "slab" version = "0.4.5" @@ -1261,7 +1444,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand", "libc", "redox_syscall", @@ -1269,6 +1452,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1399,6 +1591,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.1" @@ -1411,7 +1612,7 @@ version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "pin-project-lite", "tracing-core", ] @@ -1494,6 +1695,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf16string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b62a1e85e12d5d712bf47a85f426b73d303e2d00a90de5f3004df3596e9d216" +dependencies = [ + "byteorder", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1534,7 +1744,7 @@ version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -1559,7 +1769,7 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -1623,6 +1833,15 @@ dependencies = [ "webpki", ] +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1639,6 +1858,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index f8383df..8db5c29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,18 +6,28 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +c_fixed_string = { version = "0.2", optional = true } chrono = "0.4" dateparser = "0.1.6" futures-util = "0.3" getopts = "0.2" gettext = "0.4" html_parser = "0.6.2" +int-enum = { version = "0.4", optional = true } lazy_static = "1.4" json = "0.12" +utf16string = { version= "0.2", optional = true } regex = "1" reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "rustls-tls", "socks", "stream"] } spin_on = "0.1.1" tokio = { version = "1.17", features = ["rt", "macros", "rt-multi-thread", "time"] } +[build-dependencies] +cmake = "0.1" +bindgen = "0.53" + +[features] +exif = ["c_fixed_string", "int-enum", "utf16string"] + [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["winnls", "stringapiset"] } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5bae004 --- /dev/null +++ b/build.rs @@ -0,0 +1,94 @@ +extern crate bindgen; +extern crate cmake; + +use std::env; +use std::fs::create_dir; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + +fn main() { + #[cfg(any(feature = "exif"))] + { + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + let utils_build_path = out_path.join("utils"); + if !utils_build_path.exists() { + create_dir(&utils_build_path).unwrap(); + } + cmake::Config::new("utils") + .define("CMAKE_INSTALL_PREFIX", out_path.to_str().unwrap()) + .out_dir(utils_build_path) + .define("ENABLE_ICONV", "OFF") + .define("ENABLE_STANDALONE", "ON") + .define("ENABLE_CXX17", "ON") + .define("CMAKE_BUILD_TYPE", "Release") + .define("INSTALL_DEP_FILES", "ON") + .build(); + let dep_path = out_path.join("utils_dep.txt"); + let mut f = File::open(dep_path).unwrap(); + let mut s = String::from(""); + f.read_to_string(&mut s).unwrap(); + println!("cargo:rustc-link-lib=static=exif"); + let l: Vec<&str> = s.split(";").collect(); + for i in l.iter() { + let mut p = PathBuf::from(i); + let p2 = p.clone(); + let file_name = p2.file_stem().unwrap(); + let file_name = file_name.to_str().unwrap(); + p.pop(); + let pa = p.to_str().unwrap(); + if pa != "" { + println!("cargo:rustc-link-search={}", pa); + } + println!("cargo:rustc-link-lib={}", file_name); + } + println!("cargo:rerun-if-changed=utils/"); + } + #[cfg(feature = "exif")] + { + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + let exif_build_path = out_path.join("exif"); + let utils_build_path = out_path.join("utils"); + if !exif_build_path.exists() { + create_dir(&exif_build_path).unwrap(); + } + cmake::Config::new("exif") + .define("CMAKE_INSTALL_PREFIX", out_path.to_str().unwrap()) + .out_dir(exif_build_path) + .define("UTILS_LIBRARY", utils_build_path.to_str().unwrap()) + .define("CMAKE_BUILD_TYPE", "Release") + .build(); + println!("cargo:rustc-link-search=native={}/lib", out_path.display()); + let dep_path = out_path.join("exif_dep.txt"); + let mut f = File::open(dep_path).unwrap(); + let mut s = String::from(""); + f.read_to_string(&mut s).unwrap(); + println!("cargo:rustc-link-lib=static=exif"); + let l: Vec<&str> = s.split(";").collect(); + for i in l.iter() { + let mut p = PathBuf::from(i); + let p2 = p.clone(); + let file_name = p2.file_stem().unwrap(); + let file_name = file_name.to_str().unwrap(); + p.pop(); + println!("cargo:rustc-link-search={}", p.to_str().unwrap()); + println!("cargo:rustc-link-lib={}", file_name); + } + println!("cargo:rerun-if-changed=exif/"); + let bindings = bindgen::Builder::default() + // The input header we would like to generate + // bindings for. + .header("exif/exif.h") + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + bindings + .write_to_file(out_path.join("exif.rs")) + .expect("Couldn't write bindings!"); + } +} diff --git a/exif/.gitignore b/exif/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/exif/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/exif/CMakeLists.txt b/exif/CMakeLists.txt new file mode 100644 index 0000000..0217189 --- /dev/null +++ b/exif/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.9) + +project(exif) + +if (MSVC) + add_compile_options(/utf-8) + add_compile_options(/EHsc) +endif() + +include(GNUInstallDirs) + +option(UTILS_LIBRARY "The path of utils of library." "") +if (UTILS_LIBRARY) + set(UTILS_TARGET "${UTILS_LIBRARY}") +else() + set(ENABLE_ICONV OFF CACHE BOOL "Libiconv is not needed.") + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../utils" "${CMAKE_BINARY_DIR}/utils") + set(UTILS_TARGET utils) +endif() + +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../utils") + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +find_package(Exiv2) + +include_directories(${Exiv2_INCLUDE_DIRS}) +include_directories("${CMAKE_CURRENT_SOURCE_DIR}") +add_library(exif exif.h src/exif_priv.h src/exif.cpp) +target_compile_features(exif PRIVATE cxx_std_17) +target_link_libraries(exif ${Exiv2_LIBRARIES}) +target_link_libraries(exif ${UTILS_TARGET}) + +if (BUILD_SHARED_LIBS AND WIN32) + target_compile_definitions(exif PRIVATE -DBUILD_DLL -DWIN32_DLL) +endif() + +get_target_property(OUT exif LINK_LIBRARIES) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/exif_dep.txt" "${OUT}") + +install(TARGETS exif) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/exif_dep.txt" DESTINATION ${CMAKE_INSTALL_PREFIX}) diff --git a/exif/cmake/FindExiv2.cmake b/exif/cmake/FindExiv2.cmake new file mode 100644 index 0000000..4f093f4 --- /dev/null +++ b/exif/cmake/FindExiv2.cmake @@ -0,0 +1,34 @@ +find_path(Exiv2_INCLUDE_DIR + NAMES exiv2/exiv2.hpp +) +find_library(Exiv2_LIBRARY + NAMES exiv2 +) +if (Exiv2_INCLUDE_DIR) + if (EXISTS "${Exiv2_INCLUDE_DIR}/exiv2/exv_conf.h") + file(STRINGS "${Exiv2_INCLUDE_DIR}/exiv2/exv_conf.h" EXV_CONF_H) + foreach(LINE IN LISTS EXV_CONF_H) + string(REGEX MATCH "^#define EXV_PACKAGE_VERSION \"([^\"]+)\"" OUTPUT "${LINE}") + if (OUTPUT) + set(EXIV2_VERSION "${CMAKE_MATCH_1}") + endif() + endforeach() + endif() +endif() + +if (Exiv2_INCLUDE_DIR AND Exiv2_LIBRARY) + set(Exiv2_FOUND TRUE) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Exiv2 + FOUND_VAR Exiv2_FOUND + REQUIRED_VARS + Exiv2_LIBRARY + Exiv2_INCLUDE_DIR + VERSION_VAR + EXIV2_VERSION +) + +set(Exiv2_INCLUDE_DIRS "${Exiv2_INCLUDE_DIR}") +set(Exiv2_LIBRARIES "${Exiv2_LIBRARY}") diff --git a/exif/exif.h b/exif/exif.h new file mode 100644 index 0000000..359e1f6 --- /dev/null +++ b/exif/exif.h @@ -0,0 +1,65 @@ +#ifndef _EXIF_H +#define _EXIF_H +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +///
+typedef struct ExifImage ExifImage; +///
+typedef struct ExifKey ExifKey; +///
+typedef struct ExifValue ExifValue; +///
+typedef struct ExifData ExifData; +///
+typedef struct ExifDatum ExifDatum; +#if defined _WIN32 && defined WIN32_DLL +#if BUILD_DLL +#define EXIF_API __declspec(dllexport) +#else +#define EXIF_API __declspec(dllimport) +#endif +#else +#define EXIF_API +#endif + +EXIF_API ExifImage* create_exif_image(const char* path); +EXIF_API int exif_image_set_exif_data(ExifImage* image, ExifData* data); +EXIF_API int exif_image_write_metadata(ExifImage* image); +EXIF_API void free_exif_image(ExifImage* img); +EXIF_API ExifKey* exif_create_key_by_key(const char* key); +EXIF_API ExifKey* exif_create_key_by_id(uint16_t id, const char* group_name); +EXIF_API const char* exif_get_key_key(ExifKey* key); +EXIF_API const char* exif_get_key_family_name(ExifKey* key); +EXIF_API char* exif_get_key_group_name(ExifKey* key); +EXIF_API char* exif_get_key_tag_name(ExifKey* key); +EXIF_API uint16_t exif_get_key_tag_tag(ExifKey* key); +EXIF_API char* exif_get_key_tag_label(ExifKey* key); +EXIF_API char* exif_get_key_tag_desc(ExifKey* key); +EXIF_API int exif_get_key_default_type_id(ExifKey* key); +EXIF_API void exif_free(void* v); +EXIF_API void exif_free_key(ExifKey* key); +EXIF_API ExifValue* exif_create_value(int type_id); +EXIF_API int exif_get_value_type_id(ExifValue* value); +EXIF_API long exif_get_value_count(ExifValue* value); +EXIF_API long exif_get_value_size(ExifValue* value); +EXIF_API long exif_get_value_size_data_area(ExifValue *value); +EXIF_API int exif_value_read(ExifValue* value, const uint8_t* bytes, long len, int byte_order); +EXIF_API int exif_get_value_ok(ExifValue* value); +EXIF_API char* exif_value_to_string(ExifValue* value, size_t* len); +EXIF_API char* exif_value_to_string2(ExifValue* value, size_t* len, long i); +EXIF_API int64_t exif_value_to_int64(ExifValue* value, long i); +EXIF_API ExifData* exif_data_new(); +EXIF_API int exif_data_add(ExifData* d, ExifKey* key, ExifValue* value); +EXIF_API int exif_data_clear(ExifData* d); +EXIF_API int exif_data_is_empty(ExifData* d); +EXIF_API long exif_data_get_count(ExifData* d); +EXIF_API void exif_free_value(ExifValue* value); +EXIF_API void exif_free_data(ExifData* d); +EXIF_API void exif_free_datum(ExifDatum* d); +#ifdef __cplusplus +} +#endif +#endif diff --git a/exif/patchs/basicio.cpp.patch b/exif/patchs/basicio.cpp.patch new file mode 100644 index 0000000..5b975a1 --- /dev/null +++ b/exif/patchs/basicio.cpp.patch @@ -0,0 +1,93 @@ +diff --git a/src/basicio.cpp b/src/basicio.cpp +index 807d75c8..49841f36 100644 +--- a/src/basicio.cpp ++++ b/src/basicio.cpp +@@ -87,6 +87,64 @@ namespace { + } + + namespace Exiv2 { ++#if defined(__MINGW__) || (defined(WIN32) && !defined(__CYGWIN__)) ++ /// Remove unsuportted options flags ++ unsigned long getMultiByteToWideCharOptions(const unsigned long ori_options, unsigned int cp) { ++ if (cp == CP_ACP) cp = GetACP(); ++ if (cp == CP_OEMCP) cp = GetOEMCP(); ++ switch (cp) ++ { ++ case 50220: ++ case 50221: ++ case 50222: ++ case 50225: ++ case 50227: ++ case 50229: ++ case CP_UTF7: ++ case 42: ++ return 0; ++ default: ++ break; ++ } ++ if (cp >= 57002 && cp <= 57011) return 0; ++ if (cp == CP_UTF8 || cp == 54936) { ++ return MB_ERR_INVALID_CHARS & ori_options; ++ } ++ return ori_options; ++ } ++ bool str_to_wstr(std::wstring& out, std::string inp, unsigned int cp) { ++ DWORD opt = getMultiByteToWideCharOptions(MB_ERR_INVALID_CHARS, cp); ++ int wlen = MultiByteToWideChar(cp, opt, inp.c_str(), inp.length(), nullptr, 0); ++ if (!wlen) { ++ return false; ++ } ++ wchar_t* wstr = (wchar_t*)malloc(wlen * sizeof(wchar_t)); ++ if (wstr == nullptr) { ++ return false; ++ } ++ if (!MultiByteToWideChar(cp, opt, inp.c_str(), inp.length(), wstr, wlen)) { ++ free(wstr); ++ return false; ++ } ++ out = std::wstring(wstr, wlen); ++ free(wstr); ++ return true; ++ } ++ FILE* win32_fopen(const char* path, const char* mode) { ++ std::string p(path), m(mode); ++ FILE* f = nullptr; ++ unsigned long cps[] = { CP_UTF8, CP_ACP, CP_OEMCP }; ++ for (int i = 0; i < 3; i++) { ++ std::wstring wp, wm; ++ if (str_to_wstr(wp, p, cps[i]) && str_to_wstr(wm, m, cps[i])) { ++ if ((f = _wfopen(wp.c_str(), wm.c_str()))) { ++ return f; ++ } ++ } ++ } ++ return std::fopen(path, mode); ++ } ++#endif + void BasicIo::readOrThrow(byte* buf, long rcount, ErrorCode err) { + const long nread = read(buf, rcount); + enforce(nread == rcount, err); +@@ -201,7 +259,11 @@ namespace Exiv2 { + } + openMode_ = "r+b"; + opMode_ = opSeek; ++#if defined(__MINGW__) || (defined(WIN32) && !defined(__CYGWIN__)) ++ fp_ = win32_fopen(path_.c_str(), openMode_.c_str()); ++#else + fp_ = std::fopen(path_.c_str(), openMode_.c_str()); ++#endif + if (!fp_) return 1; + return std::fseek(fp_, offset, SEEK_SET); + } // FileIo::Impl::switchMode +@@ -534,7 +596,11 @@ namespace Exiv2 { + close(); + p_->openMode_ = mode; + p_->opMode_ = Impl::opSeek; ++#if defined(__MINGW__) || (defined(WIN32) && !defined(__CYGWIN__)) ++ p_->fp_ = win32_fopen(path().c_str(), mode.c_str()); ++#else + p_->fp_ = ::fopen(path().c_str(), mode.c_str()); ++#endif + if (!p_->fp_) + return 1; + return 0; diff --git a/exif/src/exif.cpp b/exif/src/exif.cpp new file mode 100644 index 0000000..6c6ffff --- /dev/null +++ b/exif/src/exif.cpp @@ -0,0 +1,250 @@ +#include "exif.h" +#include "exif_priv.h" +#include "cpp2c.h" + +#include +#include +#include "fileop.h" + +#define string2char cpp2c::string2char + +ExifImage* create_exif_image(const char* path) { + if (!path) return nullptr; + if (!fileop::exists(path)) return nullptr; + auto img = new ExifImage; + try { + img->image = Exiv2::ImageFactory::open(path); + } catch (std::exception& e) { + printf("%s\n", e.what()); + goto end; + } + return img; +end: + delete img; + return nullptr; +} + +int exif_image_set_exif_data(ExifImage* image, ExifData* data) { + if (!image || !data) return 1; + image->image->setExifData(data->data); + return 0; +} + +int exif_image_write_metadata(ExifImage* image) { + if (!image) return 1; + try { + image->image->writeMetadata(); + } catch (std::exception& e) { + printf("%s\n", e.what()); + return 1; + } + return 0; +} + +void free_exif_image(ExifImage* img) { + if (!img) return; + delete img; +} + +ExifKey* exif_create_key_by_key(const char* key) { + if (!key) return nullptr; + ExifKey* k = new ExifKey; + try { + k->key = new Exiv2::ExifKey(key); + } catch (std::exception& e) { + printf("%s\n", e.what()); + goto end; + } + return k; +end: + if (k->key) delete k->key; + delete k; + return nullptr; +} + +ExifKey* exif_create_key_by_id(uint16_t id, const char* group_name) { + if (!group_name) return nullptr; + ExifKey* k = new ExifKey; + try { + k->key = new Exiv2::ExifKey(id, group_name); + } catch (std::exception& e) { + printf("%s\n", e.what()); + goto end; + } + return k; +end: + if (k->key) delete k->key; + delete k; + return nullptr; +} + +const char* exif_get_key_key(ExifKey* key) { + if (!key || !key->key) return nullptr; + return std::move(key->key->key().c_str()); +} + +const char* exif_get_key_family_name(ExifKey* key) { + if (!key || !key->key) return nullptr; + return key->key->familyName(); +} + +char* exif_get_key_group_name(ExifKey* key) { + if (!key || !key->key) return nullptr; + auto s = key->key->groupName(); + char* re = nullptr; + if (!string2char(s, re)) return nullptr; + return re; +} + +char* exif_get_key_tag_name(ExifKey* key) { + if (!key || !key->key) return nullptr; + auto s = key->key->tagName(); + char* re = nullptr; + if (!string2char(s, re)) return nullptr; + return re; +} + +uint16_t exif_get_key_tag_tag(ExifKey* key) { + if (!key || !key->key) return -1; + return key->key->tag(); +} + +char* exif_get_key_tag_label(ExifKey* key) { + if (!key || !key->key) return nullptr; + auto s = key->key->tagLabel(); + char* re = nullptr; + if (!string2char(s, re)) return nullptr; + return re; +} + +char* exif_get_key_tag_desc(ExifKey* key) { + if (!key || !key->key) return nullptr; + auto s = key->key->tagDesc(); + char* re = nullptr; + if (!string2char(s, re)) return nullptr; + return re; +} + +int exif_get_key_default_type_id(ExifKey* key) { + if (!key || !key->key) return -1; + return key->key->defaultTypeId(); +} + +void exif_free(void* v) { + free(v); +} + +void exif_free_key(ExifKey* key) { + if (!key) return; + if (key->key) delete key->key; + delete key; +} + +ExifValue* exif_create_value(int type_id) { + ExifValue* v = new ExifValue; + try { + auto t = static_cast(type_id); + v->value = Exiv2::Value::create(t); + } catch (std::exception& e) { + printf("%s\n", e.what()); + goto end; + } + return v; +end: + if (v) delete v; + return nullptr; +} + +int exif_get_value_type_id(ExifValue* value) { + if (!value) return -1; + return value->value->typeId(); +} + +long exif_get_value_count(ExifValue* value) { + if (!value) return -1; + return value->value->count(); +} + +long exif_get_value_size(ExifValue* value) { + if (!value) return -1; + return value->value->size(); +} + +long exif_get_value_size_data_area(ExifValue* value) { + if (!value) return -1; + return value->value->sizeDataArea(); +} + +int exif_value_read(ExifValue* value, const uint8_t* bytes, long len, int byte_order) { + if (!value || !bytes) return -1; + return value->value->read(bytes, len, static_cast(byte_order)); +} + +int exif_get_value_ok(ExifValue* value) { + if (!value) return 0; + return value->value->ok() ? 1 : 0; +} + +char* exif_value_to_string(ExifValue* value, size_t* len) { + if (!value || !len) return nullptr; + auto s = value->value->toString(); + *len = s.size(); + char* tmp = nullptr; + if (!string2char(s, tmp)) return nullptr; + return tmp; +} + +char* exif_value_to_string2(ExifValue* value, size_t* len, long i) { + if (!value || !len) return nullptr; + auto s = value->value->toString(i); + *len = s.size(); + char* tmp = nullptr; + if (!string2char(s, tmp)) return nullptr; + return tmp; +} + +int64_t exif_value_to_int64(ExifValue* value, long i) { + if (!value) return -1; + return value->value->toInt64(i); +} + +ExifData* exif_data_new() { + return new ExifData; +} + +int exif_data_add(ExifData* d, ExifKey* key, ExifValue* value) { + if (!d || !key || !value || !key->key) return 0; + d->data.add(*key->key, value->value.get()); + return 1; +} + +int exif_data_clear(ExifData* d) { + if (!d) return 0; + d->data.clear(); + return 1; +} + +int exif_data_is_empty(ExifData* d) { + if (!d) return -1; + return d->data.empty() ? 1 : 0; +} + +long exif_data_get_count(ExifData* d) { + if (!d) return -1; + return d->data.count(); +} + +void exif_free_value(ExifValue* value) { + if (!value) return; + delete value; +} + +void exif_free_data(ExifData* d) { + if (!d) return; + delete d; +} + +void exif_free_datum(ExifDatum* d) { + if (!d) return; + if (d->data) delete d->data; +} diff --git a/exif/src/exif_priv.h b/exif/src/exif_priv.h new file mode 100644 index 0000000..296684e --- /dev/null +++ b/exif/src/exif_priv.h @@ -0,0 +1,19 @@ +#ifndef _EXIF_EXIF_PRIV_H +#define _EXIF_EXIF_PRIV_H +#include "exiv2/exiv2.hpp" +typedef struct ExifImage { + Exiv2::Image::UniquePtr image; +} ExifImage; +typedef struct ExifKey { + Exiv2::ExifKey* key = nullptr; +} ExifKey; +typedef struct ExifValue { + Exiv2::Value::UniquePtr value; +} ExifValue; +typedef struct ExifData { + Exiv2::ExifData data; +} ExifData; +typedef struct ExifDatum { + Exiv2::Exifdatum* data; +} ExifDatum; +#endif diff --git a/src/_exif.rs b/src/_exif.rs new file mode 100644 index 0000000..4948891 --- /dev/null +++ b/src/_exif.rs @@ -0,0 +1,5 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] +#![allow(deref_nullptr)] +include!(concat!(env!("OUT_DIR"), "/exif.rs")); diff --git a/src/data/data.rs b/src/data/data.rs new file mode 100644 index 0000000..3141a24 --- /dev/null +++ b/src/data/data.rs @@ -0,0 +1,49 @@ +use crate::pixiv_link::ToPixivID; +use crate::pixiv_link::PixivID; +use json::JsonValue; +use std::convert::TryInto; + +/// Pixiv's basic data +pub struct PixivData { + /// ID + pub id: PixivID, + /// The title + pub title: Option, + /// The author + pub author: Option, +} + +impl PixivData { + pub fn new(id: T) -> Option { + let i = id.to_pixiv_id(); + if i.is_none() { + return None; + } + Some(Self { + id: i.unwrap(), + title: None, + author: None, + }) + } + + /// Read data from JSON object. + /// The object is from https://www.pixiv.net/artworks/ + /// * `value` - The JSON object + /// * `allow_overwrite` - Allow overwrite the data existing. + pub fn from_web_page_data(&mut self, value: &JsonValue, allow_overwrite: bool) { + let id: u64 = (&self.id).try_into().unwrap(); + let ids = format!("{}", id); + if self.title.is_none() || allow_overwrite { + let title = value["illust"][ids.as_str()]["illustTitle"].as_str(); + if title.is_some() { + self.title = Some(String::from(title.unwrap())); + } + } + if self.author.is_none() || allow_overwrite { + let author = value["illust"][ids.as_str()]["userName"].as_str(); + if author.is_some() { + self.author = Some(String::from(author.unwrap())); + } + } + } +} diff --git a/src/data/exif.rs b/src/data/exif.rs new file mode 100644 index 0000000..ce80d09 --- /dev/null +++ b/src/data/exif.rs @@ -0,0 +1,60 @@ +use crate::data::data::PixivData; +use crate::exif::ExifData; +use crate::exif::ExifImage; +use crate::exif::ExifKey; +use crate::exif::ExifTypeID; +use crate::exif::ExifValue; +use std::convert::TryFrom; +use std::ffi::OsStr; +use utf16string::LittleEndian; +use utf16string::WString; + +fn add_image_id(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { + let link = d.id.to_link(); + let key = ExifKey::try_from("Exif.Image.ImageID")?; + let mut value = ExifValue::try_from(ExifTypeID::AsciiString)?; + value.read(link.as_bytes(), None)?; + data.add(&key, &value)?; + Ok(()) +} + +fn add_image_title(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { + if d.title.is_none() { + return Ok(()); + } + let title = d.title.as_ref().unwrap(); + let key = ExifKey::try_from("Exif.Image.ImageDescription")?; + let mut value = ExifValue::try_from(ExifTypeID::AsciiString)?; + value.read(title.as_bytes(), None)?; + data.add(&key, &value)?; + let key = ExifKey::try_from("Exif.Image.XPTitle")?; + let mut value = ExifValue::try_from(ExifTypeID::BYTE)?; + let s: WString = WString::from(title); + value.read(s.as_bytes(), None)?; + data.add(&key, &value)?; + Ok(()) +} + +fn add_image_author(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { + if d.author.is_none() { + return Ok(()); + } + let author = d.author.as_ref().unwrap(); + let key = ExifKey::try_from("Exif.Image.XPAuthor")?; + let mut value = ExifValue::try_from(ExifTypeID::BYTE)?; + let s: WString = WString::from(author); + value.read(s.as_bytes(), None)?; + data.add(&key, &value)?; + Ok(()) +} + +pub fn add_exifdata_to_image + ?Sized>(file_name: &S, data: &PixivData) -> Result<(), ()> { + let mut f = ExifImage::new(file_name)?; + let mut d = ExifData::new()?; + add_image_id(&mut d, data)?; + add_image_title(&mut d, data)?; + add_image_author(&mut d, data)?; + f.set_exif_data(&d)?; + f.write_metadata()?; + Ok(()) +} diff --git a/src/data/json.rs b/src/data/json.rs index cf1f5f2..99994f3 100644 --- a/src/data/json.rs +++ b/src/data/json.rs @@ -1,8 +1,10 @@ +use crate::data::data::PixivData; use crate::gettext; use crate::pixiv_link::PixivID; use crate::pixiv_link::ToPixivID; use json::JsonValue; use std::collections::HashMap; +use std::convert::From; use std::ffi::OsStr; use std::fs::File; use std::fs::remove_file; @@ -20,6 +22,7 @@ pub struct JSONDataFile { } impl JSONDataFile { + #[allow(dead_code)] pub fn new(id: T) -> Option { let i = id.to_pixiv_id(); if i.is_none() { @@ -31,6 +34,16 @@ impl JSONDataFile { }) } + pub fn add(&mut self, key: &str, value: T) -> Result<(), ()> { + let v = value.to_json(); + if v.is_some() { + self.maps.insert(String::from(key), v.unwrap()); + Ok(()) + } else { + Err(()) + } + } + pub fn save + ?Sized>(&self, path: &S) -> bool { let p = Path::new(path); if p.exists() { @@ -60,6 +73,28 @@ impl JSONDataFile { } } +impl From for JSONDataFile { + fn from(p: PixivData) -> Self { + JSONDataFile::from(&p) + } +} + +impl From<&PixivData> for JSONDataFile { + fn from(p: &PixivData) -> Self { + let mut f = Self { + id: p.id.clone(), + maps: HashMap::new(), + }; + if p.title.is_some() { + f.add("title", p.title.as_ref().unwrap()).unwrap(); + } + if p.author.is_some() { + f.add("author", p.author.as_ref().unwrap()).unwrap(); + } + f + } +} + impl ToJson for JSONDataFile { fn to_json(&self) -> Option { let mut value = json::object! {}; @@ -74,3 +109,15 @@ impl ToJson for JSONDataFile { Some(value) } } + +impl ToJson for &str { + fn to_json(&self) -> Option { + Some(JsonValue::String(String::from(*self))) + } +} + +impl ToJson for &String { + fn to_json(&self) -> Option { + Some(JsonValue::String((*self).to_string())) + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs index 22fdbb3..cc06fe7 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1 +1,4 @@ +pub mod data; +#[cfg(feature = "exif")] +pub mod exif; pub mod json; diff --git a/src/download.rs b/src/download.rs index e225ee5..653da8b 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,3 +1,6 @@ +use crate::data::data::PixivData; +#[cfg(feature = "exif")] +use crate::data::exif::add_exifdata_to_image; use crate::data::json::JSONDataFile; use crate::gettext; use crate::pixiv_link::PixivID; @@ -40,7 +43,8 @@ impl Main { if re.is_none() { return 1; } - let pages = (&(re.as_ref().unwrap())["illust"][format!("{}", id).as_str()]["sl"]).as_u64(); + let re = re.unwrap(); + let pages = (&re["illust"][format!("{}", id).as_str()]["pageCount"]).as_u64(); if pages.is_none() { println!("{}", gettext("Failed to get page count.")); return 1; @@ -56,7 +60,9 @@ impl Main { } let base = PathBuf::from("."); let json_file = base.join(format!("{}.json", id)); - let json_data = JSONDataFile::new(id).unwrap(); + let mut datas = PixivData::new(id).unwrap(); + datas.from_web_page_data(&re, true); + let json_data = JSONDataFile::from(&datas); if !json_data.save(&json_file) { println!("{}", gettext("Failed to save metadata to JSON file.")); return 1; @@ -93,7 +99,58 @@ impl Main { gettext("Downloaded image:"), link, file_name.to_str().unwrap_or("(null)") - ) + ); + #[cfg(feature = "exif")] + { + if add_exifdata_to_image(&file_name, &datas).is_err() { + println!( + "{} {}", + gettext("Failed to add exif data to image:"), + file_name.to_str().unwrap_or("(null)") + ); + } + } + } + } else { + let link = (&re["illust"][format!("{}", id)]["urls"]["original"]).as_str(); + if link.is_none() { + println!("{}", gettext("Failed to get original picture's link.")); + return 1; + } + let link = link.unwrap(); + let file_name = get_file_name_from_url(link); + if file_name.is_none() { + println!("{} {}", gettext("Failed to get file name from url:"), link); + return 1; + } + let file_name = file_name.unwrap(); + let file_name = base.join(file_name); + let r = pw.download_image(link); + if r.is_none() { + println!("{} {}", gettext("Failed to download image:"), link); + return 1; + } + let r = r.unwrap(); + let re = WebClient::download_stream(&file_name, r, pw.helper.overwrite()); + if re.is_err() { + println!("{} {}", gettext("Failed to download image:"), link); + return 1; + } + println!( + "{} {} -> {}", + gettext("Downloaded image:"), + link, + file_name.to_str().unwrap_or("(null)") + ); + #[cfg(feature = "exif")] + { + if add_exifdata_to_image(&file_name, &datas).is_err() { + println!( + "{} {}", + gettext("Failed to add exif data to image:"), + file_name.to_str().unwrap_or("(null)") + ); + } } } 0 diff --git a/src/exif.rs b/src/exif.rs new file mode 100644 index 0000000..5e8299c --- /dev/null +++ b/src/exif.rs @@ -0,0 +1,720 @@ +use crate::_exif; +use c_fixed_string::CFixedStr; +use int_enum::IntEnum; +use std::convert::TryFrom; +use std::ffi::CStr; +use std::ffi::CString; +use std::ffi::OsStr; +#[cfg(test)] +use std::fs::copy; +#[cfg(test)] +use std::fs::create_dir; +use std::ops::Drop; +use std::os::raw::c_long; +use std::path::Path; + +/// Used primarily as identifiers when creating ExifValue +#[repr(i32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +pub enum ExifTypeID { + /// Exif BYTE type, 8-bit unsigned integer. + BYTE = 1, + /// Exif ASCII type, 8-bit byte. + AsciiString = 2, + /// Exif SHORT type, 16-bit (2-byte) unsigned integer. + UShort = 3, + /// Exif LONG type, 32-bit (4-byte) unsigned integer. + ULong = 4, + /// Exif RATIONAL type, two LONGs: numerator and denumerator of a fraction. + URational = 5, + /// Exif SBYTE type, an 8-bit signed (twos-complement) integer. + SBYTE = 6, + /// Exif UNDEFINED type, an 8-bit byte that may contain anything. + Undefined = 7, + /// Exif SSHORT type, a 16-bit (2-byte) signed (twos-complement) integer. + SShort = 8, + /// Exif SLONG type, a 32-bit (4-byte) signed (twos-complement) integer. + SLong = 9, + /// Exif SRATIONAL type, two SLONGs: numerator and denominator of a fraction. + SRational = 10, + /// TIFF FLOAT type, single precision (4-byte) IEEE format. + TiffFloat = 11, + /// TIFF DOUBLE type, double precision (8-byte) IEEE format. + TiffDouble = 12, + /// TIFF IFD type, 32-bit (4-byte) unsigned integer. + TiffIfd = 13, + /// Exif LONG LONG type, 64-bit (8-byte) unsigned integer. + ULongLong = 16, + /// Exif LONG LONG type, 64-bit (8-byte) signed integer. + SLongLong = 17, + /// TIFF IFD type, 64-bit (8-byte) unsigned integer. + TiffIfd8 = 18, + /// IPTC string type. + String = 0x10000, + /// IPTC date type. + Date = 0x10001, + /// IPTC time type. + Time = 0x10002, + /// Exiv2 type for the Exif user comment. + Comment = 0x10003, + /// Exiv2 type for a CIFF directory. + Directory = 0x10004, + /// XMP text type. + XmpText = 0x10005, + /// XMP alternative type. + XmpAlt = 0x10006, + /// XMP bag type. + XmpBag = 0x10007, + /// XMP sequence type. + XmpSeq = 0x10008, + /// XMP language alternative type. + LangAlt = 0x10009, + /// Invalid type id. + InvalidTypeId = 0x1fffe, + /// Last type id. + LastTypeId = 0x1ffff, +} + +/// Type to express the byte orde +#[repr(i32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +pub enum ExifByteOrder { + Invalid = 0, + Little = 1, + Big = 2, +} + +/// Exif Key
+/// See [all available keys](https://exiv2.org/tags.html). +pub struct ExifKey { + key: *mut _exif::ExifKey, +} + +/// Return raw pointer of the handle +pub trait ToRawHandle { + /// Return raw pointer of the handle + unsafe fn to_raw_handle(&self) -> *mut T; +} + +impl TryFrom for ExifKey { + type Error = (); + fn try_from(value: CString) -> Result { + let ptr = value.as_ptr(); + let re = unsafe { _exif::exif_create_key_by_key(ptr) }; + if re.is_null() { + return Err(()); + } + Ok(Self { key: re }) + } +} + +impl TryFrom<&str> for ExifKey { + type Error = (); + fn try_from(value: &str) -> Result { + let p = CString::new(value); + if p.is_err() { + return Err(()); + } + Self::try_from(p.unwrap()) + } +} + +impl Drop for ExifKey { + fn drop(&mut self) { + if !self.key.is_null() { + unsafe { _exif::exif_free_key(self.key) }; + } + } +} + +impl ToRawHandle<_exif::ExifKey> for ExifKey { + unsafe fn to_raw_handle(&self) -> *mut _exif::ExifKey { + self.key + } +} + +#[allow(dead_code)] +impl ExifKey { + /// Create an Exif key from the tag number and group name + pub fn from_id(id: u16, group_name: &str) -> Result { + let p = CString::new(group_name); + if p.is_err() { + return Err(()); + } + let p = p.unwrap(); + let ptr = p.as_ptr(); + let re = unsafe { _exif::exif_create_key_by_id(id, ptr) }; + if re.is_null() { + return Err(()); + } + Ok(Self { key: re }) + } + + /// Return the key of the metadatum as a string. + /// The key is of the form `familyName.groupName.tagName` + /// # Note + /// However that the key is not necessarily unique, e.g., an ExifData may contain multiple metadata with the same key. + pub fn key(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_key(self.key) }; + if r.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(r) }; + let s = s.to_str(); + if s.is_err() { + return None; + } + let s = s.unwrap(); + Some(s.to_owned()) + } + + /// Return an identifier for the type of metadata (the first part of the key) + pub fn family_name(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_family_name(self.key) }; + if r.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(r) }; + let s = s.to_str(); + if s.is_err() { + return None; + } + let s = s.unwrap(); + Some(s.to_owned()) + } + + /// Return the name of the group (the second part of the key) + pub fn group_name(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_group_name(self.key) }; + if r.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(r) }; + let s = s.to_owned(); + unsafe { _exif::exif_free(r as *mut ::std::os::raw::c_void) }; + let s = s.to_str(); + if s.is_err() { + return None; + } + let s = s.unwrap(); + Some(s.to_owned()) + } + + /// Return the name of the tag (which is also the third part of the key) + pub fn tag_name(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_tag_name(self.key) }; + if r.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(r) }; + let s = s.to_owned(); + unsafe { _exif::exif_free(r as *mut ::std::os::raw::c_void) }; + let s = s.to_str(); + if s.is_err() { + return None; + } + let s = s.unwrap(); + Some(s.to_owned()) + } + + /// Return the tag number. + pub fn tag(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_tag_tag(self.key) }; + if r == 65535 { + None + } else { + Some(r) + } + } + + /// Return a label for the tag. + pub fn tag_label(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_tag_label(self.key) }; + if r.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(r) }; + let s = s.to_owned(); + unsafe { _exif::exif_free(r as *mut ::std::os::raw::c_void) }; + let s = s.to_str(); + if s.is_err() { + return None; + } + let s = s.unwrap(); + Some(s.to_owned()) + } + + /// Return the tag description. + pub fn tag_desc(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_tag_desc(self.key) }; + if r.is_null() { + return None; + } + let s = unsafe { CStr::from_ptr(r) }; + let s = s.to_owned(); + unsafe { _exif::exif_free(r as *mut ::std::os::raw::c_void) }; + let s = s.to_str(); + if s.is_err() { + return None; + } + let s = s.unwrap(); + Some(s.to_owned()) + } + + /// Return the default type id for this tag. + pub fn default_typeid(&self) -> Option { + if self.key.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_key_default_type_id(self.key) }; + if r == -1 { + return None; + } + let re = ExifTypeID::from_int(r); + if re.is_err() { + return None; + } + Some(re.unwrap()) + } +} + +/// Common interface for all types of values used with metadata. +pub struct ExifValue { + value: *mut _exif::ExifValue, +} + +impl TryFrom for ExifValue { + type Error = (); + fn try_from(value: ExifTypeID) -> Result { + let d = value.int_value(); + let r = unsafe { _exif::exif_create_value(d) }; + if r.is_null() { + return Err(()); + } + Ok(Self { value: r }) + } +} + +impl TryFrom for ExifValue { + type Error = (); + fn try_from(value: i32) -> Result { + let e = ExifTypeID::from_int(value); + if e.is_err() { + return Err(()); + } + Self::try_from(e.unwrap()) + } +} + +impl Drop for ExifValue { + fn drop(&mut self) { + if !self.value.is_null() { + unsafe { _exif::exif_free_value(self.value) }; + } + } +} + +impl ToRawHandle<_exif::ExifValue> for ExifValue { + unsafe fn to_raw_handle(&self) -> *mut _exif::ExifValue { + self.value + } +} + +#[allow(dead_code)] +impl ExifValue { + /// Return the type identifier (Exif data format type). + pub fn type_id(&self) -> Option { + if self.value.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_value_type_id(self.value) }; + if r == 0 { + return None; + } + let re = ExifTypeID::from_int(r); + if re.is_err() { + return None; + } + Some(re.unwrap()) + } + + /// Return the number of components of the value. + pub fn count(&self) -> Option { + if self.value.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_value_count(self.value) }; + if r < 0 { + return None; + } + Some(r as usize) + } + + /// Return the size of the value in bytes. + pub fn size(&self) -> Option { + if self.value.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_value_size(self.value) }; + if r < 0 { + return None; + } + Some(r as usize) + } + + /// Return the size of the data area, 0 if there is none. + pub fn size_data_area(&self) -> Option { + if self.value.is_null() { + return None; + } + let r = unsafe { _exif::exif_get_value_size_data_area(self.value) }; + if r < 0 { + return None; + } + Some(r as usize) + } + + /// Read the value from a character buffer. + /// * `buf` - Buffer + /// * `byte_order` - Applicable byte order (little or big endian). Default: invaild. + pub fn read(&mut self, buf: &[u8], byte_order: Option) -> Result<(), ()> { + if self.value.is_null() { + return Err(()); + } + let buf_len = buf.len() as c_long; + let order = match byte_order { + Some(o) => o, + None => ExifByteOrder::Invalid, + }; + let order = order.int_value(); + let ptr = buf.as_ptr(); + let r = unsafe { _exif::exif_value_read(self.value, ptr, buf_len, order) }; + if r == 0 { + Ok(()) + } else { + Err(()) + } + } + + /// Return the value / n-th component of the value as a string + /// * `n` - specify the component, if None, return whole value + pub fn to_string(&self, n: Option) -> Option { + if self.value.is_null() { + return None; + } + if n.is_some() { + let c = self.count(); + if c.is_none() { + return None; + } + if n.as_ref().unwrap() >= c.as_ref().unwrap() { + return None; + } + } + let mut size: Vec<_exif::size_t> = vec![0]; + let ptr = size.as_mut_ptr(); + if ptr.is_null() { + return None; + } + let r = if n.is_none() { + unsafe { _exif::exif_value_to_string(self.value, ptr) } + } else { + unsafe { _exif::exif_value_to_string2(self.value, ptr, n.unwrap() as c_long) } + }; + if r.is_null() { + return None; + } + let s = unsafe { CFixedStr::from_mut_ptr(r, size[0] as usize) }; + let s = s.to_owned(); + unsafe { _exif::exif_free(r as *mut ::std::os::raw::c_void) }; + if !self.ok() { + return None; + } + Some(s.into_c_string()) + } + + /// Check the ok status indicator. + /// After a to conversion, this indicator shows whether the conversion was successful. + pub fn ok(&self) -> bool { + if self.value.is_null() { + return false; + } + let r = unsafe { _exif::exif_get_value_ok(self.value) }; + r != 0 + } + + /// Convert the n-th component of the value to a int64 + pub fn to_int64(&self, n: usize) -> Option { + if self.value.is_null() { + return None; + } + let c = self.count(); + if c.is_none() { + return None; + } + let c = c.unwrap(); + if n >= c { + return None; + } + let r = unsafe { _exif::exif_value_to_int64(self.value, n as i32) }; + if !self.ok() { + return None; + } + Some(r) + } +} + +/// A container for Exif data. +pub struct ExifData { + data: *mut _exif::ExifData, +} + +#[allow(dead_code)] +impl ExifData { + /// Create a new container + pub fn new() -> Result { + let d = unsafe { _exif::exif_data_new() }; + if d.is_null() { + return Err(()); + } + Ok(Self { data: d }) + } + + /// Add a data from the supplied key and value pair. + /// No duplicate checks are performed, i.e., it is possible to add multiple metadata with the same key. + pub fn add(&mut self, key: &ExifKey, value: &ExifValue) -> Result<(), ()> { + let k = unsafe { key.to_raw_handle() }; + let v = unsafe { value.to_raw_handle() }; + if k.is_null() || v.is_null() { + return Err(()); + } + let r = unsafe { _exif::exif_data_add(self.data, k, v) }; + if r == 0 { + Err(()) + } else { + Ok(()) + } + } + + /// Delete all Exifdatum instances resulting in an empty container. + /// Note that this also removes thumbnails. + pub fn clear(&mut self) -> Result<(), ()> { + if self.data.is_null() { + return Err(()); + } + let r = unsafe { _exif::exif_data_clear(self.data) }; + if r == 0 { + Err(()) + } else { + Ok(()) + } + } + + /// Get the number of metadata entries. + pub fn count(&self) -> Option { + if self.data.is_null() { + return None; + } + let r = unsafe { _exif::exif_data_get_count(self.data) }; + if r == -1 { + return None; + } + Some(r as usize) + } + + /// Return true if there is no Exif metadata. + pub fn empty(&self) -> Option { + if self.data.is_null() { + return None; + } + let r = unsafe { _exif::exif_data_is_empty(self.data) }; + if r == -1 { + None + } else if r == 0 { + Some(false) + } else { + Some(true) + } + } +} + +impl Drop for ExifData { + fn drop(&mut self) { + if !self.data.is_null() { + unsafe { _exif::exif_free_data(self.data) }; + } + } +} + +impl ToRawHandle<_exif::ExifData> for ExifData { + unsafe fn to_raw_handle(&self) -> *mut _exif::ExifData { + self.data + } +} + +pub struct ExifImage { + img: *mut _exif::ExifImage, +} + +#[allow(dead_code)] +impl ExifImage { + pub fn new + ?Sized>(file_name: &S) -> Result { + let p = Path::new(file_name); + if !p.exists() { + return Err(()); + } + let p = p.to_str(); + if p.is_none() { + return Err(()); + } + let p = CString::new(p.unwrap()); + if p.is_err() { + return Err(()); + } + let p = p.unwrap(); + let ptr = p.as_c_str().as_ptr(); + let f = unsafe { _exif::create_exif_image(ptr) }; + if f.is_null() { + return Err(()); + } + Ok(Self { img: f }) + } + + pub fn set_exif_data(&mut self, data: &ExifData) -> Result<(), ()> { + if self.img.is_null() { + return Err(()); + } + let d = unsafe { data.to_raw_handle() }; + if d.is_null() { + return Err(()); + } + let d = unsafe { _exif::exif_image_set_exif_data(self.img, d) }; + if d == 0 { + Ok(()) + } else { + Err(()) + } + } + + pub fn write_metadata(&mut self) -> Result<(), ()> { + if self.img.is_null() { + return Err(()); + } + let d = unsafe { _exif::exif_image_write_metadata(self.img) }; + if d == 0 { + Ok(()) + } else { + Err(()) + } + } +} + +impl Drop for ExifImage { + fn drop(&mut self) { + if !self.img.is_null() { + unsafe { _exif::free_exif_image(self.img) }; + } + } +} + +#[test] +fn test_exif_key() { + let k = ExifKey::try_from("Exif.Image.XPTitle"); + assert!(k.is_ok()); + let k = k.unwrap(); + assert_eq!(Some(String::from("Exif.Image.XPTitle")), k.key()); + assert_eq!(Some(String::from("Exif")), k.family_name()); + assert_eq!(Some(String::from("Image")), k.group_name()); + assert_eq!(Some(String::from("XPTitle")), k.tag_name()); + assert_eq!(Some(40091), k.tag()); + assert_eq!(Some(String::from("Windows Title")), k.tag_label()); + assert_eq!( + Some(String::from("Title tag used by Windows, encoded in UCS2")), + k.tag_desc() + ); + assert_eq!(Some(ExifTypeID::BYTE), k.default_typeid()); + let k2 = ExifKey::from_id(40091, "Image"); + assert!(k2.is_ok()); + let k2 = k2.unwrap(); + assert_eq!(Some(String::from("Exif.Image.XPTitle")), k2.key()); +} + +#[test] +fn test_exif_value() { + let v = ExifValue::try_from(ExifTypeID::BYTE); + assert!(v.is_ok()); + let mut v = v.unwrap(); + assert_eq!(Some(ExifTypeID::BYTE), v.type_id()); + assert_eq!(Some(0), v.count()); + assert_eq!(Some(0), v.size()); + assert_eq!(Some(0), v.size_data_area()); + assert!(v.read("test".as_bytes(), None).is_ok()); + assert_eq!(Some(4), v.count()); + assert_eq!(Some(4), v.size()); + let c = CString::new("116 101 115 116").unwrap(); + assert_eq!(Some(c), v.to_string(None)); + assert_eq!(Some(4), v.count()); + assert_eq!(Some(4), v.size()); + assert_eq!(Some(CString::new("116").unwrap()), v.to_string(Some(0))); + let mut v2 = ExifValue::try_from(ExifTypeID::SLongLong).unwrap(); + v2.read(&(102345 as i64).to_le_bytes(), Some(ExifByteOrder::Little)) + .unwrap(); + assert_eq!(Some(8), v2.count()); +} + +#[test] +fn test_exif_data() { + let mut d = ExifData::new().unwrap(); + assert_eq!(Some(0), d.count()); + assert_eq!(Some(true), d.empty()); + let k = ExifKey::try_from("Exif.Image.XPTitle").unwrap(); + let mut v = ExifValue::try_from(ExifTypeID::BYTE).unwrap(); + v.read("test".as_bytes(), None).unwrap(); + d.add(&k, &v).unwrap(); + assert_eq!(Some(1), d.count()); + assert_eq!(Some(false), d.empty()); + d.clear().unwrap(); + assert_eq!(Some(0), d.count()); + assert_eq!(Some(true), d.empty()); +} + +#[test] +fn test_exif_image() { + let p = Path::new("./test"); + if !p.exists() { + create_dir("./test").unwrap(); + } + let target = "./test/夏のチマメ隊🏖️_91055644_p0.jpg"; + copy("./testdata/夏のチマメ隊🏖️_91055644_p0.jpg", target).unwrap(); + let mut d = ExifData::new().unwrap(); + let k = ExifKey::try_from("Exif.Image.ImageDescription").unwrap(); + let mut v = ExifValue::try_from(ExifTypeID::AsciiString).unwrap(); + v.read("夏のチマメ隊🏖️".as_bytes(), None).unwrap(); + d.add(&k, &v).unwrap(); + { + let mut img = ExifImage::new(target).unwrap(); + img.set_exif_data(&d).unwrap(); + img.write_metadata().unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index 6346773..fe1f315 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,27 @@ +#[cfg(feature = "c_fixed_string")] +extern crate c_fixed_string; extern crate chrono; extern crate dateparser; extern crate futures_util; extern crate json; +#[cfg(feature = "int-enum")] +extern crate int_enum; #[macro_use] extern crate lazy_static; extern crate tokio; extern crate regex; extern crate reqwest; +#[cfg(feature = "utf16string")] +extern crate utf16string; +#[cfg(feature = "exif")] +#[doc(hidden)] +mod _exif; mod cookies; mod data; mod download; +#[cfg(feature = "exif")] +mod exif; mod i18n; mod opthelper; mod opts; diff --git a/src/pixiv_link.rs b/src/pixiv_link.rs index f110d05..1c51d31 100644 --- a/src/pixiv_link.rs +++ b/src/pixiv_link.rs @@ -1,6 +1,7 @@ use crate::data::json::ToJson; use json::JsonValue; use regex::Regex; +use std::convert::TryInto; lazy_static! { #[doc(hidden)] @@ -36,13 +37,21 @@ impl PixivID { } None } + + pub fn to_link(&self) -> String { + match self { + Self::Artwork(id) => { + format!("https://www.pixiv.net/artworks/{}", id) + } + } + } } impl ToJson for PixivID { fn to_json(&self) -> Option { match *self { PixivID::Artwork(id) => { - Some(json::value!({"type": "artwork", "id": id})) + Some(json::value!({"type": "artwork", "id": id, "link": self.to_link()})) } } } @@ -71,3 +80,21 @@ impl ToPixivID for u64 { Some(PixivID::Artwork(*self)) } } + +impl TryInto for PixivID { + type Error = (); + fn try_into(self) -> Result { + match self { + Self::Artwork(id) => { Ok(id) } + } + } +} + +impl TryInto for &PixivID { + type Error = (); + fn try_into(self) -> Result { + match *self { + PixivID::Artwork(id) => { Ok(id) } + } + } +} diff --git a/src/utils.rs b/src/utils.rs index e95e0b1..2635bf1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,7 @@ use crate::gettext; use reqwest::IntoUrl; use std::env; +use std::io::Write; use std::path::Path; use std::path::PathBuf; @@ -28,6 +29,7 @@ pub fn check_file_exists(path: &str) -> bool { pub fn ask_need_overwrite(path: &str) -> bool { let s = gettext("Do you want to delete file \"\"?").replace("", path); print!("{}(y/n)", s.as_str()); + std::io::stdout().flush().unwrap(); let mut d = String::from(""); loop { let re = std::io::stdin().read_line(&mut d); diff --git a/testdata/README.md b/testdata/README.md new file mode 100644 index 0000000..aacf2ca --- /dev/null +++ b/testdata/README.md @@ -0,0 +1,2 @@ +# Source +[夏のチマメ隊🏖️_91055644_p0.jpg](夏のチマメ隊🏖️_91055644_p0.jpg): [Pixiv](https://www.pixiv.net/artworks/91055644) diff --git a/testdata/夏のチマメ隊🏖️_91055644_p0.jpg b/testdata/夏のチマメ隊🏖️_91055644_p0.jpg new file mode 100644 index 0000000..6115857 Binary files /dev/null and b/testdata/夏のチマメ隊🏖️_91055644_p0.jpg differ diff --git a/utils b/utils new file mode 160000 index 0000000..3fe5fa5 --- /dev/null +++ b/utils @@ -0,0 +1 @@ +Subproject commit 3fe5fa507b18c212099b3d2afeed1ad9842fa4c9