Class: SafeImage::Processor

Inherits:
Object
  • Object
show all
Defined in:
lib/safe_image/processor.rb

Constant Summary collapse

SUPPORTED_INPUTS =
%w[jpg jpeg png webp heic heif avif].freeze
SUPPORTED_OUTPUTS =
%w[jpg jpeg png webp avif].freeze

Instance Method Summary collapse

Constructor Details

#initialize(max_pixels: nil, backend: :vips, execution: :inline, encoder: :auto, chroma_subsampling: :auto) ⇒ Processor

Returns a new instance of Processor.



12
13
14
15
16
17
18
# File 'lib/safe_image/processor.rb', line 12

def initialize(max_pixels: nil, backend: :vips, execution: :inline, encoder: :auto, chroma_subsampling: :auto)
  @max_pixels = max_pixels
  @backend = backend.to_sym
  @execution = execution.to_sym
  @encoder = encoder.to_sym
  @chroma_subsampling = chroma_subsampling
end

Instance Method Details

#probe(path) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/safe_image/processor.rb', line 20

def probe(path)
  input = safe_existing_file!(path)
  info = Native.probe(input.to_s)
  validate_pixels!(info.fetch(:width), info.fetch(:height))
  Result.new(
    input: input.to_s,
    output: nil,
    input_format: info.fetch(:format),
    output_format: nil,
    width: info.fetch(:width),
    height: info.fetch(:height),
    filesize: File.size(input),
    backend: "libvips-direct",
    duration_ms: info.fetch(:duration_ms),
    optimizer: nil
  )
end

#thumbnail(input:, output:, width:, height:, format: nil, quality: 85, optimize: false, optimize_mode: :lossless) ⇒ Object

Raises:

  • (ArgumentError)


38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/safe_image/processor.rb', line 38

def thumbnail(input:, output:, width:, height:, format: nil, quality: 85, optimize: false, optimize_mode: :lossless)
  input = safe_existing_file!(input)
  output = safe_output_path!(output)
  width = Integer(width)
  height = Integer(height)
  quality = Integer(quality)
  raise ArgumentError, "width and height must be positive" if width <= 0 || height <= 0
  raise ArgumentError, "quality must be 1..100" unless (1..100).cover?(quality)

  out_format = (format || output.extname.delete_prefix(".")).downcase
  out_format = "jpg" if out_format == "jpeg"
  unless SUPPORTED_OUTPUTS.include?(out_format)
    raise UnsupportedFormatError, "unsupported output format: #{out_format.inspect}"
  end

  if @execution == :sandbox || @execution == :sandbox_if_available
    if @execution == :sandbox && !Sandbox.available?
      raise Error, "sandbox execution requested but Landlock::SafeExec is unavailable"
    end

    info = Sandbox.thumbnail(
      input: input.to_s,
      output: output.to_s,
      width: width,
      height: height,
      format: out_format,
      quality: quality,
      max_pixels: @max_pixels,
      backend: @backend,
      optimize: optimize,
      optimize_mode: optimize_mode
    )
    if info
      return Result.new(
        input: input.to_s,
        output: output.to_s,
        input_format: info.fetch(:input_format),
        output_format: info.fetch(:output_format),
        width: info.fetch(:width),
        height: info.fetch(:height),
        filesize: File.size(output),
        backend: "sandboxed-#{info.fetch(:backend)}",
        duration_ms: info.fetch(:duration_ms),
        optimizer: info[:optimizer]
      )
    end
  end

  output.dirname.mkpath
  info =
    if out_format == "jpg" && use_jpegli_for_generated_jpeg?(input)
      jpegli_thumbnail(input: input, output: output, width: width, height: height, quality: quality, source_format: input.extname.delete_prefix(".").downcase)
    else
      case @backend
      when :vips
        Native.thumbnail(input.to_s, output.to_s, width, height, out_format, quality, @max_pixels)
      when :imagemagick, :magick
        probe_info = Native.probe(input.to_s)
        validate_pixels!(probe_info.fetch(:width), probe_info.fetch(:height))
        ImageMagickBackend.thumbnail(
          input: input.to_s,
          output: output.to_s,
          width: width,
          height: height,
          format: out_format,
          quality: quality
        )
      else
        raise ArgumentError, "unknown backend: #{@backend.inspect}"
      end
    end

  opt_info = nil
  if optimize
    opt_info = Optimizer.optimize(output, mode: optimize_mode, strip_metadata: true, quality: out_format == "jpg" ? quality : nil)
  end

  Result.new(
    input: input.to_s,
    output: output.to_s,
    input_format: info.fetch(:input_format),
    output_format: info.fetch(:output_format),
    width: info.fetch(:width),
    height: info.fetch(:height),
    filesize: File.size(output),
    backend: result_backend(info),
    duration_ms: info.fetch(:duration_ms),
    optimizer: opt_info&.fetch(:tools, nil)
  )
end