oximg 0.18.0 → 0.19.0
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/README.md +52 -9
- data/lib/oximg/binary.rb +8 -2
- data/lib/oximg/native.rb +5 -4
- data/lib/oximg/version.rb +1 -1
- data/lib/oximg.rb +144 -18
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f234cc2b72a4f0a06af417190cec9d9c2502c56fd5dcb88f4d56108f2ac1a277
|
|
4
|
+
data.tar.gz: 54f825fc86fbbe0149149393c25ffd4be99eec3fecb6ee3630b634088327e400
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed00e440e6e93a7d045b73c2a0826efcc182b4420c76827bf233f821c7f45f909a2f2987083e86c585a2d79bed1791b019c4786efdb0f4be1033a2c181c25f1b
|
|
7
|
+
data.tar.gz: b0d9fe2516572b8495b754c60c0bccda2614bc26a5429141399a560ad873d264e0247d1aeab96d3af86856de647bd4c12939a003af75fdf858fab16d9b20c78f
|
data/README.md
CHANGED
|
@@ -20,7 +20,8 @@ Oximg.resize("in.jpg", "out.jpg", width: 750)
|
|
|
20
20
|
Oximg.resize("in.jpg", "out.webp", width: 750, height: 500, quality: 80)
|
|
21
21
|
Oximg.strip("upload.jpg", "clean.jpg")
|
|
22
22
|
Oximg.probe("in.jpg")
|
|
23
|
-
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000
|
|
23
|
+
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000,
|
|
24
|
+
# orientation: 1, display_width: 4000, display_height: 3000}
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
A GIF source is read and probed like the others (`format: :gif`), but
|
|
@@ -31,7 +32,8 @@ the name does not say, and `:gif` is not a `format:` you can ask for.
|
|
|
31
32
|
|
|
32
33
|
```ruby
|
|
33
34
|
Oximg.resize(source, destination, width: 0, height: 0,
|
|
34
|
-
quality: nil, format: nil, preset: nil
|
|
35
|
+
quality: nil, format: nil, preset: nil,
|
|
36
|
+
icc: nil, auto_rotate: nil)
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
The source is fitted within `width` x `height` and **never enlarged**. A
|
|
@@ -50,6 +52,12 @@ Oximg.resize("photo.jpg", "smaller.jpg", quality: 70)
|
|
|
50
52
|
| `quality` | 1–100, default 80. JPEG quality |
|
|
51
53
|
| `format` | `:jpg`, `:jpeg`, `:png`, `:webp`, `:avif`. Defaults to the destination's extension, else the source's own format |
|
|
52
54
|
| `preset` | `:jpegli` (default — maximum quality per byte), `:fast`, `:small` |
|
|
55
|
+
| `icc` | `:keep` or `:strip` the source's colour profile. Default: `OXIMG_ICC`, else keep |
|
|
56
|
+
| `auto_rotate` | `false` keeps the stored orientation, `true` applies the EXIF one. Default: `OXIMG_AUTO_ROTATE`, else true |
|
|
57
|
+
|
|
58
|
+
`icc` and `auto_rotate` apply to that call only, and win over the
|
|
59
|
+
environment, so a threaded job runner can treat one image differently
|
|
60
|
+
without touching `ENV`.
|
|
53
61
|
|
|
54
62
|
`resize` returns the destination path and raises `Oximg::ProcessingError`
|
|
55
63
|
on failure, carrying the binary's own message and exit status. Paths are
|
|
@@ -59,7 +67,7 @@ cannot be read as a flag.
|
|
|
59
67
|
## Stripping metadata
|
|
60
68
|
|
|
61
69
|
```ruby
|
|
62
|
-
Oximg.strip(source, destination, icc: nil)
|
|
70
|
+
Oximg.strip(source, destination, icc: nil, auto_rotate: nil, env: nil)
|
|
63
71
|
```
|
|
64
72
|
|
|
65
73
|
Removes EXIF (GPS, camera, timestamps), XMP, IPTC and comments from a
|
|
@@ -71,10 +79,36 @@ still displays upright. `icc: :strip` also drops the colour profile.
|
|
|
71
79
|
The default is `:keep`, or `OXIMG_ICC`. Dropping a CMYK JPEG's profile
|
|
72
80
|
raises `Oximg::ProcessingError`, as do GIF and AVIF sources.
|
|
73
81
|
|
|
82
|
+
`auto_rotate: true` turns a rotated JPEG upright instead, still without
|
|
83
|
+
re-encoding: its compressed 8x8 blocks are moved, as `jpegtran
|
|
84
|
+
-perfect` does, so the stored and displayed sizes agree for every
|
|
85
|
+
reader, not only for ones that honor EXIF. That is only exact when the
|
|
86
|
+
edges it moves are whole 8 or 16 px blocks (a phone's 4032x3024 is);
|
|
87
|
+
otherwise, and for PNG and WebP, the file is stripped as without it,
|
|
88
|
+
orientation kept. `Oximg.probe(destination)` tells which: orientation
|
|
89
|
+
1, or the one kept. It costs a full entropy decode and encode, about
|
|
90
|
+
170 ms for a 12 MP photo.
|
|
91
|
+
|
|
92
|
+
## Knobs, per process or per call
|
|
93
|
+
|
|
74
94
|
Everything the server tunes through `OXIMG_*` environment variables
|
|
75
95
|
applies here too, and is validated the same fail-closed way: a typo'd
|
|
76
96
|
knob is a startup error, never a silent default.
|
|
77
97
|
|
|
98
|
+
To set one for a single call, pass `env:` to `resize` or `strip`:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
Oximg.resize("in.jpg", "out.jpg", width: 750, env: {"OXIMG_JPEG_PROGRESSIVE" => "0"})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Each call spawns its own process, and `env:` reaches that process
|
|
105
|
+
only: `ENV` is never touched, so concurrent calls in a threaded job
|
|
106
|
+
runner each get their own settings. Names must be in `Oximg::KNOBS`
|
|
107
|
+
(the pipeline knobs; server settings such as `OXIMG_KEY` are refused),
|
|
108
|
+
values are Strings or Integers, and `nil` unsets a name for that call.
|
|
109
|
+
An unknown name raises `ArgumentError` before anything runs; a bad
|
|
110
|
+
value is refused by the binary (`ProcessingError`, status 2).
|
|
111
|
+
|
|
78
112
|
## Probing
|
|
79
113
|
|
|
80
114
|
```ruby
|
|
@@ -83,12 +117,21 @@ Oximg.probe(Pathname("in.jpg")) # also a path
|
|
|
83
117
|
Oximg.probe(upload.tempfile) # anything with #path
|
|
84
118
|
Oximg.probe(File.open("in.jpg", "rb")) # an open File
|
|
85
119
|
Oximg.probe(StringIO.new(bytes)) # an IO: bytes in memory
|
|
86
|
-
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000
|
|
120
|
+
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000,
|
|
121
|
+
# orientation: 1, display_width: 4000, display_height: 3000}
|
|
87
122
|
```
|
|
88
123
|
|
|
89
|
-
`probe` reads the header only and decodes no pixels.
|
|
90
|
-
the stored ones, before any EXIF rotation.
|
|
91
|
-
|
|
124
|
+
`probe` reads the header only and decodes no pixels. `width` and
|
|
125
|
+
`height` are the stored ones, before any EXIF rotation. `orientation`
|
|
126
|
+
is the EXIF value, 1..8 (1 when there is none or it is invalid, as
|
|
127
|
+
`resize` reads it), and `display_width` / `display_height` are what a
|
|
128
|
+
viewer honoring it shows: the axes exchanged for 5..8. A portrait
|
|
129
|
+
phone photo stored 4000x3000 with orientation 6, as `strip` leaves it,
|
|
130
|
+
lays out at 3000x4000. A WebP keeps its EXIF after the image data, so
|
|
131
|
+
probing one that has it reads the whole source. An `oximg` executable
|
|
132
|
+
older than this gem reports every source as orientation 1.
|
|
133
|
+
|
|
134
|
+
A String is always a path, so bytes already in memory go in as a `StringIO`. An open `File` is
|
|
92
135
|
read from its start, its position untouched, and it is the file you
|
|
93
136
|
hold that is read, even if its name has since been replaced, renamed,
|
|
94
137
|
or was relative to a directory the process has left. A closed or
|
|
@@ -138,8 +181,8 @@ Oximg.in_process_probe? #=> true on a platform gem
|
|
|
138
181
|
|
|
139
182
|
```ruby
|
|
140
183
|
Oximg.available? #=> true
|
|
141
|
-
Oximg.executable #=> "/…/gems/oximg-0.
|
|
142
|
-
Oximg.version #=> "0.
|
|
184
|
+
Oximg.executable #=> "/…/gems/oximg-0.19.0-arm64-darwin/exe/oximg"
|
|
185
|
+
Oximg.version #=> "0.19.0" # the binary's version, not the gem's
|
|
143
186
|
```
|
|
144
187
|
|
|
145
188
|
`resize` shells out to that binary — one process per image, which is
|
data/lib/oximg/binary.rb
CHANGED
|
@@ -60,8 +60,14 @@ module Oximg
|
|
|
60
60
|
# nothing in a filename can be interpreted as syntax. Returns
|
|
61
61
|
# [stdout, stderr]; a non-zero exit raises with the binary's own
|
|
62
62
|
# stderr, which already names what it refused.
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
#
|
|
64
|
+
# +env+ is merged into the child's environment only (a nil value
|
|
65
|
+
# unsets that name there): one process per call means one call's
|
|
66
|
+
# knobs never reach another's, even across threads, and the
|
|
67
|
+
# parent's ENV is never touched. Validate it first; Oximg.resize
|
|
68
|
+
# and Oximg.strip do.
|
|
69
|
+
def run(*args, env: {})
|
|
70
|
+
out, err, status = Open3.capture3(env, path, *args)
|
|
65
71
|
unless status.success?
|
|
66
72
|
message = err.strip
|
|
67
73
|
message = "oximg exited #{status.exitstatus}" if message.empty?
|
data/lib/oximg/native.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Oximg
|
|
|
29
29
|
BUNDLED = File.expand_path("native/#{LIBRARY}", __dir__)
|
|
30
30
|
|
|
31
31
|
# The library's oximg_probe_abi_version this code was written for.
|
|
32
|
-
ABI_VERSION =
|
|
32
|
+
ABI_VERSION = 2
|
|
33
33
|
|
|
34
34
|
# Statuses (ext/src/lib.rs).
|
|
35
35
|
OK = 0
|
|
@@ -110,7 +110,8 @@ module Oximg
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def call
|
|
113
|
-
|
|
113
|
+
# Stored width, stored height, orientation (ext/src/lib.rs).
|
|
114
|
+
dims = FFI::MemoryPointer.new(:uint64, 3)
|
|
114
115
|
msg = FFI::MemoryPointer.new(:char, MESSAGE_BYTES)
|
|
115
116
|
status = yield(dims, msg)
|
|
116
117
|
return if status == READ
|
|
@@ -118,8 +119,8 @@ module Oximg
|
|
|
118
119
|
text = msg.read_string.force_encoding(Encoding::UTF_8)
|
|
119
120
|
raise ProcessingError.new(text, 1) unless status == OK
|
|
120
121
|
|
|
121
|
-
width, height = dims.read_array_of_uint64(
|
|
122
|
-
|
|
122
|
+
width, height, orientation = dims.read_array_of_uint64(3)
|
|
123
|
+
Header.build(text, width, height, orientation)
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
# nil when there is no library to use. Memoized either way, so a
|
data/lib/oximg/version.rb
CHANGED
data/lib/oximg.rb
CHANGED
|
@@ -45,9 +45,52 @@ module Oximg
|
|
|
45
45
|
# mozjpeg's fast/small trade differently.
|
|
46
46
|
PRESETS = %i[jpegli fast small].freeze
|
|
47
47
|
|
|
48
|
-
# What `strip` may do with an embedded colour profile.
|
|
48
|
+
# What `resize` and `strip` may do with an embedded colour profile.
|
|
49
49
|
ICC_MODES = %i[keep strip].freeze
|
|
50
50
|
|
|
51
|
+
# The OXIMG_* pipeline knobs an +env:+ may set for one call. Pinned to
|
|
52
|
+
# the binary's own inventory (KNOBS in src/config.rs) by a test there,
|
|
53
|
+
# so a knob the binary gains or drops cannot drift from this list.
|
|
54
|
+
# Server-startup settings (OXIMG_KEY, OXIMG_BIND, ...) are not here:
|
|
55
|
+
# a one-shot never reads them.
|
|
56
|
+
KNOBS = %w[
|
|
57
|
+
OXIMG_TIMING
|
|
58
|
+
OXIMG_RESIZE
|
|
59
|
+
OXIMG_RESIZE_BACKEND
|
|
60
|
+
OXIMG_AUTO_ROTATE
|
|
61
|
+
OXIMG_ICC
|
|
62
|
+
OXIMG_DCT_MARGIN
|
|
63
|
+
OXIMG_LINEAR_SHRINK
|
|
64
|
+
OXIMG_JPEG_PROGRESSIVE
|
|
65
|
+
OXIMG_JPEG_SEQUENTIAL_PIXELS
|
|
66
|
+
OXIMG_JPEG_STREAMING_PIXELS
|
|
67
|
+
OXIMG_FLATTEN_BG
|
|
68
|
+
OXIMG_PNG_EFFORT
|
|
69
|
+
OXIMG_PNG_QUANTIZE
|
|
70
|
+
OXIMG_PNG_QUANTIZE_COLORS
|
|
71
|
+
OXIMG_WEBP_QUALITY
|
|
72
|
+
OXIMG_WEBP_EFFORT
|
|
73
|
+
OXIMG_WEBP_DECODE_THREADS
|
|
74
|
+
OXIMG_AVIF_QUALITY
|
|
75
|
+
OXIMG_AVIF_ALPHA_QUALITY
|
|
76
|
+
OXIMG_AVIF_SPEED
|
|
77
|
+
OXIMG_AVIF_DECODE_THREADS
|
|
78
|
+
OXIMG_MAX_SOURCE_BYTES
|
|
79
|
+
OXIMG_MAX_SRC_PIXELS
|
|
80
|
+
OXIMG_GIF_ANIMATION
|
|
81
|
+
OXIMG_MAX_ANIM_FRAMES
|
|
82
|
+
OXIMG_MAX_ANIM_WORK
|
|
83
|
+
OXIMG_ANIM_FRAME_STEP
|
|
84
|
+
OXIMG_MAX_DECODED_BYTES
|
|
85
|
+
OXIMG_LOG_DECODED_BYTES_ABOVE
|
|
86
|
+
OXIMG_UPSTREAM_CONNECT_TIMEOUT
|
|
87
|
+
OXIMG_UPSTREAM_TIMEOUT
|
|
88
|
+
OXIMG_GCS_ENDPOINT
|
|
89
|
+
OXIMG_S3_ENDPOINT
|
|
90
|
+
OXIMG_S3_PATH_STYLE
|
|
91
|
+
OXIMG_OVERLAP
|
|
92
|
+
].freeze
|
|
93
|
+
|
|
51
94
|
CONTENT_TYPE_FORMATS = {
|
|
52
95
|
"image/jpeg" => :jpeg,
|
|
53
96
|
"image/png" => :png,
|
|
@@ -59,8 +102,12 @@ module Oximg
|
|
|
59
102
|
# The line `oximg probe` prints (src/cli.rs):
|
|
60
103
|
#
|
|
61
104
|
# in.gif: image/gif 120x90 (10800 stored pixels), 3 frames, 1500ms, looping forever
|
|
105
|
+
# in.jpg: image/jpeg 3000x2000 (6000000 stored pixels), orientation 6 (displayed 2000x3000)
|
|
62
106
|
#
|
|
63
|
-
# The three fields the gem reports are pinned exactly
|
|
107
|
+
# The three fields the gem reports are pinned exactly, and so is the
|
|
108
|
+
# orientation, which a non-upright source prints first after the
|
|
109
|
+
# comma (its absence means 1; the displayed size is derived here from
|
|
110
|
+
# it, not read). Everything after
|
|
64
111
|
# the comma — the animation summary, printed for an animated source
|
|
65
112
|
# only — is information the gem does not expose, and is deliberately
|
|
66
113
|
# left unparsed rather than matched: a pattern that stopped at
|
|
@@ -70,7 +117,25 @@ module Oximg
|
|
|
70
117
|
# a field missing, a continuation without the comma, or anything
|
|
71
118
|
# printed before or after it. stderr is captured separately, so a
|
|
72
119
|
# warning never lands here.
|
|
73
|
-
PROBE_LINE = /\A[^\n]*: (\S+) (\d+)x(\d+) \(\d+ stored pixels\)(?:,[^\n]*)?\n?\z/
|
|
120
|
+
PROBE_LINE = /\A[^\n]*: (\S+) (\d+)x(\d+) \(\d+ stored pixels\)(?:, orientation ([1-8]) \(displayed \d+x\d+\))?(?:,[^\n]*)?\n?\z/
|
|
121
|
+
|
|
122
|
+
# The hash probe returns, built in one place for both of its paths
|
|
123
|
+
# (the in-process library and the spawned executable).
|
|
124
|
+
module Header # :nodoc:
|
|
125
|
+
def self.build(content_type, width, height, orientation)
|
|
126
|
+
# Orientations 5-8 exchange the axes on display.
|
|
127
|
+
swapped = orientation >= 5
|
|
128
|
+
{
|
|
129
|
+
content_type: content_type,
|
|
130
|
+
format: CONTENT_TYPE_FORMATS[content_type],
|
|
131
|
+
width: width,
|
|
132
|
+
height: height,
|
|
133
|
+
orientation: orientation,
|
|
134
|
+
display_width: swapped ? height : width,
|
|
135
|
+
display_height: swapped ? width : height
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
74
139
|
|
|
75
140
|
class << self
|
|
76
141
|
# Absolute path to the executable in use; raises
|
|
@@ -106,10 +171,24 @@ module Oximg
|
|
|
106
171
|
# The output format follows +format:+, else +destination+'s
|
|
107
172
|
# extension, else the source's own format. Returns the destination
|
|
108
173
|
# path.
|
|
109
|
-
|
|
174
|
+
#
|
|
175
|
+
# +icc:+ (:keep or :strip) and +auto_rotate:+ (true or false)
|
|
176
|
+
# override the binary's OXIMG_ICC and OXIMG_AUTO_ROTATE for this
|
|
177
|
+
# call only; nil leaves them to the environment (default: keep the
|
|
178
|
+
# profile, apply the EXIF orientation).
|
|
179
|
+
#
|
|
180
|
+
# +env:+ sets any other pipeline knob for this call only, e.g.
|
|
181
|
+
# <tt>env: {"OXIMG_JPEG_PROGRESSIVE" => "0"}</tt>: it reaches the
|
|
182
|
+
# one child process this call spawns, never ENV, so it is safe in a
|
|
183
|
+
# threaded job runner. Names must be in KNOBS; nil unsets a name
|
|
184
|
+
# for the call. The binary validates the values, fail-closed.
|
|
185
|
+
def resize(source, destination, width: 0, height: 0, quality: nil, format: nil, preset: nil,
|
|
186
|
+
icc: nil, auto_rotate: nil, env: nil)
|
|
110
187
|
output = expand(destination, "destination")
|
|
111
|
-
|
|
112
|
-
width: width, height: height, quality: quality, format: format, preset: preset
|
|
188
|
+
argv = resize_argv(source, output,
|
|
189
|
+
width: width, height: height, quality: quality, format: format, preset: preset,
|
|
190
|
+
icc: icc, auto_rotate: auto_rotate)
|
|
191
|
+
Binary.run(*argv, env: knob_env(env))
|
|
113
192
|
output
|
|
114
193
|
end
|
|
115
194
|
|
|
@@ -123,23 +202,62 @@ module Oximg
|
|
|
123
202
|
#
|
|
124
203
|
# This is what <tt>saver(strip: true)</tt> / <tt>-strip</tt> did
|
|
125
204
|
# under libvips or ImageMagick, minus the re-encode.
|
|
126
|
-
|
|
205
|
+
#
|
|
206
|
+
# <tt>auto_rotate: true</tt> turns a rotated JPEG upright instead,
|
|
207
|
+
# still without re-encoding: the compressed 8x8 blocks are moved
|
|
208
|
+
# (what <tt>jpegtran -perfect</tt> does), so its stored and displayed
|
|
209
|
+
# dimensions agree for every reader and no orientation tag is left.
|
|
210
|
+
# That is only exact when the edges it moves are whole 8 or 16 px
|
|
211
|
+
# blocks; otherwise, and for PNG and WebP, the orientation is kept
|
|
212
|
+
# as without it. Probe the destination to see which happened.
|
|
213
|
+
#
|
|
214
|
+
# +env:+ is per-call knobs, as for resize.
|
|
215
|
+
def strip(source, destination, icc: nil, auto_rotate: nil, env: nil)
|
|
127
216
|
output = expand(destination, "destination")
|
|
128
|
-
|
|
217
|
+
argv = strip_argv(source, output, icc: icc, auto_rotate: auto_rotate)
|
|
218
|
+
Binary.run(*argv, env: knob_env(env))
|
|
129
219
|
output
|
|
130
220
|
end
|
|
131
221
|
|
|
222
|
+
# The child environment an +env:+ turns into: String names and
|
|
223
|
+
# values, each name a known knob. A typo raises here rather than
|
|
224
|
+
# being passed along and ignored, which is the same fail-closed
|
|
225
|
+
# promise the binary makes about the values. Public for the same
|
|
226
|
+
# reason as resize_argv.
|
|
227
|
+
def knob_env(env)
|
|
228
|
+
return {} if env.nil?
|
|
229
|
+
raise ArgumentError, "env must be a Hash, got #{env.inspect}" unless env.is_a?(Hash)
|
|
230
|
+
|
|
231
|
+
env.each_with_object({}) do |(name, value), out|
|
|
232
|
+
key = name.to_s
|
|
233
|
+
unless KNOBS.include?(key)
|
|
234
|
+
raise ArgumentError, "unknown knob #{name.inspect} in env (one of Oximg::KNOBS)"
|
|
235
|
+
end
|
|
236
|
+
unless value.nil? || value.is_a?(String) || value.is_a?(Integer)
|
|
237
|
+
raise ArgumentError, "#{key} must be a String, an Integer or nil, got #{value.inspect}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
out[key] = value&.to_s
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
132
244
|
# The argv `strip` would run, minus the executable; public for the
|
|
133
245
|
# same reason as resize_argv.
|
|
134
|
-
def strip_argv(source, destination, icc: nil)
|
|
246
|
+
def strip_argv(source, destination, icc: nil, auto_rotate: nil)
|
|
135
247
|
argv = ["strip", expand(source, "source"), expand(destination, "destination")]
|
|
136
248
|
argv.push("--icc", token(icc, ICC_MODES, "icc")) unless icc.nil?
|
|
249
|
+
# Off is strip's own default, so false adds nothing.
|
|
250
|
+
argv.push("--auto-rotate") if !auto_rotate.nil? && boolean(auto_rotate, "auto_rotate")
|
|
137
251
|
argv
|
|
138
252
|
end
|
|
139
253
|
|
|
140
254
|
# Header-only inspection — no pixels are decoded. Returns
|
|
141
|
-
# <tt>{content_type:, format:, width:, height
|
|
142
|
-
#
|
|
255
|
+
# <tt>{content_type:, format:, width:, height:, orientation:,
|
|
256
|
+
# display_width:, display_height:}</tt>. +width+ and +height+ are the
|
|
257
|
+
# stored ones; +orientation+ is the EXIF value, 1..8 (1 when there is
|
|
258
|
+
# none: the rule resize's auto-rotate applies and strip keeps), and
|
|
259
|
+
# the display size is what a viewer honoring it shows, with the axes
|
|
260
|
+
# exchanged for 5..8.
|
|
143
261
|
#
|
|
144
262
|
# +source+ is a path — a String, a Pathname, or anything with #path
|
|
145
263
|
# such as a Tempfile — an open File, or a stream read from its
|
|
@@ -180,7 +298,8 @@ module Oximg
|
|
|
180
298
|
# The argv `resize` would run, minus the executable. Public because
|
|
181
299
|
# it is the honest way to see what a call turns into — and the way
|
|
182
300
|
# to test that without a binary present.
|
|
183
|
-
def resize_argv(source, destination, width: 0, height: 0, quality: nil, format: nil, preset: nil
|
|
301
|
+
def resize_argv(source, destination, width: 0, height: 0, quality: nil, format: nil, preset: nil,
|
|
302
|
+
icc: nil, auto_rotate: nil)
|
|
184
303
|
argv = [
|
|
185
304
|
"resize",
|
|
186
305
|
expand(source, "source"),
|
|
@@ -191,6 +310,8 @@ module Oximg
|
|
|
191
310
|
argv.push("-q", bounded(quality, "quality", 1, 100).to_s) unless quality.nil?
|
|
192
311
|
argv.push("-f", token(format, FORMATS, "format")) unless format.nil?
|
|
193
312
|
argv.push("--preset", token(preset, PRESETS, "preset")) unless preset.nil?
|
|
313
|
+
argv.push("--icc", token(icc, ICC_MODES, "icc")) unless icc.nil?
|
|
314
|
+
argv.push(boolean(auto_rotate, "auto_rotate") ? "--auto-rotate" : "--no-auto-rotate") unless auto_rotate.nil?
|
|
194
315
|
argv
|
|
195
316
|
end
|
|
196
317
|
|
|
@@ -278,12 +399,7 @@ module Oximg
|
|
|
278
399
|
match = output.match(PROBE_LINE)
|
|
279
400
|
raise ProcessingError, "unparsable probe output: #{output.inspect}" unless match
|
|
280
401
|
|
|
281
|
-
|
|
282
|
-
content_type: match[1],
|
|
283
|
-
format: CONTENT_TYPE_FORMATS[match[1]],
|
|
284
|
-
width: Integer(match[2]),
|
|
285
|
-
height: Integer(match[3])
|
|
286
|
-
}
|
|
402
|
+
Header.build(match[1], Integer(match[2]), Integer(match[3]), match[4] ? Integer(match[4]) : 1)
|
|
287
403
|
end
|
|
288
404
|
|
|
289
405
|
# Absolute paths throughout: a relative name beginning with "-"
|
|
@@ -319,6 +435,16 @@ module Oximg
|
|
|
319
435
|
value
|
|
320
436
|
end
|
|
321
437
|
|
|
438
|
+
# Strictly true or false: a truthy "0" or "false" read as true would
|
|
439
|
+
# rotate exactly the images the caller asked not to.
|
|
440
|
+
def boolean(value, name)
|
|
441
|
+
unless value == true || value == false
|
|
442
|
+
raise ArgumentError, "#{name} must be true or false, got #{value.inspect}"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
value
|
|
446
|
+
end
|
|
447
|
+
|
|
322
448
|
def token(value, allowed, name)
|
|
323
449
|
token = value.to_s.downcase
|
|
324
450
|
unless allowed.include?(token.to_sym)
|