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.
- checksums.yaml +4 -4
- data/README.md +22 -0
- data/lib/jekyll-imgflow/parser.rb +32 -25
- 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 +198 -0
- data/lib/jekyll-imgflow/providers/flyimg.rb +71 -141
- 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 +53 -81
- 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,144 +2,98 @@
|
|
|
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
|
# Flyimg provider implementation using the standardized tag interface
|
|
11
|
-
class Flyimg <
|
|
10
|
+
class Flyimg < HttpBase
|
|
12
11
|
TIMEOUT = 60
|
|
13
12
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
return false unless @config.respond_to?(:flyimg_url) && @config.flyimg_url
|
|
17
|
-
|
|
18
|
-
check_http_service(@config.flyimg_url)
|
|
13
|
+
def service_url
|
|
14
|
+
@config.respond_to?(:flyimg_url) ? @config.flyimg_url : nil
|
|
19
15
|
end
|
|
20
16
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Build single Flyimg URL with all operations combined
|
|
25
|
-
url = build_combined_flyimg_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_flyimg_url(input_path)
|
|
18
|
+
build_combined_url(input_path)
|
|
32
19
|
end
|
|
33
20
|
|
|
34
|
-
def
|
|
35
|
-
raise "flyimg_url not set" unless
|
|
21
|
+
def build_combined_url(input_path)
|
|
22
|
+
raise "flyimg_url not set" unless service_url
|
|
36
23
|
|
|
37
24
|
encoded_url = encode_file_url(input_path)
|
|
38
25
|
options = []
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# c_1 forces the image to fill the exact width x height area.
|
|
46
|
-
# pns_0 allows upscaling when the source is smaller than the target
|
|
47
|
-
# (Flyimg's pns/preserve-natural-size defaults to 1, blocking enlarge).
|
|
48
|
-
options << "w_#{operation[:width]}"
|
|
49
|
-
options << "pns_0"
|
|
50
|
-
if operation[:height]
|
|
51
|
-
options << "h_#{operation[:height]}"
|
|
52
|
-
options << "c_1"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
when :crop
|
|
56
|
-
opts = operation[:options]
|
|
57
|
-
params = operation[:params] || {}
|
|
58
|
-
|
|
59
|
-
# Check for keep parameter (smartcrop support)
|
|
60
|
-
keep = opts[:keep] || params[:keep] || params[:position]
|
|
61
|
-
|
|
62
|
-
if operation[:ratio] && keep && SMARTCROP_POSITIONS.include?(keep.to_s)
|
|
63
|
-
# Use smartcrop (Flyimg's smc option picks the best crop area)
|
|
64
|
-
crop_width = opts[:calculated_width]
|
|
65
|
-
crop_height = opts[:calculated_height]
|
|
66
|
-
|
|
67
|
-
options << "w_#{crop_width}"
|
|
68
|
-
options << "h_#{crop_height}"
|
|
69
|
-
options << "c_1"
|
|
70
|
-
options << "smc_1"
|
|
71
|
-
else
|
|
72
|
-
# Use basic cropping via extract (top-left / bottom-right coordinates)
|
|
73
|
-
if operation[:ratio]
|
|
74
|
-
crop_x = opts[:calculated_x]
|
|
75
|
-
crop_y = opts[:calculated_y]
|
|
76
|
-
crop_width = opts[:calculated_width]
|
|
77
|
-
crop_height = opts[:calculated_height]
|
|
78
|
-
else
|
|
79
|
-
crop_x = opts[:x]
|
|
80
|
-
crop_y = opts[:y]
|
|
81
|
-
crop_width = opts[:width]
|
|
82
|
-
crop_height = opts[:height]
|
|
83
|
-
end
|
|
84
|
-
options << "e_1"
|
|
85
|
-
options << "p1x_#{crop_x}"
|
|
86
|
-
options << "p1y_#{crop_y}"
|
|
87
|
-
options << "p2x_#{crop_x + crop_width}"
|
|
88
|
-
options << "p2y_#{crop_y + crop_height}"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
when :quality
|
|
92
|
-
flyimg_quality = translate_quality_to_flyimg(operation[:quality])
|
|
93
|
-
options << "q_#{flyimg_quality}"
|
|
94
|
-
|
|
95
|
-
when :format
|
|
96
|
-
# Assume validated input from tags
|
|
97
|
-
options << "o_#{operation[:format]}"
|
|
98
|
-
|
|
99
|
-
when :watermark
|
|
100
|
-
watermark_path = operation[:watermark_path]
|
|
101
|
-
position = operation[:options][:position]
|
|
102
|
-
opacity = operation[:options][:opacity]
|
|
103
|
-
|
|
104
|
-
# Flyimg watermark parameters
|
|
105
|
-
encoded_watermark = encode_file_url(watermark_path)
|
|
106
|
-
position_param = translate_flyimg_position(position)
|
|
107
|
-
opacity_param = (opacity * 100).round
|
|
108
|
-
options << "wm_#{encoded_watermark}_#{position_param}_#{opacity_param}"
|
|
109
|
-
|
|
110
|
-
when :alpha_opacity
|
|
111
|
-
opacity = operation[:opacity]
|
|
112
|
-
options << "a_#{(opacity * 255).round}"
|
|
113
|
-
end
|
|
114
|
-
end
|
|
27
|
+
@operations.each { |operation| append_flyimg_operation(operation, options) }
|
|
28
|
+
|
|
29
|
+
preserve_input_format(input_path, options)
|
|
30
|
+
"#{service_url}/upload/#{options.join(',')}/#{encoded_url}"
|
|
31
|
+
end
|
|
115
32
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
options
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def append_flyimg_operation(operation, options)
|
|
36
|
+
case operation[:type]
|
|
37
|
+
when :resize
|
|
38
|
+
append_flyimg_resize(operation, options)
|
|
39
|
+
when :crop
|
|
40
|
+
append_flyimg_crop(operation, options)
|
|
41
|
+
when :quality
|
|
42
|
+
options << "q_#{operation[:quality]}"
|
|
43
|
+
when :format
|
|
44
|
+
options << "o_#{operation[:format]}"
|
|
45
|
+
when :watermark
|
|
46
|
+
append_flyimg_watermark(operation, options)
|
|
47
|
+
when :alpha_opacity
|
|
48
|
+
options << "a_#{alpha_byte_value(operation[:opacity])}"
|
|
122
49
|
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def append_flyimg_resize(operation, options)
|
|
53
|
+
# c_1 forces the image to fill the exact width x height area.
|
|
54
|
+
# pns_0 allows upscaling when the source is smaller than the target
|
|
55
|
+
# (Flyimg's pns/preserve-natural-size defaults to 1, blocking enlarge).
|
|
56
|
+
options << "w_#{operation[:width]}"
|
|
57
|
+
options << "pns_0"
|
|
58
|
+
return unless operation[:height]
|
|
123
59
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"#{@config.flyimg_url}/upload/#{options_string}/#{encoded_url}"
|
|
60
|
+
options << "h_#{operation[:height]}"
|
|
61
|
+
options << "c_1"
|
|
127
62
|
end
|
|
128
63
|
|
|
129
|
-
|
|
64
|
+
def append_flyimg_crop(operation, options)
|
|
65
|
+
geo = crop_geometry(operation)
|
|
130
66
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
67
|
+
if geo[:smartcrop]
|
|
68
|
+
options << "w_#{geo[:width]}"
|
|
69
|
+
options << "h_#{geo[:height]}"
|
|
70
|
+
options << "c_1"
|
|
71
|
+
options << "smc_1"
|
|
72
|
+
else
|
|
73
|
+
# Use basic cropping via extract (top-left / bottom-right coordinates)
|
|
74
|
+
options << "e_1"
|
|
75
|
+
options << "p1x_#{geo[:x]}"
|
|
76
|
+
options << "p1y_#{geo[:y]}"
|
|
77
|
+
options << "p2x_#{geo[:x] + geo[:width]}"
|
|
78
|
+
options << "p2y_#{geo[:y] + geo[:height]}"
|
|
140
79
|
end
|
|
141
80
|
end
|
|
142
81
|
|
|
82
|
+
def append_flyimg_watermark(operation, options)
|
|
83
|
+
parts = watermark_parts(operation)
|
|
84
|
+
encoded_watermark = encode_file_url(parts[:watermark_path])
|
|
85
|
+
position_param = compass_to_short(parts[:position])
|
|
86
|
+
opacity_param = (parts[:opacity] * 100).round
|
|
87
|
+
options << "wm_#{encoded_watermark}_#{position_param}_#{opacity_param}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def preserve_input_format(input_path, options)
|
|
91
|
+
return if op?(:format)
|
|
92
|
+
|
|
93
|
+
ext = input_format_ext(input_path)
|
|
94
|
+
options << "o_#{map_format(ext)}" unless ext.empty?
|
|
95
|
+
end
|
|
96
|
+
|
|
143
97
|
def map_format(format)
|
|
144
98
|
case format&.downcase
|
|
145
99
|
when "jpeg", "jpg"
|
|
@@ -155,33 +109,9 @@ module JekyllImgFlow
|
|
|
155
109
|
end
|
|
156
110
|
end
|
|
157
111
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
FileUtils.mkdir_p(File.dirname(output_path))
|
|
163
|
-
File.write(output_path, response)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def fetch_with_timeout(url)
|
|
167
|
-
uri = URI(url)
|
|
168
|
-
Net::HTTP.start(uri.host, uri.port,
|
|
169
|
-
use_ssl: uri.scheme == "https",
|
|
170
|
-
open_timeout: TIMEOUT,
|
|
171
|
-
read_timeout: TIMEOUT) do |http|
|
|
172
|
-
request = Net::HTTP::Get.new(uri)
|
|
173
|
-
response = http.request(request)
|
|
174
|
-
|
|
175
|
-
raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
176
|
-
|
|
177
|
-
response.body
|
|
178
|
-
end
|
|
179
|
-
rescue StandardError => e
|
|
180
|
-
raise "Flyimg request failed: #{e.message}"
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def translate_quality_to_flyimg(quality)
|
|
184
|
-
quality
|
|
112
|
+
# Thin wrapper kept for test compatibility.
|
|
113
|
+
def translate_flyimg_position(position)
|
|
114
|
+
compass_to_short(position)
|
|
185
115
|
end
|
|
186
116
|
end
|
|
187
117
|
end
|
|
@@ -7,31 +7,22 @@ require_relative "base_provider"
|
|
|
7
7
|
module JekyllImgFlow
|
|
8
8
|
module Providers
|
|
9
9
|
# ImageMagick provider implementation using the standardized tag interface
|
|
10
|
-
class Imagemagick <
|
|
10
|
+
class Imagemagick < CliBase
|
|
11
11
|
# Cache rsvg delegate check across all instances (checked once per build)
|
|
12
12
|
@rsvg_available = nil
|
|
13
13
|
@svg_warning_shown = false
|
|
14
14
|
|
|
15
15
|
def available?
|
|
16
|
-
|
|
17
|
-
_, _, status1 = Open3.capture3("which", "magick")
|
|
18
|
-
_, _, status2 = Open3.capture3("which", "convert")
|
|
19
|
-
status1.success? || status2.success?
|
|
16
|
+
cli_available?("magick", "convert")
|
|
20
17
|
end
|
|
21
18
|
|
|
22
|
-
def
|
|
23
|
-
return if @operations.empty?
|
|
24
|
-
|
|
19
|
+
def before_execute(input_path)
|
|
25
20
|
# Warn once per build about SVG performance with ImageMagick
|
|
26
21
|
warn_svg_performance if svg?(input_path) && !self.class.instance_variable_get(:@svg_warning_shown)
|
|
22
|
+
end
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
execute_command(command)
|
|
31
|
-
|
|
32
|
-
output_path
|
|
33
|
-
ensure
|
|
34
|
-
reset_operations
|
|
24
|
+
def build_commands(input_path, output_path)
|
|
25
|
+
build_combined_imagemagick_command(input_path, output_path)
|
|
35
26
|
end
|
|
36
27
|
|
|
37
28
|
def build_combined_imagemagick_command(input_path, output_path)
|
|
@@ -47,85 +38,7 @@ module JekyllImgFlow
|
|
|
47
38
|
["magick", input_path.shellescape]
|
|
48
39
|
end
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
@operations.each do |operation|
|
|
52
|
-
case operation[:type]
|
|
53
|
-
when :resize
|
|
54
|
-
# Tags now provide complete calculated values
|
|
55
|
-
command_parts << "-resize" << if operation[:height]
|
|
56
|
-
"#{operation[:width]}x#{operation[:height]}!"
|
|
57
|
-
else
|
|
58
|
-
operation[:width].to_s
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
when :crop
|
|
62
|
-
opts = operation[:options]
|
|
63
|
-
params = operation[:params] || {}
|
|
64
|
-
|
|
65
|
-
# Check for keep parameter (ImageMagick doesn't support smartcrop, but we handle it gracefully)
|
|
66
|
-
keep = opts[:keep] || params[:keep] || params[:position]
|
|
67
|
-
|
|
68
|
-
if operation[:ratio] && keep && SMARTCROP_POSITIONS.include?(keep.to_s)
|
|
69
|
-
# ImageMagick doesn't support smartcrop, but we handle the keep parameter
|
|
70
|
-
# Use center gravity as a reasonable fallback for smartcrop requests
|
|
71
|
-
crop_width = opts[:calculated_width]
|
|
72
|
-
crop_height = opts[:calculated_height]
|
|
73
|
-
crop_x = opts[:calculated_x]
|
|
74
|
-
crop_y = opts[:calculated_y]
|
|
75
|
-
|
|
76
|
-
# Use gravity center for smartcrop-like behavior
|
|
77
|
-
crop_spec = "#{crop_width}x#{crop_height}+#{crop_x}+#{crop_y}"
|
|
78
|
-
command_parts << "-gravity" << "center"
|
|
79
|
-
else
|
|
80
|
-
# Use basic cropping
|
|
81
|
-
if operation[:ratio]
|
|
82
|
-
crop_x = opts[:calculated_x]
|
|
83
|
-
crop_y = opts[:calculated_y]
|
|
84
|
-
crop_width = opts[:calculated_width]
|
|
85
|
-
crop_height = opts[:calculated_height]
|
|
86
|
-
else
|
|
87
|
-
crop_x = opts[:x]
|
|
88
|
-
crop_y = opts[:y]
|
|
89
|
-
crop_width = opts[:width]
|
|
90
|
-
crop_height = opts[:height]
|
|
91
|
-
end
|
|
92
|
-
crop_spec = "#{crop_width}x#{crop_height}+#{crop_x}+#{crop_y}"
|
|
93
|
-
end
|
|
94
|
-
command_parts << "-crop" << crop_spec
|
|
95
|
-
|
|
96
|
-
when :quality
|
|
97
|
-
magick_quality = translate_quality_to_imagemagick(operation[:quality])
|
|
98
|
-
command_parts << "-quality" << magick_quality.to_s
|
|
99
|
-
|
|
100
|
-
when :format
|
|
101
|
-
# Assume validated input from tags
|
|
102
|
-
# Format is handled by output filename extension
|
|
103
|
-
# Quality is set separately if needed
|
|
104
|
-
unless @operations.any? { |op| op[:type] == :quality }
|
|
105
|
-
default_quality = @config&.quality || raise("No quality configured")
|
|
106
|
-
command_parts << "-quality" << default_quality.to_s
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
when :watermark
|
|
110
|
-
watermark_path = operation[:watermark_path]
|
|
111
|
-
position = operation[:options][:position]
|
|
112
|
-
|
|
113
|
-
# Translate compass directions to ImageMagick gravity format
|
|
114
|
-
gravity = translate_position(position)
|
|
115
|
-
|
|
116
|
-
# Add watermark as composite operation
|
|
117
|
-
command_parts << watermark_path.shellescape
|
|
118
|
-
command_parts << "-gravity" << gravity
|
|
119
|
-
command_parts << "-composite"
|
|
120
|
-
|
|
121
|
-
when :alpha_opacity
|
|
122
|
-
opacity = operation[:opacity]
|
|
123
|
-
alpha_value = (opacity * 100).round
|
|
124
|
-
command_parts << "-alpha" << "set"
|
|
125
|
-
command_parts << "-channel" << "A"
|
|
126
|
-
command_parts << "-evaluate" << "multiply" << "#{alpha_value}%"
|
|
127
|
-
end
|
|
128
|
-
end
|
|
41
|
+
@operations.each { |operation| append_imagemagick_operation(operation, command_parts) }
|
|
129
42
|
|
|
130
43
|
# Add output filename
|
|
131
44
|
command_parts << output_path.shellescape
|
|
@@ -146,9 +59,56 @@ module JekyllImgFlow
|
|
|
146
59
|
|
|
147
60
|
private
|
|
148
61
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
62
|
+
def append_imagemagick_operation(operation, command_parts)
|
|
63
|
+
case operation[:type]
|
|
64
|
+
when :resize
|
|
65
|
+
command_parts << "-resize" << if operation[:height]
|
|
66
|
+
"#{operation[:width]}x#{operation[:height]}!"
|
|
67
|
+
else
|
|
68
|
+
operation[:width].to_s
|
|
69
|
+
end
|
|
70
|
+
when :crop
|
|
71
|
+
append_imagemagick_crop(operation, command_parts)
|
|
72
|
+
when :quality
|
|
73
|
+
command_parts << "-quality" << operation[:quality].to_s
|
|
74
|
+
when :format
|
|
75
|
+
# Format is handled by output filename extension.
|
|
76
|
+
# Quality is set separately if needed.
|
|
77
|
+
unless op?(:quality)
|
|
78
|
+
default_quality = @config&.quality || raise("No quality configured")
|
|
79
|
+
command_parts << "-quality" << default_quality.to_s
|
|
80
|
+
end
|
|
81
|
+
when :watermark
|
|
82
|
+
append_imagemagick_watermark(operation, command_parts)
|
|
83
|
+
when :alpha_opacity
|
|
84
|
+
alpha_value = (operation[:opacity] * 100).round
|
|
85
|
+
command_parts << "-alpha" << "set"
|
|
86
|
+
command_parts << "-channel" << "A"
|
|
87
|
+
command_parts << "-evaluate" << "multiply" << "#{alpha_value}%"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def append_imagemagick_crop(operation, command_parts)
|
|
92
|
+
geo = crop_geometry(operation)
|
|
93
|
+
|
|
94
|
+
if geo[:smartcrop]
|
|
95
|
+
# ImageMagick doesn't support smartcrop, but we handle the keep parameter
|
|
96
|
+
# Use center gravity as a reasonable fallback for smartcrop requests
|
|
97
|
+
command_parts << "-gravity" << "center"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
crop_spec = "#{geo[:width]}x#{geo[:height]}+#{geo[:x]}+#{geo[:y]}"
|
|
101
|
+
command_parts << "-crop" << crop_spec
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def append_imagemagick_watermark(operation, command_parts)
|
|
105
|
+
parts = watermark_parts(operation)
|
|
106
|
+
gravity = translate_position(parts[:position])
|
|
107
|
+
|
|
108
|
+
# Add watermark as composite operation
|
|
109
|
+
command_parts << parts[:watermark_path].shellescape
|
|
110
|
+
command_parts << "-gravity" << gravity
|
|
111
|
+
command_parts << "-composite"
|
|
152
112
|
end
|
|
153
113
|
|
|
154
114
|
# Choose a rasterization density (DPI) for SVG input.
|
|
@@ -2,162 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "net/http"
|
|
5
|
-
require "tempfile"
|
|
6
5
|
require_relative "base_provider"
|
|
7
6
|
|
|
8
7
|
module JekyllImgFlow
|
|
9
8
|
module Providers
|
|
10
9
|
# Imgproxy provider implementation using the standardized tag interface
|
|
11
|
-
class Imgproxy <
|
|
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
|