oximg 0.18.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8221098e170ea503a46a4c89169861c5368eb07f5764acbfded816f75b0eae5
4
- data.tar.gz: a255bfd60744a39d76b844582730d9e85cf1b468e5c7dc56f017984ce8fdf103
3
+ metadata.gz: f234cc2b72a4f0a06af417190cec9d9c2502c56fd5dcb88f4d56108f2ac1a277
4
+ data.tar.gz: 54f825fc86fbbe0149149393c25ffd4be99eec3fecb6ee3630b634088327e400
5
5
  SHA512:
6
- metadata.gz: 7f7100d38ed24e0bc72237fb18d124abce78dd995bfbf949658a87e4fc13b96848b1ca3cc12c5574cb74455900661a038b3f149d34c28267539c9acc10145a26
7
- data.tar.gz: 490bba95709fa12cced8408e0b52f4c2d45470e2e3a0b68382f5d6e8ef19b2eb1478b4dda12e8b2e3f0b0c9cd65121ce1e72b7a7d46082646d05f4635dda1b3c
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
@@ -66,7 +67,7 @@ cannot be read as a flag.
66
67
  ## Stripping metadata
67
68
 
68
69
  ```ruby
69
- Oximg.strip(source, destination, icc: nil, env: nil)
70
+ Oximg.strip(source, destination, icc: nil, auto_rotate: nil, env: nil)
70
71
  ```
71
72
 
72
73
  Removes EXIF (GPS, camera, timestamps), XMP, IPTC and comments from a
@@ -78,6 +79,16 @@ still displays upright. `icc: :strip` also drops the colour profile.
78
79
  The default is `:keep`, or `OXIMG_ICC`. Dropping a CMYK JPEG's profile
79
80
  raises `Oximg::ProcessingError`, as do GIF and AVIF sources.
80
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
+
81
92
  ## Knobs, per process or per call
82
93
 
83
94
  Everything the server tunes through `OXIMG_*` environment variables
@@ -106,12 +117,21 @@ Oximg.probe(Pathname("in.jpg")) # also a path
106
117
  Oximg.probe(upload.tempfile) # anything with #path
107
118
  Oximg.probe(File.open("in.jpg", "rb")) # an open File
108
119
  Oximg.probe(StringIO.new(bytes)) # an IO: bytes in memory
109
- #=> {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}
110
122
  ```
111
123
 
112
- `probe` reads the header only and decodes no pixels. The dimensions are
113
- the stored ones, before any EXIF rotation. A String is always a path,
114
- so bytes already in memory go in as a `StringIO`. An open `File` is
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
115
135
  read from its start, its position untouched, and it is the file you
116
136
  hold that is read, even if its name has since been replaced, renamed,
117
137
  or was relative to a directory the process has left. A closed or
@@ -161,8 +181,8 @@ Oximg.in_process_probe? #=> true on a platform gem
161
181
 
162
182
  ```ruby
163
183
  Oximg.available? #=> true
164
- Oximg.executable #=> "/…/gems/oximg-0.18.1-arm64-darwin/exe/oximg"
165
- Oximg.version #=> "0.18.1" # the binary's version, not the gem's
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
166
186
  ```
167
187
 
168
188
  `resize` shells out to that binary — one process per image, which is
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 = 1
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
- dims = FFI::MemoryPointer.new(:uint64, 2)
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(2)
122
- {content_type: text, format: CONTENT_TYPE_FORMATS[text], width: width, height: height}
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
@@ -7,5 +7,5 @@ module Oximg
7
7
  # names a binary it does not carry. `Oximg.version` reports what the
8
8
  # resolved executable actually is, which can differ when it comes
9
9
  # from PATH.
10
- VERSION = "0.18.1"
10
+ VERSION = "0.19.0"
11
11
  end
data/lib/oximg.rb CHANGED
@@ -102,8 +102,12 @@ module Oximg
102
102
  # The line `oximg probe` prints (src/cli.rs):
103
103
  #
104
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)
105
106
  #
106
- # The three fields the gem reports are pinned exactly. Everything after
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
107
111
  # the comma — the animation summary, printed for an animated source
108
112
  # only — is information the gem does not expose, and is deliberately
109
113
  # left unparsed rather than matched: a pattern that stopped at
@@ -113,7 +117,25 @@ module Oximg
113
117
  # a field missing, a continuation without the comma, or anything
114
118
  # printed before or after it. stderr is captured separately, so a
115
119
  # warning never lands here.
116
- 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
117
139
 
118
140
  class << self
119
141
  # Absolute path to the executable in use; raises
@@ -181,10 +203,18 @@ module Oximg
181
203
  # This is what <tt>saver(strip: true)</tt> / <tt>-strip</tt> did
182
204
  # under libvips or ImageMagick, minus the re-encode.
183
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
+ #
184
214
  # +env:+ is per-call knobs, as for resize.
185
- def strip(source, destination, icc: nil, env: nil)
215
+ def strip(source, destination, icc: nil, auto_rotate: nil, env: nil)
186
216
  output = expand(destination, "destination")
187
- argv = strip_argv(source, output, icc: icc)
217
+ argv = strip_argv(source, output, icc: icc, auto_rotate: auto_rotate)
188
218
  Binary.run(*argv, env: knob_env(env))
189
219
  output
190
220
  end
@@ -213,15 +243,21 @@ module Oximg
213
243
 
214
244
  # The argv `strip` would run, minus the executable; public for the
215
245
  # same reason as resize_argv.
216
- def strip_argv(source, destination, icc: nil)
246
+ def strip_argv(source, destination, icc: nil, auto_rotate: nil)
217
247
  argv = ["strip", expand(source, "source"), expand(destination, "destination")]
218
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")
219
251
  argv
220
252
  end
221
253
 
222
254
  # Header-only inspection — no pixels are decoded. Returns
223
- # <tt>{content_type:, format:, width:, height:}</tt>, where the
224
- # dimensions are the stored ones (an EXIF rotation is not applied).
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.
225
261
  #
226
262
  # +source+ is a path — a String, a Pathname, or anything with #path
227
263
  # such as a Tempfile — an open File, or a stream read from its
@@ -363,12 +399,7 @@ module Oximg
363
399
  match = output.match(PROBE_LINE)
364
400
  raise ProcessingError, "unparsable probe output: #{output.inspect}" unless match
365
401
 
366
- {
367
- content_type: match[1],
368
- format: CONTENT_TYPE_FORMATS[match[1]],
369
- width: Integer(match[2]),
370
- height: Integer(match[3])
371
- }
402
+ Header.build(match[1], Integer(match[2]), Integer(match[3]), match[4] ? Integer(match[4]) : 1)
372
403
  end
373
404
 
374
405
  # Absolute paths throughout: a relative name beginning with "-"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oximg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.1
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - oximg contributors