jekyll-imgflow 0.3.3 → 0.4.1

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.
@@ -7,31 +7,22 @@ require_relative "base_provider"
7
7
  module JekyllImgFlow
8
8
  module Providers
9
9
  # ImageMagick provider implementation using the standardized tag interface
10
- class Imagemagick < BaseProvider
10
+ class Imagemagick < CliBase
11
11
  # Cache rsvg delegate check across all instances (checked once per build)
12
12
  @rsvg_available = nil
13
13
  @svg_warning_shown = false
14
14
 
15
15
  def available?
16
- # Check if magick or convert CLI is available
17
- _, _, status1 = Open3.capture3("which", "magick")
18
- _, _, status2 = Open3.capture3("which", "convert")
19
- status1.success? || status2.success?
16
+ cli_available?("magick", "convert")
20
17
  end
21
18
 
22
- def execute(input_path, output_path)
23
- return if @operations.empty?
24
-
19
+ def before_execute(input_path)
25
20
  # Warn once per build about SVG performance with ImageMagick
26
21
  warn_svg_performance if svg?(input_path) && !self.class.instance_variable_get(:@svg_warning_shown)
22
+ end
27
23
 
28
- # Build single ImageMagick command with all operations combined
29
- command = build_combined_imagemagick_command(input_path, output_path)
30
- execute_command(command)
31
-
32
- output_path
33
- ensure
34
- reset_operations
24
+ def build_commands(input_path, output_path)
25
+ build_combined_imagemagick_command(input_path, output_path)
35
26
  end
36
27
 
37
28
  def build_combined_imagemagick_command(input_path, output_path)
@@ -47,85 +38,7 @@ module JekyllImgFlow
47
38
  ["magick", input_path.shellescape]
48
39
  end
49
40
 
50
- # Add all operations
51
- @operations.each do |operation|
52
- case operation[:type]
53
- when :resize
54
- # Tags now provide complete calculated values
55
- command_parts << "-resize" << if operation[:height]
56
- "#{operation[:width]}x#{operation[:height]}!"
57
- else
58
- operation[:width].to_s
59
- end
60
-
61
- when :crop
62
- opts = operation[:options]
63
- params = operation[:params] || {}
64
-
65
- # Check for keep parameter (ImageMagick doesn't support smartcrop, but we handle it gracefully)
66
- keep = opts[:keep] || params[:keep] || params[:position]
67
-
68
- if operation[:ratio] && keep && SMARTCROP_POSITIONS.include?(keep.to_s)
69
- # ImageMagick doesn't support smartcrop, but we handle the keep parameter
70
- # Use center gravity as a reasonable fallback for smartcrop requests
71
- crop_width = opts[:calculated_width]
72
- crop_height = opts[:calculated_height]
73
- crop_x = opts[:calculated_x]
74
- crop_y = opts[:calculated_y]
75
-
76
- # Use gravity center for smartcrop-like behavior
77
- crop_spec = "#{crop_width}x#{crop_height}+#{crop_x}+#{crop_y}"
78
- command_parts << "-gravity" << "center"
79
- else
80
- # Use basic cropping
81
- if operation[:ratio]
82
- crop_x = opts[:calculated_x]
83
- crop_y = opts[:calculated_y]
84
- crop_width = opts[:calculated_width]
85
- crop_height = opts[:calculated_height]
86
- else
87
- crop_x = opts[:x]
88
- crop_y = opts[:y]
89
- crop_width = opts[:width]
90
- crop_height = opts[:height]
91
- end
92
- crop_spec = "#{crop_width}x#{crop_height}+#{crop_x}+#{crop_y}"
93
- end
94
- command_parts << "-crop" << crop_spec
95
-
96
- when :quality
97
- magick_quality = translate_quality_to_imagemagick(operation[:quality])
98
- command_parts << "-quality" << magick_quality.to_s
99
-
100
- when :format
101
- # Assume validated input from tags
102
- # Format is handled by output filename extension
103
- # Quality is set separately if needed
104
- unless @operations.any? { |op| op[:type] == :quality }
105
- default_quality = @config&.quality || raise("No quality configured")
106
- command_parts << "-quality" << default_quality.to_s
107
- end
108
-
109
- when :watermark
110
- watermark_path = operation[:watermark_path]
111
- position = operation[:options][:position]
112
-
113
- # Translate compass directions to ImageMagick gravity format
114
- gravity = translate_position(position)
115
-
116
- # Add watermark as composite operation
117
- command_parts << watermark_path.shellescape
118
- command_parts << "-gravity" << gravity
119
- command_parts << "-composite"
120
-
121
- when :alpha_opacity
122
- opacity = operation[:opacity]
123
- alpha_value = (opacity * 100).round
124
- command_parts << "-alpha" << "set"
125
- command_parts << "-channel" << "A"
126
- command_parts << "-evaluate" << "multiply" << "#{alpha_value}%"
127
- end
128
- end
41
+ @operations.each { |operation| append_imagemagick_operation(operation, command_parts) }
129
42
 
130
43
  # Add output filename
131
44
  command_parts << output_path.shellescape
@@ -146,9 +59,56 @@ module JekyllImgFlow
146
59
 
147
60
  private
148
61
 
149
- def translate_quality_to_imagemagick(quality)
150
- # ImageMagick uses 1-100 directly, no translation needed
151
- quality
62
+ def append_imagemagick_operation(operation, command_parts)
63
+ case operation[:type]
64
+ when :resize
65
+ command_parts << "-resize" << if operation[:height]
66
+ "#{operation[:width]}x#{operation[:height]}!"
67
+ else
68
+ operation[:width].to_s
69
+ end
70
+ when :crop
71
+ append_imagemagick_crop(operation, command_parts)
72
+ when :quality
73
+ command_parts << "-quality" << operation[:quality].to_s
74
+ when :format
75
+ # Format is handled by output filename extension.
76
+ # Quality is set separately if needed.
77
+ unless op?(:quality)
78
+ default_quality = @config&.quality || raise("No quality configured")
79
+ command_parts << "-quality" << default_quality.to_s
80
+ end
81
+ when :watermark
82
+ append_imagemagick_watermark(operation, command_parts)
83
+ when :alpha_opacity
84
+ alpha_value = (operation[:opacity] * 100).round
85
+ command_parts << "-alpha" << "set"
86
+ command_parts << "-channel" << "A"
87
+ command_parts << "-evaluate" << "multiply" << "#{alpha_value}%"
88
+ end
89
+ end
90
+
91
+ def append_imagemagick_crop(operation, command_parts)
92
+ geo = crop_geometry(operation)
93
+
94
+ if geo[:smartcrop]
95
+ # ImageMagick doesn't support smartcrop, but we handle the keep parameter
96
+ # Use center gravity as a reasonable fallback for smartcrop requests
97
+ command_parts << "-gravity" << "center"
98
+ end
99
+
100
+ crop_spec = "#{geo[:width]}x#{geo[:height]}+#{geo[:x]}+#{geo[:y]}"
101
+ command_parts << "-crop" << crop_spec
102
+ end
103
+
104
+ def append_imagemagick_watermark(operation, command_parts)
105
+ parts = watermark_parts(operation)
106
+ gravity = translate_position(parts[:position])
107
+
108
+ # Add watermark as composite operation
109
+ command_parts << parts[:watermark_path].shellescape
110
+ command_parts << "-gravity" << gravity
111
+ command_parts << "-composite"
152
112
  end
153
113
 
154
114
  # Choose a rasterization density (DPI) for SVG input.
@@ -2,162 +2,89 @@
2
2
 
3
3
  require "uri"
4
4
  require "net/http"
5
- require "tempfile"
6
5
  require_relative "base_provider"
7
6
 
8
7
  module JekyllImgFlow
9
8
  module Providers
10
9
  # Imgproxy provider implementation using the standardized tag interface
11
- class Imgproxy < BaseProvider
10
+ class Imgproxy < HttpBase
12
11
  TIMEOUT = 10 # seconds
13
12
 
14
- def available?
15
- # Check if imgproxy service is running
16
- return false unless @config.respond_to?(:imgproxy_url) && @config.imgproxy_url
17
-
18
- check_http_service(@config.imgproxy_url)
13
+ def service_url
14
+ @config.respond_to?(:imgproxy_url) ? @config.imgproxy_url : nil
19
15
  end
20
16
 
21
- def execute(input_path, output_path)
22
- return if @operations.empty?
23
-
24
- # Build single Imgproxy URL with all operations combined
25
- url = build_combined_imgproxy_url(input_path)
26
-
27
- # Fetch the result in one request
28
- fetch_and_save(url, output_path)
29
- output_path
30
- ensure
31
- reset_operations
17
+ def build_combined_imgproxy_url(input_path)
18
+ build_combined_url(input_path)
32
19
  end
33
20
 
34
- def build_combined_imgproxy_url(input_path)
35
- raise "imgproxy_url not set" unless @config.imgproxy_url
21
+ def build_combined_url(input_path)
22
+ raise "imgproxy_url not set" unless service_url
36
23
 
37
24
  encoded_url = encode_file_url(input_path)
38
25
  operations = []
39
26
 
40
- # Add all operations to single URL path
41
- @operations.each do |operation|
42
- case operation[:type]
43
- when :resize
44
- # Tags now provide complete calculated values
45
- operations << if operation[:height]
46
- "rs:fill:#{operation[:width]}:#{operation[:height]}:1"
47
- else
48
- "rs:fit:#{operation[:width]}::1"
49
- end
50
-
51
- when :crop
52
- opts = operation[:options]
53
- params = operation[:params] || {}
54
-
55
- # Check for keep parameter (smartcrop support)
56
- keep = opts[:keep] || params[:keep] || params[:position]
57
-
58
- if operation[:ratio] && keep && SMARTCROP_POSITIONS.include?(keep.to_s)
59
- # Use smartcrop with gravity:sm (libvips smart crop)
60
- crop_width = opts[:calculated_width]
61
- crop_height = opts[:calculated_height]
62
-
63
- operations << "g:sm"
64
- else
65
- # Use basic cropping with explicit coordinates
66
- if operation[:ratio]
67
- crop_x = opts[:calculated_x]
68
- crop_y = opts[:calculated_y]
69
- crop_width = opts[:calculated_width]
70
- crop_height = opts[:calculated_height]
71
- else
72
- crop_x = opts[:x]
73
- crop_y = opts[:y]
74
- crop_width = opts[:width]
75
- crop_height = opts[:height]
76
- end
77
- # imgproxy crop format: c:width:height:gravity
78
- # Use nowe (north-west) gravity with x/y offsets for pixel-precise cropping
79
- operations << "g:nowe:#{crop_x}:#{crop_y}"
80
- end
81
- operations << "c:#{crop_width}:#{crop_height}"
82
-
83
- when :quality
84
- imgproxy_quality = translate_quality_to_imgproxy(operation[:quality])
85
- operations << "q:#{imgproxy_quality}"
86
-
87
- when :format
88
- # Assume validated input from tags
89
- operations << "f:#{operation[:format]}"
90
-
91
- when :watermark
92
- watermark_path = operation[:watermark_path]
93
- position = operation[:options][:position]
94
- operation[:options][:opacity]
95
-
96
- # Imgproxy watermark parameters
97
- encoded_watermark = encode_file_url(watermark_path)
98
- position_param = translate_imgproxy_position(position)
99
- operations << "wm:#{encoded_watermark}:#{position_param}:10:10"
100
-
101
- when :alpha_opacity
102
- opacity = operation[:opacity]
103
- operations << "a:#{(opacity * 255).round}"
104
- end
105
- end
27
+ @operations.each { |operation| append_imgproxy_operation(operation, operations) }
106
28
 
107
- # Preserve format when no explicit format operation is requested.
108
- # Without this, chained HTTP requests (e.g. quality-only steps after a
109
- # prior format conversion) would lose the previously converted format.
110
- unless @operations.any? { |op| op[:type] == :format }
111
- input_ext = File.extname(input_path).delete(".").downcase
112
- operations << "f:#{input_ext}" unless input_ext.empty?
113
- end
114
-
115
- # Build combined URL: base/insecure/operations/operations/.../plain/encoded_url
29
+ preserve_input_format(input_path, operations)
116
30
  operations_path = operations.join("/")
117
- "#{@config.imgproxy_url}/insecure/#{operations_path}/plain/#{encoded_url}"
31
+ "#{service_url}/insecure/#{operations_path}/plain/#{encoded_url}"
118
32
  end
119
33
 
120
34
  private
121
35
 
122
- def translate_imgproxy_position(position)
123
- # Translate compass directions to Imgproxy position format
124
- case position
125
- when "northwest" then "tl"
126
- when "northeast" then "tr"
127
- when "southwest" then "bl"
128
- when "southeast" then "br"
129
- when "center" then "c"
130
- else position
36
+ def append_imgproxy_operation(operation, operations)
37
+ case operation[:type]
38
+ when :resize
39
+ operations << if operation[:height]
40
+ "rs:fill:#{operation[:width]}:#{operation[:height]}:1"
41
+ else
42
+ "rs:fit:#{operation[:width]}::1"
43
+ end
44
+ when :crop
45
+ append_imgproxy_crop(operation, operations)
46
+ when :quality
47
+ operations << "q:#{operation[:quality]}"
48
+ when :format
49
+ operations << "f:#{operation[:format]}"
50
+ when :watermark
51
+ append_imgproxy_watermark(operation, operations)
52
+ when :alpha_opacity
53
+ operations << "a:#{alpha_byte_value(operation[:opacity])}"
131
54
  end
132
55
  end
133
56
 
134
- def fetch_and_save(url, output_path)
135
- response = fetch_with_timeout(url)
136
- raise "Imgproxy request failed" if response.empty?
137
-
138
- FileUtils.mkdir_p(File.dirname(output_path))
139
- File.write(output_path, response)
57
+ def append_imgproxy_crop(operation, operations)
58
+ geo = crop_geometry(operation)
59
+
60
+ operations << if geo[:smartcrop]
61
+ # Use smartcrop with gravity:sm (libvips smart crop)
62
+ "g:sm"
63
+ else
64
+ # imgproxy crop format: c:width:height:gravity
65
+ # Use nowe (north-west) gravity with x/y offsets for pixel-precise cropping
66
+ "g:nowe:#{geo[:x]}:#{geo[:y]}"
67
+ end
68
+ operations << "c:#{geo[:width]}:#{geo[:height]}"
140
69
  end
141
70
 
142
- def fetch_with_timeout(url)
143
- uri = URI(url)
144
- Net::HTTP.start(uri.host, uri.port,
145
- use_ssl: uri.scheme == "https",
146
- open_timeout: TIMEOUT,
147
- read_timeout: TIMEOUT) do |http|
148
- request = Net::HTTP::Get.new(uri)
149
- response = http.request(request)
71
+ def append_imgproxy_watermark(operation, operations)
72
+ parts = watermark_parts(operation)
73
+ encoded_watermark = encode_file_url(parts[:watermark_path])
74
+ position_param = compass_to_short(parts[:position])
75
+ operations << "wm:#{encoded_watermark}:#{position_param}:10:10"
76
+ end
150
77
 
151
- raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
78
+ def preserve_input_format(input_path, operations)
79
+ return if op?(:format)
152
80
 
153
- response.body
154
- end
155
- rescue StandardError => e
156
- raise "Imgproxy request failed: #{e.message}"
81
+ ext = input_format_ext(input_path)
82
+ operations << "f:#{ext}" unless ext.empty?
157
83
  end
158
84
 
159
- def translate_quality_to_imgproxy(quality)
160
- quality
85
+ # Thin wrapper kept for test compatibility.
86
+ def translate_imgproxy_position(position)
87
+ compass_to_short(position)
161
88
  end
162
89
  end
163
90
  end
@@ -7,24 +7,17 @@ require_relative "base_provider"
7
7
  module JekyllImgFlow
8
8
  module Providers
9
9
  # Libvips provider implementation using the standardized tag interface
10
- class Libvips < BaseProvider
10
+ class Libvips < CliBase
11
11
  def available?
12
- # Check if vips CLI is available
13
- _, _, status = Open3.capture3("which", "vips")
14
- status.success?
12
+ cli_available?("vips")
15
13
  end
16
14
 
17
- def execute(input_path, output_path)
18
- return if @operations.empty?
19
-
20
- # Build and execute vips commands (array form, no shell).
21
- # Cleanup markers ([:cleanup, path]) are handled by execute_command.
22
- commands = build_vips_commands(input_path, output_path)
23
- commands.each { |cmd_array| execute_command(cmd_array) }
15
+ def build_commands(input_path, output_path)
16
+ build_vips_commands(input_path, output_path)
17
+ end
24
18
 
25
- output_path
26
- ensure
27
- reset_operations
19
+ def run_command(cmd)
20
+ execute_command(cmd)
28
21
  end
29
22
 
30
23
  # Build all vips commands as an array of command arrays.
@@ -48,10 +41,10 @@ module JekyllImgFlow
48
41
 
49
42
  def operation_flags
50
43
  {
51
- crop: @operations.any? { |op| op[:type] == :crop },
52
- resize: @operations.any? { |op| op[:type] == :resize },
53
- watermark: @operations.any? { |op| op[:type] == :watermark },
54
- alpha: @operations.any? { |op| op[:type] == :alpha_opacity }
44
+ crop: op?(:crop),
45
+ resize: op?(:resize),
46
+ watermark: op?(:watermark),
47
+ alpha: op?(:alpha_opacity)
55
48
  }
56
49
  end
57
50
 
@@ -72,19 +65,19 @@ module JekyllImgFlow
72
65
  end
73
66
 
74
67
  def build_alpha_pipeline(input_path, output_path, has_crop, has_resize)
75
- temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_base.jpg")
76
- commands, base_path = build_base_pipeline(input_path, temp_path, has_crop, has_resize)
68
+ temp = temp_path(input_path, "tmp_base.jpg")
69
+ commands, base_path = build_base_pipeline(input_path, temp, has_crop, has_resize)
77
70
  commands << build_alpha_command(base_path, output_path)
78
- commands << [:cleanup, temp_path] unless base_path == input_path
71
+ commands << [:cleanup, temp] unless base_path == input_path
79
72
  commands
80
73
  end
81
74
 
82
75
  def build_watermark_pipeline(input_path, output_path, has_crop, has_resize)
83
- temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_base.jpg")
84
- commands, base_path = build_base_pipeline(input_path, temp_path, has_crop, has_resize)
76
+ temp = temp_path(input_path, "tmp_base.jpg")
77
+ commands, base_path = build_base_pipeline(input_path, temp, has_crop, has_resize)
85
78
  base_path, commands = apply_watermark_alpha(base_path, input_path, commands)
86
79
  commands.concat(build_composite_commands(base_path, output_path))
87
- cleanup_paths(commands, input_path, temp_path, base_path)
80
+ cleanup_paths(commands, input_path, temp, base_path)
88
81
  commands
89
82
  end
90
83
 
@@ -97,10 +90,10 @@ module JekyllImgFlow
97
90
  end
98
91
 
99
92
  def apply_watermark_alpha(base_path, input_path, commands)
100
- alpha_op = @operations.find { |op| op[:type] == :alpha_opacity }
93
+ alpha_op = find_op(:alpha_opacity)
101
94
  return [base_path, commands] unless alpha_op
102
95
 
103
- alpha_temp = input_path.gsub(/\.[^.]+$/, ".tmp_alpha.jpg")
96
+ alpha_temp = temp_path(input_path, "tmp_alpha.jpg")
104
97
  [alpha_temp, commands << build_alpha_command(base_path, alpha_temp)]
105
98
  end
106
99
 
@@ -110,26 +103,24 @@ module JekyllImgFlow
110
103
  end
111
104
 
112
105
  def build_composite_commands(base_path, output_path)
113
- wm_op = @operations.find { |op| op[:type] == :watermark }
114
- watermark_path = wm_op[:watermark_path]
115
- position = wm_op[:options][:position]
116
- opacity = wm_op[:options][:opacity]
117
-
118
- if opacity && opacity < 1.0
119
- wm_temp = watermark_path.gsub(/\.[^.]+$/, ".tmp_wm.png")
120
- alpha_value = opacity
106
+ wm_op = find_op(:watermark)
107
+ parts = watermark_parts(wm_op)
108
+
109
+ if parts[:opacity] && parts[:opacity] < 1.0
110
+ wm_temp = temp_path(parts[:watermark_path], "tmp_wm.png")
111
+ alpha_value = parts[:opacity]
121
112
  # Step 1: Apply alpha to watermark
122
- alpha_cmd = ["vips", "linear", watermark_path,
113
+ alpha_cmd = ["vips", "linear", parts[:watermark_path],
123
114
  "#{wm_temp}[alpha]", "1 1 1 #{alpha_value}", "0"]
124
115
  # Step 2: Composite
125
- xy_args = position_to_vips_xy(position, base_path, watermark_path)
116
+ xy_args = position_to_vips_xy(parts[:position], base_path, parts[:watermark_path])
126
117
  composite_cmd = ["vips", "composite2", base_path, wm_temp,
127
118
  build_format_spec(output_path), "over"] + xy_args
128
119
  # Step 3: Cleanup wm_temp
129
120
  [alpha_cmd, composite_cmd, [:cleanup, wm_temp]]
130
121
  else
131
- xy_args = position_to_vips_xy(position, base_path, watermark_path)
132
- [["vips", "composite2", base_path, watermark_path,
122
+ xy_args = position_to_vips_xy(parts[:position], base_path, parts[:watermark_path])
123
+ [["vips", "composite2", base_path, parts[:watermark_path],
133
124
  build_format_spec(output_path), "over"] + xy_args]
134
125
  end
135
126
  end
@@ -168,9 +159,8 @@ module JekyllImgFlow
168
159
  end
169
160
 
170
161
  def build_alpha_command(input_path, output_path)
171
- alpha_op = @operations.find { |op| op[:type] == :alpha_opacity }
172
- opacity = alpha_op[:opacity]
173
- alpha_value = opacity.round(2)
162
+ alpha_op = find_op(:alpha_opacity)
163
+ alpha_value = alpha_op[:opacity].round(2)
174
164
 
175
165
  # vips linear multiplies alpha band: "1 1 1 alpha" scales alpha
176
166
  ["vips", "linear", input_path, output_path,
@@ -180,22 +170,22 @@ module JekyllImgFlow
180
170
  def build_sequential_crop_resize(input_path, output_path)
181
171
  if svg?(input_path)
182
172
  # SVG: use thumbnail with --crop to do crop+resize in one step
183
- resize_op = @operations.find { |op| op[:type] == :resize }
173
+ resize_op = find_op(:resize)
184
174
  format_spec = build_format_spec(output_path)
185
175
  width = resize_op[:width]
186
176
  height = resize_op[:height]
187
177
  [["vips", "thumbnail", input_path, format_spec,
188
178
  width.to_s, "--height=#{height}", "--crop=attention"]]
189
179
  else
190
- temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_crop.jpg")
191
- [build_crop_command(input_path, temp_path),
192
- build_resize_command(temp_path, output_path),
193
- [:cleanup, temp_path]]
180
+ temp = temp_path(input_path, "tmp_crop.jpg")
181
+ [build_crop_command(input_path, temp),
182
+ build_resize_command(temp, output_path),
183
+ [:cleanup, temp]]
194
184
  end
195
185
  end
196
186
 
197
187
  def build_resize_command(input_path, output_path)
198
- resize_op = @operations.find { |op| op[:type] == :resize }
188
+ resize_op = find_op(:resize)
199
189
  format_spec = build_format_spec(output_path)
200
190
  width = resize_op[:width]
201
191
  height = resize_op[:height]
@@ -216,35 +206,23 @@ module JekyllImgFlow
216
206
  end
217
207
 
218
208
  def build_crop_command(input_path, output_path)
219
- crop_op = @operations.find { |op| op[:type] == :crop }
220
- opts = crop_op[:options] || {}
221
- params = crop_op[:params] || {}
222
- keep = opts[:keep] || params[:keep] || params[:position]
223
- return build_smartcrop_command(crop_op, opts, keep, input_path, output_path) if smartcrop?(crop_op, keep)
209
+ crop_op = find_op(:crop)
210
+ geo = crop_geometry(crop_op)
211
+ return build_smartcrop_command(geo, input_path, output_path) if geo[:smartcrop]
224
212
 
225
- build_extract_command(crop_op, opts, input_path, output_path)
213
+ build_extract_command(geo, input_path, output_path)
226
214
  end
227
215
 
228
- def smartcrop?(crop_op, keep)
229
- crop_op[:ratio] && keep && %w[attention entropy center centre].include?(keep.to_s)
230
- end
231
-
232
- def build_smartcrop_command(_crop_op, opts, keep, input_path, output_path)
233
- interestingness = %w[center centre].include?(keep.to_s) ? "centre" : keep.to_s
234
- interestingness = "attention" unless %w[entropy centre].include?(interestingness)
216
+ def build_smartcrop_command(geo, input_path, output_path)
217
+ interestingness = smartcrop_interestingness(geo[:keep])
235
218
  ["vips", "smartcrop", input_path, output_path,
236
- opts[:calculated_width].to_s, opts[:calculated_height].to_s,
219
+ geo[:width].to_s, geo[:height].to_s,
237
220
  "--interesting=#{interestingness}"]
238
221
  end
239
222
 
240
- def build_extract_command(crop_op, opts, input_path, output_path)
241
- ratio = crop_op[:ratio]
242
- x = ratio ? opts[:calculated_x] : (opts[:x] || 0)
243
- y = ratio ? opts[:calculated_y] : (opts[:y] || 0)
244
- width = ratio ? opts[:calculated_width] : opts[:width]
245
- height = ratio ? opts[:calculated_height] : opts[:height]
223
+ def build_extract_command(geo, input_path, output_path)
246
224
  ["vips", "extract_area", input_path, output_path,
247
- x.to_s, y.to_s, width.to_s, height.to_s]
225
+ geo[:x].to_s, geo[:y].to_s, geo[:width].to_s, geo[:height].to_s]
248
226
  end
249
227
 
250
228
  # SVG crop: use `vips thumbnail` with --crop to crop during thumbnailing.
@@ -254,18 +232,12 @@ module JekyllImgFlow
254
232
  # the most interesting region — this is the correct behavior for SVGs
255
233
  # since they have no fixed pixel dimensions.
256
234
  def build_svg_crop_pipeline(input_path, output_path)
257
- crop_op = @operations.find { |op| op[:type] == :crop }
258
- opts = crop_op[:options] || {}
259
- crop_width = crop_op[:ratio] ? opts[:calculated_width] : (opts[:width] || 1000)
260
- crop_height = crop_op[:ratio] ? opts[:calculated_height] : (opts[:height] || 1000)
235
+ crop_op = find_op(:crop)
236
+ geo = crop_geometry(crop_op)
237
+ crop_width = geo[:width] || 1000
238
+ crop_height = geo[:height] || 1000
261
239
  format_spec = build_format_spec(output_path)
262
-
263
- keep = opts[:keep] || crop_op[:params]&.[](:keep) || crop_op[:params]&.[](:position)
264
- interestingness = case keep.to_s
265
- when "entropy" then "entropy"
266
- when "center", "centre" then "centre"
267
- else "attention"
268
- end
240
+ interestingness = smartcrop_interestingness(geo[:keep])
269
241
 
270
242
  [["vips", "thumbnail", input_path, format_spec,
271
243
  crop_width.to_s, "--height=#{crop_height}",
@@ -287,8 +259,8 @@ module JekyllImgFlow
287
259
  # are executed via Open3.capture3 array form (no shell).
288
260
  def build_format_spec(output_path)
289
261
  # Build vips output specification with format and quality
290
- format_op = @operations.find { |op| op[:type] == :format }
291
- quality_op = @operations.find { |op| op[:type] == :quality }
262
+ format_op = find_op(:format)
263
+ quality_op = find_op(:quality)
292
264
 
293
265
  # Simple case: no format/quality operations
294
266
  return output_path unless format_op || quality_op