metaclean 1.0.2 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +134 -47
- data/bin/metaclean +1 -22
- data/lib/metaclean/cli.rb +42 -92
- data/lib/metaclean/display.rb +59 -40
- data/lib/metaclean/exiftool.rb +70 -89
- data/lib/metaclean/ffmpeg.rb +84 -0
- data/lib/metaclean/mat2.rb +43 -40
- data/lib/metaclean/qpdf.rb +29 -25
- data/lib/metaclean/runner.rb +317 -168
- data/lib/metaclean/strategy.rb +118 -39
- data/lib/metaclean/version.rb +1 -3
- data/lib/metaclean.rb +75 -26
- metadata +11 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b514075fb238c6274263922e87d7218b6c27b86f46c07dfa6fe36e8a1387b382
|
|
4
|
+
data.tar.gz: 7eff78f7bd882717bc03cc6f98cb140faa4c9123d5b6a0806ccdb0e342cd3935
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b674107a22b4965a30bd5c990c90fec5d4944828a8ca755f27124029383de2a543fdf73fad42934b7fa6c4600d0670ffc79ff1602a9b23caf4da9bec7e239ccf
|
|
7
|
+
data.tar.gz: d265c366f6b7f0c76acd866e883dc8459ab4e35c27ef092abb4488262e4a5726f7b812d00bba5419282a47d05c80ac3e5e4b97254d2849f3bb4320d7cb19e5b8
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,32 +1,87 @@
|
|
|
1
1
|
# metaclean
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
```
|
|
4
|
+
███╗ ███╗███████╗████████╗ █████╗ ██████╗██╗ ███████╗ █████╗ ███╗ ██╗
|
|
5
|
+
████╗ ████║██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔════╝██╔══██╗████╗ ██║
|
|
6
|
+
██╔████╔██║█████╗ ██║ ███████║██║ ██║ █████╗ ███████║██╔██╗ ██║
|
|
7
|
+
██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║ ██╔══╝ ██╔══██║██║╚██╗██║
|
|
8
|
+
██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╗███████╗███████╗██║ ██║██║ ╚████║
|
|
9
|
+
╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
10
|
+
strip EXIF · IPTC · XMP · GPS · ID3 — leave the file clean
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
[](https://github.com/26zl/metaclean/actions/workflows/ci.yml)
|
|
14
|
+
[](https://rubygems.org/gems/metaclean)
|
|
15
|
+
[](https://www.ruby-lang.org)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
|
|
18
|
+
A small Ruby CLI that strips metadata from almost any file —
|
|
19
|
+
images, audio, video, PDFs, Office documents — and shows a colored before/after
|
|
20
|
+
diff of exactly what was removed.
|
|
6
21
|
|
|
7
|
-
It wraps
|
|
22
|
+
It wraps four battle-tested tools and routes each file to the right one:
|
|
8
23
|
|
|
9
24
|
- **ExifTool** — the broadest format coverage (EXIF, IPTC, XMP, GPS, ID3, …)
|
|
10
|
-
- **mat2** — stricter on `.docx` / `.
|
|
25
|
+
- **mat2** — stricter on `.docx` / `.png` and Office/OpenDocument files (rebuilds the file)
|
|
11
26
|
- **qpdf** — rebuilds PDFs and clears residual metadata in unused streams
|
|
27
|
+
- **ffmpeg** — strips the Matroska containers (`.mkv` / `.webm`) the others can't write, by remuxing losslessly (stream copy, no re-encode)
|
|
28
|
+
|
|
29
|
+
## Why metaclean?
|
|
30
|
+
|
|
31
|
+
- **Verification-first:** it re-reads the cleaned file and refuses to write a
|
|
32
|
+
result when known privacy metadata survives.
|
|
33
|
+
- **Safer defaults:** it writes `*_clean` copies by default; `--in-place` keeps a
|
|
34
|
+
`.bak` and asks for confirmation unless `--force` is set.
|
|
35
|
+
- **Lossless routing:** it avoids mat2 paths that recompress JPEG/WebP or
|
|
36
|
+
downconvert TIFF, and uses ffmpeg stream-copy for Matroska.
|
|
37
|
+
- **Batch-friendly:** failed or unverified files exit non-zero, so scripts and CI
|
|
38
|
+
do not mistake uncertainty for success.
|
|
39
|
+
|
|
40
|
+
## What it looks like
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
📄 photo.jpg
|
|
45
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
46
|
+
▸ Before (5 embedded tags)
|
|
47
|
+
[GPS]
|
|
48
|
+
GPSLatitude 59.9139
|
|
49
|
+
GPSLongitude 10.7522
|
|
50
|
+
[IFD0]
|
|
51
|
+
Artist Jane Doe
|
|
52
|
+
Make Apple
|
|
53
|
+
Model iPhone 15
|
|
54
|
+
Pipeline: exiftool
|
|
55
|
+
✓ exiftool
|
|
56
|
+
▸ After (0 embedded tags)
|
|
57
|
+
(no embedded metadata)
|
|
58
|
+
▸ Diff
|
|
59
|
+
▸ Removed (5)
|
|
60
|
+
- GPS:GPSLatitude 59.9139
|
|
61
|
+
- GPS:GPSLongitude 10.7522
|
|
62
|
+
- IFD0:Artist Jane Doe
|
|
63
|
+
- IFD0:Make Apple
|
|
64
|
+
- IFD0:Model iPhone 15
|
|
65
|
+
✓ → photo_clean.jpg
|
|
66
|
+
```
|
|
12
67
|
|
|
13
68
|
## Install
|
|
14
69
|
|
|
15
70
|
```bash
|
|
16
|
-
# 1. Install the
|
|
17
|
-
brew install exiftool mat2 qpdf # macOS
|
|
18
|
-
sudo apt install libimage-exiftool-perl mat2 qpdf # Debian / Ubuntu
|
|
19
|
-
sudo dnf install perl-Image-ExifTool mat2 qpdf # Fedora
|
|
20
|
-
sudo pacman -S perl-image-exiftool mat2 qpdf # Arch
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# 2. Install metaclean
|
|
71
|
+
# 1. Install the four required tools — metaclean refuses to run without all of them
|
|
72
|
+
brew install exiftool mat2 qpdf ffmpeg # macOS
|
|
73
|
+
sudo apt install libimage-exiftool-perl mat2 qpdf ffmpeg # Debian / Ubuntu
|
|
74
|
+
sudo dnf install perl-Image-ExifTool mat2 qpdf ffmpeg # Fedora
|
|
75
|
+
sudo pacman -S perl-image-exiftool mat2 qpdf ffmpeg # Arch
|
|
76
|
+
# Windows: use WSL2 (see below), then the Debian / Ubuntu line above
|
|
77
|
+
|
|
78
|
+
# 2. Install metaclean from RubyGems
|
|
24
79
|
gem install metaclean
|
|
25
80
|
metaclean --version
|
|
26
81
|
metaclean --help
|
|
27
82
|
```
|
|
28
83
|
|
|
29
|
-
From a source checkout
|
|
84
|
+
From a source checkout:
|
|
30
85
|
|
|
31
86
|
```bash
|
|
32
87
|
git clone https://github.com/26zl/metaclean.git && cd metaclean
|
|
@@ -41,17 +96,31 @@ You can also run the checkout without installing it by using
|
|
|
41
96
|
|
|
42
97
|
### Windows
|
|
43
98
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
99
|
+
Native Windows isn't supported: `mat2` depends on Python + GTK and doesn't
|
|
100
|
+
install cleanly there, and metaclean requires all four tools. Use
|
|
101
|
+
**[WSL2](https://learn.microsoft.com/windows/wsl/install)** with Ubuntu and
|
|
102
|
+
follow the Debian / Ubuntu line above — everything runs inside WSL.
|
|
103
|
+
|
|
104
|
+
### Android
|
|
49
105
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
106
|
+
There's no native Android app, but the full CLI runs unchanged inside
|
|
107
|
+
**[Termux](https://f-droid.org/packages/com.termux/)** (install it from F-Droid,
|
|
108
|
+
not the Play Store) via a Debian proot — where `mat2` and the other three tools
|
|
109
|
+
install cleanly from `apt`:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pkg install proot-distro
|
|
113
|
+
termux-setup-storage # tap "Allow" to grant access to your files
|
|
114
|
+
proot-distro install debian
|
|
115
|
+
proot-distro login debian --bind ~/storage/shared:/sdcard
|
|
116
|
+
# now inside Debian:
|
|
117
|
+
apt update && apt install -y ruby mat2 libimage-exiftool-perl qpdf ffmpeg
|
|
118
|
+
gem install metaclean
|
|
119
|
+
metaclean /sdcard/DCIM/Camera/photo.jpg
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The `--bind` is what lets metaclean reach your photos — without it Debian can't
|
|
123
|
+
see the phone's storage.
|
|
55
124
|
|
|
56
125
|
## Quick start
|
|
57
126
|
|
|
@@ -68,8 +137,8 @@ metaclean photo.jpg
|
|
|
68
137
|
# Overwrite the original (a .bak is kept by default)
|
|
69
138
|
metaclean --in-place photo.jpg
|
|
70
139
|
|
|
71
|
-
# Clean a whole folder, recursively, no prompts
|
|
72
|
-
metaclean -r --in-place --
|
|
140
|
+
# Clean a whole folder, recursively, no prompts
|
|
141
|
+
metaclean -r --in-place --force ./vacation
|
|
73
142
|
|
|
74
143
|
# See what would change without writing anything
|
|
75
144
|
metaclean --dry-run photo.jpg
|
|
@@ -78,31 +147,27 @@ metaclean --dry-run photo.jpg
|
|
|
78
147
|
## Flags
|
|
79
148
|
|
|
80
149
|
| Flag | What it does |
|
|
81
|
-
|
|
150
|
+
| --- | --- |
|
|
82
151
|
| `--inspect` | Read-only — print metadata, never write |
|
|
83
|
-
| `--json` | JSON output (with `--inspect`) |
|
|
84
152
|
| `--dry-run` | Simulate cleaning, show diff, write nothing |
|
|
85
|
-
| `-i`, `--in-place` | Overwrite originals |
|
|
86
|
-
| `--no-backup` | Do not keep `<file>.bak` (with `--in-place`) |
|
|
153
|
+
| `-i`, `--in-place` | Overwrite originals (keeps a `<file>.bak`) |
|
|
87
154
|
| `-r`, `--recursive` | Recurse into directories |
|
|
88
|
-
| `--types=jpg,png,…` | Only process these extensions |
|
|
89
|
-
| `--follow-symlinks` | Follow symlinks (default: skip) |
|
|
90
|
-
| `--keep-orientation` | Preserve EXIF Orientation |
|
|
91
|
-
| `--keep-color-profile` | Preserve embedded ICC profile |
|
|
92
|
-
| `--exiftool-only` | Use only ExifTool |
|
|
93
|
-
| `--no-mat2` / `--no-qpdf` / `--no-exiftool` | Disable a specific tool |
|
|
94
155
|
| `-f`, `--force` | Skip the confirmation prompt |
|
|
95
|
-
| `--
|
|
156
|
+
| `-h`, `--help` | Show usage and exit |
|
|
157
|
+
| `-v`, `--version` | Show metaclean's version **and** the detected versions of exiftool/mat2/qpdf/ffmpeg (prints `not found` for any missing) |
|
|
96
158
|
|
|
97
159
|
## Publishing
|
|
98
160
|
|
|
99
|
-
The release workflow builds a `.gem`
|
|
100
|
-
|
|
101
|
-
|
|
161
|
+
The release workflow builds a `.gem` for every `v*` tag, attaches it to a
|
|
162
|
+
GitHub Release, and publishes it to RubyGems via a
|
|
163
|
+
[Trusted Publisher](https://guides.rubygems.org/trusted-publishing/) (OIDC) —
|
|
164
|
+
`rubygems/release-gem` with `id-token: write`. There is **no `RUBYGEMS_API_KEY`
|
|
165
|
+
secret**; the one-time prerequisite is registering this gem's Trusted Publisher
|
|
166
|
+
on rubygems.org.
|
|
102
167
|
|
|
103
168
|
```bash
|
|
104
|
-
git tag
|
|
105
|
-
git push origin
|
|
169
|
+
git tag v3.0.0
|
|
170
|
+
git push origin v3.0.0
|
|
106
171
|
```
|
|
107
172
|
|
|
108
173
|
## Safety
|
|
@@ -111,12 +176,22 @@ git push origin v1.0.1
|
|
|
111
176
|
with spaces, quotes, or shell metacharacters are safe.
|
|
112
177
|
- `--in-place` writes atomically: the file is built in a temp file and
|
|
113
178
|
renamed into place, so a crash mid-run cannot leave a half-written original.
|
|
114
|
-
- Symlinks are skipped
|
|
179
|
+
- Symlinks are always skipped — metaclean never cleans through a link.
|
|
115
180
|
- Filename collisions (`photo_clean.jpg` already exists, `.bak` already
|
|
116
|
-
exists) are resolved with `_1`, `_2`, … suffixes
|
|
181
|
+
exists) are resolved with `_1`, `_2`, … suffixes, including late collisions
|
|
182
|
+
that appear while a file is being cleaned.
|
|
117
183
|
- After cleaning, metaclean re-reads the file and warns if known
|
|
118
184
|
privacy-relevant tags (GPS, MakerNotes, Author, camera serial number, etc.)
|
|
119
|
-
survived.
|
|
185
|
+
survived.
|
|
186
|
+
- A file whose strip leaves a privacy residual is **never written** — no
|
|
187
|
+
`_clean` copy and no `--in-place` overwrite — it is reported failed and the
|
|
188
|
+
original is left untouched.
|
|
189
|
+
- metaclean requires ExifTool, mat2, qpdf, and ffmpeg, and refuses to run (with
|
|
190
|
+
install instructions, exit code 2) if any is missing — so the post-clean
|
|
191
|
+
residual check always runs.
|
|
192
|
+
- Naming no files (a missing path, or everything filtered out), failed files,
|
|
193
|
+
and unverified cleans exit non-zero, so scripts do not mistake uncertainty for
|
|
194
|
+
success.
|
|
120
195
|
|
|
121
196
|
## What it does *not* do
|
|
122
197
|
|
|
@@ -124,8 +199,20 @@ git push origin v1.0.1
|
|
|
124
199
|
- Filesystem metadata (mtime, ownership) — that's the OS, not the file.
|
|
125
200
|
- Office macros or PDF JavaScript — open untrusted files in a sandbox.
|
|
126
201
|
|
|
127
|
-
|
|
128
|
-
|
|
202
|
+
The post-clean "still present" check is bounded by what ExifTool can re-read.
|
|
203
|
+
For container formats that mat2 cleans but ExifTool only partially parses (e.g.
|
|
204
|
+
`.zip`, `.epub` internals), metadata may be removed that the verification can't
|
|
205
|
+
independently confirm — a clean report means "the tools ran", not "every byte
|
|
206
|
+
was audited".
|
|
207
|
+
|
|
208
|
+
**SVG:** some mat2 builds (e.g. 0.14.0 on recent Python) crash on SVG, and
|
|
209
|
+
ExifTool is read-only for it — so on those systems metaclean cannot clean
|
|
210
|
+
`.svg`. It reports the file as **failed** (exit 1) and leaves the original
|
|
211
|
+
untouched rather than claiming a false "clean". Where mat2 handles SVG, it
|
|
212
|
+
cleans normally.
|
|
213
|
+
|
|
214
|
+
Keep ExifTool, mat2, qpdf, and ffmpeg updated; they parse hostile binary
|
|
215
|
+
formats and have had CVEs in the past.
|
|
129
216
|
|
|
130
217
|
## License
|
|
131
218
|
|
data/bin/metaclean
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
# bin/metaclean — entry point invoked by the user's shell.
|
|
6
|
-
#
|
|
7
|
-
# The shebang `#!/usr/bin/env ruby` makes the file executable on macOS/Linux
|
|
8
|
-
# (after `chmod +x`). `env` looks up Ruby on PATH so it works regardless of
|
|
9
|
-
# where Ruby is installed (Homebrew, rbenv, asdf, etc.).
|
|
10
|
-
#
|
|
11
|
-
# `frozen_string_literal: true` is a Ruby 3 magic comment: every string in
|
|
12
|
-
# this file is implicitly frozen (immutable). This prevents accidental
|
|
13
|
-
# mutation and gives a small performance/memory win.
|
|
14
|
-
# ───────────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
# Add `<repo>/lib` to Ruby's load path so `require 'metaclean'` finds our code
|
|
17
|
-
# without needing the gem installed. `__dir__` is the directory of this file
|
|
18
|
-
# (`bin/`), so `../lib` is the sibling `lib/` directory. `expand_path` turns
|
|
19
|
-
# the relative path into an absolute one.
|
|
4
|
+
# Add <repo>/lib to the load path so this runs from a checkout, gem or not.
|
|
20
5
|
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
21
|
-
|
|
22
|
-
# Pull in the whole library. See `lib/metaclean.rb` — that file `require`s
|
|
23
|
-
# every sub-component in dependency order.
|
|
24
6
|
require 'metaclean'
|
|
25
7
|
|
|
26
|
-
# Hand control to the CLI. `ARGV` is Ruby's built-in array of command-line
|
|
27
|
-
# arguments (everything after the script name). `CLI.start` parses them and
|
|
28
|
-
# decides what to do.
|
|
29
8
|
Metaclean::CLI.start(ARGV)
|
data/lib/metaclean/cli.rb
CHANGED
|
@@ -1,52 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
# Command-line argument parser.
|
|
5
|
-
#
|
|
6
|
-
# We use Ruby's standard library `OptionParser` instead of a gem like Thor
|
|
7
|
-
# because it has zero dependencies and is plenty for our needs. The pattern is:
|
|
8
|
-
#
|
|
9
|
-
# 1. Define each flag inside an `OptionParser.new do |o| ... end` block.
|
|
10
|
-
# 2. `o.on(...)` takes the flag spec and a block that runs when the flag
|
|
11
|
-
# is seen. The block usually mutates `@options` to record the choice.
|
|
12
|
-
# 3. `parser.parse!(@argv)` consumes flags from `@argv` and leaves
|
|
13
|
-
# positional args (the file paths) behind.
|
|
14
|
-
# ───────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# CLI argument parser. Uses stdlib OptionParser (zero deps) over a gem like Thor.
|
|
15
4
|
|
|
16
5
|
require 'optparse'
|
|
17
6
|
|
|
18
7
|
module Metaclean
|
|
19
8
|
class CLI
|
|
20
|
-
# Class-level convenience: `Metaclean::CLI.start(ARGV)` reads cleaner
|
|
21
|
-
# than `Metaclean::CLI.new(ARGV).run`.
|
|
22
9
|
def self.start(argv)
|
|
23
10
|
new(argv).run
|
|
24
11
|
end
|
|
25
12
|
|
|
26
13
|
def initialize(argv)
|
|
27
|
-
# `dup` makes a shallow copy so we can mutate `@argv` without
|
|
28
|
-
# surprising the caller (ARGV itself stays intact).
|
|
29
14
|
@argv = argv.dup
|
|
30
|
-
|
|
31
|
-
# All options default to safe/off values. `parse!` flips them
|
|
32
|
-
# selectively as it sees flags.
|
|
33
15
|
@options = {
|
|
34
|
-
recursive:
|
|
35
|
-
in_place:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
format: :pretty,
|
|
40
|
-
keep_orientation: false,
|
|
41
|
-
keep_color_profile: false,
|
|
42
|
-
dry_run: false,
|
|
43
|
-
follow_symlinks: false,
|
|
44
|
-
strict_verify: false,
|
|
45
|
-
no_mat2: false,
|
|
46
|
-
no_qpdf: false,
|
|
47
|
-
no_exiftool: false,
|
|
48
|
-
exiftool_only: false,
|
|
49
|
-
types: nil
|
|
16
|
+
recursive: false,
|
|
17
|
+
in_place: false,
|
|
18
|
+
force: false,
|
|
19
|
+
inspect_only: false,
|
|
20
|
+
dry_run: false
|
|
50
21
|
}
|
|
51
22
|
@paths = []
|
|
52
23
|
end
|
|
@@ -55,26 +26,32 @@ module Metaclean
|
|
|
55
26
|
# shells/CI can act on:
|
|
56
27
|
# 0 → success
|
|
57
28
|
# 1 → general failure
|
|
58
|
-
# 2 →
|
|
29
|
+
# 2 → a required tool (exiftool/mat2/qpdf/ffmpeg) is missing (install hint shown)
|
|
59
30
|
# 130→ user pressed Ctrl-C (matches the standard SIGINT exit code)
|
|
60
31
|
def run
|
|
61
32
|
parse!
|
|
33
|
+
# Refuse to run unless all four external tools are present (see
|
|
34
|
+
# Metaclean.ensure_tools!). --help/--version already exited inside parse!,
|
|
35
|
+
# so this only gates an actual inspect/clean.
|
|
36
|
+
Metaclean.ensure_tools!
|
|
62
37
|
runner = Runner.new(@options)
|
|
63
38
|
if @options[:inspect_only]
|
|
64
39
|
runner.inspect_paths(@paths)
|
|
65
40
|
else
|
|
66
41
|
runner.clean_paths(@paths)
|
|
67
42
|
end
|
|
68
|
-
rescue
|
|
69
|
-
warn Display.error('
|
|
43
|
+
rescue ToolsMissing => e
|
|
44
|
+
warn Display.error('Missing required tools')
|
|
70
45
|
warn e.message
|
|
71
46
|
exit 2
|
|
72
|
-
rescue Error => e
|
|
47
|
+
rescue Error, SystemCallError => e
|
|
48
|
+
# Errno::* (disk full, permission denied, read-only fs) is a SIBLING of
|
|
49
|
+
# our Error, not a subclass; naming it here gives filesystem failures a
|
|
50
|
+
# clean message + exit 1 instead of a raw backtrace.
|
|
73
51
|
warn Display.error(e.message)
|
|
74
52
|
exit 1
|
|
75
53
|
rescue Interrupt
|
|
76
|
-
#
|
|
77
|
-
# clean message instead of a Ruby stack trace.
|
|
54
|
+
# Print a clean message instead of a stack trace.
|
|
78
55
|
warn "\n#{Display.error('Interrupted.')}"
|
|
79
56
|
exit 130
|
|
80
57
|
end
|
|
@@ -83,82 +60,55 @@ module Metaclean
|
|
|
83
60
|
|
|
84
61
|
def parse!
|
|
85
62
|
parser = OptionParser.new do |o|
|
|
86
|
-
# The banner shows up at the top of `--help`.
|
|
87
63
|
o.banner = 'Usage: metaclean [options] <path> [<path>...]'
|
|
88
64
|
o.separator ''
|
|
89
|
-
o.separator '
|
|
90
|
-
o.separator 'MakerNotes, ID3, document properties, etc. — uses ExifTool, mat2'
|
|
91
|
-
o.separator 'and
|
|
65
|
+
o.separator 'Metadata cleaner. Strips EXIF, IPTC, XMP, GPS,'
|
|
66
|
+
o.separator 'MakerNotes, ID3, document properties, etc. — uses ExifTool, mat2,'
|
|
67
|
+
o.separator 'qpdf and ffmpeg together for maximum coverage.'
|
|
92
68
|
o.separator ''
|
|
93
69
|
|
|
94
|
-
# Each `o.on` registers a flag. The block runs when that flag is
|
|
95
|
-
# found in argv. `_v` is the captured value (unused for booleans).
|
|
96
70
|
o.separator 'Modes:'
|
|
97
|
-
o.on('--inspect',
|
|
98
|
-
o.on('--
|
|
99
|
-
o.on('--dry-run', 'Simulate cleaning, show diff, write nothing') { @options[:dry_run] = true }
|
|
71
|
+
o.on('--inspect', 'Only show metadata, do not modify files') { @options[:inspect_only] = true }
|
|
72
|
+
o.on('--dry-run', 'Simulate cleaning, show diff, write nothing') { @options[:dry_run] = true }
|
|
100
73
|
|
|
101
74
|
o.separator ''
|
|
102
75
|
o.separator 'Output:'
|
|
103
|
-
o.on('-i', '--in-place', 'Overwrite originals (default:
|
|
104
|
-
o.on('
|
|
105
|
-
|
|
106
|
-
o.separator ''
|
|
107
|
-
o.separator 'Selection:'
|
|
108
|
-
o.on('-r', '--recursive', 'Recurse into directories') { @options[:recursive] = true }
|
|
109
|
-
o.on('--follow-symlinks', 'Follow symlinks (default: skip them)') { @options[:follow_symlinks] = true }
|
|
110
|
-
# `--types=LIST, Array` tells OptionParser to split the value on
|
|
111
|
-
# commas. So `--types=jpg,png` arrives as ["jpg", "png"].
|
|
112
|
-
o.on('--types=LIST', Array, 'Only process these extensions (e.g. jpg,png,pdf)') do |v|
|
|
113
|
-
@options[:types] = v.map { |x| x.to_s.downcase.delete('.') }
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
o.separator ''
|
|
117
|
-
o.separator 'Tool selection:'
|
|
118
|
-
o.on('--exiftool-only', 'Use only ExifTool (skip mat2 and qpdf)') { @options[:exiftool_only] = true }
|
|
119
|
-
o.on('--no-mat2', 'Disable mat2 even if available') { @options[:no_mat2] = true }
|
|
120
|
-
o.on('--no-qpdf', 'Disable qpdf even if available') { @options[:no_qpdf] = true }
|
|
121
|
-
o.on('--no-exiftool', 'Disable ExifTool') { @options[:no_exiftool] = true }
|
|
122
|
-
|
|
123
|
-
o.separator ''
|
|
124
|
-
o.separator 'Preservation:'
|
|
125
|
-
o.on('--keep-orientation', 'Preserve EXIF Orientation tag') { @options[:keep_orientation] = true }
|
|
126
|
-
o.on('--keep-color-profile', 'Preserve embedded ICC profile') { @options[:keep_color_profile] = true }
|
|
127
|
-
|
|
128
|
-
o.separator ''
|
|
129
|
-
o.separator 'Verification:'
|
|
130
|
-
o.on('-f', '--force', 'Skip confirmation prompt') { @options[:force] = true }
|
|
131
|
-
o.on('--strict-verify', 'Exit non-zero if privacy tags survive') { @options[:strict_verify] = true }
|
|
76
|
+
o.on('-i', '--in-place', 'Overwrite originals (keeps a .bak; default: *_clean.<ext>)') { @options[:in_place] = true }
|
|
77
|
+
o.on('-r', '--recursive', 'Recurse into directories') { @options[:recursive] = true }
|
|
78
|
+
o.on('-f', '--force', 'Skip confirmation prompt') { @options[:force] = true }
|
|
132
79
|
|
|
133
80
|
o.separator ''
|
|
134
81
|
o.separator 'Other:'
|
|
135
|
-
o.on('-h', '--help') { puts o; exit }
|
|
82
|
+
o.on('-h', '--help') { Display.banner; puts o; exit }
|
|
136
83
|
o.on('-v', '--version') do
|
|
84
|
+
Display.banner
|
|
137
85
|
puts "metaclean #{Metaclean::VERSION}"
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
puts " exiftool: #{Exiftool.version || 'not found'}"
|
|
142
|
-
puts " mat2: #{Mat2.version || 'not found'}"
|
|
143
|
-
puts " qpdf: #{Qpdf.version
|
|
86
|
+
# Route tool versions (from the binaries' own stdout) through printable,
|
|
87
|
+
# like every other output path, so a tool emitting ANSI/OSC control
|
|
88
|
+
# bytes on its version line can't inject the terminal.
|
|
89
|
+
puts " exiftool: #{Display.printable(Exiftool.version || 'not found')}"
|
|
90
|
+
puts " mat2: #{Display.printable(Mat2.version || 'not found')}"
|
|
91
|
+
puts " qpdf: #{Display.printable(Qpdf.version || 'not found')}"
|
|
92
|
+
puts " ffmpeg: #{Display.printable(Ffmpeg.version || 'not found')}"
|
|
144
93
|
exit
|
|
145
94
|
end
|
|
146
95
|
end
|
|
147
96
|
|
|
148
|
-
# `parse!` mutates @argv in place: known flags are consumed,
|
|
149
|
-
# positional args (file paths) are left behind.
|
|
150
97
|
begin
|
|
151
98
|
parser.parse!(@argv)
|
|
152
|
-
rescue OptionParser::
|
|
153
|
-
#
|
|
99
|
+
rescue OptionParser::ParseError => e
|
|
100
|
+
# Any malformed flag — unknown, missing argument, or an ambiguous
|
|
101
|
+
# abbreviation like `--i` (matches both --inspect and --in-place) —
|
|
102
|
+
# shows the message + help and exits non-zero, never a raw backtrace.
|
|
103
|
+
# ParseError is the base class of all of OptionParser's error types.
|
|
154
104
|
warn Display.error(e.message)
|
|
155
105
|
warn parser
|
|
156
106
|
exit 1
|
|
157
107
|
end
|
|
158
108
|
|
|
159
|
-
# No paths
|
|
160
|
-
# Show help and exit non-zero so scripts notice.
|
|
109
|
+
# No paths: show help, exit non-zero so scripts notice.
|
|
161
110
|
if @argv.empty?
|
|
111
|
+
Display.banner
|
|
162
112
|
puts parser
|
|
163
113
|
exit 1
|
|
164
114
|
end
|