├── .Rbuildignore ├── .devcontainer └── devcontainer.json ├── .github ├── .gitignore └── workflows │ └── pkgdown.yaml ├── .gitignore ├── DESCRIPTION ├── NAMESPACE ├── NEWS.md ├── R ├── exif_read.R ├── globals.R ├── hooks.R ├── install.R └── utils.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── inst ├── exiftool │ └── README └── images │ ├── LaSals.jpg │ ├── Lizard.jpg │ └── QS_Hongg.jpg └── man ├── configure_exiftoolr.Rd ├── exif_call.Rd ├── exif_read.Rd ├── figures ├── LaSals.jpg ├── LaSals_annotated.jpg └── Lizard.jpg └── install_exiftool.Rd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^README\.Rmd$ 2 | ^README\.md$ 3 | ^README\.html$ 4 | ^inst/exiftool/win_exe/.*$ 5 | ^inst/exiftool/lib/.*$ 6 | ^inst/exiftool/exiftool$ 7 | ^man/figures/.*$ 8 | ^docs/.* 9 | ^img$ 10 | ^TODO.org$ 11 | ^_pkgdown\.yml$ 12 | ^docs$ 13 | ^pkgdown$ 14 | ^\.github$ 15 | ^\.devcontainer/.*$ 16 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/rocker-org/devcontainer-features/r-apt:latest": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | permissions: 23 | contents: write 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - uses: r-lib/actions/setup-pandoc@v2 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | with: 31 | use-public-rspm: true 32 | 33 | - uses: r-lib/actions/setup-r-dependencies@v2 34 | with: 35 | extra-packages: any::pkgdown, local::. 36 | needs: website 37 | 38 | - name: Build site 39 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 40 | shell: Rscript {0} 41 | 42 | - name: Deploy to GitHub pages 🚀 43 | if: github.event_name != 'pull_request' 44 | uses: JamesIves/github-pages-deploy-action@v4.4.1 45 | with: 46 | clean: false 47 | branch: gh-pages 48 | folder: docs 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | inst/__MACOSX/* 2 | inst/__WINDOWS/* 3 | docs/_site/* 4 | /exiftool/* 5 | /README.html 6 | /inst/exiftool/win_exe/* 7 | /docs/ 8 | *.swp 9 | docs 10 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: exiftoolr 2 | Type: Package 3 | Title: ExifTool Functionality from R 4 | Version: 0.2.8 5 | Date: 2025-04-19 6 | Authors@R: c(person("Joshua", "O'Brien", role = c("aut", "cre"), 7 | email = "joshmobrien@gmail.com")) 8 | Maintainer: Joshua O'Brien 9 | Description: Reads, writes, and edits EXIF and other file metadata using 10 | ExifTool , returning read results as a data 11 | frame. ExifTool supports many different metadata formats including EXIF, 12 | GPS, IPTC, XMP, JFIF, GeoTIFF, ICC Profile, Photoshop IRB, FlashPix, AFCP 13 | and ID3, Lyrics3, as well as the maker notes of many digital cameras by 14 | Canon, Casio, DJI, FLIR, FujiFilm, GE, GoPro, HP, JVC/Victor, Kodak, Leaf, 15 | Minolta/Konica-Minolta, Motorola, Nikon, Nintendo, Olympus/Epson, 16 | Panasonic/Leica, Pentax/Asahi, Phase One, Reconyx, Ricoh, Samsung, Sanyo, 17 | Sigma/Foveon and Sony. 18 | License: GPL-2 19 | URL: https://github.com/JoshOBrien/exiftoolr#readme, https://joshobrien.github.io/exiftoolr/ 20 | BugReports: https://github.com/JoshOBrien/exiftoolr/issues 21 | SystemRequirements: Perl 22 | Depends: 23 | R (>= 3.0.0) 24 | Imports: 25 | backports, 26 | curl, 27 | jsonlite, 28 | zip, 29 | data.table 30 | Encoding: UTF-8 31 | Language: en-US 32 | RoxygenNote: 7.3.2 33 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,exiftoolr) 4 | export(configure_exiftoolr) 5 | export(exif_call) 6 | export(exif_read) 7 | export(exif_version) 8 | export(install_exiftool) 9 | importFrom(curl,curl_download) 10 | importFrom(curl,has_internet) 11 | importFrom(data.table,fread) 12 | importFrom(jsonlite,fromJSON) 13 | importFrom(utils,untar) 14 | importFrom(zip,unzip) 15 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # Version 0.2.8 2 | 3 | * `configure_exiftoolr()` now checks for and more gracefully handles zero length 4 | string command candidates. (Those had caused a problem on, at least, MacOS.) 5 | Thanks to George Ostrouchov for describing the issue and suggesting the fix. 6 | 7 | # Version 0.2.7 8 | 9 | * On Windows, `install_exiftool()` now changes the name of the installed 10 | executable from `"exiftool(-k).exe"` to `"exiftool.exe"` as recommended for 11 | command-line use by Phil Harvey here: 12 | https://exiftool.org/install.html#Windows. `exiftoolr:::configure_exiftoolr()`, 13 | which searches for locally available executables, now searches first for 14 | `"exiftool.exe"` and then for `"exiftool(-k).exe"`, so that it will still find 15 | ExifTool executables installed by previous versions of this R package. Thanks 16 | to Andy Lyons for suggesting this change, which fixes a problem that plagued 17 | at least some Windows users. 18 | 19 | # Version 0.2.6 20 | 21 | * One more modification to `install_exiftool()` needed to handle changes in how 22 | Phil Harvey's http://exiftool.org provides Windows ExifTool executables. 23 | 24 | # Version 0.2.5 25 | 26 | * Modified `install_exiftool()` (and its helper function, `download_exiftool()`) 27 | to handle a couple of changes in how Phil Harvey's http://exiftool.org 28 | provides Windows ExifTool executables. 29 | 30 | # Version 0.2.4 31 | 32 | * Ensures that Unicode tags are correctly written to files even in non-Unicode 33 | locales. Thanks to Trevor Davis for the excellent, as usual, issue description 34 | and PR. 35 | 36 | * Improves the error message returned to the user when the call to Exiftool 37 | finds no files to act upon. In addition to more gracefully relaying the error 38 | message emitted by Exiftool, such calls now return a value of `NULL`. Thanks 39 | to jcblum for reporting this issue. 40 | 41 | # Version 0.2.3 42 | 43 | * Adds arguments `config_file=` and `common_args=` to `exif_call()`. These are 44 | passed on to exiftool options `-config` and `-common_args` respectively, 45 | neither of which was supported here before due to their not being allowed in 46 | the `-@ ARGFILE` option used 'behind the scenes' by all calls to 47 | `exif_call()`. 48 | 49 | # Version 0.2.2 50 | 51 | * Fixes bug that could cause a failure to locate an already-installed version of 52 | ExifTool on MacOS (and possibly also on *NIX OS's). Thanks to Courtney Meier 53 | for the bug report. 54 | 55 | # Version 0.2.0 56 | 57 | * Changes default location into which exiftool executable is downloaded by a 58 | call to `install_exiftool()`. Formerly, the executable was installed into the 59 | directory returned by `system.file("exiftool", package = "exiftoolr")`, which 60 | will not always be writable (as, e.g., when the package is installed by an 61 | admin in the shared library of a multi-user server, and hence is owned by 62 | "root"). Now, the executable is by default installed to the directory given by 63 | `backports::R_user_dir()`, which should be more generally writable. 64 | 65 | # Version 0.1.8 66 | 67 | * Fixes an issue that caused `configure_exiftoolr()` (and thus essentially all 68 | **exiftoolr** functionality) to fail on Windows machines that do not have Perl 69 | installed in a location findable by the `exiftoolr:::configure_perl()`. Thanks 70 | to Tom Yamashita for reporting this issue. 71 | 72 | * Adds tests (using the **tinytest** package). 73 | 74 | # Version 0.1.7 75 | 76 | * Fixes an issue that could cause `configure_exiftoolr()` to fail if the path to 77 | the ExifTool executable on a user's computer contained any spaces. Now 78 | `configure_exiftoolr()` should work even if there are spaces in the path(s) to 79 | the user's installation of Perl and/or their ExifTool executable or 80 | library. Thanks to Lafont Rapnouil Tristan for reporting the issue. 81 | 82 | # Version 0.1.6 83 | 84 | * Fixes a problem likely to affect Linux users (but also any others relying on a 85 | local installation of Perl to execute calls to ExifTool), caused by the 86 | internal change in `exif_call()` in 0.1.5 from `system()` to 87 | `system2()`. Thanks to Daniel Baumgartner for bringing this to my attention. 88 | 89 | # Version 0.1.5 90 | 91 | * Added a `pipeline` option to `exif_read()`, which allows users to direct the 92 | exif executable to output results in csv rather than json format. This is 93 | helpful for use with images whose metadata contains non-UTF-8-encoded 94 | characters. As is documented 95 | [here](https://exiftool.org/exiftool_pod.html#Input-output-text-formatting), 96 | ExifTool's JSON output does not properly handle non-UTF-8 character 97 | sets. Setting `pipeline="csv"` ensures that non-UTF-8 character sets **are** 98 | properly handled, as demonstrated in a new example in `?exif_read`. 99 | 100 | # Version 0.1.4 101 | 102 | * Fixed `exif_read()` to now allow repeated elements in `args=`. This can be 103 | necessary when (to take one example) a user needs to separately specify the 104 | encoding used in the image file names and in the tags respectively. Now, a 105 | call like the following works as it should: 106 | 107 | exif_read(path = "myimage.jpg", 108 | args = c("-charset", "exiftool=cp850", "-charset", "filename=cp1250")) 109 | 110 | # Version 0.1.1 111 | 112 | * Failure of `configure_exiftoolr()` to find a local installation of ExifTool 113 | now throws an error rather than just a warning, preventing infinite recursion 114 | by `exif_read()` and `exif_version()`. 115 | -------------------------------------------------------------------------------- /R/exif_read.R: -------------------------------------------------------------------------------- 1 | 2 | ##' Read EXIF and other metadata from files 3 | ##' 4 | ##' Reads EXIF and other metadata into a \code{data.frame} by calling Phil 5 | ##' Harvey's ExifTool command-line application. 6 | ##' 7 | ##' From the \href{https://exiftool.org}{ExifTool website}: 8 | ##' "ExifTool is a platform-independent Perl library plus a command-line 9 | ##' application for reading, writing and editing meta information in a wide 10 | ##' variety of files. ExifTool supports many different metadata formats 11 | ##' including EXIF, GPS, IPTC, XMP, JFIF, GeoTIFF, ICC Profile, Photoshop IRB, 12 | ##' FlashPix, AFCP and ID3, as well as the maker notes of many digital cameras 13 | ##' by Canon, Casio, DJI, FLIR, FujiFilm, GE, GoPro, HP, JVC/Victor, Kodak, 14 | ##' Leaf, Minolta/Konica-Minolta, Motorola, Nikon, Nintendo, Olympus/Epson, 15 | ##' Panasonic/Leica, Pentax/Asahi, Phase One, Reconyx, Ricoh, Samsung, Sanyo, 16 | ##' Sigma/Foveon and Sony." 17 | ##' 18 | ##' For more information, see the \href{https://exiftool.org}{ExifTool 19 | ##' website}. 20 | ##' 21 | ##' @param path A vector of filenames. 22 | ##' @param tags A vector of tags to output. It is a good idea to specify this 23 | ##' when reading large numbers of files, as it decreases the output overhead 24 | ##' significantly. Spaces will be stripped in the output data frame. This 25 | ##' parameter is not case-sensitive. 26 | ##' @param recursive \code{TRUE} to pass the \code{"-r"} option to ExifTool. 27 | ##' @param args Additional arguments. 28 | ##' @param quiet Use \code{FALSE} to display diagnostic information. Default 29 | ##' value is \code{TRUE} 30 | ##' @param pipeline One of \code{"json"} (the default) or \code{"csv"}. Controls 31 | ##' whether the exiftool executable, behind the scenes, extracts metadata 32 | ##' into a JSON data structure or a tabular csv. The JSON pipeline works 33 | ##' well in most cases, but (as documented at 34 | ##' \url{https://exiftool.org/exiftool_pod.html}) does not properly handle 35 | ##' non-UTF-8 character sets. If the metadata fields include characters that 36 | ##' are not encoded using UTF-8 and that need to be handled by setting the 37 | ##' \code{"-charset"} option, use the \code{"csv"} pipeline as demonstrated 38 | ##' in the second example below. 39 | ##' @return A data frame of class \code{"exiftoolr"} with one row per file 40 | ##' processed. The first column, named \code{"SourceFile"} gives the name(s) 41 | ##' of the processed files. Subsequent columns contain info from the tags 42 | ##' read from those files. 43 | ##' 44 | ##' Note that binary tags such as thumbnails are loaded as 45 | ##' \href{https://en.wikipedia.org/wiki/Base64}{base64-encoded strings} that 46 | ##' start with \code{"base64:"}. Although these are truncated in the printed 47 | ##' representation of the \code{data.frame} returned by the function, they 48 | ##' are left unaltered in the \code{data.frame} itself. 49 | ##' @references \url{https://exiftool.org} 50 | ##' @importFrom jsonlite fromJSON 51 | ##' @importFrom data.table fread 52 | ##' @export 53 | ##' 54 | ##' @examples 55 | ##' \dontrun{ 56 | ##' files <- dir(system.file(package = "exiftoolr", "images"), 57 | ##' pattern = "LaSals|Lizard", full.names = TRUE) 58 | ##' exif_read(files) 59 | ##' exif_read(files, tags = c("filename", "imagesize")) 60 | ##' 61 | ##' ## Use pipeline="csv" for images needing explicit specification 62 | ##' ## and proper handling of a non-default character sets 63 | ##' img_file <- system.file(package = "exiftoolr", "images", "QS_Hongg.jpg") 64 | ##' args <- c("-charset", "exiftool=cp1250") 65 | ##' res <- exif_read(img_file, args = args, pipeline = "csv") 66 | ##' res[["City"]] ## "Zurich", with an umlaut over the "u" 67 | ##' } 68 | exif_read <- function(path, tags = NULL, 69 | recursive = FALSE, 70 | args = NULL, 71 | quiet = TRUE, 72 | pipeline = c("json", "csv")) { 73 | pipeline <- match.arg(pipeline) 74 | ## Ensure that exiftoolr is properly configured 75 | if (!is_exiftoolr_configured()) { 76 | configure_exiftoolr(quiet = quiet) 77 | message("Using ExifTool version ", exif_version(), "\n") 78 | } 79 | 80 | ## ---- general input processing ---- 81 | ## expand path 82 | path <- path.expand(path) 83 | 84 | ## check that all files exist (files that do not exist cause 85 | ## problems later, as do directories without recursive = TRUE) 86 | if (recursive) { 87 | missing_dirs <- path[!dir.exists(path)] 88 | if (length(missing_dirs)) { 89 | stop("Did you mean recursive = TRUE? ", 90 | "The following directories are missing", 91 | "(or are not directories): ", 92 | paste(missing_files, collapse = ", ")) 93 | } 94 | } else { 95 | missing_files <- path[!file.exists(path) | dir.exists(path)] 96 | if (length(missing_files)) { 97 | stop("Did you mean recursive = TRUE? ", 98 | "The following files are missing (or are not files): ", 99 | paste(missing_files, collapse = ", ")) 100 | } 101 | } 102 | 103 | if (recursive) { 104 | args <- c(args, "-r") 105 | } 106 | 107 | ## an extra -q further silences warnings 108 | if (quiet) { 109 | args <- c(args, "-q") 110 | } 111 | 112 | if (!is.null(tags)) { 113 | ## tags cannot have spaces...whitespace is stripped by ExifTool 114 | tags <- gsub("\\s", "", tags) 115 | args <- c(paste0("-", tags), args) 116 | } 117 | 118 | ##-----------------------------------------## 119 | ## Process using JSON intermediate output ## 120 | ##-----------------------------------------## 121 | 122 | ## required args: 123 | ## -n for numeric output 124 | ## -j for JSON output 125 | ## -q for quiet 126 | ## -b to ensure output is base64 encoded 127 | json_args <- c("-n", "-j", "-q", "-b", args) 128 | ## Construct and execute a call to Exiftool 129 | return_value <- 130 | suppressWarnings(exif_call(args = json_args, path = path)) 131 | 132 | ## Handle rare case in which ExifTool finds no files to read 133 | ## (e.g. https://github.com/JoshOBrien/exiftoolr/issues/20) 134 | if (!length(return_value)) { 135 | args <- setdiff(json_args, "-q") 136 | return_value <- 137 | suppressWarnings(exif_call(args = args, path = path)) 138 | warning(paste0(return_value, collapse = "\n")) 139 | return(NULL) 140 | } 141 | 142 | ## Postprocess the results 143 | return_value <- fromJSON(paste0(return_value, collapse = "")) 144 | 145 | 146 | if (pipeline == "csv") { 147 | ## Get name and class of each column read in via JSON 148 | json_names <- names(return_value) 149 | json_classes <- unname(sapply(return_value, class)) 150 | ## required args: 151 | ## -n for numeric output 152 | ## -T for tabular output 153 | ## -csv for CSV output 154 | ## -api filter to properly handle tag values containing commas, double 155 | ## quotes, newline characters, or leading or trailing spaces. 156 | ## -q for quiet 157 | ## -b to ensure output is base64 encoded 158 | filter <- 159 | if (.Platform$OS.type == "windows") { 160 | x <- '$_ = qq($_) if s/""/""""/g or /(^\\s+|\\s+$)/ or /[,\\n\\r]/' 161 | shQuote(x) 162 | } else { 163 | x <- '$_ = qq{"$_"} if s/"/""/g or /(^\\s+|\\s+$)/ or /[,\\n\\r]/' 164 | sQuote(x) 165 | } 166 | filter <- paste0("filter=", filter) 167 | csv_args <- c("-n", "-T", "-csv", "-q", "-b", "-api", filter, args) 168 | ## Call Exiftool, writing results to a temp file 169 | res_file <- tempfile("exif_results") 170 | on.exit(unlink(res_file)) 171 | exif_call(args = csv_args, path = path, stdout = res_file) 172 | ## Prepare colClasses, accounting for possibility that the 173 | ## "csv" pipeline produces more columns than the "json" one 174 | csv_names <- names(fread(res_file, data.table = FALSE)) 175 | colClasses <- json_classes[match(csv_names, json_names)] 176 | colClasses[is.na(colClasses)] <- "character" 177 | ## Read in results using best available column class info 178 | return_value <- fread(res_file, colClasses = colClasses, 179 | data.table = FALSE) 180 | } 181 | 182 | class(return_value) <- c("exiftoolr", class(return_value)) 183 | return_value 184 | } 185 | 186 | ##' Call ExifTool from R 187 | ##' 188 | ##' Uses \code{system2()} to run a basic call to \code{exiftool}. 189 | ##' @param args Character vector of arguments, each written in same 190 | ##' form as you would if writing them on the command line 191 | ##' (e.g. \code{"-n"} or \code{"-csv"}) 192 | ##' @param path A character vector giving one or more file paths. 193 | ##' @param stdout Where output to stdout should be sent. If 194 | ##' \code{TRUE} (the default), the output is captured in a 195 | ##' character vector. For other options, see the help file for 196 | ##' \code{\link[base]{system2}}, the function to which this 197 | ##' argument's value gets passed along. 198 | ##' @param quiet Use \code{FALSE} to display diagnostic 199 | ##' information. Default value is \code{FALSE}. 200 | ##' @param ... Additional arguments to be passed to \code{system2()}. 201 | ##' @param config_file Path to a config file of the format expected by 202 | ##' Exiftool's command line \code{-config} option. (See Details 203 | ##' for an explanation of why this one option cannot be passed 204 | ##' directly to \code{args} via the \code{-config} argument.) 205 | ##' @param common_args A character vector of arguments to be applied 206 | ##' to all executed commands when the Exiftool \code{-execute} 207 | ##' option is being used. (See Details for an explanation of why 208 | ##' this option cannot be passed directly to \code{args} via 209 | ##' \code{-common_args} argument.) 210 | ##' @details For examples of the command-line calls to ExifTool (all 211 | ##' of which can be reproduced by calls to \code{exif_call}), see 212 | ##' \url{https://exiftool.org/examples.html}. 213 | ##' 214 | ##' Under the hood, \code{exif_call()} writes the options in 215 | ##' \code{args} to a text file and then calls Exiftool, passing 216 | ##' that text file's contents to Exiftool via its \code{-@ 217 | ##' ARGFILE} option. \code{-config} and \code{-common_args} are 218 | ##' the two options that may not be used in such a \code{-@ 219 | ##' ARGFILE}, so we handle that option separately using 220 | ##' \code{exif_call()}'s \code{config_file} argument. 221 | ##' @return The standard output as a character vector. 222 | ##' @export 223 | ##' 224 | ##' @examples 225 | ##' \dontrun{ 226 | ##' ## Find local ExifTool version using exif_version() or exif_call() 227 | ##' exif_version() 228 | ##' exif_call(args = "-ver") 229 | ##' 230 | ##' ## Make temporary copies of a couple jpeg files 231 | ##' tmpdir <- tempdir() 232 | ##' src_files <- dir(system.file(package = "exiftoolr", "images"), 233 | ##' full.names = TRUE) 234 | ##' files <- file.path(tmpdir, basename(src_files)) 235 | ##' file.copy(src_files, files) 236 | ##' 237 | ##' ## Both of the following extract the same tags: 238 | ##' exif_read(files, tags = c("filename", "imagesize")) 239 | ##' exif_call(args = c("-n", "-j", "-q", "-filename", "-imagesize"), 240 | ##' path = files) 241 | ##' 242 | ##' ## Set value of a new "Artist" field in photo's metadata 243 | ##' file1 <- files[1] 244 | ##' exif_read(file1, tags = "artist") 245 | ##' exif_call(path = file1, args = "-Artist=me") 246 | ##' exif_read(file1, tags = "artist") 247 | ##' 248 | ##' ## Remove all but a few essential fields 249 | ##' length(exif_read(file1)) 250 | ##' exif_call(path = file1, args = "-all=") 251 | ##' length(exif_read(file1)) 252 | ##' exif_read(file1) 253 | ##' 254 | ##' ## Clean up 255 | ##' unlink(files) 256 | ##' } 257 | exif_call <- function(args = NULL, 258 | path = NULL, 259 | stdout = TRUE, 260 | quiet = FALSE, 261 | ..., 262 | config_file = NULL, 263 | common_args = NULL) { 264 | ## Ensure that exiftoolr is properly configured 265 | if (!is_exiftoolr_configured()) { 266 | configure_exiftoolr(quiet = quiet) 267 | message("Using ExifTool version ", exif_version(), "\n") 268 | } 269 | 270 | ## Exiftool command 271 | exiftoolpath <- get_exiftool_command() 272 | if (is.null(exiftoolpath)) { 273 | stop("ExifTool not properly installed or configured") 274 | } 275 | 276 | ## Put all 'command-line' arguments in a file 277 | argfile <- construct_argfile(args = args, path = path) 278 | on.exit(unlink(argfile)) 279 | 280 | ## Construct and then execute the command-line call 281 | args <- c("-@", shQuote(argfile)) 282 | ## "-config" must come first and can't be used in an -@ ARGFILE 283 | if (!is.null(config_file)) { 284 | args <- c(paste("-config", shQuote(config_file)), args) 285 | } 286 | ## "-config" must come first and can't be used in an -@ ARGFILE 287 | if (!is.null(common_args)) { 288 | args <- c(args, "-common_args", common_args) 289 | } 290 | ## Handle case where exiftoolpath is something like 291 | ## c("/path/to/perl", "/path/to/exiftool") 292 | if (length(exiftoolpath) > 1) { 293 | args <- c(exiftoolpath[-1], args) 294 | exiftoolpath <- exiftoolpath[1] 295 | } 296 | system2(exiftoolpath, args = args, stdout = stdout) 297 | } 298 | 299 | ##' @rdname exif_call 300 | ##' @export 301 | exif_version <- function(quiet = TRUE) { 302 | exif_call(args = "-ver", quiet = quiet) 303 | } 304 | 305 | ## private helper command to generate call to exiftool 306 | construct_argfile <- function(args, path) { 307 | if (any(gl <- grepl("\n", args))) { 308 | args <- ifelse(gl, 309 | paste0("#[CSTR]", gsub("\n", "\\\\n", args)), 310 | args) 311 | } 312 | all_args <- c(args, path) 313 | tmpfile <- tempfile("args.cmd") 314 | write_utf8(all_args, tmpfile) 315 | tmpfile 316 | } 317 | 318 | ## Better than `base::writeLines()` when writing UTF-8 `args` 319 | ## in a non-Unicode locale such as the "C" locale 320 | ## `write_utf8()` from https://github.com/gaborcsardi/rencfaq 321 | ## which is under CC0-1.0 Public Domain declaration 322 | ## Note by default `exiftool` converts to UTF-8: https://exiftool.org/faq.html#Q10 323 | write_utf8 <- function(text, path) { 324 | ## Step 1: Ensure our text is utf8 encoded 325 | utf8 <- enc2utf8(text) 326 | upath <- enc2utf8(path) 327 | 328 | ## Step 2: Create a connection with 'native' encoding 329 | ## this signals to R that translation before writing 330 | ## to the connection should be skipped 331 | con <- file(upath, open = "w+", encoding = "native.enc") 332 | on.exit(close(con), add = TRUE) 333 | 334 | ## Step 3: Write to the connection with 'useBytes = TRUE', 335 | ## telling R to skip translation to the native encoding 336 | writeLines(utf8, con = con, useBytes = TRUE) 337 | } 338 | -------------------------------------------------------------------------------- /R/globals.R: -------------------------------------------------------------------------------- 1 | .globals <- new.env(parent = emptyenv()) 2 | .globals$exiftoolr_configured <- FALSE 3 | .globals$exiftool_command <- NULL 4 | .globals$perl_path <- NULL 5 | 6 | 7 | is_exiftoolr_configured <- function() { 8 | .globals$exiftoolr_configured 9 | } 10 | 11 | 12 | set_exiftool_command <- function(com) { 13 | .globals$exiftool_command <- com 14 | .globals$exiftoolr_configured <- TRUE 15 | } 16 | 17 | get_exiftool_command <- function() { 18 | .globals$exiftool_command 19 | } 20 | 21 | 22 | set_perl_path <- function(path) { 23 | .globals$perl_path <- path 24 | } 25 | 26 | get_perl_path <- function() { 27 | .globals$perl_path 28 | } 29 | -------------------------------------------------------------------------------- /R/hooks.R: -------------------------------------------------------------------------------- 1 | .onLoad <- function(libname, pkgname) { 2 | backports::import(pkgname, "R_user_dir", force = TRUE) 3 | } 4 | -------------------------------------------------------------------------------- /R/install.R: -------------------------------------------------------------------------------- 1 | 2 | ##' Install the current version of ExifTool 3 | ##' 4 | ##' 5 | ##' @title Install ExifTool, downloading (by default) the current version 6 | ##' @param install_location Path to the directory into which ExifTool should be 7 | ##' installed. If \code{NULL} (the default), installation will be into the 8 | ##' directory returned by \code{backports::R_user_dir("exiftoolr")}. 9 | ##' @param win_exe Logical, only used on Windows machines. Should we install the 10 | ##' standalone ExifTool Windows executable or the ExifTool Perl library? 11 | ##' (The latter relies, for its execution, on an existing installation of 12 | ##' Perl being present on the user's machine.) If set to \code{NULL} (the 13 | ##' default), the function installs the Windows executable on Windows 14 | ##' machines and the Perl library on other operating systems. 15 | ##' @param local_exiftool If installing ExifTool from a local "*.zip" or 16 | ##' ".tar.gz", supply the path to that file as a character string. With 17 | ##' default value, `NULL`, the function downloads ExifTool from 18 | ##' \url{https://exiftool.org} and then installs it. 19 | ##' @param quiet Logical. Should function should be chatty? 20 | ##' @return Called for its side effect 21 | ##' @export 22 | ##' @importFrom curl curl_download has_internet 23 | ##' @importFrom utils untar 24 | ##' @importFrom zip unzip 25 | install_exiftool <- function(install_location = NULL, 26 | win_exe = NULL, 27 | local_exiftool = NULL, 28 | quiet = FALSE) { 29 | tmpdir <- tempdir() 30 | if (is.null(win_exe)) { 31 | win_exe <- is_windows() 32 | } 33 | 34 | ##------------------------------------------------## 35 | ## If needed, download ExifTool *.zip or *.tar.gz ## 36 | ##------------------------------------------------## 37 | if (is.null(local_exiftool)) { 38 | tmpfile <- file.path(tmpdir, "xx") 39 | on.exit(unlink(tmpfile)) 40 | download_exiftool(win_exe = win_exe, 41 | download_path = tmpfile, 42 | quiet = quiet) 43 | } else { 44 | tmpfile <- local_exiftool 45 | win_exe <- (tools::file_ext(tmpfile) == "zip") 46 | } 47 | 48 | ##---------------------------## 49 | ## Install *.zip or *.tar.gz ## 50 | ##---------------------------## 51 | if (is.null(install_location)) { 52 | ## Default install location 53 | install_location <- R_user_dir("exiftoolr", which = "data") 54 | if (!dir.exists(install_location)) { 55 | dir.create(install_location, recursive = TRUE) 56 | } 57 | } 58 | 59 | ## Find writable locations 60 | write_dir <- find_writable(install_location) 61 | if (win_exe) { 62 | write_dir <- file.path(write_dir, "win_exe") 63 | } 64 | if (!quiet) { 65 | message("Installing ExifTool in ", write_dir) 66 | } 67 | 68 | ## Install 69 | if (win_exe) { 70 | ## Windows executable 71 | if(!dir.exists(write_dir)) { 72 | dir.create(write_dir) 73 | } 74 | ## This calls zip::unzip, not utils::unzip 75 | unzip(tmpfile, exdir = tmpdir) 76 | exif_dir <- dir(tmpdir, pattern = "exiftool-", full.names = TRUE) 77 | file.copy(dir(exif_dir, full.names = TRUE), write_dir, recursive = TRUE) 78 | file.rename(file.path(write_dir, "exiftool(-k).exe"), 79 | file.path(write_dir, "exiftool.exe")) 80 | } else { 81 | ## Perl library 82 | untar(tmpfile, exdir = tmpdir) 83 | dd <- dir(tmpdir, pattern = "Image-ExifTool-", full.names = TRUE) 84 | if(!dir.exists(file.path(write_dir, "lib"))) { 85 | dir.create(file.path(write_dir, "lib")) 86 | } 87 | ## Install the `lib` directory, main Perl script, and `README` 88 | file.copy(from = file.path(dd, c("lib", "exiftool", "README")), 89 | to = write_dir, 90 | recursive = TRUE, 91 | overwrite = TRUE) 92 | } 93 | } 94 | 95 | 96 | 97 | ## ##' Download the current version of ExifTool 98 | ## ##' 99 | ## ##' @inheritParams install_exiftool 100 | ## ##' @param download_path Path indicating the location to which 101 | ## ##' ExifTool should be downloaded. 102 | ## ##' @return A character string giving the path to the downloaded 103 | ## ##' ExifTool. 104 | ## ##' @author Joshua O'Brien 105 | download_exiftool <- function(win_exe = FALSE, 106 | download_path = NULL, 107 | quiet = FALSE) { 108 | base_url <- "https://exiftool.org" 109 | ## Got internet? 110 | if(!has_internet()) { 111 | stop("No internet connection detected, so cannot download ", 112 | "ExifTool from:\n ", base_url) 113 | } 114 | ## Construct URL of file to be downloaded, uncompressed, & 115 | ## installed 116 | ver <- current_exiftool_version() 117 | exiftool_url <- 118 | if(win_exe & is_windows()) { 119 | platform <- ifelse(.Machine$sizeof.pointer == 8, "_64", "_32") 120 | file.path(base_url, paste0("exiftool-", ver, platform, ".zip")) 121 | } else { 122 | file.path(base_url, paste0("Image-ExifTool-", ver, ".tar.gz")) 123 | } 124 | if (is.null(download_path)) { 125 | download_path <- file.path(tempdir(), basename(exiftool_url)) 126 | } 127 | ## Attempt to download the file 128 | if (!quiet) { 129 | message("Attempting to download ExifTool from ", exiftool_url) 130 | } 131 | curl_download(url = exiftool_url, destfile = download_path, quiet = quiet) 132 | return(download_path) 133 | } 134 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | 2 | ##' Configure package to point to ExifTool and/or Perl 3 | ##' 4 | ##' @param command Character string giving the exiftool command. 5 | ##' @param perl_path Path to a Perl executable. 6 | ##' @param allow_win_exe Logical. If running on a Windows machine, and if a 7 | ##' standalone exiftool executable is available, should it be used? 8 | ##' @param quiet Logical. Should function should be chatty? 9 | ##' @return A character string giving the exiftool command, returned invisibly. 10 | ##' @export 11 | ##' 12 | configure_exiftoolr <- function(command = NULL, 13 | perl_path = NULL, 14 | allow_win_exe = TRUE, 15 | quiet = FALSE) { 16 | ## Configure Perl (returning NULL if none found) 17 | if (is.null(perl_path)) { 18 | perl_path <- configure_perl(quiet = quiet) 19 | } 20 | 21 | if (is.null(command)) { 22 | ## Construct default list of possible commands/locations 23 | ## 24 | ## ## (a) In case command has already been set by previous call 25 | ## ## to configure_exiftoolr() 26 | ## command <- get_exiftool_command() 27 | ## 28 | ## (a) Try path stored in environment variable 29 | command <- env_exiftool_path() 30 | ## (b) On Windows, check for a locally installed standalone executable. 31 | ## Beginning in version 0.2.7, install_exiftool() renames the 32 | ## ExifTool executable it installs to "exiftool.exe", as suggested 33 | ## by Phil Harvey, so we now look for both that and 34 | ## "exiftool(-k).exe", which will have been installed by any 35 | ## previous versions of the package. 36 | if (allow_win_exe & is_windows()) { 37 | internal_win_exe <- 38 | file.path(R_user_dir("exiftoolr", which = "data"), 39 | "win_exe", c("exiftool.exe", "exiftool(-k).exe")) 40 | if (nchar(internal_win_exe[1])) 41 | command <- c(command, internal_win_exe) 42 | } 43 | ## (c) Maybe "exiftool" is on search path? 44 | command <- c(command, "exiftool") 45 | ## (d) Check for locally installed ExifTool Perl scripts 46 | if (!is.null(perl_path)) { 47 | internal_exiftool <- 48 | file.path(R_user_dir("exiftoolr", which = "data"), 49 | "exiftool") 50 | if (nchar(internal_exiftool)) { 51 | command <- 52 | c(command, 53 | shQuote(internal_exiftool), 54 | paste(shQuote(perl_path), shQuote(internal_exiftool))) 55 | } 56 | } 57 | } else { 58 | ## For commands supplied as argument to configure_exiftoolr(), if Perl 59 | ## is present, try the command with and without prepended invocation of 60 | ## Perl 61 | if (!is.null(perl_path)) { 62 | command <- c(command, 63 | paste(shQuote(perl_path), shQuote(command))) 64 | } 65 | } 66 | 67 | for(com in command) { 68 | ## Empty string causes an error on MacOS as reported in Issue #25 69 | if (!nzchar(com)) { 70 | next 71 | } 72 | ## automatically fail perl_path '' 73 | if (!is.null(perl_path) && 74 | com == paste(shQuote(perl_path), shQuote(NULL))) { 75 | next 76 | } 77 | ## If the command string includes single-quoted substrings split them 78 | ## out into a character vector. (This is most likely due to 79 | ## shQuote()'ing of perl_path and command above, but could also be from 80 | ## the value supplied to command= or to ET_EXIFTOOL_PATH (and retrieved 81 | ## using env_exiftool_path())), 82 | ## 83 | ## So, for example, convert this: 84 | ## 85 | ## "'/path/to/perl' '/path/to/exiftool'" 86 | ## 87 | ## to this: 88 | ## 89 | ## c("/path/to/perl", "/path/to/exiftool")) 90 | ## 91 | ## This addresses the issue described at: 92 | ## https://github.com/JoshOBrien/exiftoolr/issues/4 93 | if(grepl("'", com)) { 94 | ## Use of scan() here based on: 95 | ## https://stackoverflow.com/a/13628436/980833 96 | com <- scan(text = com, what = "character", quiet = TRUE) 97 | } 98 | if (test_exiftool(com, quiet = quiet)) { 99 | if(!quiet) message("ExifTool found at: ", 100 | paste(shQuote(com), collapse = " ")) 101 | set_exiftool_command(com) 102 | return(invisible(com)) 103 | } 104 | } 105 | 106 | stop("No functioning version of Exiftool has been found. To\n", 107 | "download and install a local version into the exiftoolr\n", 108 | "package, try doing install_exiftool().") 109 | } 110 | 111 | 112 | configure_perl <- function(perl_path = NULL, quiet = FALSE) { 113 | if (is.null(perl_path)) { 114 | ## use default list of possible locations 115 | perl_path <- c(get_perl_path(), 116 | env_perl_path(), 117 | "perl", 118 | "C:\\Perl64\\bin\\perl", 119 | "C:\\Perl\\bin\\perl", 120 | "C:\\Strawberry\\perl\\bin\\perl") 121 | } 122 | 123 | for(p in perl_path) { 124 | if (test_perl(p, quiet = quiet)) { 125 | if(!quiet) message("Perl found at: ", 126 | paste(shQuote(p), collapse = " ")) 127 | set_perl_path(p) 128 | return(invisible(p)) 129 | } 130 | } 131 | 132 | ## warning("Could not find Perl at any of the following locations: ", 133 | ## paste(perl_path, collapse = ", "), 134 | ## '. Specify perl location using set_perl_path("my/path/to/perl")') 135 | return(NULL) 136 | } 137 | 138 | 139 | 140 | test_perl <- function(command, quiet = TRUE) { 141 | if(!quiet) message("Trying perl command: ", 142 | paste(shQuote(command), collapse = " "), 143 | " --version") 144 | suppressWarnings( 145 | suppressMessages(0 == try(system2(command, 146 | args = "--version", 147 | stdout = FALSE, 148 | stderr = FALSE), 149 | silent = TRUE))) 150 | } 151 | 152 | 153 | 154 | test_exiftool <- function(command, quiet = TRUE) { 155 | if(!quiet) message("Trying exiftool command: ", 156 | paste(shQuote(command), collapse = " "), 157 | " -ver") 158 | args <- "-ver" 159 | if (length(command) > 1) { 160 | args <- c(command[-1], args) 161 | command <- command[1] 162 | } 163 | command_works <- 164 | suppressWarnings( 165 | suppressMessages(0 == try(system2(command, 166 | args = args, 167 | stdout = FALSE), 168 | silent = TRUE))) 169 | if (command_works) { 170 | ## check that version is a numeric value like 10.96 171 | ver_string <- paste(system2(command, args = args, stdout = TRUE, stderr = FALSE), 172 | collapse = "\n") 173 | ver_number <- suppressWarnings(as.numeric(ver_string)) 174 | return(!is.na(ver_number)) 175 | } else { 176 | return(FALSE) 177 | } 178 | } 179 | 180 | 181 | 182 | env_perl_path <- function() { 183 | path <- Sys.getenv("ET_PERL_PATH") 184 | if(is.na(path)) character(0) else path 185 | } 186 | 187 | 188 | 189 | env_exiftool_path <- function() { 190 | path <- Sys.getenv("ET_EXIFTOOL_PATH") 191 | if(is.na(path)) character(0) else path 192 | } 193 | 194 | 195 | 196 | is_windows <- function() { 197 | .Platform$OS.type == "windows" 198 | } 199 | 200 | 201 | 202 | find_writable <- function(install_location) { 203 | for(il in install_location) { 204 | testfile <- file.path(il, ".dummyfile") 205 | file.create(testfile, showWarnings = FALSE) 206 | if (file.exists(testfile)) { 207 | unlink(testfile) 208 | return(il) 209 | } 210 | } 211 | stop("Could not find a writable directory in which to install ExifTool.", 212 | " Tried ", paste(install_location, collapse = ", ")) 213 | } 214 | 215 | 216 | 217 | current_exiftool_version <- function() { 218 | ## Holds current version of ExifTool, as announced by P. Harvey at: 219 | ## https://exiftool.org/forum/index.php?topic=3754.0 220 | url <- "https://exiftool.org/ver.txt" 221 | readLines(url, warn=FALSE) 222 | } 223 | 224 | 225 | 226 | ##' @export 227 | print.exiftoolr <- function(x, ...) { 228 | trunc64 <- function(X) { 229 | paste0(substr(X, 1, 60), "[..", nchar(X) - 60, " more..]") 230 | } 231 | ## Find columns of "base64:" strings more than 60 characters long 232 | ii <- which(grepl("base64:.{53,}", x[1,], perl = TRUE)) 233 | ## Replace those with truncated strings 234 | if (length(ii)) { 235 | for(i in ii) { 236 | x[, i] <- trunc64(x[, i]) 237 | } 238 | } 239 | NextMethod(x, ...) 240 | } 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```{r, echo = FALSE} 4 | knitr::opts_chunk$set( 5 | collapse = TRUE, 6 | comment = "#>", 7 | fig.path = "README-" 8 | ) 9 | options(width = 85) 10 | ``` 11 | 12 | # ExifTool functionality from R 13 | 14 | [![Project Status: Active - The project has reached a stable, usable state and is being actively developed.](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) 15 | [![License](https://JoshOBrien.github.io/badges/GPL2+.svg)](http://www.gnu.org/licenses/gpl-2.0.html) 16 | [![](http://www.r-pkg.org/badges/version/exiftoolr)](http://www.r-pkg.org/pkg/exiftoolr) 17 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/exiftoolr)](http://www.r-pkg.org/pkg/exiftoolr) 18 | 19 | [ExifTool][ExifTool-home] is a comprehensive open source utility for 20 | reading, writing and editing meta information in a wide variety of 21 | file types. As noted on its [project homepage][ExifTool-home]: 22 | 23 | > ExifTool supports many different metadata formats including EXIF, GPS, 24 | > IPTC, XMP, JFIF, GeoTIFF, ICC Profile, Photoshop IRB, FlashPix, AFCP 25 | > and ID3, as well as the maker notes of many digital cameras by Canon, 26 | > Casio, DJI, FLIR, FujiFilm, GE, GoPro, HP, JVC/Victor, Kodak, Leaf, 27 | > Minolta/Konica-Minolta, Motorola, Nikon, Nintendo, Olympus/Epson, 28 | > Panasonic/Leica, Pentax/Asahi, Phase One, Reconyx, Ricoh, Samsung, 29 | > Sanyo, Sigma/Foveon and Sony. 30 | 31 | [**exiftoolr**](https://CRAN.R-project.org/package=exiftoolr) wraps a 32 | local installation of ExifTool, giving users easy access to its 33 | functionality from within R. `exif_read()` can be used to read 34 | metadata from one or many files into a `data.frame` with one column 35 | per metadata field and one row per file. `exif_call()`, supports more 36 | general calls to the underlying ExifTool utility, examples of which 37 | are displayed [here][ExifTool-examples]. 38 | 39 | 40 | ## Example 41 | 42 | Here is a photo taken in the La Sal mountains of southeastern Utah, 43 | USA. 44 | 45 | ![](man/figures/LaSals.jpg) 46 | 47 | Suppose you would like to annotate it with a bit of text indicating 48 | the time and place at which the photo was taken. You could do that as 49 | follows, using **exiftoolr** to extract the relevant data from the 50 | file, and the 51 | [**magick**](https://CRAN.R-project.org/package=magick) package to 52 | annotate the image: 53 | 54 | ```{r, photo_annotation, eval = FALSE} 55 | library(exiftoolr) 56 | library(magick) 57 | 58 | ## Read and extract image metadata 59 | dat <- exif_read("LaSals.jpg") 60 | DateTime <- dat[["CreateDate"]] 61 | Longitude <- dat[["GPSLongitude"]] 62 | Latitude <- dat[["GPSLatitude"]] 63 | 64 | ## Prepare annotation text 65 | txt <- paste0(DateTime, "\n", 66 | "Longitude: ", round(Longitude, 5), "\n", 67 | "Latitude: ", round(Latitude, 5)) 68 | 69 | ## Annotate image and write to file 70 | out <- image_annotate(image_read(infile), txt, 71 | gravity = "northwest", color = "red", 72 | boxcolor = adjustcolor("black", alpha=0.2), 73 | size = 15, location = "+10+10") 74 | image_write(out, "LaSals_annotated.jpg") 75 | ``` 76 | 77 | ![](man/figures/LaSals_annotated.jpg) 78 | 79 | 80 | # Installation 81 | 82 | To install **exiftoolr** from 83 | [CRAN](https://CRAN.R-project.org/package=exiftoolr) do, as usual: 84 | 85 | ```{r, CRAN-installation, eval = FALSE} 86 | install.packages("exiftoolr") 87 | ``` 88 | 89 | To install the most recent version from GitHub, do this: 90 | 91 | ```{r, gh-installation, eval = FALSE} 92 | if(!require(devtools)) {install.packages("devtools")} 93 | devtools::install_github("JoshOBrien/exiftoolr") 94 | ``` 95 | 96 | 97 | # Setup 98 | 99 | **exiftoolr** can be configured to use an existing ExifTool 100 | installation. Alternatively, run `install_exiftool()` once following 101 | package installation to install a copy of ExifTool in the package's 102 | directory tree, where calls to functions in the **exiftoolr** package 103 | will automatically find it: 104 | 105 | ```{r, exiftool-installation, eval = FALSE} 106 | exiftoolr::install_exiftool() 107 | ``` 108 | 109 | **exiftoolr** makes a reasonable attempt to find local copies of Perl 110 | and ExifTool and, in most cases, will need no hints to find them. For 111 | direct control over which Perl or ExifTool is used, set their paths 112 | either with an explicit call to `configure_exiftoolr()` or with the 113 | environment variables `"ET_PERL_PATH"` and `"ET_EXIFTOOL_PATH"`. 114 | 115 | 116 | # Usage 117 | 118 | `exif_read()` reads metadata from one or more image files, returning 119 | the results in a plain `data.frame`: 120 | 121 | ```{r, example} 122 | library(exiftoolr) 123 | image_files <- dir(system.file("images", package = "exiftoolr"), 124 | full.names = TRUE) 125 | exifinfo <- exif_read(image_files) 126 | dim(exifinfo) 127 | names(exifinfo)[1:20] ## Display first 20 metadata fields read by ExifTool 128 | ``` 129 | 130 | To extract only those tags that are actually needed, use the `tags` 131 | argument: 132 | 133 | ```{r} 134 | exif_read(image_files, tags = c("filename", "imagesize")) 135 | ``` 136 | 137 | The `tags` argument also accepts simple regular expressions. For 138 | instance, to extract all fields with names containing the substring 139 | `"GPS"`, you could use the following call: 140 | 141 | ```{r} 142 | exif_read(image_files[1], tags = "*GPS*") 143 | ``` 144 | 145 | To access more general ExifTool functionality (many examples of which 146 | are shown [here][ExifTool-examples]), use the `exif_call()` 147 | function. For example, the call just above, if run using 148 | `exif_call()`, would look something like this: 149 | 150 | ```{r, eval=FALSE} 151 | exif_call(args = c("-n", "-j", "-q", "-*GPS*"), path = image_files[1]) 152 | ``` 153 | 154 | 155 | # Why another R package for reading image file metadata? 156 | 157 | Several R packages can read EXIF metadata from image files. The 158 | [**exif**](https://CRAN.R-project.org/package=exif) and 159 | [**magick**](https://CRAN.R-project.org/package=magick) packages both 160 | include functions (`exif::read_exif()`, and 161 | `magick::image_attributes()`, respectively) that extract files' EXIF 162 | data. Often, though, EXIF tags comprise only a subset of the metadata 163 | in a file. Despite its name, ExifTool reads data stored in many 164 | additional metadata formats. 165 | 166 | The [**exifr**](https://CRAN.R-project.org/package=exifr) package -- 167 | also a thin wrapper around ExifTool -- is much more similar in the 168 | functionality that it provides. The packages differ mainly in their 169 | support for easy installation and configuration on all operating 170 | systems. **exiftoolr**, in particular, was designed to make it as easy 171 | for Windows users -- even those without Perl installations -- to 172 | access ExifTool functionality as it is for *NIX and Mac 173 | users. Relative to **exifr**, **exiftoolr** also makes it easier to 174 | update ExifTool to its most current version. 175 | 176 | 177 | 178 | [ExifTool-home]: https://exiftool.org/ 179 | [ExifTool-examples]: https://exiftool.org/examples.html 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # ExifTool functionality from R 6 | 7 | [![Project Status: Active - The project has reached a stable, usable state and is being actively developed.](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active) 8 | [![License](https://JoshOBrien.github.io/badges/GPL2+.svg)](http://www.gnu.org/licenses/gpl-2.0.html) 9 | [![](http://www.r-pkg.org/badges/version/exiftoolr)](http://www.r-pkg.org/pkg/exiftoolr) 10 | [![CRAN RStudio mirror downloads](http://cranlogs.r-pkg.org/badges/exiftoolr)](http://www.r-pkg.org/pkg/exiftoolr) 11 | 12 | [ExifTool][ExifTool-home] is a comprehensive open source utility for 13 | reading, writing and editing meta information in a wide variety of 14 | file types. As noted on its [project homepage][ExifTool-home]: 15 | 16 | > ExifTool supports many different metadata formats including EXIF, GPS, 17 | > IPTC, XMP, JFIF, GeoTIFF, ICC Profile, Photoshop IRB, FlashPix, AFCP 18 | > and ID3, as well as the maker notes of many digital cameras by Canon, 19 | > Casio, DJI, FLIR, FujiFilm, GE, GoPro, HP, JVC/Victor, Kodak, Leaf, 20 | > Minolta/Konica-Minolta, Motorola, Nikon, Nintendo, Olympus/Epson, 21 | > Panasonic/Leica, Pentax/Asahi, Phase One, Reconyx, Ricoh, Samsung, 22 | > Sanyo, Sigma/Foveon and Sony. 23 | 24 | [**exiftoolr**](https://CRAN.R-project.org/package=exiftoolr) wraps a 25 | local installation of ExifTool, giving users easy access to its 26 | functionality from within R. `exif_read()` can be used to read 27 | metadata from one or many files into a `data.frame` with one column 28 | per metadata field and one row per file. `exif_call()`, supports more 29 | general calls to the underlying ExifTool utility, examples of which 30 | are displayed [here][ExifTool-examples]. 31 | 32 | 33 | ## Example 34 | 35 | Here is a photo taken in the La Sal mountains of southeastern Utah, 36 | USA. 37 | 38 | ![](man/figures/LaSals.jpg) 39 | 40 | Suppose you would like to annotate it with a bit of text indicating 41 | the time and place at which the photo was taken. You could do that as 42 | follows, using **exiftoolr** to extract the relevant data from the 43 | file, and the 44 | [**magick**](https://CRAN.R-project.org/package=magick) package to 45 | annotate the image: 46 | 47 | 48 | ```r 49 | library(exiftoolr) 50 | library(magick) 51 | 52 | ## Read and extract image metadata 53 | dat <- exif_read("LaSals.jpg") 54 | DateTime <- dat[["CreateDate"]] 55 | Longitude <- dat[["GPSLongitude"]] 56 | Latitude <- dat[["GPSLatitude"]] 57 | 58 | ## Prepare annotation text 59 | txt <- paste0(DateTime, "\n", 60 | "Longitude: ", round(Longitude, 5), "\n", 61 | "Latitude: ", round(Latitude, 5)) 62 | 63 | ## Annotate image and write to file 64 | out <- image_annotate(image_read(infile), txt, 65 | gravity = "northwest", color = "red", 66 | boxcolor = adjustcolor("black", alpha=0.2), 67 | size = 15, location = "+10+10") 68 | image_write(out, "LaSals_annotated.jpg") 69 | ``` 70 | 71 | ![](man/figures/LaSals_annotated.jpg) 72 | 73 | 74 | # Installation 75 | 76 | To install **exiftoolr** from 77 | [CRAN](https://CRAN.R-project.org/package=exiftoolr) do, as usual: 78 | 79 | 80 | ```r 81 | install.packages("exiftoolr") 82 | ``` 83 | 84 | To install the most recent version from GitHub, do this: 85 | 86 | 87 | ```r 88 | if(!require(devtools)) {install.packages("devtools")} 89 | devtools::install_github("JoshOBrien/exiftoolr") 90 | ``` 91 | 92 | 93 | # Setup 94 | 95 | **exiftoolr** can be configured to use an existing ExifTool 96 | installation. Alternatively, run `install_exiftool()` once following 97 | package installation to install a copy of ExifTool in the package's 98 | directory tree, where calls to functions in the **exiftoolr** package 99 | will automatically find it: 100 | 101 | 102 | ```r 103 | exiftoolr::install_exiftool() 104 | ``` 105 | 106 | **exiftoolr** makes a reasonable attempt to find local copies of Perl 107 | and ExifTool and, in most cases, will need no hints to find them. For 108 | direct control over which Perl or ExifTool is used, set their paths 109 | either with an explicit call to `configure_exiftoolr()` or with the 110 | environment variables `"ET_PERL_PATH"` and `"ET_EXIFTOOL_PATH"`. 111 | 112 | 113 | # Usage 114 | 115 | `exif_read()` reads metadata from one or more image files, returning 116 | the results in a plain `data.frame`: 117 | 118 | 119 | ```r 120 | library(exiftoolr) 121 | image_files <- dir(system.file("images", package = "exiftoolr"), 122 | full.names = TRUE) 123 | exifinfo <- exif_read(image_files) 124 | dim(exifinfo) 125 | #> [1] 3 181 126 | names(exifinfo)[1:20] ## Display first 20 metadata fields read by ExifTool 127 | #> [1] "SourceFile" "ExifToolVersion" "FileName" "Directory" 128 | #> [5] "FileSize" "FileModifyDate" "FileAccessDate" "FileCreateDate" 129 | #> [9] "FilePermissions" "FileType" "FileTypeExtension" "MIMEType" 130 | #> [13] "ExifByteOrder" "Make" "Model" "Orientation" 131 | #> [17] "XResolution" "YResolution" "ResolutionUnit" "Software" 132 | ``` 133 | 134 | To extract only those tags that are actually needed, use the `tags` 135 | argument: 136 | 137 | 138 | ```r 139 | exif_read(image_files, tags = c("filename", "imagesize")) 140 | #> SourceFile FileName ImageSize 141 | #> 1 C:/R/Library/exiftoolr/images/LaSals.jpg LaSals.jpg 640 480 142 | #> 2 C:/R/Library/exiftoolr/images/Lizard.jpg Lizard.jpg 4032 3024 143 | #> 3 C:/R/Library/exiftoolr/images/QS_Hongg.jpg QS_Hongg.jpg 4674 3456 144 | ``` 145 | 146 | The `tags` argument also accepts simple regular expressions. For 147 | instance, to extract all fields with names containing the substring 148 | `"GPS"`, you could use the following call: 149 | 150 | 151 | ```r 152 | exif_read(image_files[1], tags = "*GPS*") 153 | #> SourceFile GPSLatitudeRef GPSLongitudeRef 154 | #> 1 C:/R/Library/exiftoolr/images/LaSals.jpg N W 155 | #> GPSAltitudeRef GPSTimeStamp GPSSpeedRef GPSSpeed GPSImgDirectionRef 156 | #> 1 0 23:05:36 K 0 T 157 | #> GPSImgDirection GPSDestBearingRef GPSDestBearing GPSDateStamp GPSHPositioningError 158 | #> 1 107.2073 T 107.2073 2016:09:21 5 159 | #> GPSAltitude GPSDateTime GPSLatitude GPSLongitude 160 | #> 1 2257.414 2016:09:21 23:05:36Z 39.64798 -111.3705 161 | #> GPSPosition 162 | #> 1 39.6479805555556 -111.370505555556 163 | ``` 164 | 165 | To access more general ExifTool functionality (many examples of which 166 | are shown [here][ExifTool-examples]), use the `exif_call()` 167 | function. For example, the call just above, if run using 168 | `exif_call()`, would look something like this: 169 | 170 | 171 | ```r 172 | exif_call(args = c("-n", "-j", "-q", "-*GPS*"), path = image_files[1]) 173 | ``` 174 | 175 | 176 | # Why another R package for reading image file metadata? 177 | 178 | Several R packages can read EXIF metadata from image files. The 179 | [**exif**](https://CRAN.R-project.org/package=exif) and 180 | [**magick**](https://CRAN.R-project.org/package=magick) packages both 181 | include functions (`exif::read_exif()`, and 182 | `magick::image_attributes()`, respectively) that extract files' EXIF 183 | data. Often, though, EXIF tags comprise only a subset of the metadata 184 | in a file. Despite its name, ExifTool reads data stored in many 185 | additional metadata formats. 186 | 187 | The [**exifr**](https://CRAN.R-project.org/package=exifr) package -- 188 | also a thin wrapper around ExifTool -- is much more similar in the 189 | functionality that it provides. The packages differ mainly in their 190 | support for easy installation and configuration on all operating 191 | systems. **exiftoolr**, in particular, was designed to make it as easy 192 | for Windows users -- even those without Perl installations -- to 193 | access ExifTool functionality as it is for *NIX and Mac 194 | users. Relative to **exifr**, **exiftoolr** also makes it easier to 195 | update ExifTool to its most current version. 196 | 197 | 198 | 199 | [ExifTool-home]: https://exiftool.org/ 200 | [ExifTool-examples]: https://exiftool.org/examples.html 201 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://joshobrien.github.io/exiftoolr/ 2 | template: 3 | bootstrap: 5 4 | 5 | -------------------------------------------------------------------------------- /inst/exiftool/README: -------------------------------------------------------------------------------- 1 | This is the directory into which `install_exiftool()` will, by default, 2 | install the external ExifTool library or binary executable used by the 3 | package. -------------------------------------------------------------------------------- /inst/images/LaSals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshOBrien/exiftoolr/99716fb9e9ac980d45590fb4b64d47554c33ccde/inst/images/LaSals.jpg -------------------------------------------------------------------------------- /inst/images/Lizard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshOBrien/exiftoolr/99716fb9e9ac980d45590fb4b64d47554c33ccde/inst/images/Lizard.jpg -------------------------------------------------------------------------------- /inst/images/QS_Hongg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshOBrien/exiftoolr/99716fb9e9ac980d45590fb4b64d47554c33ccde/inst/images/QS_Hongg.jpg -------------------------------------------------------------------------------- /man/configure_exiftoolr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{configure_exiftoolr} 4 | \alias{configure_exiftoolr} 5 | \title{Configure package to point to ExifTool and/or Perl} 6 | \usage{ 7 | configure_exiftoolr( 8 | command = NULL, 9 | perl_path = NULL, 10 | allow_win_exe = TRUE, 11 | quiet = FALSE 12 | ) 13 | } 14 | \arguments{ 15 | \item{command}{Character string giving the exiftool command.} 16 | 17 | \item{perl_path}{Path to a Perl executable.} 18 | 19 | \item{allow_win_exe}{Logical. If running on a Windows machine, and if a 20 | standalone exiftool executable is available, should it be used?} 21 | 22 | \item{quiet}{Logical. Should function should be chatty?} 23 | } 24 | \value{ 25 | A character string giving the exiftool command, returned invisibly. 26 | } 27 | \description{ 28 | Configure package to point to ExifTool and/or Perl 29 | } 30 | -------------------------------------------------------------------------------- /man/exif_call.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/exif_read.R 3 | \name{exif_call} 4 | \alias{exif_call} 5 | \alias{exif_version} 6 | \title{Call ExifTool from R} 7 | \usage{ 8 | exif_call( 9 | args = NULL, 10 | path = NULL, 11 | stdout = TRUE, 12 | quiet = FALSE, 13 | ..., 14 | config_file = NULL, 15 | common_args = NULL 16 | ) 17 | 18 | exif_version(quiet = TRUE) 19 | } 20 | \arguments{ 21 | \item{args}{Character vector of arguments, each written in same 22 | form as you would if writing them on the command line 23 | (e.g. \code{"-n"} or \code{"-csv"})} 24 | 25 | \item{path}{A character vector giving one or more file paths.} 26 | 27 | \item{stdout}{Where output to stdout should be sent. If 28 | \code{TRUE} (the default), the output is captured in a 29 | character vector. For other options, see the help file for 30 | \code{\link[base]{system2}}, the function to which this 31 | argument's value gets passed along.} 32 | 33 | \item{quiet}{Use \code{FALSE} to display diagnostic 34 | information. Default value is \code{FALSE}.} 35 | 36 | \item{...}{Additional arguments to be passed to \code{system2()}.} 37 | 38 | \item{config_file}{Path to a config file of the format expected by 39 | Exiftool's command line \code{-config} option. (See Details 40 | for an explanation of why this one option cannot be passed 41 | directly to \code{args} via the \code{-config} argument.)} 42 | 43 | \item{common_args}{A character vector of arguments to be applied 44 | to all executed commands when the Exiftool \code{-execute} 45 | option is being used. (See Details for an explanation of why 46 | this option cannot be passed directly to \code{args} via 47 | \code{-common_args} argument.)} 48 | } 49 | \value{ 50 | The standard output as a character vector. 51 | } 52 | \description{ 53 | Uses \code{system2()} to run a basic call to \code{exiftool}. 54 | } 55 | \details{ 56 | For examples of the command-line calls to ExifTool (all 57 | of which can be reproduced by calls to \code{exif_call}), see 58 | \url{https://exiftool.org/examples.html}. 59 | 60 | Under the hood, \code{exif_call()} writes the options in 61 | \code{args} to a text file and then calls Exiftool, passing 62 | that text file's contents to Exiftool via its \code{-@ 63 | ARGFILE} option. \code{-config} and \code{-common_args} are 64 | the two options that may not be used in such a \code{-@ 65 | ARGFILE}, so we handle that option separately using 66 | \code{exif_call()}'s \code{config_file} argument. 67 | } 68 | \examples{ 69 | \dontrun{ 70 | ## Find local ExifTool version using exif_version() or exif_call() 71 | exif_version() 72 | exif_call(args = "-ver") 73 | 74 | ## Make temporary copies of a couple jpeg files 75 | tmpdir <- tempdir() 76 | src_files <- dir(system.file(package = "exiftoolr", "images"), 77 | full.names = TRUE) 78 | files <- file.path(tmpdir, basename(src_files)) 79 | file.copy(src_files, files) 80 | 81 | ## Both of the following extract the same tags: 82 | exif_read(files, tags = c("filename", "imagesize")) 83 | exif_call(args = c("-n", "-j", "-q", "-filename", "-imagesize"), 84 | path = files) 85 | 86 | ## Set value of a new "Artist" field in photo's metadata 87 | file1 <- files[1] 88 | exif_read(file1, tags = "artist") 89 | exif_call(path = file1, args = "-Artist=me") 90 | exif_read(file1, tags = "artist") 91 | 92 | ## Remove all but a few essential fields 93 | length(exif_read(file1)) 94 | exif_call(path = file1, args = "-all=") 95 | length(exif_read(file1)) 96 | exif_read(file1) 97 | 98 | ## Clean up 99 | unlink(files) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /man/exif_read.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/exif_read.R 3 | \name{exif_read} 4 | \alias{exif_read} 5 | \title{Read EXIF and other metadata from files} 6 | \usage{ 7 | exif_read( 8 | path, 9 | tags = NULL, 10 | recursive = FALSE, 11 | args = NULL, 12 | quiet = TRUE, 13 | pipeline = c("json", "csv") 14 | ) 15 | } 16 | \arguments{ 17 | \item{path}{A vector of filenames.} 18 | 19 | \item{tags}{A vector of tags to output. It is a good idea to specify this 20 | when reading large numbers of files, as it decreases the output overhead 21 | significantly. Spaces will be stripped in the output data frame. This 22 | parameter is not case-sensitive.} 23 | 24 | \item{recursive}{\code{TRUE} to pass the \code{"-r"} option to ExifTool.} 25 | 26 | \item{args}{Additional arguments.} 27 | 28 | \item{quiet}{Use \code{FALSE} to display diagnostic information. Default 29 | value is \code{TRUE}} 30 | 31 | \item{pipeline}{One of \code{"json"} (the default) or \code{"csv"}. Controls 32 | whether the exiftool executable, behind the scenes, extracts metadata 33 | into a JSON data structure or a tabular csv. The JSON pipeline works 34 | well in most cases, but (as documented at 35 | \url{https://exiftool.org/exiftool_pod.html}) does not properly handle 36 | non-UTF-8 character sets. If the metadata fields include characters that 37 | are not encoded using UTF-8 and that need to be handled by setting the 38 | \code{"-charset"} option, use the \code{"csv"} pipeline as demonstrated 39 | in the second example below.} 40 | } 41 | \value{ 42 | A data frame of class \code{"exiftoolr"} with one row per file 43 | processed. The first column, named \code{"SourceFile"} gives the name(s) 44 | of the processed files. Subsequent columns contain info from the tags 45 | read from those files. 46 | 47 | Note that binary tags such as thumbnails are loaded as 48 | \href{https://en.wikipedia.org/wiki/Base64}{base64-encoded strings} that 49 | start with \code{"base64:"}. Although these are truncated in the printed 50 | representation of the \code{data.frame} returned by the function, they 51 | are left unaltered in the \code{data.frame} itself. 52 | } 53 | \description{ 54 | Reads EXIF and other metadata into a \code{data.frame} by calling Phil 55 | Harvey's ExifTool command-line application. 56 | } 57 | \details{ 58 | From the \href{https://exiftool.org}{ExifTool website}: 59 | "ExifTool is a platform-independent Perl library plus a command-line 60 | application for reading, writing and editing meta information in a wide 61 | variety of files. ExifTool supports many different metadata formats 62 | including EXIF, GPS, IPTC, XMP, JFIF, GeoTIFF, ICC Profile, Photoshop IRB, 63 | FlashPix, AFCP and ID3, as well as the maker notes of many digital cameras 64 | by Canon, Casio, DJI, FLIR, FujiFilm, GE, GoPro, HP, JVC/Victor, Kodak, 65 | Leaf, Minolta/Konica-Minolta, Motorola, Nikon, Nintendo, Olympus/Epson, 66 | Panasonic/Leica, Pentax/Asahi, Phase One, Reconyx, Ricoh, Samsung, Sanyo, 67 | Sigma/Foveon and Sony." 68 | 69 | For more information, see the \href{https://exiftool.org}{ExifTool 70 | website}. 71 | } 72 | \examples{ 73 | \dontrun{ 74 | files <- dir(system.file(package = "exiftoolr", "images"), 75 | pattern = "LaSals|Lizard", full.names = TRUE) 76 | exif_read(files) 77 | exif_read(files, tags = c("filename", "imagesize")) 78 | 79 | ## Use pipeline="csv" for images needing explicit specification 80 | ## and proper handling of a non-default character sets 81 | img_file <- system.file(package = "exiftoolr", "images", "QS_Hongg.jpg") 82 | args <- c("-charset", "exiftool=cp1250") 83 | res <- exif_read(img_file, args = args, pipeline = "csv") 84 | res[["City"]] ## "Zurich", with an umlaut over the "u" 85 | } 86 | } 87 | \references{ 88 | \url{https://exiftool.org} 89 | } 90 | -------------------------------------------------------------------------------- /man/figures/LaSals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshOBrien/exiftoolr/99716fb9e9ac980d45590fb4b64d47554c33ccde/man/figures/LaSals.jpg -------------------------------------------------------------------------------- /man/figures/LaSals_annotated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshOBrien/exiftoolr/99716fb9e9ac980d45590fb4b64d47554c33ccde/man/figures/LaSals_annotated.jpg -------------------------------------------------------------------------------- /man/figures/Lizard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoshOBrien/exiftoolr/99716fb9e9ac980d45590fb4b64d47554c33ccde/man/figures/Lizard.jpg -------------------------------------------------------------------------------- /man/install_exiftool.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/install.R 3 | \name{install_exiftool} 4 | \alias{install_exiftool} 5 | \title{Install ExifTool, downloading (by default) the current version} 6 | \usage{ 7 | install_exiftool( 8 | install_location = NULL, 9 | win_exe = NULL, 10 | local_exiftool = NULL, 11 | quiet = FALSE 12 | ) 13 | } 14 | \arguments{ 15 | \item{install_location}{Path to the directory into which ExifTool should be 16 | installed. If \code{NULL} (the default), installation will be into the 17 | directory returned by \code{backports::R_user_dir("exiftoolr")}.} 18 | 19 | \item{win_exe}{Logical, only used on Windows machines. Should we install the 20 | standalone ExifTool Windows executable or the ExifTool Perl library? 21 | (The latter relies, for its execution, on an existing installation of 22 | Perl being present on the user's machine.) If set to \code{NULL} (the 23 | default), the function installs the Windows executable on Windows 24 | machines and the Perl library on other operating systems.} 25 | 26 | \item{local_exiftool}{If installing ExifTool from a local "*.zip" or 27 | ".tar.gz", supply the path to that file as a character string. With 28 | default value, `NULL`, the function downloads ExifTool from 29 | \url{https://exiftool.org} and then installs it.} 30 | 31 | \item{quiet}{Logical. Should function should be chatty?} 32 | } 33 | \value{ 34 | Called for its side effect 35 | } 36 | \description{ 37 | Install the current version of ExifTool 38 | } 39 | --------------------------------------------------------------------------------