jekyll-imgflow 0.3.3 → 0.4.1
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 +23 -0
- data/lib/jekyll-imgflow/build_time_processor.rb +42 -29
- data/lib/jekyll-imgflow/config.rb +15 -8
- data/lib/jekyll-imgflow/modal_assets.rb +12 -6
- 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 +48 -38
- data/lib/jekyll-imgflow/providers/base_provider.rb +216 -11
- 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 +30 -23
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +0 -17
- data/lib/jekyll-imgflow/version.rb +1 -1
- metadata +1 -1
|
@@ -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
|
|
@@ -7,24 +7,17 @@ require_relative "base_provider"
|
|
|
7
7
|
module JekyllImgFlow
|
|
8
8
|
module Providers
|
|
9
9
|
# Libvips provider implementation using the standardized tag interface
|
|
10
|
-
class Libvips <
|
|
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.
|
|
@@ -48,10 +41,10 @@ module JekyllImgFlow
|
|
|
48
41
|
|
|
49
42
|
def operation_flags
|
|
50
43
|
{
|
|
51
|
-
crop:
|
|
52
|
-
resize:
|
|
53
|
-
watermark:
|
|
54
|
-
alpha:
|
|
44
|
+
crop: op?(:crop),
|
|
45
|
+
resize: op?(:resize),
|
|
46
|
+
watermark: op?(:watermark),
|
|
47
|
+
alpha: op?(:alpha_opacity)
|
|
55
48
|
}
|
|
56
49
|
end
|
|
57
50
|
|
|
@@ -72,19 +65,19 @@ module JekyllImgFlow
|
|
|
72
65
|
end
|
|
73
66
|
|
|
74
67
|
def build_alpha_pipeline(input_path, output_path, has_crop, has_resize)
|
|
75
|
-
|
|
76
|
-
commands, base_path = build_base_pipeline(input_path,
|
|
68
|
+
temp = temp_path(input_path, "tmp_base.jpg")
|
|
69
|
+
commands, base_path = build_base_pipeline(input_path, temp, has_crop, has_resize)
|
|
77
70
|
commands << build_alpha_command(base_path, output_path)
|
|
78
|
-
commands << [:cleanup,
|
|
71
|
+
commands << [:cleanup, temp] unless base_path == input_path
|
|
79
72
|
commands
|
|
80
73
|
end
|
|
81
74
|
|
|
82
75
|
def build_watermark_pipeline(input_path, output_path, has_crop, has_resize)
|
|
83
|
-
|
|
84
|
-
commands, base_path = build_base_pipeline(input_path,
|
|
76
|
+
temp = temp_path(input_path, "tmp_base.jpg")
|
|
77
|
+
commands, base_path = build_base_pipeline(input_path, temp, has_crop, has_resize)
|
|
85
78
|
base_path, commands = apply_watermark_alpha(base_path, input_path, commands)
|
|
86
79
|
commands.concat(build_composite_commands(base_path, output_path))
|
|
87
|
-
cleanup_paths(commands, input_path,
|
|
80
|
+
cleanup_paths(commands, input_path, temp, base_path)
|
|
88
81
|
commands
|
|
89
82
|
end
|
|
90
83
|
|
|
@@ -97,10 +90,10 @@ module JekyllImgFlow
|
|
|
97
90
|
end
|
|
98
91
|
|
|
99
92
|
def apply_watermark_alpha(base_path, input_path, commands)
|
|
100
|
-
alpha_op =
|
|
93
|
+
alpha_op = find_op(:alpha_opacity)
|
|
101
94
|
return [base_path, commands] unless alpha_op
|
|
102
95
|
|
|
103
|
-
alpha_temp = input_path
|
|
96
|
+
alpha_temp = temp_path(input_path, "tmp_alpha.jpg")
|
|
104
97
|
[alpha_temp, commands << build_alpha_command(base_path, alpha_temp)]
|
|
105
98
|
end
|
|
106
99
|
|
|
@@ -110,26 +103,24 @@ module JekyllImgFlow
|
|
|
110
103
|
end
|
|
111
104
|
|
|
112
105
|
def build_composite_commands(base_path, output_path)
|
|
113
|
-
wm_op =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
wm_temp = watermark_path.gsub(/\.[^.]+$/, ".tmp_wm.png")
|
|
120
|
-
alpha_value = opacity
|
|
106
|
+
wm_op = find_op(:watermark)
|
|
107
|
+
parts = watermark_parts(wm_op)
|
|
108
|
+
|
|
109
|
+
if parts[:opacity] && parts[:opacity] < 1.0
|
|
110
|
+
wm_temp = temp_path(parts[:watermark_path], "tmp_wm.png")
|
|
111
|
+
alpha_value = parts[:opacity]
|
|
121
112
|
# Step 1: Apply alpha to watermark
|
|
122
|
-
alpha_cmd = ["vips", "linear", watermark_path,
|
|
113
|
+
alpha_cmd = ["vips", "linear", parts[:watermark_path],
|
|
123
114
|
"#{wm_temp}[alpha]", "1 1 1 #{alpha_value}", "0"]
|
|
124
115
|
# Step 2: Composite
|
|
125
|
-
xy_args = position_to_vips_xy(position, base_path, watermark_path)
|
|
116
|
+
xy_args = position_to_vips_xy(parts[:position], base_path, parts[:watermark_path])
|
|
126
117
|
composite_cmd = ["vips", "composite2", base_path, wm_temp,
|
|
127
118
|
build_format_spec(output_path), "over"] + xy_args
|
|
128
119
|
# Step 3: Cleanup wm_temp
|
|
129
120
|
[alpha_cmd, composite_cmd, [:cleanup, wm_temp]]
|
|
130
121
|
else
|
|
131
|
-
xy_args = position_to_vips_xy(position, base_path, watermark_path)
|
|
132
|
-
[["vips", "composite2", base_path, watermark_path,
|
|
122
|
+
xy_args = position_to_vips_xy(parts[:position], base_path, parts[:watermark_path])
|
|
123
|
+
[["vips", "composite2", base_path, parts[:watermark_path],
|
|
133
124
|
build_format_spec(output_path), "over"] + xy_args]
|
|
134
125
|
end
|
|
135
126
|
end
|
|
@@ -168,9 +159,8 @@ module JekyllImgFlow
|
|
|
168
159
|
end
|
|
169
160
|
|
|
170
161
|
def build_alpha_command(input_path, output_path)
|
|
171
|
-
alpha_op =
|
|
172
|
-
|
|
173
|
-
alpha_value = opacity.round(2)
|
|
162
|
+
alpha_op = find_op(:alpha_opacity)
|
|
163
|
+
alpha_value = alpha_op[:opacity].round(2)
|
|
174
164
|
|
|
175
165
|
# vips linear multiplies alpha band: "1 1 1 alpha" scales alpha
|
|
176
166
|
["vips", "linear", input_path, output_path,
|
|
@@ -180,22 +170,22 @@ module JekyllImgFlow
|
|
|
180
170
|
def build_sequential_crop_resize(input_path, output_path)
|
|
181
171
|
if svg?(input_path)
|
|
182
172
|
# SVG: use thumbnail with --crop to do crop+resize in one step
|
|
183
|
-
resize_op =
|
|
173
|
+
resize_op = find_op(:resize)
|
|
184
174
|
format_spec = build_format_spec(output_path)
|
|
185
175
|
width = resize_op[:width]
|
|
186
176
|
height = resize_op[:height]
|
|
187
177
|
[["vips", "thumbnail", input_path, format_spec,
|
|
188
178
|
width.to_s, "--height=#{height}", "--crop=attention"]]
|
|
189
179
|
else
|
|
190
|
-
|
|
191
|
-
[build_crop_command(input_path,
|
|
192
|
-
build_resize_command(
|
|
193
|
-
[: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]]
|
|
194
184
|
end
|
|
195
185
|
end
|
|
196
186
|
|
|
197
187
|
def build_resize_command(input_path, output_path)
|
|
198
|
-
resize_op =
|
|
188
|
+
resize_op = find_op(:resize)
|
|
199
189
|
format_spec = build_format_spec(output_path)
|
|
200
190
|
width = resize_op[:width]
|
|
201
191
|
height = resize_op[:height]
|
|
@@ -216,35 +206,23 @@ module JekyllImgFlow
|
|
|
216
206
|
end
|
|
217
207
|
|
|
218
208
|
def build_crop_command(input_path, output_path)
|
|
219
|
-
crop_op =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
keep = opts[:keep] || params[:keep] || params[:position]
|
|
223
|
-
return build_smartcrop_command(crop_op, opts, keep, input_path, output_path) if smartcrop?(crop_op, keep)
|
|
209
|
+
crop_op = find_op(:crop)
|
|
210
|
+
geo = crop_geometry(crop_op)
|
|
211
|
+
return build_smartcrop_command(geo, input_path, output_path) if geo[:smartcrop]
|
|
224
212
|
|
|
225
|
-
build_extract_command(
|
|
213
|
+
build_extract_command(geo, input_path, output_path)
|
|
226
214
|
end
|
|
227
215
|
|
|
228
|
-
def
|
|
229
|
-
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def build_smartcrop_command(_crop_op, opts, keep, input_path, output_path)
|
|
233
|
-
interestingness = %w[center centre].include?(keep.to_s) ? "centre" : keep.to_s
|
|
234
|
-
interestingness = "attention" unless %w[entropy centre].include?(interestingness)
|
|
216
|
+
def build_smartcrop_command(geo, input_path, output_path)
|
|
217
|
+
interestingness = smartcrop_interestingness(geo[:keep])
|
|
235
218
|
["vips", "smartcrop", input_path, output_path,
|
|
236
|
-
|
|
219
|
+
geo[:width].to_s, geo[:height].to_s,
|
|
237
220
|
"--interesting=#{interestingness}"]
|
|
238
221
|
end
|
|
239
222
|
|
|
240
|
-
def build_extract_command(
|
|
241
|
-
ratio = crop_op[:ratio]
|
|
242
|
-
x = ratio ? opts[:calculated_x] : (opts[:x] || 0)
|
|
243
|
-
y = ratio ? opts[:calculated_y] : (opts[:y] || 0)
|
|
244
|
-
width = ratio ? opts[:calculated_width] : opts[:width]
|
|
245
|
-
height = ratio ? opts[:calculated_height] : opts[:height]
|
|
223
|
+
def build_extract_command(geo, input_path, output_path)
|
|
246
224
|
["vips", "extract_area", input_path, output_path,
|
|
247
|
-
x.to_s, y.to_s, width.to_s, height.to_s]
|
|
225
|
+
geo[:x].to_s, geo[:y].to_s, geo[:width].to_s, geo[:height].to_s]
|
|
248
226
|
end
|
|
249
227
|
|
|
250
228
|
# SVG crop: use `vips thumbnail` with --crop to crop during thumbnailing.
|
|
@@ -254,18 +232,12 @@ module JekyllImgFlow
|
|
|
254
232
|
# the most interesting region — this is the correct behavior for SVGs
|
|
255
233
|
# since they have no fixed pixel dimensions.
|
|
256
234
|
def build_svg_crop_pipeline(input_path, output_path)
|
|
257
|
-
crop_op =
|
|
258
|
-
|
|
259
|
-
crop_width =
|
|
260
|
-
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
|
|
261
239
|
format_spec = build_format_spec(output_path)
|
|
262
|
-
|
|
263
|
-
keep = opts[:keep] || crop_op[:params]&.[](:keep) || crop_op[:params]&.[](:position)
|
|
264
|
-
interestingness = case keep.to_s
|
|
265
|
-
when "entropy" then "entropy"
|
|
266
|
-
when "center", "centre" then "centre"
|
|
267
|
-
else "attention"
|
|
268
|
-
end
|
|
240
|
+
interestingness = smartcrop_interestingness(geo[:keep])
|
|
269
241
|
|
|
270
242
|
[["vips", "thumbnail", input_path, format_spec,
|
|
271
243
|
crop_width.to_s, "--height=#{crop_height}",
|
|
@@ -287,8 +259,8 @@ module JekyllImgFlow
|
|
|
287
259
|
# are executed via Open3.capture3 array form (no shell).
|
|
288
260
|
def build_format_spec(output_path)
|
|
289
261
|
# Build vips output specification with format and quality
|
|
290
|
-
format_op =
|
|
291
|
-
quality_op =
|
|
262
|
+
format_op = find_op(:format)
|
|
263
|
+
quality_op = find_op(:quality)
|
|
292
264
|
|
|
293
265
|
# Simple case: no format/quality operations
|
|
294
266
|
return output_path unless format_op || quality_op
|