Module: SafeImage::JpegliBackend

Defined in:
lib/safe_image/jpegli_backend.rb

Constant Summary collapse

DIRECT_INPUTS =
Formats::CJPEGLI_DIRECT_INPUTS
CHROMA_SUBSAMPLING =
%w[420 422 444].freeze
DEFAULT_QUALITY =
QualityDefaults::JPEG

Class Method Summary collapse

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


11
12
13
# File 'lib/safe_image/jpegli_backend.rb', line 11

def available?
  Runner.available?("cjpegli")
end

.convert(input:, output:, quality: DEFAULT_QUALITY, chroma_subsampling: :auto, timeout: Runner::DEFAULT_TIMEOUT) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/safe_image/jpegli_backend.rb', line 19

def convert(input:, output:, quality: DEFAULT_QUALITY, chroma_subsampling: :auto, timeout: Runner::DEFAULT_TIMEOUT)
  raise UnsupportedFormatError, "cjpegli is not installed" unless available?

  input = PathSafety.ensure_regular_file!(input)
  output = PathSafety.ensure_safe_output_path!(output).to_s
  ensure_jpeg_output!(output)

  input_format = normalized_ext(input)
  if DIRECT_INPUTS.none? { |candidate| candidate == input_format }
    raise UnsupportedFormatError, "cjpegli direct input format is unsupported: #{input_format.inspect}"
  end

  quality = validate_quality!(quality)
  chroma_subsampling = validate_chroma_subsampling!(chroma_subsampling, input_format: input_format)
  encode(
    input: input,
    output: output,
    quality: quality,
    chroma_subsampling: chroma_subsampling,
    timeout: timeout,
    input_format: input_format
  )
end

.encode(input:, output:, quality: DEFAULT_QUALITY, chroma_subsampling: "420", timeout: Runner::DEFAULT_TIMEOUT, input_format: nil) ⇒ Object



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
# File 'lib/safe_image/jpegli_backend.rb', line 57

def encode(
  input:,
  output:,
  quality: DEFAULT_QUALITY,
  chroma_subsampling: "420",
  timeout: Runner::DEFAULT_TIMEOUT,
  input_format: nil
)
  raise UnsupportedFormatError, "cjpegli is not installed" unless available?

  input = PathSafety.ensure_regular_file!(input)
  output_path = PathSafety.ensure_safe_output_path!(output)
  ensure_jpeg_output!(output_path)
  output_path.dirname.mkpath

  input_format ||= normalized_ext(input)
  quality = validate_quality!(quality)
  chroma_subsampling = validate_chroma_subsampling!(chroma_subsampling, input_format: input_format)

  info =
    StagedOutput.replace(output_path, suffix: ".cjpegli.jpg") do |tmp_path|
      argv = [
        "cjpegli",
        input.to_s,
        tmp_path.to_s,
        "--quality=#{quality}",
        "--chroma_subsampling=#{chroma_subsampling}"
      ]
      Runner.run!(argv, timeout: timeout, read: [input.to_s], write: [output_path.dirname.to_s])
      raise Error, "cjpegli did not create output" unless tmp_path.file? && File.size(tmp_path).positive?

      # cjpegli works without libvips in the Ruby process; fall back to
      # identify when the helper/native header read is unavailable.
      info = Native.available? ? Native.probe(tmp_path.to_s) : ImageMagickBackend.probe(tmp_path.to_s)
      {
        input_format: input_format,
        output_format: "jpg",
        width: info.fetch(:width),
        height: info.fetch(:height),
        duration_ms: info.fetch(:duration_ms),
        encoder: "cjpegli",
        chroma_subsampling: chroma_subsampling
      }
    end
  info
end

.encode_generated_jpeg(output:, quality: DEFAULT_QUALITY, chroma_subsampling: :auto, input_format: nil) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/safe_image/jpegli_backend.rb', line 43

def encode_generated_jpeg(output:, quality: DEFAULT_QUALITY, chroma_subsampling: :auto, input_format: nil)
  StagedOutput.with_temp_path_near(output, suffix: ".safe-image.png") do |tmp_path|
    decoded = yield tmp_path
    source_format = input_format || decoded.fetch(:input_format)
    encode(
      input: tmp_path,
      output: output,
      quality: quality,
      chroma_subsampling: validate_chroma_subsampling!(chroma_subsampling, input_format: source_format),
      input_format: source_format
    )
  end
end

.ensure_jpeg_output!(output) ⇒ Object



122
123
124
125
# File 'lib/safe_image/jpegli_backend.rb', line 122

def ensure_jpeg_output!(output)
  ext = normalized_ext(output)
  raise UnsupportedFormatError, "cjpegli only outputs jpg/jpeg, got #{ext.inspect}" unless ext == "jpg"
end

.normalized_ext(path) ⇒ Object



127
128
129
# File 'lib/safe_image/jpegli_backend.rb', line 127

def normalized_ext(path)
  Formats.extension(path)
end

.suitable_direct_input?(input) ⇒ Boolean

Returns:

  • (Boolean)


15
16
17
# File 'lib/safe_image/jpegli_backend.rb', line 15

def suitable_direct_input?(input)
  Formats.cjpegli_direct_input?(normalized_ext(input))
end

.validate_chroma_subsampling!(value, input_format: nil) ⇒ Object



111
112
113
114
115
116
117
118
119
120
# File 'lib/safe_image/jpegli_backend.rb', line 111

def validate_chroma_subsampling!(value, input_format: nil)
  value = :auto if value.nil?
  value = "444" if value.to_sym == :auto && input_format.to_s == "png"
  value = "420" if value.to_sym == :auto
  value = value.to_s
  if CHROMA_SUBSAMPLING.none? { |candidate| candidate == value }
    raise ArgumentError, "chroma_subsampling must be one of #{CHROMA_SUBSAMPLING.join(", ")}"
  end
  value
end

.validate_quality!(quality) ⇒ Object

Raises:

  • (ArgumentError)


104
105
106
107
108
109
# File 'lib/safe_image/jpegli_backend.rb', line 104

def validate_quality!(quality)
  quality = DEFAULT_QUALITY if quality.nil?
  quality = Integer(quality)
  raise ArgumentError, "quality must be 1..100" unless (1..100).cover?(quality)
  quality
end