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
|
@@ -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 <
|
|
10
|
+
class Imgproxy < HttpBase
|
|
12
11
|
TIMEOUT = 10 # seconds
|
|
13
12
|
|
|
14
|
-
def
|
|
15
|
-
|
|
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
|
|
22
|
-
|
|
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
|
|
35
|
-
raise "imgproxy_url not set" unless
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"#{
|
|
31
|
+
"#{service_url}/insecure/#{operations_path}/plain/#{encoded_url}"
|
|
118
32
|
end
|
|
119
33
|
|
|
120
34
|
private
|
|
121
35
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
78
|
+
def preserve_input_format(input_path, operations)
|
|
79
|
+
return if op?(:format)
|
|
152
80
|
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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,48 +7,49 @@ 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 <
|
|
10
|
+
class Libvips < CliBase
|
|
11
11
|
def available?
|
|
12
|
-
|
|
13
|
-
_, _, status = Open3.capture3("which", "vips")
|
|
14
|
-
status.success?
|
|
12
|
+
cli_available?("vips")
|
|
15
13
|
end
|
|
16
14
|
|
|
17
|
-
def
|
|
18
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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.
|
|
31
24
|
# Each sub-array is passed directly to Open3.capture3 (no shell).
|
|
32
25
|
# Returns Array[Array[String]].
|
|
33
26
|
def build_vips_commands(input_path, output_path)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
flags = operation_flags
|
|
28
|
+
operation_builder = select_operation_builder(flags)
|
|
29
|
+
operation_builder.call(input_path, output_path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def select_operation_builder(flags)
|
|
33
|
+
return ->(input, output) { build_watermark_pipeline(input, output, flags[:crop], flags[:resize]) } if flags[:watermark]
|
|
34
|
+
return ->(input, output) { build_alpha_pipeline(input, output, flags[:crop], flags[:resize]) } if flags[:alpha]
|
|
35
|
+
return ->(input, output) { build_sequential_crop_resize(input, output) } if flags[:crop] && flags[:resize]
|
|
36
|
+
return ->(input, output) { [build_resize_command(input, output)] } if flags[:resize]
|
|
37
|
+
return ->(input, output) { crop_commands(input, output) } if flags[:crop]
|
|
38
|
+
|
|
39
|
+
->(input, output) { [build_copy_command(input, output)] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def operation_flags
|
|
43
|
+
{
|
|
44
|
+
crop: op?(:crop),
|
|
45
|
+
resize: op?(:resize),
|
|
46
|
+
watermark: op?(:watermark),
|
|
47
|
+
alpha: op?(:alpha_opacity)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def crop_commands(input_path, output_path)
|
|
52
|
+
svg?(input_path) ? build_svg_crop_pipeline(input_path, output_path) : [build_crop_command(input_path, output_path)]
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
# Convenience wrapper for backward compatibility with tests.
|
|
@@ -64,85 +65,62 @@ module JekyllImgFlow
|
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
def build_alpha_pipeline(input_path, output_path, has_crop, has_resize)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if has_crop && has_resize
|
|
71
|
-
commands.concat(build_sequential_crop_resize(input_path, temp_path))
|
|
72
|
-
base_path = temp_path
|
|
73
|
-
elsif has_resize
|
|
74
|
-
commands << build_resize_command(input_path, temp_path)
|
|
75
|
-
base_path = temp_path
|
|
76
|
-
elsif has_crop
|
|
77
|
-
commands << build_crop_command(input_path, temp_path)
|
|
78
|
-
base_path = temp_path
|
|
79
|
-
else
|
|
80
|
-
base_path = input_path
|
|
81
|
-
end
|
|
82
|
-
|
|
68
|
+
temp = temp_path(input_path, "tmp_base.jpg")
|
|
69
|
+
commands, base_path = build_base_pipeline(input_path, temp, has_crop, has_resize)
|
|
83
70
|
commands << build_alpha_command(base_path, output_path)
|
|
84
|
-
commands << [:cleanup,
|
|
71
|
+
commands << [:cleanup, temp] unless base_path == input_path
|
|
85
72
|
commands
|
|
86
73
|
end
|
|
87
74
|
|
|
88
75
|
def build_watermark_pipeline(input_path, output_path, has_crop, has_resize)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
commands =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
base_path = temp_path
|
|
97
|
-
elsif has_resize
|
|
98
|
-
commands << build_resize_command(input_path, temp_path)
|
|
99
|
-
base_path = temp_path
|
|
100
|
-
elsif has_crop
|
|
101
|
-
commands << build_crop_command(input_path, temp_path)
|
|
102
|
-
base_path = temp_path
|
|
103
|
-
else
|
|
104
|
-
base_path = input_path
|
|
105
|
-
end
|
|
76
|
+
temp = temp_path(input_path, "tmp_base.jpg")
|
|
77
|
+
commands, base_path = build_base_pipeline(input_path, temp, has_crop, has_resize)
|
|
78
|
+
base_path, commands = apply_watermark_alpha(base_path, input_path, commands)
|
|
79
|
+
commands.concat(build_composite_commands(base_path, output_path))
|
|
80
|
+
cleanup_paths(commands, input_path, temp, base_path)
|
|
81
|
+
commands
|
|
82
|
+
end
|
|
106
83
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if
|
|
110
|
-
|
|
111
|
-
commands << build_alpha_command(base_path, alpha_temp)
|
|
112
|
-
base_path = alpha_temp
|
|
113
|
-
end
|
|
84
|
+
def build_base_pipeline(input_path, temp_path, has_crop, has_resize)
|
|
85
|
+
return [build_sequential_crop_resize(input_path, temp_path), temp_path] if has_crop && has_resize
|
|
86
|
+
return [[build_resize_command(input_path, temp_path)], temp_path] if has_resize
|
|
87
|
+
return [[build_crop_command(input_path, temp_path)], temp_path] if has_crop
|
|
114
88
|
|
|
115
|
-
|
|
116
|
-
|
|
89
|
+
[[], input_path]
|
|
90
|
+
end
|
|
117
91
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
92
|
+
def apply_watermark_alpha(base_path, input_path, commands)
|
|
93
|
+
alpha_op = find_op(:alpha_opacity)
|
|
94
|
+
return [base_path, commands] unless alpha_op
|
|
121
95
|
|
|
122
|
-
|
|
96
|
+
alpha_temp = temp_path(input_path, "tmp_alpha.jpg")
|
|
97
|
+
[alpha_temp, commands << build_alpha_command(base_path, alpha_temp)]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cleanup_paths(commands, input_path, temp_path, base_path)
|
|
101
|
+
temp_files = [temp_path, base_path == input_path ? nil : base_path].compact
|
|
102
|
+
temp_files.each { |file| commands << [:cleanup, file] }
|
|
123
103
|
end
|
|
124
104
|
|
|
125
105
|
def build_composite_commands(base_path, output_path)
|
|
126
|
-
wm_op =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
wm_temp = watermark_path.gsub(/\.[^.]+$/, ".tmp_wm.png")
|
|
133
|
-
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]
|
|
134
112
|
# Step 1: Apply alpha to watermark
|
|
135
|
-
alpha_cmd = ["vips", "linear", watermark_path,
|
|
113
|
+
alpha_cmd = ["vips", "linear", parts[:watermark_path],
|
|
136
114
|
"#{wm_temp}[alpha]", "1 1 1 #{alpha_value}", "0"]
|
|
137
115
|
# Step 2: Composite
|
|
138
|
-
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])
|
|
139
117
|
composite_cmd = ["vips", "composite2", base_path, wm_temp,
|
|
140
118
|
build_format_spec(output_path), "over"] + xy_args
|
|
141
119
|
# Step 3: Cleanup wm_temp
|
|
142
120
|
[alpha_cmd, composite_cmd, [:cleanup, wm_temp]]
|
|
143
121
|
else
|
|
144
|
-
xy_args = position_to_vips_xy(position, base_path, watermark_path)
|
|
145
|
-
[["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],
|
|
146
124
|
build_format_spec(output_path), "over"] + xy_args]
|
|
147
125
|
end
|
|
148
126
|
end
|
|
@@ -169,6 +147,9 @@ module JekyllImgFlow
|
|
|
169
147
|
# Get an image dimension (width or height) using vips header.
|
|
170
148
|
# Returns integer or 0 if vips is unavailable.
|
|
171
149
|
def vips_image_dimension(image_path, field)
|
|
150
|
+
return 0 unless %w[width height].include?(field)
|
|
151
|
+
|
|
152
|
+
# nosemgrep: ruby.lang.security.dangerous-exec.dangerous-exec -- Open3 receives an argument array and field is allowlisted.
|
|
172
153
|
stdout, _, status = Open3.capture3("vips", "header", image_path, field)
|
|
173
154
|
return 0 unless status.success?
|
|
174
155
|
|
|
@@ -178,9 +159,8 @@ module JekyllImgFlow
|
|
|
178
159
|
end
|
|
179
160
|
|
|
180
161
|
def build_alpha_command(input_path, output_path)
|
|
181
|
-
alpha_op =
|
|
182
|
-
|
|
183
|
-
alpha_value = opacity.round(2)
|
|
162
|
+
alpha_op = find_op(:alpha_opacity)
|
|
163
|
+
alpha_value = alpha_op[:opacity].round(2)
|
|
184
164
|
|
|
185
165
|
# vips linear multiplies alpha band: "1 1 1 alpha" scales alpha
|
|
186
166
|
["vips", "linear", input_path, output_path,
|
|
@@ -190,22 +170,22 @@ module JekyllImgFlow
|
|
|
190
170
|
def build_sequential_crop_resize(input_path, output_path)
|
|
191
171
|
if svg?(input_path)
|
|
192
172
|
# SVG: use thumbnail with --crop to do crop+resize in one step
|
|
193
|
-
resize_op =
|
|
173
|
+
resize_op = find_op(:resize)
|
|
194
174
|
format_spec = build_format_spec(output_path)
|
|
195
175
|
width = resize_op[:width]
|
|
196
176
|
height = resize_op[:height]
|
|
197
177
|
[["vips", "thumbnail", input_path, format_spec,
|
|
198
178
|
width.to_s, "--height=#{height}", "--crop=attention"]]
|
|
199
179
|
else
|
|
200
|
-
|
|
201
|
-
[build_crop_command(input_path,
|
|
202
|
-
build_resize_command(
|
|
203
|
-
[:cleanup,
|
|
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]]
|
|
204
184
|
end
|
|
205
185
|
end
|
|
206
186
|
|
|
207
187
|
def build_resize_command(input_path, output_path)
|
|
208
|
-
resize_op =
|
|
188
|
+
resize_op = find_op(:resize)
|
|
209
189
|
format_spec = build_format_spec(output_path)
|
|
210
190
|
width = resize_op[:width]
|
|
211
191
|
height = resize_op[:height]
|
|
@@ -226,31 +206,23 @@ module JekyllImgFlow
|
|
|
226
206
|
end
|
|
227
207
|
|
|
228
208
|
def build_crop_command(input_path, output_path)
|
|
229
|
-
crop_op =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
else
|
|
247
|
-
crop_x = crop_op[:ratio] ? opts[:calculated_x] : (opts[:x] || 0)
|
|
248
|
-
crop_y = crop_op[:ratio] ? opts[:calculated_y] : (opts[:y] || 0)
|
|
249
|
-
crop_width = crop_op[:ratio] ? opts[:calculated_width] : opts[:width]
|
|
250
|
-
crop_height = crop_op[:ratio] ? opts[:calculated_height] : opts[:height]
|
|
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]
|
|
253
|
-
end
|
|
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]
|
|
212
|
+
|
|
213
|
+
build_extract_command(geo, input_path, output_path)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def build_smartcrop_command(geo, input_path, output_path)
|
|
217
|
+
interestingness = smartcrop_interestingness(geo[:keep])
|
|
218
|
+
["vips", "smartcrop", input_path, output_path,
|
|
219
|
+
geo[:width].to_s, geo[:height].to_s,
|
|
220
|
+
"--interesting=#{interestingness}"]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_extract_command(geo, input_path, output_path)
|
|
224
|
+
["vips", "extract_area", input_path, output_path,
|
|
225
|
+
geo[:x].to_s, geo[:y].to_s, geo[:width].to_s, geo[:height].to_s]
|
|
254
226
|
end
|
|
255
227
|
|
|
256
228
|
# SVG crop: use `vips thumbnail` with --crop to crop during thumbnailing.
|
|
@@ -260,18 +232,12 @@ module JekyllImgFlow
|
|
|
260
232
|
# the most interesting region — this is the correct behavior for SVGs
|
|
261
233
|
# since they have no fixed pixel dimensions.
|
|
262
234
|
def build_svg_crop_pipeline(input_path, output_path)
|
|
263
|
-
crop_op =
|
|
264
|
-
|
|
265
|
-
crop_width =
|
|
266
|
-
crop_height =
|
|
235
|
+
crop_op = find_op(:crop)
|
|
236
|
+
geo = crop_geometry(crop_op)
|
|
237
|
+
crop_width = geo[:width] || 1000
|
|
238
|
+
crop_height = geo[:height] || 1000
|
|
267
239
|
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
|
|
240
|
+
interestingness = smartcrop_interestingness(geo[:keep])
|
|
275
241
|
|
|
276
242
|
[["vips", "thumbnail", input_path, format_spec,
|
|
277
243
|
crop_width.to_s, "--height=#{crop_height}",
|
|
@@ -293,8 +259,8 @@ module JekyllImgFlow
|
|
|
293
259
|
# are executed via Open3.capture3 array form (no shell).
|
|
294
260
|
def build_format_spec(output_path)
|
|
295
261
|
# Build vips output specification with format and quality
|
|
296
|
-
format_op =
|
|
297
|
-
quality_op =
|
|
262
|
+
format_op = find_op(:format)
|
|
263
|
+
quality_op = find_op(:quality)
|
|
298
264
|
|
|
299
265
|
# Simple case: no format/quality operations
|
|
300
266
|
return output_path unless format_op || quality_op
|
|
@@ -323,6 +289,7 @@ module JekyllImgFlow
|
|
|
323
289
|
Jekyll.logger.debug "LibVips command: #{command.join(' ')}"
|
|
324
290
|
|
|
325
291
|
# Execute command array directly (no shell)
|
|
292
|
+
# nosemgrep: ruby.lang.security.dangerous-exec.dangerous-exec -- command is an argument array; no shell is invoked.
|
|
326
293
|
stdout, stderr, status = Open3.capture3(*command)
|
|
327
294
|
output = "#{stdout}#{stderr}"
|
|
328
295
|
success = status.success?
|