oximg 0.18.0-aarch64-linux-gnu → 0.19.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: fb8e10a4f8d26016ab0a30feea53bd15ae14f8c40c101af4af254faa385b2431
4
- data.tar.gz: 86643933723d7deb8f050df9b090baea89b70a30263eee9df848f13fa2831b5a
3
+ metadata.gz: b2f87ce359e8fc26ca90d9d0a92019f6bfeb15d6e0666a3fb5e209ac8c52393d
4
+ data.tar.gz: 1f9b6406f2aef0f42a99519b846cc4f9e4617e1041ab3bbebda532a09d47e514
5
5
  SHA512:
6
- metadata.gz: ec4435ce5ef51e46423e3cac7316f44dd2976c287481d4f67feeb074eefb234c6855ab0e766d30632da625a7e2c4f1f331eef6c15bc6a60959b201df85c48db0
7
- data.tar.gz: 8edf8c025ad4fd2a143a7e430d5e730a80577344b6a358d3406e20e2ed9057ee3fa8b98a8fafea0e2ec0cce6b484479405facbb6f36e6046cd449986bff743d0
6
+ metadata.gz: 2a36f44d6923ed1978380a9c516381730313a10c97d7b8fdb4233f7cc9f8968e5478891cfc2aa490833521d7e03ac642f6b63ece421064539da133a0f623a783
7
+ data.tar.gz: 8e0a0482b8860043bdc0c641243646be4159cbfa533a47837d38f9ac87ae6375d961a41d8e1e57f702ee2037e6fc9a10f574f77931dda4503d2be9871bf960b4
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. The dimensions are
90
- the stored ones, before any EXIF rotation. A String is always a path,
91
- 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
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.18.0-arm64-darwin/exe/oximg"
142
- Oximg.version #=> "0.18.0" # 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
143
186
  ```
144
187
 
145
188
  `resize` shells out to that binary — one process per image, which is
data/exe/oximg CHANGED
Binary file
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
- def run(*args)
64
- out, err, status = Open3.capture3(path, *args)
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?
Binary file
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.0"
10
+ VERSION = "0.19.0"
11
11
  end
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. 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
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
- def resize(source, destination, width: 0, height: 0, quality: nil, format: nil, preset: nil)
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
- Binary.run(*resize_argv(source, output,
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
- def strip(source, destination, icc: nil)
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
- Binary.run(*strip_argv(source, output, icc: icc))
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:}</tt>, where the
142
- # 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.
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)
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.0
4
+ version: 0.19.0
5
5
  platform: aarch64-linux-gnu
6
6
  authors:
7
7
  - oximg contributors