Module: SafeImage

Defined in:
lib/safe_image.rb,
lib/safe_image.rb,
lib/safe_image/remote.rb,
lib/safe_image/result.rb,
lib/safe_image/runner.rb,
lib/safe_image/sandbox.rb,
lib/safe_image/version.rb,
lib/safe_image/optimizer.rb,
lib/safe_image/processor.rb,
lib/safe_image/path_safety.rb,
lib/safe_image/svg_metadata.rb,
lib/safe_image/vips_backend.rb,
lib/safe_image/svg_sanitizer.rb,
lib/safe_image/jpegli_backend.rb,
lib/safe_image/discourse_compat.rb,
lib/safe_image/image_magick_backend.rb,
ext/safe_image_native/safe_image_native.c

Defined Under Namespace

Modules: DiscourseCompat, ImageMagickBackend, JpegliBackend, Native, Optimizer, PathSafety, Remote, Runner, Sandbox, SvgMetadata, SvgSanitizer, VipsBackend Classes: CommandError, Error, Info, InvalidImageError, LimitError, Processor, Result, UnsafePathError, UnsupportedFormatError

Constant Summary collapse

DEFAULT_MAX_PIXELS =

Default decompression-bomb ceiling for the libvips processing path when the caller does not pass an explicit max_pixels. Mirrored in the native extension (SAFE_IMAGE_DEFAULT_MAX_PIXELS) and aligned with the 128MP area limit on the ImageMagick path. Pass max_pixels to raise or lower it.

128 * 1024 * 1024
VERSION =
"0.1.0"

Class Method Summary collapse

Class Method Details

.animated?(*args, **kwargs) ⇒ Boolean

Returns:

  • (Boolean)


254
255
256
257
258
259
# File 'lib/safe_image.rb', line 254

def animated?(*args, **kwargs)
  path = args.first
  return false if path && File.extname(PathSafety.local_path(path)).downcase == ".svg"

  maybe_sandbox(:animated?, args: args, kwargs: kwargs) { DiscourseCompat.animated?(*args, **kwargs) }
end

.convert(*args, **kwargs) ⇒ Object



234
235
236
# File 'lib/safe_image.rb', line 234

def convert(*args, **kwargs)
  maybe_sandbox(:convert, args: args, kwargs: kwargs) { DiscourseCompat.convert(*args, **kwargs) }
end

.convert_favicon_to_png(*args, **kwargs) ⇒ Object



246
247
248
# File 'lib/safe_image.rb', line 246

def convert_favicon_to_png(*args, **kwargs)
  maybe_sandbox(:convert_favicon_to_png, args: args, kwargs: kwargs) { DiscourseCompat.convert_favicon_to_png(*args, **kwargs) }
end

.convert_to_jpeg(*args, **kwargs) ⇒ Object



238
239
240
# File 'lib/safe_image.rb', line 238

def convert_to_jpeg(*args, **kwargs)
  maybe_sandbox(:convert_to_jpeg, args: args, kwargs: kwargs) { DiscourseCompat.convert_to_jpeg(*args, **kwargs) }
end

.crop(*args, **kwargs) ⇒ Object



226
227
228
# File 'lib/safe_image.rb', line 226

def crop(*args, **kwargs)
  maybe_sandbox(:crop, args: args, kwargs: kwargs) { DiscourseCompat.crop(*args, **kwargs) }
end

.dimensions(path, max_pixels: nil) ⇒ Object



125
126
127
# File 'lib/safe_image.rb', line 125

def dimensions(path, max_pixels: nil)
  size(path, max_pixels: max_pixels)
end

.disable_sandbox!Object



44
45
46
# File 'lib/safe_image.rb', line 44

def disable_sandbox!
  @sandbox_enabled = false
end

.downsize(*args, **kwargs) ⇒ Object



230
231
232
# File 'lib/safe_image.rb', line 230

def downsize(*args, **kwargs)
  maybe_sandbox(:downsize, args: args, kwargs: kwargs) { DiscourseCompat.downsize(*args, **kwargs) }
end

.enable_sandbox!Object

Raises:



39
40
41
42
# File 'lib/safe_image.rb', line 39

def enable_sandbox!
  raise Error, "landlock sandbox requested but unavailable" unless Sandbox.available?
  @sandbox_enabled = true
end

.fastimage_type(format) ⇒ Object



156
157
158
# File 'lib/safe_image.rb', line 156

def fastimage_type(format)
  format.to_s == "jpg" ? :jpeg : format.to_s.to_sym
end

.fetch_remote(url, **kwargs, &block) ⇒ Object



180
181
182
# File 'lib/safe_image.rb', line 180

def fetch_remote(url, **kwargs, &block)
  Remote.fetch(url, **kwargs, &block)
end

.fix_orientation(*args, **kwargs) ⇒ Object



242
243
244
# File 'lib/safe_image.rb', line 242

def fix_orientation(*args, **kwargs)
  maybe_sandbox(:fix_orientation, args: args, kwargs: kwargs) { DiscourseCompat.fix_orientation(*args, **kwargs) }
end

.frame_count(*args, **kwargs) ⇒ Object



250
251
252
# File 'lib/safe_image.rb', line 250

def frame_count(*args, **kwargs)
  maybe_sandbox(:frame_count, args: args, kwargs: kwargs) { DiscourseCompat.frame_count(*args, **kwargs) }
end

.info(path, max_pixels: nil, animated: false, orientation: false) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/safe_image.rb', line 129

def info(path, max_pixels: nil, animated: false, orientation: false)
  maybe_sandbox(:info, args: [path], kwargs: { max_pixels: max_pixels, animated: animated, orientation: orientation }) do
    result = probe(path, max_pixels: max_pixels)
    type = fastimage_type(result.input_format)
    Info.new(
      path: result.input,
      type: type,
      width: result.width,
      height: result.height,
      size: [result.width, result.height],
      animated: animated ? animated?(path, max_pixels: max_pixels) : nil,
      orientation: orientation ? orientation(path, max_pixels: max_pixels) : nil
    )
  end
end

.letter_avatar(*args, **kwargs) ⇒ Object



261
262
263
# File 'lib/safe_image.rb', line 261

def letter_avatar(*args, **kwargs)
  maybe_sandbox(:letter_avatar, args: args, kwargs: kwargs) { DiscourseCompat.letter_avatar(*args, **kwargs) }
end

.maybe_sandbox(operation, args: [], kwargs: {}) ⇒ Object



66
67
68
69
70
# File 'lib/safe_image.rb', line 66

def maybe_sandbox(operation, args: [], kwargs: {})
  return yield unless sandbox_enabled?

  sandbox_call(operation, args: args, kwargs: kwargs)
end

.optimize(path, mode: :lossless, strip_metadata: true, quality: nil, strict: true) ⇒ Object



216
217
218
219
220
# File 'lib/safe_image.rb', line 216

def optimize(path, mode: :lossless, strip_metadata: true, quality: nil, strict: true)
  maybe_sandbox(:optimize, args: [path], kwargs: { mode: mode, strip_metadata: , quality: quality, strict: strict }) do
    Optimizer.optimize(path, mode: mode, strip_metadata: , quality: quality, strict: strict)
  end
end

.optimize_image!(*args, **kwargs) ⇒ Object



265
266
267
# File 'lib/safe_image.rb', line 265

def optimize_image!(*args, **kwargs)
  maybe_sandbox(:optimize_image!, args: args, kwargs: kwargs) { DiscourseCompat.optimize_image!(*args, **kwargs) }
end

.orientation(path, max_pixels: nil) ⇒ Object



145
146
147
148
149
150
151
152
153
154
# File 'lib/safe_image.rb', line 145

def orientation(path, max_pixels: nil)
  maybe_sandbox(:orientation, args: [path], kwargs: { max_pixels: max_pixels }) do
    if File.extname(PathSafety.local_path(path)).downcase == ".svg"
      1
    else
      probe(path, max_pixels: max_pixels) if max_pixels
      ImageMagickBackend.orientation(path)
    end
  end
end

.probe(path, max_pixels: nil) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/safe_image.rb', line 72

def probe(path, max_pixels: nil)
  maybe_sandbox(:probe, args: [path], kwargs: { max_pixels: max_pixels }) do
    path = PathSafety.local_path(path)

    if File.extname(path).downcase == ".svg"
      info = SvgMetadata.probe(path, max_pixels: max_pixels)
      Result.new(
        input: File.expand_path(path),
        output: nil,
        input_format: "svg",
        output_format: nil,
        width: info.fetch(:width),
        height: info.fetch(:height),
        filesize: File.size(path),
        backend: "svg-metadata",
        duration_ms: info.fetch(:duration_ms),
        optimizer: nil
      )
    else
      begin
        Processor.new(max_pixels: max_pixels).probe(path)
      rescue UnsupportedFormatError
        info = ImageMagickBackend.probe(path, max_pixels: max_pixels)
        Result.new(
          input: File.expand_path(path),
          output: nil,
          input_format: info.fetch(:input_format),
          output_format: nil,
          width: info.fetch(:width),
          height: info.fetch(:height),
          filesize: File.size(path),
          backend: "imagemagick",
          duration_ms: info.fetch(:duration_ms),
          optimizer: nil
        )
      end
    end
  end
end

.remote_animated?(url, **kwargs) ⇒ Boolean

Returns:

  • (Boolean)


176
177
178
# File 'lib/safe_image.rb', line 176

def remote_animated?(url, **kwargs)
  Remote.animated?(url, **kwargs)
end

.remote_dimensions(url, **kwargs) ⇒ Object



168
169
170
# File 'lib/safe_image.rb', line 168

def remote_dimensions(url, **kwargs)
  remote_size(url, **kwargs)
end

.remote_info(url, **kwargs) ⇒ Object



160
161
162
# File 'lib/safe_image.rb', line 160

def remote_info(url, **kwargs)
  Remote.info(url, **kwargs)
end

.remote_size(url, **kwargs) ⇒ Object



164
165
166
# File 'lib/safe_image.rb', line 164

def remote_size(url, **kwargs)
  Remote.size(url, **kwargs)
end

.remote_type(url, **kwargs) ⇒ Object



172
173
174
# File 'lib/safe_image.rb', line 172

def remote_type(url, **kwargs)
  Remote.type(url, **kwargs)
end

.resize(*args, **kwargs) ⇒ Object



222
223
224
# File 'lib/safe_image.rb', line 222

def resize(*args, **kwargs)
  maybe_sandbox(:resize, args: args, kwargs: kwargs) { DiscourseCompat.resize(*args, **kwargs) }
end

.sandbox_available?Boolean

Returns:

  • (Boolean)


60
# File 'lib/safe_image.rb', line 60

def sandbox_available? = Sandbox.available?

.sandbox_call(operation, args: [], kwargs: {}) ⇒ Object



62
63
64
# File 'lib/safe_image.rb', line 62

def sandbox_call(operation, args: [], kwargs: {})
  Sandbox.public_call!(operation, args: args, kwargs: kwargs)
end

.sandbox_enabled?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/safe_image.rb', line 48

def sandbox_enabled?
  @sandbox_enabled && ENV["SAFE_IMAGE_SANDBOX_CHILD"] != "1"
end

.sanitize_svg!(*args, **kwargs) ⇒ Object



269
270
271
# File 'lib/safe_image.rb', line 269

def sanitize_svg!(*args, **kwargs)
  maybe_sandbox(:sanitize_svg!, args: args, kwargs: kwargs) { SvgSanitizer.sanitize!(*args, **kwargs) }
end

.size(path, max_pixels: nil) ⇒ Object



118
119
120
121
122
123
# File 'lib/safe_image.rb', line 118

def size(path, max_pixels: nil)
  maybe_sandbox(:size, args: [path], kwargs: { max_pixels: max_pixels }) do
    result = probe(path, max_pixels: max_pixels)
    [result.width, result.height]
  end
end

.thumbnail(input:, output:, width:, height:, format: nil, quality: 85, max_pixels: nil, backend: :vips, optimize: false, optimize_mode: :lossless, execution: :inline, encoder: :auto, chroma_subsampling: :auto) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/safe_image.rb', line 184

def thumbnail(input:, output:, width:, height:, format: nil, quality: 85, max_pixels: nil, backend: :vips, optimize: false, optimize_mode: :lossless, execution: :inline, encoder: :auto, chroma_subsampling: :auto)
  maybe_sandbox(
    :thumbnail,
    kwargs: {
      input: input,
      output: output,
      width: width,
      height: height,
      format: format,
      quality: quality,
      max_pixels: max_pixels,
      backend: backend,
      optimize: optimize,
      optimize_mode: optimize_mode,
      execution: :inline,
      encoder: encoder,
      chroma_subsampling: chroma_subsampling
    }
  ) do
    Processor.new(max_pixels: max_pixels, backend: backend, execution: execution, encoder: encoder, chroma_subsampling: chroma_subsampling).thumbnail(
      input: input,
      output: output,
      width: width,
      height: height,
      format: format,
      quality: quality,
      optimize: optimize,
      optimize_mode: optimize_mode
    )
  end
end

.type(path, max_pixels: nil) ⇒ Object



112
113
114
115
116
# File 'lib/safe_image.rb', line 112

def type(path, max_pixels: nil)
  maybe_sandbox(:type, args: [path], kwargs: { max_pixels: max_pixels }) do
    fastimage_type(probe(path, max_pixels: max_pixels).input_format)
  end
end

.with_sandbox_disabledObject



52
53
54
55
56
57
58
# File 'lib/safe_image.rb', line 52

def with_sandbox_disabled
  previous = @sandbox_enabled
  @sandbox_enabled = false
  yield
ensure
  @sandbox_enabled = previous
end