oximg 0.17.0-x86_64-linux-gnu → 0.18.0-x86_64-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: af74dea3e14dc935db47e126b810455214fcf597a5e629b42a05e822d1a79185
4
- data.tar.gz: 72fa5083e099a86362cc3413744ca99658d53d1c7f8412286525801630fc22e9
3
+ metadata.gz: a53e80ca45940382e99218e1fdab0044da037b7df59b4b90a21ee46f9e8938db
4
+ data.tar.gz: b82aa7869485725d6cc6a3eec23725d70910488db7706f5ced7cf6d53a719518
5
5
  SHA512:
6
- metadata.gz: c4d2b8006c88d71269bd797b68c453bd0628029f27cd658a4dfb96f4bed40190f3b65e20c94dcae02e3d889bcf036a73ea4b71399d74f3bd81e2c6ddf9827a3c
7
- data.tar.gz: b9107af18a29c37551c3ba3bddc5c7d7d3853b5ae15f19099acb4157fb7d4b710fd07731c316c0c8b1893c4e74188bcf08d7430e73fd3ad626c427fc35b44d2c
6
+ metadata.gz: '080ab0ec86f59aaf5aef104bf07f00bb66d3beae7a976433e7eaf18318ddd5e02d388a8943d7609f3a2850e4e565f0bfb6763cc21a9d6b0a004f508a5bba1aef'
7
+ data.tar.gz: 95e7532bc97678d967ed99bb4595aab80487cc128b6d2f88b01d23bd470075f6a135471dccfb733996f54b6e961986f049fd7be09077ec3d8afcd91e6f56641b
data/README.md CHANGED
@@ -18,6 +18,7 @@ gem "oximg"
18
18
  ```ruby
19
19
  Oximg.resize("in.jpg", "out.jpg", width: 750)
20
20
  Oximg.resize("in.jpg", "out.webp", width: 750, height: 500, quality: 80)
21
+ Oximg.strip("upload.jpg", "clean.jpg")
21
22
  Oximg.probe("in.jpg")
22
23
  #=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000}
23
24
  ```
@@ -55,10 +56,76 @@ on failure, carrying the binary's own message and exit status. Paths are
55
56
  expanded before they reach the CLI, so an uploaded file called `-q.jpg`
56
57
  cannot be read as a flag.
57
58
 
59
+ ## Stripping metadata
60
+
61
+ ```ruby
62
+ Oximg.strip(source, destination, icc: nil)
63
+ ```
64
+
65
+ Removes EXIF (GPS, camera, timestamps), XMP, IPTC and comments from a
66
+ JPEG, PNG or WebP **without re-encoding**. Decoded pixels are identical,
67
+ and it costs a fraction of a `0 x 0` resize in time and memory. This
68
+ replaces `saver(strip: true)` (libvips) or `-strip` (ImageMagick). A
69
+ rotated photo keeps an EXIF block holding only its orientation, so it
70
+ still displays upright. `icc: :strip` also drops the colour profile.
71
+ The default is `:keep`, or `OXIMG_ICC`. Dropping a CMYK JPEG's profile
72
+ raises `Oximg::ProcessingError`, as do GIF and AVIF sources.
73
+
58
74
  Everything the server tunes through `OXIMG_*` environment variables
59
75
  applies here too, and is validated the same fail-closed way: a typo'd
60
76
  knob is a startup error, never a silent default.
61
77
 
78
+ ## Probing
79
+
80
+ ```ruby
81
+ Oximg.probe("in.jpg") # a path
82
+ Oximg.probe(Pathname("in.jpg")) # also a path
83
+ Oximg.probe(upload.tempfile) # anything with #path
84
+ Oximg.probe(File.open("in.jpg", "rb")) # an open File
85
+ Oximg.probe(StringIO.new(bytes)) # an IO: bytes in memory
86
+ #=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000}
87
+ ```
88
+
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
92
+ read from its start, its position untouched, and it is the file you
93
+ hold that is read, even if its name has since been replaced, renamed,
94
+ or was relative to a directory the process has left. A closed or
95
+ write-only `File`, which cannot be read through, is read by its name.
96
+ Other IO — a `StringIO`, a pipe, a socket — is read from its current
97
+ position through `#read(n)` and left where the read stopped.
98
+
99
+ The gem does that reading itself, in Ruby, between calls into the
100
+ library, so `#read` behaves as it would anywhere: whatever it raises
101
+ comes out of `probe` unchanged, and a `Timeout.timeout` or a
102
+ `Thread#kill` lands in it as usual.
103
+
104
+ With the probe library, `probe` runs in-process: oximg's own probe,
105
+ compiled into a small library (`ext/`) the gem calls through
106
+ [`ffi`](https://github.com/ffi/ffi). It reads only as much of the source
107
+ as the header takes, typically the first 64 KiB, and costs microseconds
108
+ where a spawn costs milliseconds. `resize` is not affected and still
109
+ runs in a child process, which keeps a full decode's memory and crashes
110
+ out of your process.
111
+
112
+ The platform gems bundle the library beside the executable (x86_64 and
113
+ aarch64 Linux, glibc and musl, and arm64 macOS), so they also depend
114
+ on `ffi`; beyond that it needs only libc and libgcc_s, which Ruby
115
+ itself needs, and not libstdc++.
116
+ `OXIMG_PROBE_LIB` points at another one; like `OXIMG_BIN`, one that
117
+ does not load raises rather than falling back. To build it from this
118
+ checkout, run `rake compile` here: it leaves the library under the
119
+ repo's `target/oximg-probe/release/`.
120
+
121
+ Without the library — the plain-Ruby gem, or a bundled one that cannot
122
+ load — `probe` spawns `oximg probe` instead, copying an IO to a
123
+ tempfile first. The answer is the same; only the cost differs.
124
+
125
+ ```ruby
126
+ Oximg.in_process_probe? #=> true on a platform gem
127
+ ```
128
+
62
129
  ## Where the binary comes from
63
130
 
64
131
  `Oximg.executable` resolves, first hit wins:
@@ -71,14 +138,14 @@ knob is a startup error, never a silent default.
71
138
 
72
139
  ```ruby
73
140
  Oximg.available? #=> true
74
- Oximg.executable #=> "/…/gems/oximg-0.16.0-arm64-darwin/exe/oximg"
75
- Oximg.version #=> "0.16.0" # the binary's version, not the gem's
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
76
143
  ```
77
144
 
78
- The gem shells out to that binary — one process per image, which is
145
+ `resize` shells out to that binary — one process per image, which is
79
146
  noise next to the encode itself and invisible inside the background job
80
- where variants are generated anyway. An in-process native extension is a
81
- later option; it would not change the API above.
147
+ where variants are generated anyway. Only `probe`, where the spawn was
148
+ the whole cost, runs in-process (see [Probing](#probing)).
82
149
 
83
150
  ## Rails
84
151
 
data/exe/oximg CHANGED
Binary file
Binary file
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Oximg
6
+ # The in-process header read: oximg's own probe, compiled as a small
7
+ # C library (rubygem/oximg/ext) and called through the +ffi+ gem, so
8
+ # Oximg.probe costs microseconds instead of a process spawn.
9
+ #
10
+ # Only the header read runs in-process. It reads a bounded prefix of
11
+ # the source and decodes no pixels; resize stays a child process, for
12
+ # the memory and crash isolation a full decode wants.
13
+ #
14
+ # Resolution order, first hit wins:
15
+ #
16
+ # 1. +OXIMG_PROBE_LIB+ — an explicit path always wins, and a wrong
17
+ # one raises rather than falling back, as +OXIMG_BIN+ does.
18
+ # 2. the library bundled in this gem, beside this file — platform
19
+ # gems ship one. If it cannot load (no +ffi+ gem, a missing
20
+ # system library), probe falls back to spawning the executable:
21
+ # slower, same answer.
22
+ #
23
+ # The plain-Ruby gem ships no library and always spawns.
24
+ module Native
25
+ LIBRARY = "liboximg_probe.#{RbConfig::CONFIG["SOEXT"]}"
26
+
27
+ # Populated by the platform gems at package time; absent in the
28
+ # plain-Ruby gem.
29
+ BUNDLED = File.expand_path("native/#{LIBRARY}", __dir__)
30
+
31
+ # The library's oximg_probe_abi_version this code was written for.
32
+ ABI_VERSION = 1
33
+
34
+ # Statuses (ext/src/lib.rs).
35
+ OK = 0
36
+ READ = 3
37
+
38
+ # The byte count READ gives for "to the end".
39
+ READ_ALL = 2**64 - 1
40
+
41
+ # Room for an error message; the library cuts a longer one at a
42
+ # character boundary.
43
+ MESSAGE_BYTES = 1024
44
+
45
+ class << self
46
+ # True when the library is loaded and probe runs in-process.
47
+ def available?
48
+ !binding_module.nil?
49
+ end
50
+
51
+ # Header of the file at +path+ (already expanded).
52
+ def probe_path(path)
53
+ # Copied off the Ruby heap: the call runs without the GVL, while
54
+ # other threads may run GC.
55
+ bytes = FFI::MemoryPointer.from_string(path)
56
+ call { |dims, msg| binding_module.oximg_probe_path(bytes, path.bytesize, dims, msg, MESSAGE_BYTES) }
57
+ end
58
+
59
+ # Header of the open, readable regular +file+, read through its
60
+ # descriptor from the start with positioned reads: what it holds,
61
+ # whatever its name leads to by now, its position untouched.
62
+ def probe_file(file)
63
+ call { |dims, msg| binding_module.oximg_probe_fd(file.fileno, dims, msg, MESSAGE_BYTES) }
64
+ end
65
+
66
+ # Header of +io+, read from its current position through
67
+ # IO#read(n) — a pipe, a socket, a StringIO. Only what the header
68
+ # needs is read, and the IO is left where the read stopped.
69
+ #
70
+ # The library decides how much to read and the reading happens
71
+ # here, between calls, so the IO's code never runs under the
72
+ # library's frames: whatever it raises, a Timeout or a Thread#kill
73
+ # included, leaves the way it would leave any Ruby method.
74
+ def probe_io(io)
75
+ prefix = String.new(encoding: Encoding::BINARY)
76
+ eof = false
77
+ loop do
78
+ want = nil
79
+ result = call do |dims, msg|
80
+ status = binding_module.oximg_probe_step(prefix, prefix.bytesize, eof ? 1 : 0, dims, msg, MESSAGE_BYTES)
81
+ want = dims.read_uint64 if status == READ
82
+ status
83
+ end
84
+ return result unless want
85
+
86
+ eof = read_to(io, prefix, want)
87
+ end
88
+ end
89
+
90
+ # Forgets the loaded library. For tests, and for a process that
91
+ # changes OXIMG_PROBE_LIB after boot.
92
+ def reset!
93
+ remove_instance_variable(:@binding_module) if instance_variable_defined?(:@binding_module)
94
+ end
95
+
96
+ private
97
+
98
+ # Reads +io+ onto +prefix+ until it holds +want+ bytes; true if the
99
+ # source ended first. READ_ALL asks for everything, which is read
100
+ # a MiB at a time: a length that large would be allocated up
101
+ # front, and an IO need only answer #read(n).
102
+ def read_to(io, prefix, want)
103
+ while want == READ_ALL || prefix.bytesize < want
104
+ chunk = io.read(want == READ_ALL ? 1 << 20 : want - prefix.bytesize)
105
+ return true if chunk.nil? || chunk.empty?
106
+
107
+ prefix << chunk.b
108
+ end
109
+ false
110
+ end
111
+
112
+ def call
113
+ dims = FFI::MemoryPointer.new(:uint64, 2)
114
+ msg = FFI::MemoryPointer.new(:char, MESSAGE_BYTES)
115
+ status = yield(dims, msg)
116
+ return if status == READ
117
+
118
+ text = msg.read_string.force_encoding(Encoding::UTF_8)
119
+ raise ProcessingError.new(text, 1) unless status == OK
120
+
121
+ width, height = dims.read_array_of_uint64(2)
122
+ {content_type: text, format: CONTENT_TYPE_FORMATS[text], width: width, height: height}
123
+ end
124
+
125
+ # nil when there is no library to use. Memoized either way, so a
126
+ # gem without one does not look again on every probe.
127
+ def binding_module
128
+ return @binding_module if instance_variable_defined?(:@binding_module)
129
+
130
+ @binding_module = load_library
131
+ end
132
+
133
+ def load_library
134
+ configured = ENV["OXIMG_PROBE_LIB"]
135
+ if configured.nil? || configured.strip.empty?
136
+ return nil unless File.file?(BUNDLED)
137
+
138
+ begin
139
+ open_library(BUNDLED)
140
+ rescue LoadError
141
+ nil
142
+ end
143
+ else
144
+ unless File.file?(configured)
145
+ raise LoadError, "OXIMG_PROBE_LIB is set to #{configured.inspect}, which is not a file"
146
+ end
147
+
148
+ # Absolute, so the file loaded is the one just checked. Where
149
+ # dlopen looks for a bare name depends on the platform (glibc
150
+ # never looks in the working directory; dyld does, after other
151
+ # places). absolute_path, unlike expand_path, leaves "~" alone,
152
+ # as File.file? did.
153
+ open_library(File.absolute_path(configured))
154
+ end
155
+ end
156
+
157
+ def open_library(path)
158
+ require "ffi"
159
+ mod = Module.new do
160
+ extend FFI::Library
161
+ ffi_lib path
162
+ attach_function :oximg_probe_abi_version, [], :uint32
163
+ # A path or descriptor read blocks on the filesystem, so it runs
164
+ # without the GVL. A step only parses bytes already in memory and
165
+ # keeps it, which also keeps the prefix String where it is.
166
+ attach_function :oximg_probe_path, %i[pointer size_t pointer pointer size_t], :int, blocking: true
167
+ attach_function :oximg_probe_fd, %i[int pointer pointer size_t], :int, blocking: true
168
+ attach_function :oximg_probe_step, %i[buffer_in size_t uint8 pointer pointer size_t], :int
169
+ end
170
+ abi = mod.oximg_probe_abi_version
171
+ unless abi == ABI_VERSION
172
+ raise LoadError, "#{path} speaks probe ABI #{abi}; this gem speaks #{ABI_VERSION}"
173
+ end
174
+
175
+ mod
176
+ end
177
+ end
178
+ end
179
+ 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.17.0"
10
+ VERSION = "0.18.0"
11
11
  end
data/lib/oximg.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fcntl"
4
+ require "tempfile"
3
5
  require_relative "oximg/version"
4
6
  require_relative "oximg/binary"
7
+ require_relative "oximg/native"
5
8
 
6
9
  # Ruby bindings for oximg (https://github.com/oximg/oximg):
7
10
  # high-performance image compression and resizing, in-process work for a
@@ -9,6 +12,7 @@ require_relative "oximg/binary"
9
12
  #
10
13
  # Oximg.resize("in.jpg", "out.jpg", width: 750)
11
14
  # Oximg.resize("in.jpg", "out.webp", width: 750, height: 500, quality: 80)
15
+ # Oximg.strip("upload.jpg", "clean.jpg") # drop EXIF/GPS, XMP, IPTC; no re-encode
12
16
  # Oximg.probe("in.jpg") #=> {content_type: "image/jpeg", format: :jpeg, ...}
13
17
  #
14
18
  # Rails/ActiveStorage integration, and URL building for a remote oximg
@@ -41,6 +45,9 @@ module Oximg
41
45
  # mozjpeg's fast/small trade differently.
42
46
  PRESETS = %i[jpegli fast small].freeze
43
47
 
48
+ # What `strip` may do with an embedded colour profile.
49
+ ICC_MODES = %i[keep strip].freeze
50
+
44
51
  CONTENT_TYPE_FORMATS = {
45
52
  "image/jpeg" => :jpeg,
46
53
  "image/png" => :png,
@@ -106,14 +113,70 @@ module Oximg
106
113
  output
107
114
  end
108
115
 
116
+ # Removes metadata — EXIF (GPS, camera, timestamps), XMP, IPTC,
117
+ # comments — from a JPEG, PNG or WebP without re-encoding, and
118
+ # writes the result to +destination+. Decoded pixels are identical;
119
+ # a rotated photo keeps only its EXIF orientation, so it still
120
+ # displays upright. +icc:+ is :keep or :strip (default: the
121
+ # binary's OXIMG_ICC, else keep); dropping a CMYK JPEG's profile is
122
+ # refused with a ProcessingError. Returns the destination path.
123
+ #
124
+ # This is what <tt>saver(strip: true)</tt> / <tt>-strip</tt> did
125
+ # under libvips or ImageMagick, minus the re-encode.
126
+ def strip(source, destination, icc: nil)
127
+ output = expand(destination, "destination")
128
+ Binary.run(*strip_argv(source, output, icc: icc))
129
+ output
130
+ end
131
+
132
+ # The argv `strip` would run, minus the executable; public for the
133
+ # same reason as resize_argv.
134
+ def strip_argv(source, destination, icc: nil)
135
+ argv = ["strip", expand(source, "source"), expand(destination, "destination")]
136
+ argv.push("--icc", token(icc, ICC_MODES, "icc")) unless icc.nil?
137
+ argv
138
+ end
139
+
109
140
  # Header-only inspection — no pixels are decoded. Returns
110
141
  # <tt>{content_type:, format:, width:, height:}</tt>, where the
111
142
  # dimensions are the stored ones (an EXIF rotation is not applied).
143
+ #
144
+ # +source+ is a path — a String, a Pathname, or anything with #path
145
+ # such as a Tempfile — an open File, or a stream read from its
146
+ # current position through #read(n): a StringIO, a pipe, a socket.
147
+ # A String is always a path, so bytes already in memory go in as
148
+ # <tt>StringIO.new(bytes)</tt>. An open File on a regular file is
149
+ # read from its start, its position untouched, and it is the file
150
+ # it holds that is read, whatever its name leads to by now. A closed
151
+ # or write-only File, which cannot be read through, is read by its
152
+ # name.
153
+ #
154
+ # With the probe library this runs in-process and reads only as much
155
+ # of the source as the header takes (see in_process_probe?). Without it,
156
+ # it spawns <tt>oximg probe</tt>, which reads the whole source, and an
157
+ # IO is copied to a tempfile first. The answer is the same.
112
158
  def probe(source)
113
- out, = Binary.run("probe", expand(source, "source"))
159
+ return probe_open_file(source) if open_regular_file?(source)
160
+
161
+ if stream?(source)
162
+ return Native.available? ? Native.probe_io(source) : probe_spawned_io(source)
163
+ end
164
+
165
+ path = expand(source, "source")
166
+ return Native.probe_path(path) if Native.available?
167
+
168
+ out, = Binary.run("probe", path)
114
169
  parse_probe(out)
115
170
  end
116
171
 
172
+ # True when probe runs in-process, through the probe library (see
173
+ # Native); false when it spawns the executable. Either way
174
+ # probe answers the same, and only its cost differs — this is for a
175
+ # boot-time log, or a test that must know which path it took.
176
+ def in_process_probe?
177
+ Native.available?
178
+ end
179
+
117
180
  # The argv `resize` would run, minus the executable. Public because
118
181
  # it is the honest way to see what a call turns into — and the way
119
182
  # to test that without a binary present.
@@ -133,6 +196,80 @@ module Oximg
133
196
 
134
197
  private
135
198
 
199
+ def open_regular_file?(source)
200
+ source.is_a?(File) && !source.closed? && source.stat.file?
201
+ end
202
+
203
+ # An open File is read through its descriptor, not by the name it
204
+ # was opened under: the name may lead to another file by now
205
+ # (replaced, renamed, or relative to a working directory the
206
+ # process has since left). Positioned reads leave its position
207
+ # alone. A write-only File cannot be read that way, and is read by
208
+ # its name.
209
+ def probe_open_file(file)
210
+ readable = (file.fcntl(Fcntl::F_GETFL) & Fcntl::O_ACCMODE) != Fcntl::O_WRONLY
211
+ return Native.probe_file(file) if readable && Native.available?
212
+
213
+ # A File made from a descriptor (File.for_fd) has no name.
214
+ name = file.path.to_s.empty? ? nil : File.expand_path(file.path)
215
+ if name && (!readable || File.identical?(file, name))
216
+ return Native.probe_path(name) if Native.available?
217
+
218
+ out, = Binary.run("probe", name)
219
+ return parse_probe(out)
220
+ end
221
+ raise IOError, "a write-only File with no name cannot be probed" unless readable
222
+
223
+ # The executable reads names only, and this one has moved on: it
224
+ # gets a copy, made with positioned reads.
225
+ Tempfile.create("oximg-probe") do |copy|
226
+ copy.binmode
227
+ offset = 0
228
+ loop do
229
+ chunk = file.pread(1 << 20, offset)
230
+ copy.write(chunk)
231
+ offset += chunk.bytesize
232
+ rescue EOFError
233
+ break
234
+ end
235
+ copy.flush
236
+ out, = Binary.run("probe", copy.path)
237
+ parse_probe(out)
238
+ end
239
+ end
240
+
241
+ # Whether probe reads +source+ through #read rather than opening it
242
+ # by name. Every IO answers #path since Ruby 3.2 — nil for a pipe,
243
+ # "" for a socket, "<STDIN>" for STDIN — so for an IO that proves
244
+ # nothing: an IO is a stream, except a closed File, which has
245
+ # nothing left to read but its name (an open one on a regular file
246
+ # never gets here). Anything else keeps the duck typing: #to_path
247
+ # or #path (a Pathname, a Tempfile, an upload) is a path.
248
+ def stream?(source)
249
+ case source
250
+ when String then false
251
+ when File then !source.closed?
252
+ when IO then true
253
+ else !source.respond_to?(:to_path) && !source.respond_to?(:path) && source.respond_to?(:read)
254
+ end
255
+ end
256
+
257
+ # The executable reads paths only, so without the library an IO
258
+ # goes through a tempfile. It is read through #read(n) alone, as the
259
+ # library reads it, so probe takes the same IO either way
260
+ # (IO.copy_stream would also pass #read a buffer).
261
+ def probe_spawned_io(io)
262
+ Tempfile.create("oximg-probe") do |file|
263
+ file.binmode
264
+ while (chunk = io.read(1 << 20)) && !chunk.empty?
265
+ file.write(chunk)
266
+ end
267
+ file.flush
268
+ out, = Binary.run("probe", file.path)
269
+ parse_probe(out)
270
+ end
271
+ end
272
+
136
273
  # The hash `probe` builds from the line the CLI printed. Separate
137
274
  # from `probe` so the parser can be tested against every line the
138
275
  # CLI can print — the still form, each loop spelling — without a
@@ -153,7 +290,11 @@ module Oximg
153
290
  # would otherwise reach the CLI's argument parser as a flag, and
154
291
  # the caller rarely controls what an uploaded file is called.
155
292
  def expand(path, name)
156
- path = path.path if path.respond_to?(:path)
293
+ if path.respond_to?(:path)
294
+ path = path.path
295
+ elsif path.respond_to?(:to_path)
296
+ path = path.to_path
297
+ end
157
298
  path = path.to_s
158
299
  raise ArgumentError, "#{name} path is empty" if path.strip.empty?
159
300
 
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oximg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: x86_64-linux-gnu
6
6
  authors:
7
7
  - oximg contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.17'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.17'
12
26
  description: 'Ruby bindings for oximg, the high-performance Rust image compression
13
27
  and resizing engine: JPEG, PNG, WebP and AVIF, resized in linear light. Platform
14
28
  gems bundle the executable, so there is no system library to install.'
@@ -21,6 +35,8 @@ files:
21
35
  - exe/oximg
22
36
  - lib/oximg.rb
23
37
  - lib/oximg/binary.rb
38
+ - lib/oximg/native.rb
39
+ - lib/oximg/native/liboximg_probe.so
24
40
  - lib/oximg/version.rb
25
41
  homepage: https://github.com/oximg/oximg
26
42
  licenses: