jekyll-imgflow 0.1.9 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +24 -13
- data/lib/jekyll-imgflow/batch_manager.rb +10 -7
- data/lib/jekyll-imgflow/build_time_processor.rb +70 -61
- data/lib/jekyll-imgflow/config.rb +3 -6
- data/lib/jekyll-imgflow/filename_generator.rb +8 -10
- data/lib/jekyll-imgflow/generated_file_cleaner.rb +81 -0
- data/lib/jekyll-imgflow/hooks.rb +13 -6
- data/lib/jekyll-imgflow/html_generator.rb +14 -19
- data/lib/jekyll-imgflow/imgflow_tag.rb +84 -69
- data/lib/jekyll-imgflow/manifest_manager.rb +132 -53
- data/lib/jekyll-imgflow/operation_processor.rb +43 -22
- data/lib/jekyll-imgflow/picture_tag_adaptor.rb +1 -1
- data/lib/jekyll-imgflow/preset_manager.rb +58 -7
- data/lib/jekyll-imgflow/presets/gallery.yml +7 -0
- data/lib/jekyll-imgflow/presets/hero.yml +7 -0
- data/lib/jekyll-imgflow/presets/thumbnail.yml +7 -0
- data/lib/jekyll-imgflow/processing_stats.rb +80 -0
- data/lib/jekyll-imgflow/provider_registry.rb +18 -3
- data/lib/jekyll-imgflow/providers/base_provider.rb +12 -1
- data/lib/jekyll-imgflow/providers/flyimg.rb +2 -1
- data/lib/jekyll-imgflow/providers/imagemagick.rb +72 -3
- data/lib/jekyll-imgflow/providers/imgproxy.rb +2 -1
- data/lib/jekyll-imgflow/providers/libvips.rb +152 -100
- data/lib/jekyll-imgflow/providers/sharp.rb +2 -1
- data/lib/jekyll-imgflow/providers/weserv.rb +3 -2
- data/lib/jekyll-imgflow/tags/opacity_tag.rb +8 -1
- data/lib/jekyll-imgflow/tags/resize_tag.rb +2 -0
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +8 -1
- data/lib/jekyll-imgflow/tasks.rb +129 -0
- data/lib/jekyll-imgflow/version.rb +1 -1
- data/lib/jekyll-imgflow.rb +2 -0
- metadata +24 -156
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "shellwords"
|
|
4
3
|
require "open3"
|
|
4
|
+
require "fileutils"
|
|
5
5
|
require_relative "base_provider"
|
|
6
6
|
|
|
7
7
|
module JekyllImgFlow
|
|
@@ -17,15 +17,20 @@ module JekyllImgFlow
|
|
|
17
17
|
def execute(input_path, output_path)
|
|
18
18
|
return if @operations.empty?
|
|
19
19
|
|
|
20
|
-
# Build
|
|
21
|
-
|
|
22
|
-
|
|
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) }
|
|
23
24
|
|
|
24
|
-
reset_operations
|
|
25
25
|
output_path
|
|
26
|
+
ensure
|
|
27
|
+
reset_operations
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
# Build all vips commands as an array of command arrays.
|
|
31
|
+
# Each sub-array is passed directly to Open3.capture3 (no shell).
|
|
32
|
+
# Returns Array[Array[String]].
|
|
33
|
+
def build_vips_commands(input_path, output_path)
|
|
29
34
|
has_crop = @operations.any? { |op| op[:type] == :crop }
|
|
30
35
|
has_resize = @operations.any? { |op| op[:type] == :resize }
|
|
31
36
|
has_watermark = @operations.any? { |op| op[:type] == :watermark }
|
|
@@ -38,20 +43,32 @@ module JekyllImgFlow
|
|
|
38
43
|
elsif has_crop && has_resize
|
|
39
44
|
build_sequential_crop_resize(input_path, output_path)
|
|
40
45
|
elsif has_resize
|
|
41
|
-
build_resize_command(input_path, output_path)
|
|
46
|
+
[build_resize_command(input_path, output_path)]
|
|
42
47
|
elsif has_crop
|
|
43
|
-
build_crop_command(input_path, output_path)
|
|
48
|
+
svg?(input_path) ? build_svg_crop_pipeline(input_path, output_path) : [build_crop_command(input_path, output_path)]
|
|
44
49
|
else
|
|
45
|
-
build_copy_command(input_path, output_path)
|
|
50
|
+
[build_copy_command(input_path, output_path)]
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
|
|
54
|
+
# Convenience wrapper for backward compatibility with tests.
|
|
55
|
+
# Returns the commands joined as a debug string.
|
|
56
|
+
def build_vips_command(input_path, output_path)
|
|
57
|
+
build_vips_commands(input_path, output_path).map do |cmd|
|
|
58
|
+
if cmd.is_a?(Array) && cmd.first == :cleanup
|
|
59
|
+
"rm -f #{cmd[1]}"
|
|
60
|
+
else
|
|
61
|
+
cmd.join(" ")
|
|
62
|
+
end
|
|
63
|
+
end.join(" && ")
|
|
64
|
+
end
|
|
65
|
+
|
|
49
66
|
def build_alpha_pipeline(input_path, output_path, has_crop, has_resize)
|
|
50
67
|
commands = []
|
|
51
68
|
temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_base.jpg")
|
|
52
69
|
|
|
53
70
|
if has_crop && has_resize
|
|
54
|
-
commands
|
|
71
|
+
commands.concat(build_sequential_crop_resize(input_path, temp_path))
|
|
55
72
|
base_path = temp_path
|
|
56
73
|
elsif has_resize
|
|
57
74
|
commands << build_resize_command(input_path, temp_path)
|
|
@@ -64,19 +81,18 @@ module JekyllImgFlow
|
|
|
64
81
|
end
|
|
65
82
|
|
|
66
83
|
commands << build_alpha_command(base_path, output_path)
|
|
67
|
-
commands <<
|
|
68
|
-
commands
|
|
84
|
+
commands << [:cleanup, temp_path] unless base_path == input_path
|
|
85
|
+
commands
|
|
69
86
|
end
|
|
70
87
|
|
|
71
88
|
def build_watermark_pipeline(input_path, output_path, has_crop, has_resize)
|
|
72
89
|
temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_base.jpg")
|
|
73
|
-
final_path = output_path
|
|
74
90
|
|
|
75
91
|
commands = []
|
|
76
92
|
|
|
77
93
|
# Step 1: Process crop/resize/copy to produce base image
|
|
78
94
|
if has_crop && has_resize
|
|
79
|
-
commands
|
|
95
|
+
commands.concat(build_sequential_crop_resize(input_path, temp_path))
|
|
80
96
|
base_path = temp_path
|
|
81
97
|
elsif has_resize
|
|
82
98
|
commands << build_resize_command(input_path, temp_path)
|
|
@@ -97,106 +113,115 @@ module JekyllImgFlow
|
|
|
97
113
|
end
|
|
98
114
|
|
|
99
115
|
# Step 3: Composite watermark over base
|
|
100
|
-
commands
|
|
116
|
+
commands.concat(build_composite_commands(base_path, output_path))
|
|
101
117
|
|
|
102
118
|
# Step 4: Cleanup temp files
|
|
103
119
|
temp_files = [temp_path, base_path == input_path ? nil : base_path].compact
|
|
104
|
-
temp_files.each { |f| commands <<
|
|
120
|
+
temp_files.each { |f| commands << [:cleanup, f] }
|
|
105
121
|
|
|
106
|
-
commands
|
|
122
|
+
commands
|
|
107
123
|
end
|
|
108
124
|
|
|
109
|
-
def
|
|
125
|
+
def build_composite_commands(base_path, output_path)
|
|
110
126
|
wm_op = @operations.find { |op| op[:type] == :watermark }
|
|
111
127
|
watermark_path = wm_op[:watermark_path]
|
|
112
128
|
position = wm_op[:options][:position]
|
|
113
129
|
opacity = wm_op[:options][:opacity]
|
|
114
|
-
format_spec = build_format_spec(output_path)
|
|
115
130
|
|
|
116
131
|
if opacity && opacity < 1.0
|
|
117
132
|
wm_temp = watermark_path.gsub(/\.[^.]+$/, ".tmp_wm.png")
|
|
118
133
|
alpha_value = opacity
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
# Step 1: Apply alpha to watermark
|
|
135
|
+
alpha_cmd = ["vips", "linear", watermark_path,
|
|
136
|
+
"#{wm_temp}[alpha]", "1 1 1 #{alpha_value}", "0"]
|
|
137
|
+
# Step 2: Composite
|
|
138
|
+
xy_args = position_to_vips_xy(position, base_path, watermark_path)
|
|
139
|
+
composite_cmd = ["vips", "composite2", base_path, wm_temp,
|
|
140
|
+
build_format_spec(output_path), "over"] + xy_args
|
|
141
|
+
# Step 3: Cleanup wm_temp
|
|
142
|
+
[alpha_cmd, composite_cmd, [:cleanup, wm_temp]]
|
|
127
143
|
else
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
xy_args = position_to_vips_xy(position, base_path, watermark_path)
|
|
145
|
+
[["vips", "composite2", base_path, watermark_path,
|
|
146
|
+
build_format_spec(output_path), "over"] + xy_args]
|
|
131
147
|
end
|
|
132
148
|
end
|
|
133
149
|
|
|
150
|
+
# Return x/y arguments as an array of strings (no shell substitution).
|
|
151
|
+
# Pre-computes image dimensions via vips header in Ruby.
|
|
134
152
|
def position_to_vips_xy(position, _base_path, watermark_path)
|
|
135
|
-
# Get image dimensions to calculate x/y from compass positions
|
|
136
|
-
# vips composite2 uses --x and --y for overlay placement
|
|
137
153
|
case position.to_s
|
|
138
154
|
when "northeast", "top-right"
|
|
139
|
-
|
|
140
|
-
"--x
|
|
155
|
+
width = vips_image_dimension(watermark_path, "width")
|
|
156
|
+
["--x", width.to_s, "--y", "0"]
|
|
141
157
|
when "southwest", "bottom-left"
|
|
142
|
-
|
|
158
|
+
height = vips_image_dimension(watermark_path, "height")
|
|
159
|
+
["--x", "0", "--y", height.to_s]
|
|
143
160
|
when "southeast", "bottom-right"
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
width = vips_image_dimension(watermark_path, "width")
|
|
162
|
+
height = vips_image_dimension(watermark_path, "height")
|
|
163
|
+
["--x", width.to_s, "--y", height.to_s]
|
|
146
164
|
else
|
|
147
|
-
"--x 0 --y 0" # northwest, top-left, center, centre, and default
|
|
165
|
+
["--x", "0", "--y", "0"] # northwest, top-left, center, centre, and default
|
|
148
166
|
end
|
|
149
167
|
end
|
|
150
168
|
|
|
169
|
+
# Get an image dimension (width or height) using vips header.
|
|
170
|
+
# Returns integer or 0 if vips is unavailable.
|
|
171
|
+
def vips_image_dimension(image_path, field)
|
|
172
|
+
stdout, _, status = Open3.capture3("vips", "header", image_path, field)
|
|
173
|
+
return 0 unless status.success?
|
|
174
|
+
|
|
175
|
+
stdout.strip.to_i
|
|
176
|
+
rescue StandardError
|
|
177
|
+
0
|
|
178
|
+
end
|
|
179
|
+
|
|
151
180
|
def build_alpha_command(input_path, output_path)
|
|
152
181
|
alpha_op = @operations.find { |op| op[:type] == :alpha_opacity }
|
|
153
182
|
opacity = alpha_op[:opacity]
|
|
154
183
|
alpha_value = opacity.round(2)
|
|
155
184
|
|
|
156
185
|
# vips linear multiplies alpha band: "1 1 1 alpha" scales alpha
|
|
157
|
-
["vips", "linear", input_path
|
|
158
|
-
"
|
|
186
|
+
["vips", "linear", input_path, output_path,
|
|
187
|
+
"1 1 1 #{alpha_value}", "0"]
|
|
159
188
|
end
|
|
160
189
|
|
|
161
190
|
def build_sequential_crop_resize(input_path, output_path)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
if svg?(input_path)
|
|
192
|
+
# SVG: use thumbnail with --crop to do crop+resize in one step
|
|
193
|
+
resize_op = @operations.find { |op| op[:type] == :resize }
|
|
194
|
+
format_spec = build_format_spec(output_path)
|
|
195
|
+
width = resize_op[:width]
|
|
196
|
+
height = resize_op[:height]
|
|
197
|
+
[["vips", "thumbnail", input_path, format_spec,
|
|
198
|
+
width.to_s, "--height=#{height}", "--crop=attention"]]
|
|
199
|
+
else
|
|
200
|
+
temp_path = input_path.gsub(/\.[^.]+$/, ".tmp_crop.jpg")
|
|
201
|
+
[build_crop_command(input_path, temp_path),
|
|
202
|
+
build_resize_command(temp_path, output_path),
|
|
203
|
+
[:cleanup, temp_path]]
|
|
204
|
+
end
|
|
173
205
|
end
|
|
174
206
|
|
|
175
207
|
def build_resize_command(input_path, output_path)
|
|
176
208
|
resize_op = @operations.find { |op| op[:type] == :resize }
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
else
|
|
191
|
-
# Fallback to scale_x only if scale_y is missing
|
|
192
|
-
["vips", "VipsResize", input_path.shellescape, format_spec,
|
|
193
|
-
(scale_x || 1.0).to_s].join(" ")
|
|
194
|
-
end
|
|
209
|
+
format_spec = build_format_spec(output_path)
|
|
210
|
+
width = resize_op[:width]
|
|
211
|
+
height = resize_op[:height]
|
|
212
|
+
|
|
213
|
+
# Use `vips thumbnail` for all resize operations. thumbnail is the
|
|
214
|
+
# recommended high-level resize API — it uses shrink-on-load for
|
|
215
|
+
# JPEGs (efficient), handles SVGs safely by bounding render size
|
|
216
|
+
# during load, and supports all image formats.
|
|
217
|
+
# See https://www.libvips.org/API/current/func.thumbnail.html
|
|
218
|
+
if width && height
|
|
219
|
+
# Both dimensions: thumbnail to width, cap height, crop to fill
|
|
220
|
+
["vips", "thumbnail", input_path, format_spec,
|
|
221
|
+
width.to_s, "--height=#{height}", "--crop=attention"]
|
|
195
222
|
else
|
|
196
|
-
# Only
|
|
197
|
-
|
|
198
|
-
format_spec = build_format_spec(output_path)
|
|
199
|
-
["vips", "VipsResize", input_path.shellescape, format_spec, scale_x.to_s].join(" ")
|
|
223
|
+
# Only width: thumbnail maintains aspect ratio
|
|
224
|
+
["vips", "thumbnail", input_path, format_spec, width.to_s]
|
|
200
225
|
end
|
|
201
226
|
end
|
|
202
227
|
|
|
@@ -204,81 +229,108 @@ module JekyllImgFlow
|
|
|
204
229
|
crop_op = @operations.find { |op| op[:type] == :crop }
|
|
205
230
|
opts = crop_op[:options] || {}
|
|
206
231
|
params = crop_op[:params] || {}
|
|
207
|
-
|
|
208
|
-
# Check if we should use smartcrop (when keep parameter is specified)
|
|
209
|
-
# JPT keep options: attention (default), entropy, center
|
|
210
|
-
# Check both options (from CropTag) and params (from OperationProcessor)
|
|
211
232
|
keep = opts[:keep] || params[:keep] || params[:position]
|
|
212
233
|
|
|
213
234
|
if crop_op[:ratio] && keep && %w[attention entropy center
|
|
214
235
|
centre].include?(keep.to_s)
|
|
215
|
-
# Use libvips smartcrop for intelligent cropping
|
|
216
236
|
crop_width = opts[:calculated_width]
|
|
217
237
|
crop_height = opts[:calculated_height]
|
|
218
|
-
|
|
219
|
-
# Map keep parameter to libvips interestingness
|
|
220
238
|
interestingness = case keep.to_s
|
|
221
239
|
when "entropy" then "entropy"
|
|
222
240
|
when "center", "centre" then "centre"
|
|
223
|
-
else "attention"
|
|
241
|
+
else "attention"
|
|
224
242
|
end
|
|
225
|
-
|
|
226
|
-
# vips smartcrop input.jpg output.jpg width height --interesting=attention
|
|
227
|
-
["vips", "smartcrop", input_path.shellescape, output_path.shellescape,
|
|
243
|
+
["vips", "smartcrop", input_path, output_path,
|
|
228
244
|
crop_width.to_s, crop_height.to_s,
|
|
229
|
-
"--interesting=#{interestingness}"]
|
|
245
|
+
"--interesting=#{interestingness}"]
|
|
230
246
|
else
|
|
231
|
-
# Use basic extract_area for manual cropping or when no keep parameter
|
|
232
247
|
crop_x = crop_op[:ratio] ? opts[:calculated_x] : (opts[:x] || 0)
|
|
233
248
|
crop_y = crop_op[:ratio] ? opts[:calculated_y] : (opts[:y] || 0)
|
|
234
249
|
crop_width = crop_op[:ratio] ? opts[:calculated_width] : opts[:width]
|
|
235
250
|
crop_height = crop_op[:ratio] ? opts[:calculated_height] : opts[:height]
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
["vips", "extract_area", input_path.shellescape, output_path.shellescape,
|
|
239
|
-
crop_x.to_s, crop_y.to_s, crop_width.to_s, crop_height.to_s].join(" ")
|
|
251
|
+
["vips", "extract_area", input_path, output_path,
|
|
252
|
+
crop_x.to_s, crop_y.to_s, crop_width.to_s, crop_height.to_s]
|
|
240
253
|
end
|
|
241
254
|
end
|
|
242
255
|
|
|
256
|
+
# SVG crop: use `vips thumbnail` with --crop to crop during thumbnailing.
|
|
257
|
+
# This avoids the coordinate mismatch that occurs when rasterizing at a
|
|
258
|
+
# different size than the SVG's viewBox (which caused "bad extract area").
|
|
259
|
+
# thumbnail --crop uses smartcrop (attention/entropy/centre) to select
|
|
260
|
+
# the most interesting region — this is the correct behavior for SVGs
|
|
261
|
+
# since they have no fixed pixel dimensions.
|
|
262
|
+
def build_svg_crop_pipeline(input_path, output_path)
|
|
263
|
+
crop_op = @operations.find { |op| op[:type] == :crop }
|
|
264
|
+
opts = crop_op[:options] || {}
|
|
265
|
+
crop_width = crop_op[:ratio] ? opts[:calculated_width] : (opts[:width] || 1000)
|
|
266
|
+
crop_height = crop_op[:ratio] ? opts[:calculated_height] : (opts[:height] || 1000)
|
|
267
|
+
format_spec = build_format_spec(output_path)
|
|
268
|
+
|
|
269
|
+
keep = opts[:keep] || crop_op[:params]&.[](:keep) || crop_op[:params]&.[](:position)
|
|
270
|
+
interestingness = case keep.to_s
|
|
271
|
+
when "entropy" then "entropy"
|
|
272
|
+
when "center", "centre" then "centre"
|
|
273
|
+
else "attention"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
[["vips", "thumbnail", input_path, format_spec,
|
|
277
|
+
crop_width.to_s, "--height=#{crop_height}",
|
|
278
|
+
"--crop=#{interestingness}"]]
|
|
279
|
+
end
|
|
280
|
+
|
|
243
281
|
def build_copy_command(input_path, output_path)
|
|
244
|
-
# vips copy input.jpg output.jpg[format]
|
|
245
282
|
format_spec = build_format_spec(output_path)
|
|
246
|
-
|
|
283
|
+
# For SVGs, `vips copy` rasterizes at intrinsic size (potentially
|
|
284
|
+
# huge). Use thumbnail with a large cap to bound the render safely.
|
|
285
|
+
return ["vips", "thumbnail", input_path, format_spec, "2000"] if svg?(input_path)
|
|
286
|
+
|
|
287
|
+
# vips copy input.jpg output.jpg[format]
|
|
288
|
+
["vips", "copy", input_path, format_spec]
|
|
247
289
|
end
|
|
248
290
|
|
|
291
|
+
# Build vips output specification with format and quality.
|
|
292
|
+
# Returns a raw string — no shell quoting needed since commands
|
|
293
|
+
# are executed via Open3.capture3 array form (no shell).
|
|
249
294
|
def build_format_spec(output_path)
|
|
250
295
|
# Build vips output specification with format and quality
|
|
251
296
|
format_op = @operations.find { |op| op[:type] == :format }
|
|
252
297
|
quality_op = @operations.find { |op| op[:type] == :quality }
|
|
253
298
|
|
|
254
299
|
# Simple case: no format/quality operations
|
|
255
|
-
return output_path
|
|
300
|
+
return output_path unless format_op || quality_op
|
|
256
301
|
|
|
257
302
|
# Extract format from operation or output path
|
|
258
303
|
ext = format_op ? format_op[:format] : File.extname(output_path).delete(".")
|
|
259
304
|
base = output_path.gsub(/\.[^.]+$/, "")
|
|
260
305
|
|
|
261
306
|
# Build format spec with quality if present
|
|
262
|
-
# Use single quotes to preserve bracket syntax (shellescape breaks it)
|
|
263
307
|
if quality_op
|
|
264
308
|
quality = quality_op[:quality]
|
|
265
|
-
"
|
|
309
|
+
"#{base}.#{ext}[Q=#{quality}]"
|
|
266
310
|
else
|
|
267
|
-
"
|
|
311
|
+
"#{base}.#{ext}"
|
|
268
312
|
end
|
|
269
313
|
end
|
|
270
314
|
|
|
315
|
+
# Execute a command array via Open3.capture3 (no shell invocation).
|
|
316
|
+
# @param command [Array<String>] Command and arguments as array
|
|
271
317
|
def execute_command(command)
|
|
272
|
-
|
|
318
|
+
if command.is_a?(Array) && command.first == :cleanup
|
|
319
|
+
FileUtils.rm_f(command[1])
|
|
320
|
+
return
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
Jekyll.logger.debug "LibVips command: #{command.join(' ')}"
|
|
273
324
|
|
|
274
|
-
# Execute command
|
|
275
|
-
stdout, stderr, status = Open3.capture3(command)
|
|
325
|
+
# Execute command array directly (no shell)
|
|
326
|
+
stdout, stderr, status = Open3.capture3(*command)
|
|
276
327
|
output = "#{stdout}#{stderr}"
|
|
277
328
|
success = status.success?
|
|
278
329
|
|
|
279
330
|
unless success
|
|
331
|
+
cmd_str = command.join(" ")
|
|
280
332
|
# Filter out expected warnings and only log real errors
|
|
281
|
-
raise "LibVips command failed: #{
|
|
333
|
+
raise "LibVips command failed: #{cmd_str}\nError: #{output}" unless output.include?("same file") || output.include?("VipsForeignSave")
|
|
282
334
|
|
|
283
335
|
Jekyll.logger.warn "LibVips warning: #{output.strip}"
|
|
284
336
|
return output.strip # Continue despite warnings
|
|
@@ -9,7 +9,7 @@ module JekyllImgFlow
|
|
|
9
9
|
module Providers
|
|
10
10
|
# Weserv provider implementation using the standardized tag interface
|
|
11
11
|
class Weserv < BaseProvider
|
|
12
|
-
TIMEOUT =
|
|
12
|
+
TIMEOUT = 30
|
|
13
13
|
|
|
14
14
|
def available?
|
|
15
15
|
# Check if weserv service is running
|
|
@@ -26,8 +26,9 @@ module JekyllImgFlow
|
|
|
26
26
|
|
|
27
27
|
# Fetch the result in one request
|
|
28
28
|
fetch_and_save(url, output_path)
|
|
29
|
-
reset_operations
|
|
30
29
|
output_path
|
|
30
|
+
ensure
|
|
31
|
+
reset_operations
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def build_combined_weserv_url(input_path)
|
|
@@ -24,13 +24,20 @@ module JekyllImgFlow
|
|
|
24
24
|
|
|
25
25
|
def validate_opacity(value)
|
|
26
26
|
# Check if value is numeric before converting
|
|
27
|
-
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.01 and 0.99." unless
|
|
27
|
+
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.01 and 0.99." unless valid_numeric?(value)
|
|
28
28
|
|
|
29
29
|
opacity = value.to_f
|
|
30
30
|
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.01 and 0.99." unless opacity.between?(0.01, 0.99)
|
|
31
31
|
|
|
32
32
|
opacity
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
def valid_numeric?(value)
|
|
36
|
+
Float(value)
|
|
37
|
+
true
|
|
38
|
+
rescue ArgumentError, TypeError
|
|
39
|
+
false
|
|
40
|
+
end
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
43
|
end
|
|
@@ -47,6 +47,8 @@ module JekyllImgFlow
|
|
|
47
47
|
|
|
48
48
|
Jekyll.logger.debug "🔍 ResizeTag: About to execute provider - input: #{input_path}, output: #{output_path}"
|
|
49
49
|
@provider.resize(width, height, resize_data)
|
|
50
|
+
@provider.quality(options[:quality]) if options[:quality]
|
|
51
|
+
@provider.convert_format(options[:format]) if options[:format]
|
|
50
52
|
result = @provider.execute(input_path, output_path)
|
|
51
53
|
Jekyll.logger.debug "🔍 ResizeTag: Provider executed - result: #{result}, output exists: #{File.exist?(output_path)}"
|
|
52
54
|
result
|
|
@@ -39,13 +39,20 @@ module JekyllImgFlow
|
|
|
39
39
|
|
|
40
40
|
def validate_opacity(value)
|
|
41
41
|
# Check if value is numeric before converting
|
|
42
|
-
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.0 and 1.0." unless
|
|
42
|
+
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.0 and 1.0." unless valid_numeric?(value)
|
|
43
43
|
|
|
44
44
|
opacity = value.to_f
|
|
45
45
|
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.0 and 1.0." unless opacity.between?(0.0, 1.0)
|
|
46
46
|
|
|
47
47
|
opacity
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
def valid_numeric?(value)
|
|
51
|
+
Float(value)
|
|
52
|
+
true
|
|
53
|
+
rescue ArgumentError, TypeError
|
|
54
|
+
false
|
|
55
|
+
end
|
|
49
56
|
end
|
|
50
57
|
end
|
|
51
58
|
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rake"
|
|
5
|
+
require "rubygems"
|
|
6
|
+
|
|
7
|
+
module JekyllImgFlow
|
|
8
|
+
# Rake tasks for listing and installing built-in presets.
|
|
9
|
+
#
|
|
10
|
+
# Tasks (defined in the host project's Rakefile):
|
|
11
|
+
# imgflow:presets — list built-in presets and their install status
|
|
12
|
+
# imgflow:install_presets — copy built-in presets into the site's
|
|
13
|
+
# _data/imgflow/presets/ directory (use overwrite=true to replace)
|
|
14
|
+
module Tasks
|
|
15
|
+
extend Rake::DSL
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Path to built-in presets shipped with the gem
|
|
19
|
+
# @return [String]
|
|
20
|
+
def builtin_presets_dir
|
|
21
|
+
File.expand_path("presets", __dir__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Names of built-in presets (without .yml extension)
|
|
25
|
+
# @return [Array<String>]
|
|
26
|
+
def builtin_preset_names
|
|
27
|
+
return [] unless Dir.exist?(builtin_presets_dir)
|
|
28
|
+
|
|
29
|
+
Dir.glob(File.join(builtin_presets_dir, "*.yml")).map do |f|
|
|
30
|
+
File.basename(f, ".yml")
|
|
31
|
+
end.sort
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Path where presets are installed in the consuming site
|
|
35
|
+
# @return [String]
|
|
36
|
+
def site_presets_dir
|
|
37
|
+
File.join(Dir.pwd, "_data", "imgflow", "presets")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# List built-in presets and show their install status in the current site
|
|
41
|
+
def list_presets
|
|
42
|
+
puts "ImgFlow built-in presets:"
|
|
43
|
+
puts
|
|
44
|
+
builtin_preset_names.each do |name|
|
|
45
|
+
gem_path = File.join(builtin_presets_dir, "#{name}.yml")
|
|
46
|
+
site_path = File.join(site_presets_dir, "#{name}.yml")
|
|
47
|
+
site_exists = File.exist?(site_path)
|
|
48
|
+
|
|
49
|
+
status = if !site_exists
|
|
50
|
+
"not installed"
|
|
51
|
+
elsif same_content?(gem_path, site_path)
|
|
52
|
+
"up to date"
|
|
53
|
+
else
|
|
54
|
+
"modified or outdated"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
puts " #{name}"
|
|
58
|
+
puts " gem: #{gem_path}"
|
|
59
|
+
puts " site: #{site_path}"
|
|
60
|
+
puts " status: #{status}"
|
|
61
|
+
puts
|
|
62
|
+
end
|
|
63
|
+
puts "To install: bundle exec rake imgflow:install_presets"
|
|
64
|
+
puts "To overwrite modified copies: bundle exec rake 'imgflow:install_presets[true]'"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Copy built-in presets into the site's _data/imgflow/presets/ directory
|
|
68
|
+
# @param overwrite [Boolean] If true, replace modified copies
|
|
69
|
+
# @return [Hash] Summary with :installed and :skipped arrays
|
|
70
|
+
def install_presets(overwrite: false)
|
|
71
|
+
installed = []
|
|
72
|
+
skipped = []
|
|
73
|
+
|
|
74
|
+
FileUtils.mkdir_p(site_presets_dir)
|
|
75
|
+
|
|
76
|
+
builtin_preset_names.each do |name|
|
|
77
|
+
gem_path = File.join(builtin_presets_dir, "#{name}.yml")
|
|
78
|
+
site_path = File.join(site_presets_dir, "#{name}.yml")
|
|
79
|
+
|
|
80
|
+
if File.exist?(site_path) && !overwrite
|
|
81
|
+
if same_content?(gem_path, site_path)
|
|
82
|
+
puts " #{name}.yml: already up to date (skip)"
|
|
83
|
+
else
|
|
84
|
+
puts " #{name}.yml: exists and differs (skip — use overwrite: true to replace)"
|
|
85
|
+
skipped << name
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
existed = File.exist?(site_path)
|
|
89
|
+
FileUtils.cp(gem_path, site_path)
|
|
90
|
+
action = existed ? "updated" : "installed"
|
|
91
|
+
puts " #{name}.yml: #{action}"
|
|
92
|
+
installed << name
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
{ installed: installed, skipped: skipped }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if two files have identical content
|
|
100
|
+
# @param path_a [String]
|
|
101
|
+
# @param path_b [String]
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def same_content?(path_a, path_b)
|
|
104
|
+
File.read(path_a) == File.read(path_b)
|
|
105
|
+
rescue Errno::ENOENT
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def install_rake_tasks
|
|
110
|
+
return if Rake::Task.task_defined?("imgflow:presets")
|
|
111
|
+
|
|
112
|
+
namespace :imgflow do
|
|
113
|
+
desc "List built-in ImgFlow presets and their install status"
|
|
114
|
+
task :presets do
|
|
115
|
+
JekyllImgFlow::Tasks.list_presets
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
desc "Copy built-in ImgFlow presets into _data/imgflow/presets/"
|
|
119
|
+
task :install_presets, [:overwrite] do |_task, args|
|
|
120
|
+
overwrite = args[:overwrite] == "true"
|
|
121
|
+
JekyllImgFlow::Tasks.install_presets(overwrite: overwrite)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
install_rake_tasks
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/jekyll-imgflow.rb
CHANGED
|
@@ -8,12 +8,14 @@ require "net/http"
|
|
|
8
8
|
require "uri"
|
|
9
9
|
require_relative "jekyll-imgflow/version"
|
|
10
10
|
require_relative "jekyll-imgflow/config"
|
|
11
|
+
require_relative "jekyll-imgflow/generated_file_cleaner"
|
|
11
12
|
require_relative "jekyll-imgflow/manifest_manager"
|
|
12
13
|
require_relative "jekyll-imgflow/tag_scanner"
|
|
13
14
|
require_relative "jekyll-imgflow/path_resolver"
|
|
14
15
|
require_relative "jekyll-imgflow/filename_generator"
|
|
15
16
|
require_relative "jekyll-imgflow/animated_gif_detector"
|
|
16
17
|
require_relative "jekyll-imgflow/operation_processor"
|
|
18
|
+
require_relative "jekyll-imgflow/processing_stats"
|
|
17
19
|
require_relative "jekyll-imgflow/batch_manager"
|
|
18
20
|
require_relative "jekyll-imgflow/preset_manager"
|
|
19
21
|
require_relative "jekyll-imgflow/helpers/http_downloader"
|