Module: Metaclean::Exiftool
- Defined in:
- lib/metaclean/exiftool.rb
Overview
‘module Exiftool` (vs `class`) because we want module-level methods like `Exiftool.read(path)` — there’s no state to carry per instance.
Class Method Summary collapse
-
.available? ⇒ Boolean
Returns true if ‘exiftool` is on PATH.
-
.ensure_available! ⇒ Object
Hard-fail with a helpful install hint.
-
.read(path) ⇒ Object
Reads metadata from a file and returns a flat Hash of “Group:Tag” => value.
-
.strip!(path, keep_orientation: false, keep_color_profile: false) ⇒ Object
Removes every removable tag, in place.
-
.version ⇒ Object
Returns the version string, or nil if exiftool is missing/broken.
Class Method Details
.available? ⇒ Boolean
Returns true if ‘exiftool` is on PATH. The result is memoized in `@available` so repeated checks don’t re-spawn the process.
‘defined?(@available)` is safer than `@available.nil?` because the cached value could legitimately be `false` — we want to skip the re-check in that case too.
31 32 33 34 35 36 37 38 39 40 |
# File 'lib/metaclean/exiftool.rb', line 31 def available? return @available if defined?(@available) _out, _err, status = Open3.capture3('exiftool', '-ver') @available = status.success? rescue Errno::ENOENT # `Errno::ENOENT` ("no such file or directory") is what Open3 raises # when the executable can't be found. We treat that as "not available". @available = false end |
.ensure_available! ⇒ Object
Hard-fail with a helpful install hint. Called from ‘read`/`strip!` before any work, so users see one clear message instead of a low-level Errno. The `<<~MSG … MSG` is a “squiggly heredoc”: leading indentation is stripped automatically, so the output is left-aligned.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/metaclean/exiftool.rb', line 56 def ensure_available! return if available? raise ExiftoolMissing, <<~MSG ExifTool is not installed or not on PATH. Install: macOS: brew install exiftool Debian: sudo apt install libimage-exiftool-perl Fedora: sudo dnf install perl-Image-ExifTool Arch: sudo pacman -S perl-image-exiftool Windows: scoop install exiftool (or download exiftool.org) MSG end |
.read(path) ⇒ Object
Reads metadata from a file and returns a flat Hash of “Group:Tag” => value.
ExifTool flag glossary:
-j JSON output (machine-parseable)
-G1 Include the family-1 group name (e.g. "EXIF", "GPS", "IPTC")
-a Allow duplicate tags (some formats have several with same name)
-u Include unknown/unidentified tags
-s Short tag names (no descriptions)
-n Numeric values (no human formatting like "1/100 sec")
-api largefilesupport=1 Allow files >4 GB
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/metaclean/exiftool.rb', line 81 def read(path) ensure_available! out, err, status = Open3.capture3( 'exiftool', '-j', '-G1', '-a', '-u', '-s', '-n', '-api', 'largefilesupport=1', path.to_s ) raise Error, "ExifTool read failed: #{err.strip}" unless status.success? # ExifTool's JSON output is an array (one entry per file). We always # pass one file, so we take the first element. `|| {}` handles the # edge case where exiftool returns an empty array. data = JSON.parse(out) data.first || {} rescue JSON::ParserError => e raise Error, "Could not parse ExifTool output: #{e.}" end |
.strip!(path, keep_orientation: false, keep_color_profile: false) ⇒ Object
Removes every removable tag, in place. Returns true on success.
‘-all=` is the magic incantation: it sets every tag to nothing (= empty), which deletes them. `-overwrite_original` makes ExifTool replace the file directly instead of writing `file_original` next to it.
The optional ‘keep_*` flags are useful because:
* Orientation tells viewers how to rotate phone photos. Removing it
can show the picture sideways.
* ICC profile tells viewers which color space the image is in.
Removing it can shift colors.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/metaclean/exiftool.rb', line 108 def strip!(path, keep_orientation: false, keep_color_profile: false) ensure_available! preserving = keep_orientation || keep_color_profile args = ['exiftool', '-all='] # `-tagsFromFile @` says "copy tags from the same file you're writing # to". That sounds redundant, but combined with `-all=` running first, # it means "delete everything, then re-add only the listed tags". if preserving args.concat(['-tagsFromFile', '@']) args << '-Orientation' if keep_orientation args << '-ICC_Profile' if keep_color_profile end args.concat(['-overwrite_original', '-q', '-q', '-api', 'largefilesupport=1', path.to_s]) _out, err, status = Open3.capture3(*args) return true if status.success? # Some minimal/odd files reject the preserve-pass. Fall back to a plain # full strip — but only if we *were* preserving, otherwise the retry # would be identical to the failed attempt. raise Error, "ExifTool strip failed: #{err.strip}" unless preserving _out2, err2, status2 = Open3.capture3( 'exiftool', '-all=', '-overwrite_original', '-q', '-q', path.to_s ) return true if status2.success? raise Error, "ExifTool strip failed: #{err2.strip.empty? ? err.strip : err2.strip}" end |
.version ⇒ Object
Returns the version string, or nil if exiftool is missing/broken.
43 44 45 46 47 48 49 50 |
# File 'lib/metaclean/exiftool.rb', line 43 def version return nil unless available? out, _err, status = Open3.capture3('exiftool', '-ver') status.success? ? out.strip : nil rescue Errno::ENOENT nil end |