jekyll-imgflow 0.3.2 → 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.
- checksums.yaml +4 -4
- data/README.md +33 -2
- data/lib/jekyll-imgflow/build_time_processor.rb +3 -2
- data/lib/jekyll-imgflow/config.rb +25 -18
- data/lib/jekyll-imgflow/filename_generator.rb +2 -0
- data/lib/jekyll-imgflow/html_generator.rb +6 -6
- data/lib/jekyll-imgflow/imgflow_tag.rb +86 -52
- data/lib/jekyll-imgflow/manifest_manager.rb +64 -57
- data/lib/jekyll-imgflow/operation_processor.rb +44 -64
- data/lib/jekyll-imgflow/parser.rb +32 -25
- data/lib/jekyll-imgflow/picture_tag_adaptor.rb +113 -106
- data/lib/jekyll-imgflow/picture_tag_preset_migrator.rb +73 -69
- data/lib/jekyll-imgflow/preset_manager.rb +42 -38
- data/lib/jekyll-imgflow/providers/base_provider.rb +199 -0
- data/lib/jekyll-imgflow/providers/flyimg.rb +72 -142
- data/lib/jekyll-imgflow/providers/imagemagick.rb +57 -97
- data/lib/jekyll-imgflow/providers/imgproxy.rb +53 -126
- data/lib/jekyll-imgflow/providers/libvips.rb +105 -138
- data/lib/jekyll-imgflow/providers/sharp.rb +57 -100
- data/lib/jekyll-imgflow/providers/weserv.rb +69 -124
- data/lib/jekyll-imgflow/tag_scanner.rb +19 -24
- data/lib/jekyll-imgflow/tags/base_tag.rb +33 -0
- data/lib/jekyll-imgflow/tags/crop_tag.rb +24 -47
- data/lib/jekyll-imgflow/tags/opacity_tag.rb +1 -20
- data/lib/jekyll-imgflow/tags/optimize_tag.rb +0 -9
- data/lib/jekyll-imgflow/tags/quality_tag.rb +0 -11
- data/lib/jekyll-imgflow/tags/resize_tag.rb +18 -11
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +0 -17
- data/lib/jekyll-imgflow/version.rb +1 -1
- metadata +1 -1
|
@@ -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 <
|
|
10
|
+
class Sharp < CliBase
|
|
11
11
|
def available?
|
|
12
|
-
|
|
13
|
-
_, _, status = Open3.capture3("which", "sharp")
|
|
14
|
-
status.success?
|
|
12
|
+
cli_available?("sharp")
|
|
15
13
|
end
|
|
16
14
|
|
|
17
|
-
def
|
|
18
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
24
|
+
elsif op?(:resize)
|
|
39
25
|
build_resize_command(input_path, output_path)
|
|
40
|
-
elsif
|
|
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
|
-
|
|
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,
|
|
53
|
-
base_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,
|
|
56
|
-
base_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,
|
|
59
|
-
base_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 #{
|
|
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 =
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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 =
|
|
87
|
-
alpha_value = (opacity
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 #{
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
157
|
-
|
|
158
|
-
params = crop_op[:params] || {}
|
|
125
|
+
crop_op = find_op(:crop)
|
|
126
|
+
geo = crop_geometry(crop_op)
|
|
159
127
|
|
|
160
|
-
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
|
@@ -2,159 +2,104 @@
|
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "uri"
|
|
5
|
-
require "tempfile"
|
|
6
5
|
require_relative "base_provider"
|
|
7
6
|
|
|
8
7
|
module JekyllImgFlow
|
|
9
8
|
module Providers
|
|
10
9
|
# Weserv provider implementation using the standardized tag interface
|
|
11
|
-
class Weserv <
|
|
10
|
+
class Weserv < HttpBase
|
|
12
11
|
TIMEOUT = 30
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
# SVGs without explicit pixel dimensions (e.g. width="100%") are valid
|
|
13
|
+
# but cause librsvg to rasterize at the viewBox size, which can be huge.
|
|
14
|
+
# When no resize operation is present, cap the rasterization at 2000px
|
|
15
|
+
# on the longest edge so weserv processes the SVG at a reasonable size.
|
|
16
|
+
SVG_DEFAULT_MAX_SIZE = 2000
|
|
17
|
+
|
|
18
|
+
def service_url
|
|
19
|
+
@config.respond_to?(:weserv_url) ? @config.weserv_url : nil
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Build single Weserv URL with all operations combined
|
|
25
|
-
url = build_combined_weserv_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
|
|
22
|
+
def build_combined_weserv_url(input_path)
|
|
23
|
+
build_combined_url(input_path)
|
|
32
24
|
end
|
|
33
25
|
|
|
34
|
-
def
|
|
35
|
-
raise "weserv_url not set" unless
|
|
26
|
+
def build_combined_url(input_path)
|
|
27
|
+
raise "weserv_url not set" unless service_url
|
|
36
28
|
|
|
37
29
|
encoded_url = encode_file_url(input_path)
|
|
38
30
|
params = ["url=#{URI.encode_www_form_component(encoded_url)}"]
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
@operations.each do |operation|
|
|
42
|
-
case operation[:type]
|
|
43
|
-
when :resize
|
|
44
|
-
# Tags now provide complete calculated values
|
|
45
|
-
params << if operation[:height]
|
|
46
|
-
"w=#{operation[:width]}&h=#{operation[:height]}&fit=fill"
|
|
47
|
-
else
|
|
48
|
-
"w=#{operation[:width]}"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
when :crop
|
|
52
|
-
opts = operation[:options]
|
|
53
|
-
op_params = operation[:params] || {}
|
|
54
|
-
|
|
55
|
-
# Check for keep parameter (smartcrop support)
|
|
56
|
-
keep = opts[:keep] || op_params[:keep] || op_params[:position]
|
|
57
|
-
|
|
58
|
-
if operation[:ratio] && keep && SMARTCROP_POSITIONS.include?(keep.to_s)
|
|
59
|
-
# Use smartcrop (WeServ supports smartcrop)
|
|
60
|
-
crop_width = opts[:calculated_width]
|
|
61
|
-
crop_height = opts[:calculated_height]
|
|
62
|
-
|
|
63
|
-
# Add smartcrop parameters according to WeServ docs
|
|
64
|
-
params << "crop=#{crop_width}x#{crop_height}"
|
|
65
|
-
params << "a=smart" # smart attention
|
|
66
|
-
|
|
67
|
-
# Map keep parameter to WeServ attention type
|
|
68
|
-
case keep.to_s
|
|
69
|
-
when "attention" then params << "a=attention"
|
|
70
|
-
when "entropy" then params << "a=entropy"
|
|
71
|
-
when "center", "centre" then params << "a=center"
|
|
72
|
-
end
|
|
73
|
-
else
|
|
74
|
-
# Use basic cropping
|
|
75
|
-
if operation[:ratio]
|
|
76
|
-
crop_x = opts[:calculated_x]
|
|
77
|
-
crop_y = opts[:calculated_y]
|
|
78
|
-
crop_width = opts[:calculated_width]
|
|
79
|
-
crop_height = opts[:calculated_height]
|
|
80
|
-
else
|
|
81
|
-
crop_x = opts[:x]
|
|
82
|
-
crop_y = opts[:y]
|
|
83
|
-
crop_width = opts[:width]
|
|
84
|
-
crop_height = opts[:height]
|
|
85
|
-
end
|
|
86
|
-
params << "cx=#{crop_x}&cy=#{crop_y}&cw=#{crop_width}&ch=#{crop_height}"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
when :quality
|
|
90
|
-
weserv_quality = translate_quality_to_weserv(operation[:quality])
|
|
91
|
-
params << "q=#{weserv_quality}"
|
|
92
|
-
|
|
93
|
-
when :format
|
|
94
|
-
# Assume validated input from tags
|
|
95
|
-
params << "output=#{operation[:format]}"
|
|
96
|
-
|
|
97
|
-
when :watermark
|
|
98
|
-
watermark_path = operation[:watermark_path]
|
|
99
|
-
position = operation[:options][:position]
|
|
100
|
-
opacity = operation[:options][:opacity]
|
|
101
|
-
|
|
102
|
-
# Weserv watermark parameters
|
|
103
|
-
params << "wm=#{encode_file_url(watermark_path)}"
|
|
104
|
-
params << "wmpos=#{translate_weserv_position(position)}"
|
|
105
|
-
params << "wmo=#{opacity}" if opacity
|
|
106
|
-
|
|
107
|
-
when :alpha_opacity
|
|
108
|
-
opacity = operation[:opacity]
|
|
109
|
-
params << "alpha=#{(opacity * 255).round}"
|
|
110
|
-
end
|
|
111
|
-
end
|
|
32
|
+
@operations.each { |operation| append_weserv_operation(operation, params) }
|
|
112
33
|
|
|
113
|
-
|
|
114
|
-
|
|
34
|
+
ensure_svg_size_limit(input_path, params)
|
|
35
|
+
|
|
36
|
+
"#{service_url}/?#{params.join('&')}"
|
|
115
37
|
end
|
|
116
38
|
|
|
117
39
|
private
|
|
118
40
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
when "southwest" then "bl"
|
|
125
|
-
when "southeast" then "br"
|
|
126
|
-
when "center" then "c"
|
|
127
|
-
else position
|
|
128
|
-
end
|
|
41
|
+
def ensure_svg_size_limit(input_path, params)
|
|
42
|
+
return unless svg_file?(input_path)
|
|
43
|
+
return if params.any? { |p| p.start_with?("w=", "h=", "crop=") || p.include?("cw=") }
|
|
44
|
+
|
|
45
|
+
params << "w=#{SVG_DEFAULT_MAX_SIZE}"
|
|
129
46
|
end
|
|
130
47
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
48
|
+
def svg_file?(input_path)
|
|
49
|
+
File.extname(input_path).downcase == ".svg"
|
|
50
|
+
end
|
|
134
51
|
|
|
135
|
-
|
|
136
|
-
|
|
52
|
+
def append_weserv_operation(operation, params)
|
|
53
|
+
case operation[:type]
|
|
54
|
+
when :resize
|
|
55
|
+
params << if operation[:height]
|
|
56
|
+
"w=#{operation[:width]}&h=#{operation[:height]}&fit=fill"
|
|
57
|
+
else
|
|
58
|
+
"w=#{operation[:width]}"
|
|
59
|
+
end
|
|
60
|
+
when :crop
|
|
61
|
+
append_weserv_crop(operation, params)
|
|
62
|
+
when :quality
|
|
63
|
+
params << "q=#{operation[:quality]}"
|
|
64
|
+
when :format
|
|
65
|
+
params << "output=#{operation[:format]}"
|
|
66
|
+
when :watermark
|
|
67
|
+
append_weserv_watermark(operation, params)
|
|
68
|
+
when :alpha_opacity
|
|
69
|
+
params << "alpha=#{alpha_byte_value(operation[:opacity])}"
|
|
70
|
+
end
|
|
137
71
|
end
|
|
138
72
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
Net::HTTP.start(uri.host, uri.port,
|
|
142
|
-
use_ssl: uri.scheme == "https",
|
|
143
|
-
open_timeout: TIMEOUT,
|
|
144
|
-
read_timeout: TIMEOUT) do |http|
|
|
145
|
-
request = Net::HTTP::Get.new(uri)
|
|
146
|
-
response = http.request(request)
|
|
73
|
+
def append_weserv_crop(operation, params)
|
|
74
|
+
geo = crop_geometry(operation)
|
|
147
75
|
|
|
148
|
-
|
|
76
|
+
if geo[:smartcrop]
|
|
77
|
+
params << "crop=#{geo[:width]}x#{geo[:height]}"
|
|
78
|
+
params << "a=smart"
|
|
79
|
+
params << weserv_attention(geo[:keep])
|
|
80
|
+
else
|
|
81
|
+
params << "cx=#{geo[:x]}&cy=#{geo[:y]}&cw=#{geo[:width]}&ch=#{geo[:height]}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
149
84
|
|
|
150
|
-
|
|
85
|
+
def weserv_attention(keep)
|
|
86
|
+
case keep.to_s
|
|
87
|
+
when "attention" then "a=attention"
|
|
88
|
+
when "entropy" then "a=entropy"
|
|
89
|
+
when "center", "centre" then "a=center"
|
|
151
90
|
end
|
|
152
|
-
rescue StandardError => e
|
|
153
|
-
raise "Weserv request failed: #{e.message}"
|
|
154
91
|
end
|
|
155
92
|
|
|
156
|
-
def
|
|
157
|
-
|
|
93
|
+
def append_weserv_watermark(operation, params)
|
|
94
|
+
parts = watermark_parts(operation)
|
|
95
|
+
params << "wm=#{encode_file_url(parts[:watermark_path])}"
|
|
96
|
+
params << "wmpos=#{compass_to_short(parts[:position])}"
|
|
97
|
+
params << "wmo=#{parts[:opacity]}" if parts[:opacity]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Thin wrapper kept for test compatibility.
|
|
101
|
+
def translate_weserv_position(position)
|
|
102
|
+
compass_to_short(position)
|
|
158
103
|
end
|
|
159
104
|
end
|
|
160
105
|
end
|
|
@@ -127,34 +127,14 @@ module JekyllImgFlow
|
|
|
127
127
|
image_path = parts.shift
|
|
128
128
|
|
|
129
129
|
# Parse operations as key:value pairs separated by commas or spaces
|
|
130
|
-
operations = []
|
|
131
130
|
remaining = parts.join(" ")
|
|
131
|
+
operation_pairs = split_operation_pairs(remaining)
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
remaining.split(",")
|
|
136
|
-
else
|
|
137
|
-
# Handle space-separated: key:value key:value
|
|
138
|
-
remaining.split(/\s+/)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
operation_pairs.each do |pair|
|
|
142
|
-
next if pair.empty?
|
|
143
|
-
|
|
144
|
-
next unless pair.include?(":")
|
|
133
|
+
operations = operation_pairs.filter_map do |pair|
|
|
134
|
+
next if pair.empty? || !pair.include?(":")
|
|
145
135
|
|
|
146
136
|
key, value = pair.split(":", 2)
|
|
147
|
-
#
|
|
148
|
-
value_stripped = value.strip
|
|
149
|
-
converted_value = if value_stripped.match?(/^\d+$/)
|
|
150
|
-
value_stripped.to_i
|
|
151
|
-
elsif value_stripped.match?(/^\d+\.\d+$/)
|
|
152
|
-
value_stripped.to_f
|
|
153
|
-
else
|
|
154
|
-
value_stripped
|
|
155
|
-
end
|
|
156
|
-
# Store with converted value for downstream type correctness
|
|
157
|
-
operations << "#{key.strip}:#{converted_value}"
|
|
137
|
+
"#{key.strip}:#{convert_value(value.strip)}"
|
|
158
138
|
end
|
|
159
139
|
|
|
160
140
|
{
|
|
@@ -163,6 +143,21 @@ module JekyllImgFlow
|
|
|
163
143
|
}
|
|
164
144
|
end
|
|
165
145
|
|
|
146
|
+
def split_operation_pairs(remaining)
|
|
147
|
+
# Handle comma-separated: key:value,key:value
|
|
148
|
+
return remaining.split(",") if remaining.include?(",")
|
|
149
|
+
|
|
150
|
+
# Handle space-separated: key:value key:value
|
|
151
|
+
remaining.split(/\s+/)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def convert_value(value)
|
|
155
|
+
return value.to_i if value.match?(/^\d+$/)
|
|
156
|
+
return value.to_f if value.match?(/^\d+\.\d+$/)
|
|
157
|
+
|
|
158
|
+
value
|
|
159
|
+
end
|
|
160
|
+
|
|
166
161
|
private
|
|
167
162
|
|
|
168
163
|
# Get all scannable content from site
|
|
@@ -33,6 +33,14 @@ module JekyllImgFlow
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Like get_image_dimensions but raises if dimensions cannot be read.
|
|
37
|
+
def get_original_dimensions(image_path)
|
|
38
|
+
width, height = get_image_dimensions(image_path)
|
|
39
|
+
raise ArgumentError, "Unable to determine original image dimensions" unless width && height
|
|
40
|
+
|
|
41
|
+
[width, height]
|
|
42
|
+
end
|
|
43
|
+
|
|
36
44
|
def validate_positive_integer(value, name)
|
|
37
45
|
return if value.nil?
|
|
38
46
|
|
|
@@ -47,6 +55,31 @@ module JekyllImgFlow
|
|
|
47
55
|
# Config class already handles the fallback (cfg["quality"] || 85)
|
|
48
56
|
@provider.config&.quality
|
|
49
57
|
end
|
|
58
|
+
|
|
59
|
+
def validate_quality(value)
|
|
60
|
+
return @default_quality if value.nil?
|
|
61
|
+
|
|
62
|
+
quality = value.to_i
|
|
63
|
+
raise ArgumentError, "Invalid quality: '#{value}'. Must be between 1 and 100." unless quality.between?(1, 100)
|
|
64
|
+
|
|
65
|
+
quality
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate_opacity(value, min: 0.0, max: 1.0)
|
|
69
|
+
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between #{min} and #{max}." unless valid_numeric?(value)
|
|
70
|
+
|
|
71
|
+
opacity = value.to_f
|
|
72
|
+
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between #{min} and #{max}." unless opacity.between?(min, max)
|
|
73
|
+
|
|
74
|
+
opacity
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def valid_numeric?(value)
|
|
78
|
+
Float(value)
|
|
79
|
+
true
|
|
80
|
+
rescue ArgumentError, TypeError
|
|
81
|
+
false
|
|
82
|
+
end
|
|
50
83
|
end
|
|
51
84
|
end
|
|
52
85
|
end
|