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,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
|
|
@@ -9,25 +9,16 @@ module JekyllImgFlow
|
|
|
9
9
|
def process(input_path, output_path, options = {})
|
|
10
10
|
ensure_output_dir(output_path)
|
|
11
11
|
|
|
12
|
-
# Validate that crop information is provided
|
|
13
12
|
ratio = options[:ratio] || options[:aspect_ratio]
|
|
14
13
|
has_pixel_crop = options[:width] || options[:height]
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
raise ArgumentError,
|
|
18
|
-
"Crop operation requires ratio or at least one dimension (width or height)"
|
|
19
|
-
end
|
|
15
|
+
validate_crop_request(ratio, has_pixel_crop)
|
|
20
16
|
|
|
21
17
|
if ratio
|
|
22
|
-
# Handle aspect ratio cropping (e.g., "16:9", "4:3")
|
|
23
18
|
validate_ratio(ratio)
|
|
24
19
|
handle_aspect_ratio_crop(input_path, output_path, ratio, options)
|
|
25
20
|
else
|
|
26
|
-
|
|
27
|
-
validate_positive_integer(options[:width], "width") if options[:width]
|
|
28
|
-
validate_positive_integer(options[:height], "height") if options[:height]
|
|
29
|
-
validate_positive_integer(options[:x], "x") if options[:x]
|
|
30
|
-
validate_positive_integer(options[:y], "y") if options[:y]
|
|
21
|
+
validate_pixel_dimensions(options)
|
|
31
22
|
handle_pixel_crop(input_path, output_path, options)
|
|
32
23
|
end
|
|
33
24
|
|
|
@@ -36,16 +27,25 @@ module JekyllImgFlow
|
|
|
36
27
|
|
|
37
28
|
private
|
|
38
29
|
|
|
30
|
+
def validate_crop_request(ratio, has_pixel_crop)
|
|
31
|
+
return if ratio || has_pixel_crop
|
|
32
|
+
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"Crop operation requires ratio or at least one dimension (width or height)"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_pixel_dimensions(options)
|
|
38
|
+
validate_positive_integer(options[:width], "width") if options[:width]
|
|
39
|
+
validate_positive_integer(options[:height], "height") if options[:height]
|
|
40
|
+
validate_positive_integer(options[:x], "x") if options[:x]
|
|
41
|
+
validate_positive_integer(options[:y], "y") if options[:y]
|
|
42
|
+
end
|
|
43
|
+
|
|
39
44
|
# Handle aspect ratio cropping
|
|
40
45
|
def handle_aspect_ratio_crop(input_path, _output_path, ratio, options)
|
|
41
46
|
# Calculate optimal crop dimensions for the aspect ratio
|
|
42
47
|
calculated_options = calculate_aspect_ratio_dimensions(input_path, ratio, options)
|
|
43
|
-
|
|
44
|
-
# Add keep parameter for smartcrop (JPT compatibility)
|
|
45
|
-
if options[:keep] && %w[attention entropy center
|
|
46
|
-
centre].include?(options[:keep].to_s)
|
|
47
|
-
calculated_options[:keep] = options[:keep]
|
|
48
|
-
end
|
|
48
|
+
add_keep_option(calculated_options, options[:keep])
|
|
49
49
|
|
|
50
50
|
# Pass ratio and calculated options to provider (uniform interface)
|
|
51
51
|
@provider.crop(ratio, calculated_options)
|
|
@@ -53,57 +53,34 @@ module JekyllImgFlow
|
|
|
53
53
|
|
|
54
54
|
# Handle flexible pixel-based cropping
|
|
55
55
|
def handle_pixel_crop(input_path, _output_path, options)
|
|
56
|
-
# Get original image dimensions
|
|
57
56
|
original_width, original_height = get_original_dimensions(input_path)
|
|
58
57
|
|
|
59
|
-
# Parse dimensions (support pixels and percentages)
|
|
60
58
|
crop_width = parse_dimension(options[:width], original_width)
|
|
61
59
|
crop_height = parse_dimension(options[:height], original_height)
|
|
62
60
|
|
|
63
|
-
# At least one dimension must be specified
|
|
64
61
|
raise ArgumentError, "At least width or height must be specified for cropping" unless crop_width || crop_height
|
|
65
62
|
|
|
66
63
|
# Calculate missing dimension if only one provided
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
elsif crop_height && !crop_width
|
|
70
|
-
crop_width = original_width # Keep original width
|
|
71
|
-
end
|
|
64
|
+
crop_width ||= original_width # Keep original width
|
|
65
|
+
crop_height ||= original_height # Keep original height
|
|
72
66
|
|
|
73
67
|
# Parse positions with smart defaults - always provide values
|
|
74
68
|
crop_x = parse_position_with_default(options[:x], crop_width, original_width)
|
|
75
69
|
crop_y = parse_position_with_default(options[:y], crop_height, original_height)
|
|
76
70
|
|
|
77
|
-
# Validate crop area is not bigger than original
|
|
78
71
|
validate_crop_dimensions(crop_x, crop_y, crop_width, crop_height, original_width,
|
|
79
72
|
original_height)
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
crop_options
|
|
83
|
-
x: crop_x,
|
|
84
|
-
y: crop_y,
|
|
85
|
-
width: crop_width,
|
|
86
|
-
height: crop_height
|
|
87
|
-
}
|
|
74
|
+
crop_options = { x: crop_x, y: crop_y, width: crop_width, height: crop_height }
|
|
75
|
+
add_keep_option(crop_options, options[:keep])
|
|
88
76
|
|
|
89
|
-
# Add keep parameter for smartcrop (JPT compatibility)
|
|
90
|
-
if options[:keep] && %w[attention entropy center
|
|
91
|
-
centre].include?(options[:keep].to_s)
|
|
92
|
-
crop_options[:keep] = options[:keep]
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Execute crop with calculated dimensions
|
|
96
77
|
@provider.crop(nil, crop_options)
|
|
97
78
|
end
|
|
98
79
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
require "fastimage"
|
|
102
|
-
original_width, original_height = FastImage.size(input_path)
|
|
103
|
-
|
|
104
|
-
raise ArgumentError, "Unable to determine original image dimensions" unless original_width && original_height
|
|
80
|
+
def add_keep_option(crop_options, keep)
|
|
81
|
+
return unless keep && %w[attention entropy center centre].include?(keep.to_s)
|
|
105
82
|
|
|
106
|
-
[
|
|
83
|
+
crop_options[:keep] = keep
|
|
107
84
|
end
|
|
108
85
|
|
|
109
86
|
# Parse dimension (supports pixels and percentages)
|
|
@@ -13,31 +13,12 @@ module JekyllImgFlow
|
|
|
13
13
|
opacity = options[:opacity]
|
|
14
14
|
raise ArgumentError, "Opacity parameter is required" if opacity.nil?
|
|
15
15
|
|
|
16
|
-
validated_opacity = validate_opacity(opacity)
|
|
16
|
+
validated_opacity = validate_opacity(opacity, min: 0.01, max: 0.99)
|
|
17
17
|
|
|
18
18
|
@provider.alpha_opacity = validated_opacity
|
|
19
19
|
@provider.execute(input_path, output_path)
|
|
20
20
|
output_path
|
|
21
21
|
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def validate_opacity(value)
|
|
26
|
-
# Check if value is numeric before converting
|
|
27
|
-
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.01 and 0.99." unless valid_numeric?(value)
|
|
28
|
-
|
|
29
|
-
opacity = value.to_f
|
|
30
|
-
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.01 and 0.99." unless opacity.between?(0.01, 0.99)
|
|
31
|
-
|
|
32
|
-
opacity
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def valid_numeric?(value)
|
|
36
|
-
Float(value)
|
|
37
|
-
true
|
|
38
|
-
rescue ArgumentError, TypeError
|
|
39
|
-
false
|
|
40
|
-
end
|
|
41
22
|
end
|
|
42
23
|
end
|
|
43
24
|
end
|
|
@@ -38,15 +38,6 @@ module JekyllImgFlow
|
|
|
38
38
|
|
|
39
39
|
quality
|
|
40
40
|
end
|
|
41
|
-
|
|
42
|
-
def validate_quality(value)
|
|
43
|
-
return @default_quality if value.nil?
|
|
44
|
-
|
|
45
|
-
quality = value.to_i
|
|
46
|
-
raise ArgumentError, "Invalid quality: '#{value}'. Must be between 1 and 100." unless quality.between?(1, 100)
|
|
47
|
-
|
|
48
|
-
quality
|
|
49
|
-
end
|
|
50
41
|
end
|
|
51
42
|
end
|
|
52
43
|
end
|
|
@@ -15,17 +15,6 @@ module JekyllImgFlow
|
|
|
15
15
|
@provider.quality = quality
|
|
16
16
|
@provider.execute(input_path, output_path)
|
|
17
17
|
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def validate_quality(value)
|
|
22
|
-
return @default_quality if value.nil?
|
|
23
|
-
|
|
24
|
-
quality = value.to_i
|
|
25
|
-
raise ArgumentError, "Invalid quality: '#{value}'. Must be between 1 and 100." unless quality.between?(1, 100)
|
|
26
|
-
|
|
27
|
-
quality
|
|
28
|
-
end
|
|
29
18
|
end
|
|
30
19
|
end
|
|
31
20
|
end
|
|
@@ -13,23 +13,13 @@ module JekyllImgFlow
|
|
|
13
13
|
width = validate_positive_integer(options[:width], "width")
|
|
14
14
|
height = validate_positive_integer(options[:height], "height")
|
|
15
15
|
|
|
16
|
-
# At least one dimension must be specified
|
|
17
16
|
raise ArgumentError, "Either width or height must be specified for resize operation." if width.nil? && height.nil?
|
|
18
17
|
|
|
19
18
|
# Get original dimensions
|
|
20
19
|
original_width, original_height = get_image_dimensions(input_path)
|
|
21
20
|
|
|
22
21
|
# Calculate missing dimension to maintain aspect ratio ONLY when one dimension is missing
|
|
23
|
-
|
|
24
|
-
# Only height provided - calculate width to maintain aspect ratio
|
|
25
|
-
aspect_ratio = original_width.to_f / original_height
|
|
26
|
-
width = (height * aspect_ratio).round
|
|
27
|
-
elsif height.nil? && width
|
|
28
|
-
# Only width provided - calculate height to maintain aspect ratio
|
|
29
|
-
aspect_ratio = original_height.to_f / original_width
|
|
30
|
-
height = (width * aspect_ratio).round
|
|
31
|
-
end
|
|
32
|
-
# If both width and height are provided, use them as-is (allows aspect ratio changes)
|
|
22
|
+
width, height = calculate_missing_dimension(width, height, original_width, original_height)
|
|
33
23
|
|
|
34
24
|
# Calculate scale factors for provider
|
|
35
25
|
scale_x = width && original_width ? width.to_f / original_width : nil
|
|
@@ -54,6 +44,23 @@ module JekyllImgFlow
|
|
|
54
44
|
result
|
|
55
45
|
end
|
|
56
46
|
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def calculate_missing_dimension(width, height, original_width, original_height)
|
|
50
|
+
if width.nil? && height
|
|
51
|
+
# Only height provided - calculate width to maintain aspect ratio
|
|
52
|
+
aspect_ratio = original_width.to_f / original_height
|
|
53
|
+
[(height * aspect_ratio).round, height]
|
|
54
|
+
elsif height.nil? && width
|
|
55
|
+
# Only width provided - calculate height to maintain aspect ratio
|
|
56
|
+
aspect_ratio = original_height.to_f / original_width
|
|
57
|
+
[width, (width * aspect_ratio).round]
|
|
58
|
+
else
|
|
59
|
+
# If both width and height are provided, use them as-is (allows aspect ratio changes)
|
|
60
|
+
[width, height]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
57
64
|
# validate_positive_integer inherited from BaseTag
|
|
58
65
|
end
|
|
59
66
|
end
|
|
@@ -36,23 +36,6 @@ module JekyllImgFlow
|
|
|
36
36
|
else position.to_s
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
|
-
|
|
40
|
-
def validate_opacity(value)
|
|
41
|
-
# Check if value is numeric before converting
|
|
42
|
-
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.0 and 1.0." unless valid_numeric?(value)
|
|
43
|
-
|
|
44
|
-
opacity = value.to_f
|
|
45
|
-
raise ArgumentError, "Invalid opacity: '#{value}'. Must be between 0.0 and 1.0." unless opacity.between?(0.0, 1.0)
|
|
46
|
-
|
|
47
|
-
opacity
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def valid_numeric?(value)
|
|
51
|
-
Float(value)
|
|
52
|
-
true
|
|
53
|
-
rescue ArgumentError, TypeError
|
|
54
|
-
false
|
|
55
|
-
end
|
|
56
39
|
end
|
|
57
40
|
end
|
|
58
41
|
end
|