oximg 0.19.0-aarch64-linux-gnu → 0.21.0-aarch64-linux-gnu

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: b2f87ce359e8fc26ca90d9d0a92019f6bfeb15d6e0666a3fb5e209ac8c52393d
4
- data.tar.gz: 1f9b6406f2aef0f42a99519b846cc4f9e4617e1041ab3bbebda532a09d47e514
3
+ metadata.gz: 94d184d02e8fd6dbe8d423f80830d3d798481599bcb9b01a7387dbc0b5c9bb17
4
+ data.tar.gz: 11e4a228b1c80535f59ff27de79528c8eb67d0883f1eaac318c3690f610b40e7
5
5
  SHA512:
6
- metadata.gz: 2a36f44d6923ed1978380a9c516381730313a10c97d7b8fdb4233f7cc9f8968e5478891cfc2aa490833521d7e03ac642f6b63ece421064539da133a0f623a783
7
- data.tar.gz: 8e0a0482b8860043bdc0c641243646be4159cbfa533a47837d38f9ac87ae6375d961a41d8e1e57f702ee2037e6fc9a10f574f77931dda4503d2be9871bf960b4
6
+ metadata.gz: 93c48adca562f93280bdf47cb4a9d601aac1f5edcb03fea68a4b38dde47ab3dcd6e303092ce7e975fbfa945535652e4398784a4172fb12be29a7fae49db711ad
7
+ data.tar.gz: 1000e27011d7e95b3444ec6573e0c401261d3c9ff303fa25357ddf95d07a7bb96a8c01bc3cb29dfb7141143b30475bf3014053557d6f9cbca52bd4c9fec47692
data/README.md CHANGED
@@ -85,10 +85,32 @@ re-encoding: its compressed 8x8 blocks are moved, as `jpegtran
85
85
  reader, not only for ones that honor EXIF. That is only exact when the
86
86
  edges it moves are whole 8 or 16 px blocks (a phone's 4032x3024 is);
87
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
88
+ orientation kept. It costs a full entropy decode and encode, about
90
89
  170 ms for a 12 MP photo.
91
90
 
91
+ `strip` returns an `Oximg::Stripped`, the binary's own account of the
92
+ run, so telling which happened needs no second read:
93
+
94
+ ```ruby
95
+ result = Oximg.strip(upload, clean, icc: :keep, auto_rotate: true)
96
+ result.upright? # no orientation tag left: stored size is displayed size
97
+ result.rotated? # the pixels were turned upright
98
+ result.orientation #=> 6, the tag kept, or nil
99
+ result.not_rotated&.code #=> :unaligned_edges, :corrupt, :over_limit or :not_jpeg
100
+ result.not_rotated&.message # for humans; the wording may change
101
+ ```
102
+
103
+ `not_rotated` is nil unless `auto_rotate: true` left a rotated source
104
+ tagged. The code is the contract (the table is in
105
+ [docs/features/paths.md](../../docs/features/paths.md)), so count
106
+ fallbacks by it. The result also stands in for the destination path
107
+ `strip` used to return: `to_s`, `to_path` and `to_str` are the path and
108
+ it compares equal to that String, so `File.size(Oximg.strip(...))`
109
+ still works. With an `oximg` older than 0.20, which has no `strip
110
+ --json`, the gem reads the summary line that binary prints instead:
111
+ `orientation` and `rotated?` are still right, `removed` is nil, and
112
+ the code is `:unknown` beside the binary's own reason.
113
+
92
114
  ## Knobs, per process or per call
93
115
 
94
116
  Everything the server tunes through `OXIMG_*` environment variables
@@ -181,8 +203,8 @@ Oximg.in_process_probe? #=> true on a platform gem
181
203
 
182
204
  ```ruby
183
205
  Oximg.available? #=> true
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
206
+ Oximg.executable #=> "/…/gems/oximg-0.21.0-arm64-darwin/exe/oximg"
207
+ Oximg.version #=> "0.21.0" # the binary's version, not the gem's
186
208
  ```
187
209
 
188
210
  `resize` shells out to that binary — one process per image, which is
data/exe/oximg CHANGED
Binary file
data/lib/oximg/binary.rb CHANGED
@@ -76,11 +76,23 @@ module Oximg
76
76
  [out, err]
77
77
  end
78
78
 
79
+ # True once the executable refused `strip --json` (an oximg older
80
+ # than 0.20): Oximg.strip then runs without it and reads the
81
+ # summary line that binary prints. Memoized per executable, like
82
+ # the path, so a process
83
+ # pays the refused spawn once.
84
+ attr_accessor :legacy_strip
85
+
86
+ def legacy_strip?
87
+ @legacy_strip == true
88
+ end
89
+
79
90
  # Resets the memoized lookup. For tests, and for a process that
80
91
  # changes OXIMG_BIN after boot.
81
92
  def reset!
82
93
  @path = nil
83
94
  @version = nil
95
+ @legacy_strip = nil
84
96
  end
85
97
 
86
98
  private
Binary file
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oximg
4
+ # What Oximg.strip did: where it wrote, and whether the output
5
+ # displays upright. Built from <tt>oximg strip --json</tt>, so it is
6
+ # the binary's own account of the run, not a second read of the file.
7
+ #
8
+ # It stands in for the destination path Oximg.strip returned before
9
+ # (#176): #to_s, #to_path and #to_str are the path, and it compares
10
+ # equal to that path as a String, so
11
+ # <tt>File.size(Oximg.strip(...))</tt>, <tt>File.join</tt> and
12
+ # <tt>assert_equal out, Oximg.strip(...)</tt> keep working.
13
+ #
14
+ # result = Oximg.strip(upload, clean, auto_rotate: true)
15
+ # result.rotated? #=> false
16
+ # result.orientation #=> 6, the tag the output keeps
17
+ # result.not_rotated.code #=> :unaligned_edges
18
+ # result.not_rotated.message #=> "1601x1203 does not divide into ..."
19
+ class Stripped
20
+ # Why <tt>auto_rotate: true</tt> left a rotated source tagged.
21
+ # +code+ is the contract, a Symbol from NOT_ROTATED_CODES (or
22
+ # :unknown, below); +message+ is for humans and may change.
23
+ NotRotated = Data.define(:code, :message)
24
+
25
+ # The binary's codes (docs/features/paths.md). A code missing here
26
+ # still comes through, as its Symbol: a newer binary may add one.
27
+ NOT_ROTATED_CODES = %i[unaligned_edges corrupt over_limit not_jpeg].freeze
28
+
29
+ # The destination, absolute.
30
+ attr_reader :path
31
+ # The output's content type ("image/jpeg", ...).
32
+ attr_reader :content_type
33
+ # Output size in bytes.
34
+ attr_reader :bytes
35
+ # What was dropped: <tt>[{name: "APP1 Exif", count: 1, bytes: 1204}, ...]</tt>;
36
+ # nil from an oximg older than 0.20.
37
+ attr_reader :removed
38
+ # The orientation tag the output keeps, 2..8, or nil when it
39
+ # displays as stored (upright, untagged, or rotated upright).
40
+ attr_reader :orientation
41
+ # The orientation turned into the pixels, 2..8, or nil.
42
+ attr_reader :rotated
43
+ # A NotRotated, or nil unless auto_rotate left a rotated source
44
+ # tagged. From an oximg older than 0.20, which has no --json and
45
+ # gives the reason only as a sentence, the code is :unknown.
46
+ attr_reader :not_rotated
47
+
48
+ # From the hash <tt>oximg strip --json</tt> prints.
49
+ def self.from_json(path, report)
50
+ why = report["not_rotated"]
51
+ new(
52
+ path: path,
53
+ content_type: report["content_type"],
54
+ bytes: report["bytes"],
55
+ removed: report["removed"]&.map { |r| {name: r["name"], count: r["count"], bytes: r["bytes"]} },
56
+ orientation: report["orientation"],
57
+ rotated: report["rotated"],
58
+ not_rotated: why && NotRotated.new(code: why["code"].to_sym, message: why["message"])
59
+ )
60
+ end
61
+
62
+ def initialize(path:, bytes:, content_type:, orientation:, rotated:, not_rotated:, removed: nil)
63
+ @path = path
64
+ @content_type = content_type
65
+ @bytes = bytes
66
+ @removed = removed
67
+ @orientation = orientation
68
+ @rotated = rotated
69
+ @not_rotated = not_rotated
70
+ freeze
71
+ end
72
+
73
+ # True when the pixels were turned upright (auto_rotate).
74
+ def rotated?
75
+ !@rotated.nil?
76
+ end
77
+
78
+ # True when the output displays as stored: no orientation tag left,
79
+ # so its stored width and height are the displayed ones.
80
+ def upright?
81
+ @orientation.nil?
82
+ end
83
+
84
+ def to_s
85
+ @path
86
+ end
87
+ alias_method :to_path, :to_s
88
+ alias_method :to_str, :to_s
89
+
90
+ # Equal to another result for the same run, or to the destination
91
+ # path as a String (String#== defers here for anything with to_str).
92
+ def ==(other)
93
+ case other
94
+ when Stripped then to_h == other.to_h
95
+ when String then @path == other
96
+ else false
97
+ end
98
+ end
99
+
100
+ def to_h
101
+ {path: @path, content_type: @content_type, bytes: @bytes, removed: @removed,
102
+ orientation: @orientation, rotated: @rotated, not_rotated: @not_rotated&.to_h}
103
+ end
104
+
105
+ def inspect
106
+ "#<#{self.class.name} #{@path} #{to_h.except(:path).compact.inspect}>"
107
+ end
108
+ end
109
+ end
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.19.0"
10
+ VERSION = "0.21.0"
11
11
  end
data/lib/oximg.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fcntl"
4
+ require "json"
4
5
  require "tempfile"
5
6
  require_relative "oximg/version"
6
7
  require_relative "oximg/binary"
7
8
  require_relative "oximg/native"
9
+ require_relative "oximg/stripped"
8
10
 
9
11
  # Ruby bindings for oximg (https://github.com/oximg/oximg):
10
12
  # high-performance image compression and resizing, in-process work for a
@@ -80,6 +82,7 @@ module Oximg
80
82
  OXIMG_GIF_ANIMATION
81
83
  OXIMG_MAX_ANIM_FRAMES
82
84
  OXIMG_MAX_ANIM_WORK
85
+ OXIMG_MAX_ANIM_DECODE_WORK
83
86
  OXIMG_ANIM_FRAME_STEP
84
87
  OXIMG_MAX_DECODED_BYTES
85
88
  OXIMG_LOG_DECODED_BYTES_ABOVE
@@ -198,7 +201,12 @@ module Oximg
198
201
  # a rotated photo keeps only its EXIF orientation, so it still
199
202
  # displays upright. +icc:+ is :keep or :strip (default: the
200
203
  # binary's OXIMG_ICC, else keep); dropping a CMYK JPEG's profile is
201
- # refused with a ProcessingError. Returns the destination path.
204
+ # refused with a ProcessingError.
205
+ #
206
+ # Returns an Oximg::Stripped: the binary's own account of the run
207
+ # (orientation kept, rotated?, not_rotated with a stable code). It
208
+ # stands in for the destination path this returned before:
209
+ # #to_path, #to_str and == against a String are the path.
202
210
  #
203
211
  # This is what <tt>saver(strip: true)</tt> / <tt>-strip</tt> did
204
212
  # under libvips or ImageMagick, minus the re-encode.
@@ -209,14 +217,33 @@ module Oximg
209
217
  # dimensions agree for every reader and no orientation tag is left.
210
218
  # That is only exact when the edges it moves are whole 8 or 16 px
211
219
  # blocks; otherwise, and for PNG and WebP, the orientation is kept
212
- # as without it. Probe the destination to see which happened.
220
+ # as without it, and the result says why:
221
+ #
222
+ # result = Oximg.strip(upload, clean, auto_rotate: true)
223
+ # unless result.upright?
224
+ # result.not_rotated.code #=> :unaligned_edges, :corrupt, :over_limit, :not_jpeg
225
+ # end
213
226
  #
214
227
  # +env:+ is per-call knobs, as for resize.
215
228
  def strip(source, destination, icc: nil, auto_rotate: nil, env: nil)
216
229
  output = expand(destination, "destination")
217
230
  argv = strip_argv(source, output, icc: icc, auto_rotate: auto_rotate)
218
- Binary.run(*argv, env: knob_env(env))
219
- output
231
+ child_env = knob_env(env)
232
+ unless Binary.legacy_strip?
233
+ begin
234
+ out, = Binary.run(*argv, "--json", env: child_env)
235
+ return Stripped.from_json(output, JSON.parse(out))
236
+ rescue ProcessingError => e
237
+ # An oximg older than 0.20 refuses the flag at parse time,
238
+ # before it reads or writes anything: remember that, and run
239
+ # the strip it does know.
240
+ raise unless e.status == 2 && e.message.include?('unknown option "--json"')
241
+
242
+ Binary.legacy_strip = true
243
+ end
244
+ end
245
+ _, err = Binary.run(*argv, env: child_env)
246
+ legacy_stripped(output, err)
220
247
  end
221
248
 
222
249
  # The child environment an +env:+ turns into: String names and
@@ -241,8 +268,9 @@ module Oximg
241
268
  end
242
269
  end
243
270
 
244
- # The argv `strip` would run, minus the executable; public for the
245
- # same reason as resize_argv.
271
+ # The argv `strip` would run, minus the executable and the --json it
272
+ # adds to read the outcome; public for the same reason as
273
+ # resize_argv.
246
274
  def strip_argv(source, destination, icc: nil, auto_rotate: nil)
247
275
  argv = ["strip", expand(source, "source"), expand(destination, "destination")]
248
276
  argv.push("--icc", token(icc, ICC_MODES, "icc")) unless icc.nil?
@@ -402,6 +430,30 @@ module Oximg
402
430
  Header.build(match[1], Integer(match[2]), Integer(match[3]), match[4] ? Integer(match[4]) : 1)
403
431
  end
404
432
 
433
+ # The summary every oximg since 0.18 ends a strip with, after
434
+ # "oximg: wrote <out> " (src/cli.rs). Those releases are frozen, so
435
+ # their wording is a fixed format here, though it is not a contract
436
+ # for later ones (they have --json). Removed block names are JPEG
437
+ # segment names or 4-byte chunk types, so the tail is unambiguous.
438
+ LEGACY_STRIP_SUMMARY = /\A\((\d+) bytes, ([^;\s]+); (.*?)(?:; kept orientation ([2-8])(?:, not rotated: (.*))?|; rotated orientation ([2-8]) upright)?\)\z/m
439
+
440
+ # The result from an oximg without --json, read from that summary
441
+ # rather than by probing: probe stops looking for the orientation
442
+ # after 1 MiB of metadata and needs a header libjpeg reads, where
443
+ # strip does neither. The reason a rotation was not made has no
444
+ # code before 0.20, so it is :unknown, with the binary's sentence.
445
+ def legacy_stripped(output, stderr)
446
+ prefix = "oximg: wrote #{output} "
447
+ line = stderr.lines.map(&:chomp).reverse.find { |l| l.start_with?(prefix) }
448
+ match = line && line.delete_prefix(prefix).match(LEGACY_STRIP_SUMMARY)
449
+ raise ProcessingError, "unparsable strip summary: #{stderr.strip.inspect}" unless match
450
+
451
+ kept = match[4] && Integer(match[4])
452
+ why = match[5] && Stripped::NotRotated.new(code: :unknown, message: match[5])
453
+ Stripped.new(path: output, bytes: Integer(match[1]), content_type: match[2],
454
+ orientation: kept, rotated: match[6] && Integer(match[6]), not_rotated: why)
455
+ end
456
+
405
457
  # Absolute paths throughout: a relative name beginning with "-"
406
458
  # would otherwise reach the CLI's argument parser as a flag, and
407
459
  # the caller rarely controls what an uploaded file is called.
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.19.0
4
+ version: 0.21.0
5
5
  platform: aarch64-linux-gnu
6
6
  authors:
7
7
  - oximg contributors
@@ -37,6 +37,7 @@ files:
37
37
  - lib/oximg/binary.rb
38
38
  - lib/oximg/native.rb
39
39
  - lib/oximg/native/liboximg_probe.so
40
+ - lib/oximg/stripped.rb
40
41
  - lib/oximg/version.rb
41
42
  homepage: https://github.com/oximg/oximg
42
43
  licenses: