jekyll-imgflow 0.3.3 → 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.
@@ -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
@@ -7,37 +7,23 @@ require_relative "base_provider"
7
7
  module JekyllImgFlow
8
8
  module Providers
9
9
  # Sharp provider implementation using the standardized tag interface
10
- class Sharp < BaseProvider
10
+ class Sharp < CliBase
11
11
  def available?
12
- # Check if sharp CLI is available
13
- _, _, status = Open3.capture3("which", "sharp")
14
- status.success?
12
+ cli_available?("sharp")
15
13
  end
16
14
 
17
- def execute(input_path, output_path)
18
- return if @operations.empty?
19
-
20
- # Build Sharp command
21
- command = build_sharp_command(input_path, output_path)
22
- execute_command(command)
23
-
24
- output_path
25
- ensure
26
- reset_operations
15
+ def build_commands(input_path, output_path)
16
+ build_sharp_command(input_path, output_path)
27
17
  end
28
18
 
29
19
  def build_sharp_command(input_path, output_path)
30
- has_crop = @operations.any? { |op| op[:type] == :crop }
31
- has_resize = @operations.any? { |op| op[:type] == :resize }
32
- has_watermark = @operations.any? { |op| op[:type] == :watermark }
33
-
34
- if has_watermark
35
- build_watermark_pipeline(input_path, output_path, has_crop, has_resize)
36
- elsif has_crop && has_resize
20
+ if op?(:watermark)
21
+ build_watermark_pipeline(input_path, output_path, op?(:crop), op?(:resize))
22
+ elsif op?(:crop) && op?(:resize)
37
23
  build_sequential_crop_resize(input_path, output_path)
38
- elsif has_resize
24
+ elsif op?(:resize)
39
25
  build_resize_command(input_path, output_path)
40
- elsif has_crop
26
+ elsif op?(:crop)
41
27
  build_crop_command(input_path, output_path)
42
28
  else
43
29
  build_copy_command(input_path, output_path)
@@ -45,47 +31,41 @@ module JekyllImgFlow
45
31
  end
46
32
 
47
33
  def build_watermark_pipeline(input_path, output_path, has_crop, has_resize)
48
- temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_base.jpg")
34
+ temp = temp_path(input_path, "tmp_base.jpg")
49
35
  commands = []
50
36
 
51
37
  if has_crop && has_resize
52
- commands << build_sequential_crop_resize(input_path, temp_path)
53
- base_path = temp_path
38
+ commands << build_sequential_crop_resize(input_path, temp)
39
+ base_path = temp
54
40
  elsif has_resize
55
- commands << build_resize_command(input_path, temp_path)
56
- base_path = temp_path
41
+ commands << build_resize_command(input_path, temp)
42
+ base_path = temp
57
43
  elsif has_crop
58
- commands << build_crop_command(input_path, temp_path)
59
- base_path = temp_path
44
+ commands << build_crop_command(input_path, temp)
45
+ base_path = temp
60
46
  else
61
47
  base_path = input_path
62
48
  end
63
49
 
64
50
  commands << build_composite_command(base_path, output_path)
65
- commands << "rm -f #{temp_path.shellescape}" unless base_path == input_path
51
+ commands << "rm -f #{temp.shellescape}" unless base_path == input_path
66
52
  commands.join(" && ")
67
53
  end
68
54
 
69
55
  def build_composite_command(base_path, output_path)
70
- wm_op = @operations.find { |op| op[:type] == :watermark }
71
- watermark_path = wm_op[:watermark_path]
72
- position = wm_op[:options][:position]
73
- opacity = wm_op[:options][:opacity]
74
-
75
- gravity = translate_position(position)
56
+ wm_op = find_op(:watermark)
57
+ parts = watermark_parts(wm_op)
58
+ gravity = translate_position(parts[:position])
76
59
 
77
60
  command_parts = ["sharp", "-i", base_path.shellescape,
78
61
  "-o", output_path.shellescape]
79
62
 
80
- format_op = @operations.find { |op| op[:type] == :format }
81
- quality_op = @operations.find { |op| op[:type] == :quality }
82
- command_parts += ["-f", sharp_format(format_op[:format])] if format_op
83
- command_parts += ["-q#{quality_op[:quality]}"] if quality_op
63
+ add_format_quality(command_parts)
84
64
 
85
- if opacity && opacity < 1.0
86
- wm_temp = watermark_path.gsub(/\.[^.]+$/, ".tmp_wm.png")
87
- alpha_value = (opacity * 255).round
88
- temp_cmd = ["sharp", "-i", watermark_path.shellescape,
65
+ if parts[:opacity] && parts[:opacity] < 1.0
66
+ wm_temp = temp_path(parts[:watermark_path], "tmp_wm.png")
67
+ alpha_value = alpha_byte_value(parts[:opacity])
68
+ temp_cmd = ["sharp", "-i", parts[:watermark_path].shellescape,
89
69
  "-o", wm_temp.shellescape,
90
70
  "ensureAlpha", alpha_value.to_s].join(" ")
91
71
  command_parts += ["composite", wm_temp.shellescape,
@@ -93,7 +73,7 @@ module JekyllImgFlow
93
73
  "#{temp_cmd} && #{command_parts.join(' ')} " \
94
74
  "&& rm -f #{wm_temp.shellescape}"
95
75
  else
96
- command_parts += ["composite", watermark_path.shellescape,
76
+ command_parts += ["composite", parts[:watermark_path].shellescape,
97
77
  "--gravity", gravity, "--blend", "over"]
98
78
  command_parts.join(" ")
99
79
  end
@@ -112,20 +92,20 @@ module JekyllImgFlow
112
92
 
113
93
  def build_sequential_crop_resize(input_path, output_path)
114
94
  # Build temp file path for intermediate crop result
115
- temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_crop.jpg")
95
+ temp = temp_path(input_path, "tmp_crop.jpg")
116
96
 
117
97
  # First command: crop only
118
- crop_cmd = build_crop_command(input_path, temp_path)
98
+ crop_cmd = build_crop_command(input_path, temp)
119
99
 
120
100
  # Second command: resize + format + quality + alpha (all together)
121
- resize_cmd = build_resize_command(temp_path, output_path)
101
+ resize_cmd = build_resize_command(temp, output_path)
122
102
 
123
103
  # Combine with && and cleanup temp file
124
- "#{crop_cmd} && #{resize_cmd} && rm -f #{temp_path.shellescape}"
104
+ "#{crop_cmd} && #{resize_cmd} && rm -f #{temp.shellescape}"
125
105
  end
126
106
 
127
107
  def build_resize_command(input_path, output_path)
128
- resize_op = @operations.find { |op| op[:type] == :resize }
108
+ resize_op = find_op(:resize)
129
109
 
130
110
  # sharp -i input.jpg -o output.jpg resize width [height] -f format -q quality
131
111
  command_parts = ["sharp", "-i", input_path.shellescape, "-o", output_path.shellescape]
@@ -137,58 +117,28 @@ module JekyllImgFlow
137
117
  ["resize", resize_op[:width].to_s]
138
118
  end
139
119
 
140
- # Add format, quality, alpha
141
- format_op = @operations.find { |op| op[:type] == :format }
142
- quality_op = @operations.find { |op| op[:type] == :quality }
143
- alpha_op = @operations.find { |op| op[:type] == :alpha_opacity }
144
-
145
- command_parts += ["-f", sharp_format(format_op[:format])] if format_op
146
- command_parts += ["-q#{quality_op[:quality]}"] if quality_op
147
- if alpha_op
148
- alpha_value = (alpha_op[:opacity] * 255).round
149
- command_parts += ["alpha", "{alpha:#{alpha_value}}"]
150
- end
151
-
120
+ add_format_quality_alpha(command_parts)
152
121
  command_parts.join(" ")
153
122
  end
154
123
 
155
124
  def build_crop_command(input_path, output_path)
156
- crop_op = @operations.find { |op| op[:type] == :crop }
157
- opts = crop_op[:options] || {}
158
- params = crop_op[:params] || {}
125
+ crop_op = find_op(:crop)
126
+ geo = crop_geometry(crop_op)
159
127
 
160
- # Check if we should use smartcrop (when keep parameter is specified)
161
- # JPT keep options: attention (default), entropy, center
162
- # Check both options (from CropTag) and params (from OperationProcessor)
163
- keep = opts[:keep] || params[:keep] || params[:position]
164
-
165
- if crop_op[:ratio] && keep && %w[attention entropy center
166
- centre].include?(keep.to_s)
128
+ if geo[:smartcrop]
167
129
  # Use smartcrop for intelligent cropping (Sharp uses libvips backend)
168
- crop_width = opts[:calculated_width]
169
- crop_height = opts[:calculated_height]
170
-
171
- # Map keep parameter to libvips interestingness
172
- interestingness = case keep.to_s
173
- when "entropy" then "entropy"
174
- when "center", "centre" then "centre"
175
- else "attention" # default
176
- end
130
+ interestingness = smartcrop_interestingness(geo[:keep])
177
131
 
178
132
  # sharp -i input.jpg -o output.jpg smartcrop width height --interesting=attention
179
133
  ["sharp", "-i", input_path.shellescape, "-o", output_path.shellescape,
180
- "smartcrop", crop_width.to_s, crop_height.to_s,
134
+ "smartcrop", geo[:width].to_s, geo[:height].to_s,
181
135
  "--interesting=#{interestingness}"].join(" ")
182
136
  else
183
137
  # Use basic extract for manual cropping or when no keep parameter
184
- crop_x = crop_op[:ratio] ? opts[:calculated_x] : (opts[:x] || 0)
185
- crop_y = crop_op[:ratio] ? opts[:calculated_y] : (opts[:y] || 0)
186
- crop_width = crop_op[:ratio] ? opts[:calculated_width] : opts[:width]
187
- crop_height = crop_op[:ratio] ? opts[:calculated_height] : opts[:height]
188
-
189
138
  # sharp -i input.jpg -o output.jpg extract top left width height
190
139
  ["sharp", "-i", input_path.shellescape, "-o", output_path.shellescape,
191
- "extract", crop_y.to_s, crop_x.to_s, crop_width.to_s, crop_height.to_s].join(" ")
140
+ "extract", geo[:y].to_s, geo[:x].to_s, geo[:width].to_s,
141
+ geo[:height].to_s].join(" ")
192
142
  end
193
143
  end
194
144
 
@@ -196,17 +146,7 @@ module JekyllImgFlow
196
146
  # sharp -i input.jpg -o output.jpg -f format -q quality
197
147
  command_parts = ["sharp", "-i", input_path.shellescape, "-o", output_path.shellescape]
198
148
 
199
- format_op = @operations.find { |op| op[:type] == :format }
200
- quality_op = @operations.find { |op| op[:type] == :quality }
201
- alpha_op = @operations.find { |op| op[:type] == :alpha_opacity }
202
-
203
- command_parts += ["-f", sharp_format(format_op[:format])] if format_op
204
- command_parts += ["-q#{quality_op[:quality]}"] if quality_op
205
- if alpha_op
206
- alpha_value = (alpha_op[:opacity] * 255).round
207
- command_parts += ["alpha", "{alpha:#{alpha_value}}"]
208
- end
209
-
149
+ add_format_quality_alpha(command_parts)
210
150
  command_parts.join(" ")
211
151
  end
212
152
 
@@ -215,6 +155,23 @@ module JekyllImgFlow
215
155
  def sharp_format(format)
216
156
  format.to_s == "jpg" ? "jpeg" : format
217
157
  end
158
+
159
+ private
160
+
161
+ def add_format_quality(command_parts)
162
+ format_op = find_op(:format)
163
+ quality_op = find_op(:quality)
164
+ command_parts.push("-f", sharp_format(format_op[:format])) if format_op
165
+ command_parts.push("-q#{quality_op[:quality]}") if quality_op
166
+ end
167
+
168
+ def add_format_quality_alpha(command_parts)
169
+ add_format_quality(command_parts)
170
+ alpha_op = find_op(:alpha_opacity)
171
+ return unless alpha_op
172
+
173
+ command_parts.push("alpha", "{alpha:#{alpha_byte_value(alpha_op[:opacity])}}")
174
+ end
218
175
  end
219
176
  end
220
177
  end