safe_image 0.2.0 → 0.4.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -10
  3. data/README.md +176 -168
  4. data/SECURITY.md +22 -11
  5. data/docs/architecture.md +63 -0
  6. data/ext/safe_image_vips_helper/extconf.rb +43 -0
  7. data/ext/safe_image_vips_helper/safe_image_vips_helper.c +1007 -0
  8. data/lib/safe_image/api/metadata.rb +85 -0
  9. data/lib/safe_image/api/transform.rb +152 -0
  10. data/lib/safe_image/backend_label.rb +24 -0
  11. data/lib/safe_image/formats.rb +96 -0
  12. data/lib/safe_image/ico.rb +43 -41
  13. data/lib/safe_image/image_magick_backend.rb +219 -162
  14. data/lib/safe_image/jpegli_backend.rb +64 -44
  15. data/lib/safe_image/metadata_operations.rb +155 -0
  16. data/lib/safe_image/native.rb +96 -281
  17. data/lib/safe_image/native_helper.rb +281 -0
  18. data/lib/safe_image/operation_backends/base.rb +83 -0
  19. data/lib/safe_image/operation_backends/image_magick.rb +123 -0
  20. data/lib/safe_image/operation_backends/vips.rb +251 -0
  21. data/lib/safe_image/operation_backends.rb +27 -0
  22. data/lib/safe_image/operation_set.rb +22 -0
  23. data/lib/safe_image/optimizer.rb +250 -48
  24. data/lib/safe_image/path_safety.rb +11 -0
  25. data/lib/safe_image/processor.rb +122 -85
  26. data/lib/safe_image/quality_defaults.rb +23 -0
  27. data/lib/safe_image/remote.rb +367 -97
  28. data/lib/safe_image/result.rb +71 -23
  29. data/lib/safe_image/runner.rb +69 -24
  30. data/lib/safe_image/sandbox.rb +44 -191
  31. data/lib/safe_image/staged_output.rb +44 -0
  32. data/lib/safe_image/svg_metadata.rb +186 -55
  33. data/lib/safe_image/transform_operations.rb +139 -0
  34. data/lib/safe_image/version.rb +1 -1
  35. data/lib/safe_image/vips_backend.rb +13 -12
  36. data/lib/safe_image.rb +62 -294
  37. metadata +48 -26
  38. data/lib/safe_image/discourse_compat.rb +0 -452
  39. data/lib/safe_image/svg_sanitizer.rb +0 -102
  40. data/lib/safe_image/vips_glue.rb +0 -361
data/lib/safe_image.rb CHANGED
@@ -3,22 +3,40 @@
3
3
  require_relative "safe_image/version"
4
4
 
5
5
  module SafeImage
6
- class Error < StandardError; end
6
+ class Error < StandardError
7
+ attr_reader :original_error_class, :stderr_tail
8
+
9
+ def initialize(message = nil, original_error_class: nil, stderr_tail: nil)
10
+ super(message)
11
+ @original_error_class = original_error_class
12
+ @stderr_tail = stderr_tail
13
+ end
14
+ end
7
15
 
8
16
  # Raised when any operation is attempted before SafeImage.configure!.
9
- class NotConfiguredError < Error; end
17
+ class NotConfiguredError < Error
18
+ end
19
+
20
+ class UnsupportedFormatError < Error
21
+ end
10
22
 
11
- class UnsupportedFormatError < Error; end
23
+ # Raised when the native libvips helper cannot initialize or execute.
24
+ # configure!(backend: :vips) surfaces this at boot; operations never fall back
25
+ # to ImageMagick.
26
+ class VipsUnavailableError < UnsupportedFormatError
27
+ end
28
+
29
+ class UnsafePathError < Error
30
+ end
12
31
 
13
- # Raised when libvips cannot be loaded at runtime. configure!(backend: :vips)
14
- # surfaces this at boot; operations never fall back to ImageMagick.
15
- class VipsUnavailableError < UnsupportedFormatError; end
16
- class UnsafePathError < Error; end
17
- class InvalidImageError < Error; end
18
- class LimitError < Error; end
32
+ class InvalidImageError < Error
33
+ end
34
+
35
+ class LimitError < Error
36
+ end
19
37
 
20
38
  # Default decompression-bomb ceiling when configure! is not given an explicit
21
- # max_pixels. Mirrored in the native binding (SAFE_IMAGE_DEFAULT_MAX_PIXELS)
39
+ # max_pixels. Mirrored in the native helper (SAFE_IMAGE_DEFAULT_MAX_PIXELS)
22
40
  # and aligned with the 128MP area limit on the ImageMagick path. Per-call
23
41
  # max_pixels: overrides the configured value.
24
42
  DEFAULT_MAX_PIXELS = 128 * 1024 * 1024
@@ -33,38 +51,49 @@ end
33
51
 
34
52
  require_relative "safe_image/native"
35
53
  require_relative "safe_image/result"
54
+ require_relative "safe_image/quality_defaults"
36
55
  require_relative "safe_image/runner"
37
- require_relative "safe_image/sandbox"
38
56
  require_relative "safe_image/path_safety"
57
+ require_relative "safe_image/formats"
58
+ require_relative "safe_image/backend_label"
59
+ require_relative "safe_image/operation_set"
60
+ require_relative "safe_image/staged_output"
61
+ require_relative "safe_image/sandbox"
62
+ require_relative "safe_image/native_helper"
39
63
  require_relative "safe_image/optimizer"
40
64
  require_relative "safe_image/svg_metadata"
41
- require_relative "safe_image/svg_sanitizer"
42
65
  require_relative "safe_image/remote"
43
66
  require_relative "safe_image/ico"
44
67
  require_relative "safe_image/image_magick_backend"
45
68
  require_relative "safe_image/jpegli_backend"
46
69
  require_relative "safe_image/vips_backend"
47
70
  require_relative "safe_image/processor"
48
- require_relative "safe_image/discourse_compat"
71
+ require_relative "safe_image/operation_backends"
72
+ require_relative "safe_image/metadata_operations"
73
+ require_relative "safe_image/transform_operations"
74
+ require_relative "safe_image/api/metadata"
75
+ require_relative "safe_image/api/transform"
49
76
 
50
77
  module SafeImage
78
+ private_constant :API
79
+
51
80
  module_function
52
81
 
53
82
  @config = nil
54
83
 
55
84
  # Decides, in one place, everything that varies by host: which backend
56
- # decodes untrusted bytes, whether operations run inside the Landlock
57
- # sandbox, and the default decompression-bomb ceiling. Must be called before
58
- # any operation; calling it again replaces the configuration.
85
+ # decodes untrusted bytes, whether child helpers/tools run under Landlock,
86
+ # and the default decompression-bomb ceiling. Must be called before any
87
+ # operation; calling it again replaces the configuration.
59
88
  #
60
89
  # Validation is eager so a misconfigured host fails at boot rather than on
61
90
  # the first request.
62
91
  def configure!(backend:, landlock:, max_pixels: DEFAULT_MAX_PIXELS)
63
92
  backend = backend.to_sym
64
- unless BACKENDS.include?(backend)
93
+ if BACKENDS.none? { |candidate| candidate == backend }
65
94
  raise ArgumentError, "unknown backend: #{backend.inspect} (expected :vips or :imagemagick)"
66
95
  end
67
- unless [true, false].include?(landlock)
96
+ unless [true, false].any? { |candidate| candidate == landlock }
68
97
  raise ArgumentError, "landlock must be true or false, got: #{landlock.inspect}"
69
98
  end
70
99
  max_pixels = Integer(max_pixels)
@@ -73,9 +102,9 @@ module SafeImage
73
102
  case backend
74
103
  when :vips
75
104
  begin
76
- VipsGlue.init!
77
- rescue VipsUnavailableError => e
78
- raise Error, "backend: :vips requested but libvips is unavailable: #{e.message}"
105
+ NativeHelper.verify!
106
+ rescue Error => e
107
+ raise Error, "backend: :vips requested but the native libvips helper is unavailable: #{e.message}"
79
108
  end
80
109
  when :imagemagick
81
110
  unless Runner.available?("magick") || Runner.available?("convert")
@@ -85,295 +114,34 @@ module SafeImage
85
114
  if landlock && !Sandbox.available?
86
115
  raise Error, "landlock: true requested but the Landlock sandbox is unavailable on this host"
87
116
  end
117
+ NativeHelper.ensure_available! if landlock && backend == :vips
88
118
 
89
119
  @config = Config.new(backend: backend, landlock: landlock, max_pixels: max_pixels)
90
120
  end
91
121
 
92
122
  def config
93
- @config || raise(NotConfiguredError, "call SafeImage.configure!(backend: :vips | :imagemagick, landlock: true | false) before using SafeImage")
123
+ @config ||
124
+ raise(
125
+ NotConfiguredError,
126
+ "call SafeImage.configure!(backend: :vips | :imagemagick, landlock: true | false) before using SafeImage"
127
+ )
94
128
  end
95
129
 
96
130
  def configured? = !@config.nil?
97
131
 
98
132
  def sandbox_available? = Sandbox.available?
99
133
 
100
- # Internal: whether operations must route through the sandbox worker. False
101
- # before configure! (so configure!'s own availability probes can run
102
- # commands) and inside worker children (so sandboxed operations never nest).
134
+ # Internal: whether child commands/helpers must run under Landlock. False
135
+ # before configure! so configure!'s own availability probes can run.
103
136
  def sandbox?
104
- !!@config&.landlock && ENV["SAFE_IMAGE_SANDBOX_CHILD"] != "1"
137
+ !!@config&.landlock
105
138
  end
106
139
 
107
140
  # Internal: per-call max_pixels overrides the configured default.
108
- def resolved_max_pixels(max_pixels)
141
+ def resolved_max_pixels(max_pixels, config: self.config)
109
142
  max_pixels.nil? ? config.max_pixels : max_pixels
110
143
  end
111
144
 
112
- def maybe_sandbox(operation, args: [], kwargs: {})
113
- config
114
- return yield unless sandbox?
115
-
116
- Sandbox.public_call!(operation, args: args, kwargs: kwargs)
117
- end
118
-
119
- def probe(path, max_pixels: nil)
120
- maybe_sandbox(:probe, args: [path], kwargs: { max_pixels: max_pixels }) do
121
- path = PathSafety.local_path(path)
122
- max_pixels = resolved_max_pixels(max_pixels)
123
-
124
- case File.extname(path).downcase
125
- when ".svg"
126
- info = SvgMetadata.probe(path, max_pixels: max_pixels)
127
- Result.new(
128
- input: File.expand_path(path),
129
- output: nil,
130
- input_format: "svg",
131
- output_format: nil,
132
- width: info.fetch(:width),
133
- height: info.fetch(:height),
134
- filesize: File.size(path),
135
- backend: "svg-metadata",
136
- duration_ms: info.fetch(:duration_ms),
137
- optimizer: nil
138
- )
139
- when ".ico"
140
- # Pure-Ruby directory parse; reports the largest entry's dimensions.
141
- info = Ico.probe(path, max_pixels: max_pixels)
142
- Result.new(
143
- input: File.expand_path(path),
144
- output: nil,
145
- input_format: "ico",
146
- output_format: nil,
147
- width: info.fetch(:width),
148
- height: info.fetch(:height),
149
- filesize: File.size(path),
150
- backend: "ico-metadata",
151
- duration_ms: info.fetch(:duration_ms),
152
- optimizer: nil
153
- )
154
- else
155
- case config.backend
156
- when :vips
157
- Processor.new(max_pixels: max_pixels).probe(path)
158
- when :imagemagick
159
- info = ImageMagickBackend.probe(path, max_pixels: max_pixels)
160
- Result.new(
161
- input: File.expand_path(path),
162
- output: nil,
163
- input_format: info.fetch(:input_format),
164
- output_format: nil,
165
- width: info.fetch(:width),
166
- height: info.fetch(:height),
167
- filesize: File.size(path),
168
- backend: "imagemagick",
169
- duration_ms: info.fetch(:duration_ms),
170
- optimizer: nil
171
- )
172
- end
173
- end
174
- end
175
- end
176
-
177
- def type(path, max_pixels: nil)
178
- maybe_sandbox(:type, args: [path], kwargs: { max_pixels: max_pixels }) do
179
- fastimage_type(probe(path, max_pixels: max_pixels).input_format)
180
- end
181
- end
182
-
183
- def size(path, max_pixels: nil)
184
- maybe_sandbox(:size, args: [path], kwargs: { max_pixels: max_pixels }) do
185
- result = probe(path, max_pixels: max_pixels)
186
- [result.width, result.height]
187
- end
188
- end
189
-
190
- def dimensions(path, max_pixels: nil)
191
- size(path, max_pixels: max_pixels)
192
- end
193
-
194
- def info(path, max_pixels: nil, animated: false, orientation: false)
195
- maybe_sandbox(:info, args: [path], kwargs: { max_pixels: max_pixels, animated: animated, orientation: orientation }) do
196
- result = probe(path, max_pixels: max_pixels)
197
- type = fastimage_type(result.input_format)
198
- Info.new(
199
- path: result.input,
200
- type: type,
201
- width: result.width,
202
- height: result.height,
203
- size: [result.width, result.height],
204
- animated: animated ? animated?(path, max_pixels: max_pixels) : nil,
205
- orientation: orientation ? orientation(path, max_pixels: max_pixels) : nil
206
- )
207
- end
208
- end
209
-
210
- def orientation(path, max_pixels: nil)
211
- maybe_sandbox(:orientation, args: [path], kwargs: { max_pixels: max_pixels }) do
212
- case File.extname(PathSafety.local_path(path)).downcase
213
- when ".svg", ".ico"
214
- # No EXIF orientation in either format; upright by definition.
215
- 1
216
- else
217
- max_pixels = resolved_max_pixels(max_pixels)
218
- case config.backend
219
- when :vips
220
- # Header-only native read.
221
- VipsBackend.orientation(path, max_pixels: max_pixels)
222
- when :imagemagick
223
- # Probe first: rejects undecodable files and enforces the pixel cap.
224
- ImageMagickBackend.probe(path, max_pixels: max_pixels)
225
- ImageMagickBackend.orientation(path)
226
- end
227
- end
228
- end
229
- end
230
-
231
- def dominant_color(path, max_pixels: nil)
232
- maybe_sandbox(:dominant_color, args: [path], kwargs: { max_pixels: max_pixels }) do
233
- max_pixels = resolved_max_pixels(max_pixels)
234
- case config.backend
235
- when :vips
236
- if File.extname(PathSafety.local_path(path)).downcase == ".ico"
237
- # Pure-Ruby ICO decode; vips only averages the decoded pixels.
238
- Ico.dominant_color(path, max_pixels: max_pixels)
239
- else
240
- VipsBackend.dominant_color(path, max_pixels: max_pixels)
241
- end
242
- when :imagemagick
243
- imagemagick_dominant_color(path, max_pixels: max_pixels)
244
- end
245
- end
246
- end
247
-
248
- def imagemagick_dominant_color(path, max_pixels:)
249
- # Probe first: rejects undecodable files and enforces the pixel cap
250
- # before ImageMagick fully decodes the image to average it.
251
- probe(path, max_pixels: max_pixels)
252
- ImageMagickBackend.dominant_color(path)
253
- end
254
-
255
- def fastimage_type(format)
256
- format.to_s == "jpg" ? :jpeg : format.to_s.to_sym
257
- end
258
-
259
- def remote_info(url, **kwargs)
260
- config
261
- Remote.info(url, **kwargs)
262
- end
263
-
264
- def remote_size(url, **kwargs)
265
- config
266
- Remote.size(url, **kwargs)
267
- end
268
-
269
- def remote_dimensions(url, **kwargs)
270
- remote_size(url, **kwargs)
271
- end
272
-
273
- def remote_type(url, **kwargs)
274
- config
275
- Remote.type(url, **kwargs)
276
- end
277
-
278
- def remote_animated?(url, **kwargs)
279
- config
280
- Remote.animated?(url, **kwargs)
281
- end
282
-
283
- def remote_dominant_color(url, **kwargs)
284
- config
285
- Remote.dominant_color(url, **kwargs)
286
- end
287
-
288
- def fetch_remote(url, **kwargs, &block)
289
- config
290
- Remote.fetch(url, **kwargs, &block)
291
- end
292
-
293
- def thumbnail(input:, output:, width:, height:, format: nil, quality: 85, max_pixels: nil, optimize: false, optimize_mode: :lossless, chroma_subsampling: :auto)
294
- maybe_sandbox(
295
- :thumbnail,
296
- kwargs: {
297
- input: input,
298
- output: output,
299
- width: width,
300
- height: height,
301
- format: format,
302
- quality: quality,
303
- max_pixels: max_pixels,
304
- optimize: optimize,
305
- optimize_mode: optimize_mode,
306
- chroma_subsampling: chroma_subsampling
307
- }
308
- ) do
309
- Processor.new(max_pixels: resolved_max_pixels(max_pixels), chroma_subsampling: chroma_subsampling).thumbnail(
310
- input: input,
311
- output: output,
312
- width: width,
313
- height: height,
314
- format: format,
315
- quality: quality,
316
- optimize: optimize,
317
- optimize_mode: optimize_mode
318
- )
319
- end
320
- end
321
-
322
- def optimize(path, mode: :lossless, strip_metadata: true, quality: nil, strict: true)
323
- maybe_sandbox(:optimize, args: [path], kwargs: { mode: mode, strip_metadata: strip_metadata, quality: quality, strict: strict }) do
324
- Optimizer.optimize(path, mode: mode, strip_metadata: strip_metadata, quality: quality, strict: strict)
325
- end
326
- end
327
-
328
- def resize(*args, **kwargs)
329
- maybe_sandbox(:resize, args: args, kwargs: kwargs) { DiscourseCompat.resize(*args, **kwargs) }
330
- end
331
-
332
- def crop(*args, **kwargs)
333
- maybe_sandbox(:crop, args: args, kwargs: kwargs) { DiscourseCompat.crop(*args, **kwargs) }
334
- end
335
-
336
- def downsize(*args, **kwargs)
337
- maybe_sandbox(:downsize, args: args, kwargs: kwargs) { DiscourseCompat.downsize(*args, **kwargs) }
338
- end
339
-
340
- def convert(*args, **kwargs)
341
- maybe_sandbox(:convert, args: args, kwargs: kwargs) { DiscourseCompat.convert(*args, **kwargs) }
342
- end
343
-
344
- def convert_to_jpeg(*args, **kwargs)
345
- maybe_sandbox(:convert_to_jpeg, args: args, kwargs: kwargs) { DiscourseCompat.convert_to_jpeg(*args, **kwargs) }
346
- end
347
-
348
- def fix_orientation(*args, **kwargs)
349
- maybe_sandbox(:fix_orientation, args: args, kwargs: kwargs) { DiscourseCompat.fix_orientation(*args, **kwargs) }
350
- end
351
-
352
- def convert_favicon_to_png(*args, **kwargs)
353
- maybe_sandbox(:convert_favicon_to_png, args: args, kwargs: kwargs) { DiscourseCompat.convert_favicon_to_png(*args, **kwargs) }
354
- end
355
-
356
- def frame_count(*args, **kwargs)
357
- maybe_sandbox(:frame_count, args: args, kwargs: kwargs) { DiscourseCompat.frame_count(*args, **kwargs) }
358
- end
359
-
360
- def animated?(*args, **kwargs)
361
- config
362
- path = args.first
363
- return false if path && File.extname(PathSafety.local_path(path)).downcase == ".svg"
364
-
365
- maybe_sandbox(:animated?, args: args, kwargs: kwargs) { DiscourseCompat.animated?(*args, **kwargs) }
366
- end
367
-
368
- def letter_avatar(*args, **kwargs)
369
- maybe_sandbox(:letter_avatar, args: args, kwargs: kwargs) { DiscourseCompat.letter_avatar(*args, **kwargs) }
370
- end
371
-
372
- def optimize_image!(*args, **kwargs)
373
- maybe_sandbox(:optimize_image!, args: args, kwargs: kwargs) { DiscourseCompat.optimize_image!(*args, **kwargs) }
374
- end
375
-
376
- def sanitize_svg!(*args, **kwargs)
377
- maybe_sandbox(:sanitize_svg!, args: args, kwargs: kwargs) { SvgSanitizer.sanitize!(*args, **kwargs) }
378
- end
145
+ extend API::Metadata
146
+ extend API::Transform
379
147
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_image
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -11,33 +11,19 @@ cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: fiddle
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
27
- - !ruby/object:Gem::Dependency
28
- name: rexml
14
+ name: nokogiri
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - "~>"
32
18
  - !ruby/object:Gem::Version
33
- version: '3.4'
19
+ version: '1.16'
34
20
  type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - "~>"
39
25
  - !ruby/object:Gem::Version
40
- version: '3.4'
26
+ version: '1.16'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: minitest
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,55 +66,91 @@ dependencies:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
68
  version: '3.18'
69
+ - !ruby/object:Gem::Dependency
70
+ name: syntax_tree
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '6.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '6.3'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: landlock
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.3'
87
90
  - - ">="
88
91
  - !ruby/object:Gem::Version
89
- version: '0'
92
+ version: 0.3.0
90
93
  type: :development
91
94
  prerelease: false
92
95
  version_requirements: !ruby/object:Gem::Requirement
93
96
  requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '0.3'
94
100
  - - ">="
95
101
  - !ruby/object:Gem::Version
96
- version: '0'
102
+ version: 0.3.0
97
103
  description: 'Safe Image is a small Ruby image-processing boundary for untrusted uploads:
98
- direct libvips thumbnails/probing, hardened ImageMagick compatibility operations,
99
- optimisation, SVG sanitising, and optional atomic Landlock sandbox execution.'
104
+ helper-contained libvips thumbnails/probing, hardened ImageMagick compatibility
105
+ operations, optimisation, SVG metadata probing, and optional atomic Landlock sandbox
106
+ execution.'
100
107
  email:
101
108
  - sam@discourse.org
102
109
  executables: []
103
- extensions: []
110
+ extensions:
111
+ - ext/safe_image_vips_helper/extconf.rb
104
112
  extra_rdoc_files: []
105
113
  files:
106
114
  - CHANGELOG.md
107
115
  - LICENSE
108
116
  - README.md
109
117
  - SECURITY.md
118
+ - docs/architecture.md
119
+ - ext/safe_image_vips_helper/extconf.rb
120
+ - ext/safe_image_vips_helper/safe_image_vips_helper.c
110
121
  - lib/safe_image.rb
111
122
  - lib/safe_image/RT_sRGB.icm
112
- - lib/safe_image/discourse_compat.rb
123
+ - lib/safe_image/api/metadata.rb
124
+ - lib/safe_image/api/transform.rb
125
+ - lib/safe_image/backend_label.rb
113
126
  - lib/safe_image/fonts/DEJAVU-LICENSE
114
127
  - lib/safe_image/fonts/DejaVuSans.ttf
128
+ - lib/safe_image/formats.rb
115
129
  - lib/safe_image/ico.rb
116
130
  - lib/safe_image/image_magick_backend.rb
117
131
  - lib/safe_image/imagemagick_policy/policy.xml
118
132
  - lib/safe_image/jpegli_backend.rb
133
+ - lib/safe_image/metadata_operations.rb
119
134
  - lib/safe_image/native.rb
135
+ - lib/safe_image/native_helper.rb
136
+ - lib/safe_image/operation_backends.rb
137
+ - lib/safe_image/operation_backends/base.rb
138
+ - lib/safe_image/operation_backends/image_magick.rb
139
+ - lib/safe_image/operation_backends/vips.rb
140
+ - lib/safe_image/operation_set.rb
120
141
  - lib/safe_image/optimizer.rb
121
142
  - lib/safe_image/path_safety.rb
122
143
  - lib/safe_image/processor.rb
144
+ - lib/safe_image/quality_defaults.rb
123
145
  - lib/safe_image/remote.rb
124
146
  - lib/safe_image/result.rb
125
147
  - lib/safe_image/runner.rb
126
148
  - lib/safe_image/sandbox.rb
149
+ - lib/safe_image/staged_output.rb
127
150
  - lib/safe_image/svg_metadata.rb
128
- - lib/safe_image/svg_sanitizer.rb
151
+ - lib/safe_image/transform_operations.rb
129
152
  - lib/safe_image/version.rb
130
153
  - lib/safe_image/vips_backend.rb
131
- - lib/safe_image/vips_glue.rb
132
154
  homepage: https://github.com/sam-saffron-jarvis/safe-image
133
155
  licenses:
134
156
  - MIT