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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8400a4bc3d213b557802452b3af0816098443aa5542f27e81bf690dd2ee3f7a8
|
|
4
|
+
data.tar.gz: 1f8562de9d69d7451603623ab2e63741f4889a4d04d35d9ae01a561878e8d539
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbd0f3c0d194bd7acfdc199c9c1eb78dcb69d09c4cc9f213ceb48d1c1c5cb4402518a87a14f44f234df1487ae6eba4495f92aae2b31bb602b199bcaaab4438ef
|
|
7
|
+
data.tar.gz: 7c07124101bf734ebedce868d1329900c79acde6d90463d5fbd680fe49f02517e743ba4d7d37ebf3e26454e3e9dd4df80ada8265874bb96b1659767a33a8306f
|
data/README.md
CHANGED
|
@@ -98,6 +98,28 @@ ImgFlow has two processing flows: **Build-Time** (pre-generation) and **Runtime*
|
|
|
98
98
|
|
|
99
99
|
**See:** [providers.md](docs/providers.md) for detailed provider comparison and setup
|
|
100
100
|
|
|
101
|
+
### SVG File Handling
|
|
102
|
+
|
|
103
|
+
SVG files are supported as input by all providers. Since SVGs are vector
|
|
104
|
+
graphics, they are rasterized before format conversion (WebP, AVIF, JPG, PNG).
|
|
105
|
+
|
|
106
|
+
- **Weserv** (via librsvg): When an SVG has no explicit pixel dimensions
|
|
107
|
+
(e.g. `width="100%"` with a large `viewBox`), ImgFlow automatically
|
|
108
|
+
caps the rasterization at 2000px wide to prevent memory exhaustion.
|
|
109
|
+
If a resize or crop operation is present, the target dimensions are used
|
|
110
|
+
directly.
|
|
111
|
+
- **Sharp, LibVips, Imgproxy, Flyimg**: Handle SVGs via their respective
|
|
112
|
+
rasterization backends. SVGs with very large viewBox dimensions may be
|
|
113
|
+
slow or fail depending on the provider's memory limits.
|
|
114
|
+
- **ImageMagick**: SVG processing is slower than other providers without
|
|
115
|
+
the `librsvg` delegate installed. Set `FULL_SVG_TEST=true` to include
|
|
116
|
+
SVGs in ImageMagick test runs.
|
|
117
|
+
|
|
118
|
+
SVGs with explicit `width` and `height` attributes (in pixels, not
|
|
119
|
+
percentages) are always processed at the specified size first, then resized
|
|
120
|
+
to the target dimensions — this is the recommended way to author SVGs for
|
|
121
|
+
image processing.
|
|
122
|
+
|
|
101
123
|
## 🚀 Quick Commands
|
|
102
124
|
|
|
103
125
|
```bash
|
|
@@ -110,44 +110,51 @@ module JekyllImgFlow
|
|
|
110
110
|
operations = []
|
|
111
111
|
operation_options = options.slice(*OPERATION_PARAMS)
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
add_crop_resize_operations(operations, operation_options)
|
|
114
|
+
add_simple_operations(operations, operation_options)
|
|
115
|
+
add_format_operation(operations, operation_options)
|
|
116
|
+
add_optimize_operation(operations, operation_options)
|
|
117
|
+
|
|
118
|
+
operations
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.add_crop_resize_operations(operations, opts)
|
|
122
|
+
has_crop = opts[:ratio] || opts[:aspect_ratio]
|
|
123
|
+
has_resize = opts[:width] || opts[:height]
|
|
124
|
+
crop_params = opts.slice(:ratio, :aspect_ratio, :keep, :position)
|
|
116
125
|
|
|
117
126
|
if has_crop && has_resize
|
|
118
|
-
crop_params = operation_options.slice(:ratio, :aspect_ratio, :keep, :position)
|
|
119
|
-
resize_params = operation_options.except(:ratio, :aspect_ratio, :keep, :position)
|
|
120
127
|
operations << { type: :crop, params: crop_params }
|
|
121
|
-
operations << { type: :resize, params:
|
|
128
|
+
operations << { type: :resize, params: opts.except(:ratio, :aspect_ratio, :keep, :position) }
|
|
122
129
|
elsif has_crop
|
|
123
|
-
crop_params = operation_options.slice(:ratio, :aspect_ratio, :keep, :position)
|
|
124
130
|
operations << { type: :crop, params: crop_params }
|
|
125
131
|
elsif has_resize
|
|
126
|
-
operations << { type: :resize, params:
|
|
132
|
+
operations << { type: :resize, params: opts }
|
|
127
133
|
end
|
|
134
|
+
end
|
|
128
135
|
|
|
129
|
-
|
|
136
|
+
def self.add_simple_operations(operations, opts)
|
|
130
137
|
SIMPLE_OPERATIONS.each do |key, builder|
|
|
131
|
-
operations << builder.call(
|
|
138
|
+
operations << builder.call(opts[key]) if opts[key]
|
|
132
139
|
end
|
|
140
|
+
end
|
|
133
141
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
format_value = operation_options[:format] || operation_options[:formats]
|
|
137
|
-
operations << if format_value.is_a?(Array)
|
|
138
|
-
{ type: :format, params: { formats: format_value } }
|
|
139
|
-
else
|
|
140
|
-
{ type: :format, params: { format: format_value } }
|
|
141
|
-
end
|
|
142
|
-
end
|
|
142
|
+
def self.add_format_operation(operations, opts)
|
|
143
|
+
return unless opts[:format] || opts[:formats]
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
format_value = opts[:format] || opts[:formats]
|
|
146
|
+
operations << if format_value.is_a?(Array)
|
|
147
|
+
{ type: :format, params: { formats: format_value } }
|
|
148
|
+
else
|
|
149
|
+
{ type: :format, params: { format: format_value } }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
149
152
|
|
|
150
|
-
|
|
153
|
+
def self.add_optimize_operation(operations, opts)
|
|
154
|
+
return unless opts[:optimize] || opts[:level]
|
|
155
|
+
|
|
156
|
+
operations << { type: :optimize,
|
|
157
|
+
params: { level: opts[:level] || :medium } }
|
|
151
158
|
end
|
|
152
159
|
|
|
153
160
|
def self.extract_html_attributes(options)
|
|
@@ -99,61 +99,11 @@ module JekyllImgFlow
|
|
|
99
99
|
# @return [Hash, nil] ImgFlow preset data or nil if not convertible
|
|
100
100
|
def convert_preset_to_imgflow(preset_name, preset_data)
|
|
101
101
|
operations = []
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
elsif preset_data["base_width"]
|
|
108
|
-
# For pixel-ratio presets, use base_width
|
|
109
|
-
operations << { "resize" => { "width" => preset_data["base_width"] } }
|
|
110
|
-
elsif preset_data["width"]
|
|
111
|
-
# Direct width setting
|
|
112
|
-
operations << { "resize" => { "width" => preset_data["width"] } }
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Handle height (less common in Picture Tag)
|
|
116
|
-
if preset_data["height"]
|
|
117
|
-
if operations.any? && operations.last["resize"]
|
|
118
|
-
# Add height to existing resize operation
|
|
119
|
-
operations.last["resize"]["height"] = preset_data["height"]
|
|
120
|
-
else
|
|
121
|
-
operations << { "resize" => { "height" => preset_data["height"] } }
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Convert quality (if specified)
|
|
126
|
-
operations << { "quality" => { "quality" => preset_data["quality"] } } if preset_data["quality"]
|
|
127
|
-
|
|
128
|
-
# Convert formats - Picture Tag uses 'original', ImgFlow uses actual format
|
|
129
|
-
if preset_data["formats"]
|
|
130
|
-
formats = Array(preset_data["formats"]).map do |format|
|
|
131
|
-
case format
|
|
132
|
-
when "original"
|
|
133
|
-
"jpg" # Default to jpg for 'original'
|
|
134
|
-
else
|
|
135
|
-
format
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
operations << { "format" => { "formats" => formats } }
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Convert crop (aspect ratio) - handle both ratio and aspect_ratio with precedence
|
|
142
|
-
ratio_value = preset_data["ratio"] || preset_data["aspect_ratio"] || preset_data["crop"]
|
|
143
|
-
operations << { "crop" => { "ratio" => ratio_value } } if ratio_value
|
|
144
|
-
|
|
145
|
-
# Convert gravity/position - handle both position and gravity with precedence
|
|
146
|
-
position_value = preset_data["position"] || preset_data["gravity"]
|
|
147
|
-
if position_value
|
|
148
|
-
if operations.any? { |op| op["crop"] }
|
|
149
|
-
# Add to existing crop operation
|
|
150
|
-
crop_op = operations.find { |op| op["crop"] }
|
|
151
|
-
crop_op["crop"]["position"] = position_value
|
|
152
|
-
else
|
|
153
|
-
# Create new crop operation
|
|
154
|
-
operations << { "crop" => { "position" => position_value } }
|
|
155
|
-
end
|
|
156
|
-
end
|
|
102
|
+
add_resize_operation(operations, preset_data)
|
|
103
|
+
add_quality_operation(operations, preset_data)
|
|
104
|
+
add_format_operation(operations, preset_data)
|
|
105
|
+
add_crop_operation(operations, preset_data)
|
|
106
|
+
add_position_operation(operations, preset_data)
|
|
157
107
|
|
|
158
108
|
return if operations.empty?
|
|
159
109
|
|
|
@@ -171,30 +121,84 @@ module JekyllImgFlow
|
|
|
171
121
|
}
|
|
172
122
|
end
|
|
173
123
|
|
|
124
|
+
def add_resize_operation(operations, preset_data)
|
|
125
|
+
width = preset_width(preset_data)
|
|
126
|
+
operations << { "resize" => { "width" => width } } if width
|
|
127
|
+
|
|
128
|
+
return unless preset_data["height"]
|
|
129
|
+
|
|
130
|
+
if operations.any? && operations.last["resize"]
|
|
131
|
+
operations.last["resize"]["height"] = preset_data["height"]
|
|
132
|
+
else
|
|
133
|
+
operations << { "resize" => { "height" => preset_data["height"] } }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def preset_width(preset_data)
|
|
138
|
+
return Array(preset_data["widths"]).max if preset_data["widths"]
|
|
139
|
+
return preset_data["base_width"] if preset_data["base_width"]
|
|
140
|
+
|
|
141
|
+
preset_data["width"] if preset_data["width"]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def add_quality_operation(operations, preset_data)
|
|
145
|
+
return unless preset_data["quality"]
|
|
146
|
+
|
|
147
|
+
operations << { "quality" => { "quality" => preset_data["quality"] } }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def add_format_operation(operations, preset_data)
|
|
151
|
+
return unless preset_data["formats"]
|
|
152
|
+
|
|
153
|
+
formats = Array(preset_data["formats"]).map do |format|
|
|
154
|
+
format == "original" ? "jpg" : format
|
|
155
|
+
end
|
|
156
|
+
operations << { "format" => { "formats" => formats } }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def add_crop_operation(operations, preset_data)
|
|
160
|
+
ratio_value = preset_data["ratio"] || preset_data["aspect_ratio"] || preset_data["crop"]
|
|
161
|
+
operations << { "crop" => { "ratio" => ratio_value } } if ratio_value
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def add_position_operation(operations, preset_data)
|
|
165
|
+
position_value = preset_data["position"] || preset_data["gravity"]
|
|
166
|
+
return unless position_value
|
|
167
|
+
|
|
168
|
+
crop_op = operations.find { |op| op["crop"] }
|
|
169
|
+
if crop_op
|
|
170
|
+
crop_op["crop"]["position"] = position_value
|
|
171
|
+
else
|
|
172
|
+
operations << { "crop" => { "position" => position_value } }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
174
176
|
# Generate description for ImgFlow preset
|
|
175
177
|
# @param preset_name [String] Preset name
|
|
176
178
|
# @param preset_data [Hash] Original preset data
|
|
177
179
|
# @return [String] Description
|
|
178
180
|
def generate_description(preset_name, preset_data)
|
|
179
|
-
|
|
181
|
+
parts = []
|
|
182
|
+
add_width_description(parts, preset_data)
|
|
183
|
+
parts << "height: #{preset_data['height']}" if preset_data["height"]
|
|
184
|
+
parts << "quality: #{preset_data['quality']}" if preset_data["quality"]
|
|
185
|
+
parts << "formats: #{Array(preset_data['formats']).join(',')}" if preset_data["formats"]
|
|
186
|
+
parts << "crop: #{preset_data['crop']}" if preset_data["crop"]
|
|
187
|
+
parts << "gravity: #{preset_data['gravity']}" if preset_data["gravity"]
|
|
188
|
+
|
|
189
|
+
"Migrated from Picture Tag preset '#{preset_name}'. #{parts.join(', ')}"
|
|
190
|
+
end
|
|
180
191
|
|
|
192
|
+
def add_width_description(parts, preset_data)
|
|
181
193
|
if preset_data["widths"]
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
parts << "widths: #{Array(preset_data['widths']).join(',')}"
|
|
195
|
+
parts << "using max width: #{Array(preset_data['widths']).max}"
|
|
184
196
|
elsif preset_data["base_width"]
|
|
185
|
-
|
|
186
|
-
|
|
197
|
+
parts << "base_width: #{preset_data['base_width']}"
|
|
198
|
+
parts << "pixel_ratios: #{Array(preset_data['pixel_ratios']).join(',')}"
|
|
187
199
|
elsif preset_data["width"]
|
|
188
|
-
|
|
200
|
+
parts << "width: #{preset_data['width']}"
|
|
189
201
|
end
|
|
190
|
-
|
|
191
|
-
operations << "height: #{preset_data['height']}" if preset_data["height"]
|
|
192
|
-
operations << "quality: #{preset_data['quality']}" if preset_data["quality"]
|
|
193
|
-
operations << "formats: #{Array(preset_data['formats']).join(',')}" if preset_data["formats"]
|
|
194
|
-
operations << "crop: #{preset_data['crop']}" if preset_data["crop"]
|
|
195
|
-
operations << "gravity: #{preset_data['gravity']}" if preset_data["gravity"]
|
|
196
|
-
|
|
197
|
-
"Migrated from Picture Tag preset '#{preset_name}'. #{operations.join(', ')}"
|
|
198
202
|
end
|
|
199
203
|
|
|
200
204
|
# Save ImgFlow preset to YAML file
|
|
@@ -132,55 +132,59 @@ module JekyllImgFlow
|
|
|
132
132
|
# @return [Hash] Tags in key => value format
|
|
133
133
|
def yaml_to_tags(preset)
|
|
134
134
|
tags = {}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
preset_operations.each do |op|
|
|
135
|
+
(preset["operations"] || []).each do |op|
|
|
138
136
|
next unless op.is_a?(Hash)
|
|
139
137
|
|
|
140
138
|
op.each do |op_type, params|
|
|
141
139
|
next unless params.is_a?(Hash)
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
# This allows Parser to detect the operation type
|
|
145
|
-
case op_type.to_s
|
|
146
|
-
when "resize"
|
|
147
|
-
# Resize params: width, height (both optional, at least one required)
|
|
148
|
-
tags[:width] = params["width"] if params["width"]
|
|
149
|
-
tags[:height] = params["height"] if params["height"]
|
|
150
|
-
when "crop"
|
|
151
|
-
# Crop params: ratio or width/height
|
|
152
|
-
tags[:ratio] = params["ratio"] if params["ratio"]
|
|
153
|
-
tags[:aspect_ratio] = params["aspect_ratio"] if params["aspect_ratio"]
|
|
154
|
-
tags[:width] = params["width"] if params["width"] && !tags[:width]
|
|
155
|
-
tags[:height] = params["height"] if params["height"] && !tags[:height]
|
|
156
|
-
when "format"
|
|
157
|
-
# Format params: format or formats
|
|
158
|
-
if params["formats"]
|
|
159
|
-
tag_value = params["formats"].is_a?(Array) ? params["formats"].join(",") : params["formats"]
|
|
160
|
-
tags[:formats] = tag_value
|
|
161
|
-
elsif params["format"]
|
|
162
|
-
tags[:format] = params["format"]
|
|
163
|
-
end
|
|
164
|
-
when "quality"
|
|
165
|
-
tags[:quality] = params["quality"] if params["quality"]
|
|
166
|
-
when "optimize"
|
|
167
|
-
tags[:optimize] = true
|
|
168
|
-
tags[:level] = params["level"] if params["level"]
|
|
169
|
-
when "opacity"
|
|
170
|
-
tags[:opacity] = params["opacity"] if params["opacity"]
|
|
171
|
-
else
|
|
172
|
-
# Generic handling for unknown operations
|
|
173
|
-
params.each do |key, value|
|
|
174
|
-
tag_value = value.is_a?(Array) ? value.join(",") : value
|
|
175
|
-
tags[key.to_sym] = tag_value
|
|
176
|
-
end
|
|
177
|
-
end
|
|
141
|
+
merge_operation_tags(tags, op_type.to_s, params)
|
|
178
142
|
end
|
|
179
143
|
end
|
|
180
144
|
|
|
181
145
|
tags
|
|
182
146
|
end
|
|
183
147
|
|
|
148
|
+
def merge_operation_tags(tags, op_type, params)
|
|
149
|
+
case op_type
|
|
150
|
+
when "resize"
|
|
151
|
+
tags[:width] = params["width"] if params["width"]
|
|
152
|
+
tags[:height] = params["height"] if params["height"]
|
|
153
|
+
when "crop"
|
|
154
|
+
tags[:ratio] = params["ratio"] if params["ratio"]
|
|
155
|
+
tags[:aspect_ratio] = params["aspect_ratio"] if params["aspect_ratio"]
|
|
156
|
+
tags[:width] = params["width"] if params["width"] && !tags[:width]
|
|
157
|
+
tags[:height] = params["height"] if params["height"] && !tags[:height]
|
|
158
|
+
when "format"
|
|
159
|
+
merge_format_tag(tags, params)
|
|
160
|
+
when "quality"
|
|
161
|
+
tags[:quality] = params["quality"] if params["quality"]
|
|
162
|
+
when "optimize"
|
|
163
|
+
tags[:optimize] = true
|
|
164
|
+
tags[:level] = params["level"] if params["level"]
|
|
165
|
+
when "opacity"
|
|
166
|
+
tags[:opacity] = params["opacity"] if params["opacity"]
|
|
167
|
+
else
|
|
168
|
+
merge_generic_tags(tags, params)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def merge_format_tag(tags, params)
|
|
173
|
+
if params["formats"]
|
|
174
|
+
tag_value = params["formats"].is_a?(Array) ? params["formats"].join(",") : params["formats"]
|
|
175
|
+
tags[:formats] = tag_value
|
|
176
|
+
elsif params["format"]
|
|
177
|
+
tags[:format] = params["format"]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def merge_generic_tags(tags, params)
|
|
182
|
+
params.each do |key, value|
|
|
183
|
+
tag_value = value.is_a?(Array) ? value.join(",") : value
|
|
184
|
+
tags[key.to_sym] = tag_value
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
184
188
|
# Merge preset tags with user options (user options override)
|
|
185
189
|
# @param preset_tags [Hash] Tags from preset
|
|
186
190
|
# @param user_options [Hash] User-provided options
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "pathname"
|
|
5
|
+
require "fileutils"
|
|
5
6
|
|
|
6
7
|
module JekyllImgFlow
|
|
7
8
|
module Providers
|
|
@@ -10,6 +11,15 @@ module JekyllImgFlow
|
|
|
10
11
|
# Valid smartcrop position values
|
|
11
12
|
SMARTCROP_POSITIONS = %w[attention entropy center centre].freeze
|
|
12
13
|
|
|
14
|
+
# Short compass → provider position mapping used by HTTP providers.
|
|
15
|
+
COMPASS_TO_SHORT = {
|
|
16
|
+
"northwest" => "tl",
|
|
17
|
+
"northeast" => "tr",
|
|
18
|
+
"southwest" => "bl",
|
|
19
|
+
"southeast" => "br",
|
|
20
|
+
"center" => "c"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
13
23
|
attr_accessor :config
|
|
14
24
|
|
|
15
25
|
def initialize(config = {})
|
|
@@ -17,6 +27,86 @@ module JekyllImgFlow
|
|
|
17
27
|
@operations = []
|
|
18
28
|
end
|
|
19
29
|
|
|
30
|
+
# Operation lookup helpers — used by all providers to avoid
|
|
31
|
+
# repeating `@operations.find { |op| op[:type] == :xxx }`.
|
|
32
|
+
def find_op(type)
|
|
33
|
+
@operations.find { |op| op[:type] == type }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def op?(type)
|
|
37
|
+
@operations.any? { |op| op[:type] == type }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Normalize a crop operation into a geometry hash.
|
|
41
|
+
# Returns { smartcrop:, keep:, x:, y:, width:, height: } where
|
|
42
|
+
# smartcrop is true when ratio + keep + valid smartcrop position.
|
|
43
|
+
def crop_geometry(operation)
|
|
44
|
+
opts = operation[:options] || {}
|
|
45
|
+
params = operation[:params] || {}
|
|
46
|
+
keep = opts[:keep] || params[:keep] || params[:position]
|
|
47
|
+
smartcrop = operation[:ratio] && keep && SMARTCROP_POSITIONS.include?(keep.to_s)
|
|
48
|
+
coords = if operation[:ratio]
|
|
49
|
+
{ x: opts[:calculated_x], y: opts[:calculated_y],
|
|
50
|
+
width: opts[:calculated_width], height: opts[:calculated_height] }
|
|
51
|
+
else
|
|
52
|
+
{ x: opts[:x] || 0, y: opts[:y] || 0,
|
|
53
|
+
width: opts[:width], height: opts[:height] }
|
|
54
|
+
end
|
|
55
|
+
coords.merge(smartcrop: smartcrop, keep: keep)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Extract watermark operation parts into a hash.
|
|
59
|
+
def watermark_parts(operation)
|
|
60
|
+
{
|
|
61
|
+
watermark_path: operation[:watermark_path],
|
|
62
|
+
position: operation[:options][:position],
|
|
63
|
+
opacity: operation[:options][:opacity]
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Shared compass → short position mapping (tl/tr/bl/br/c).
|
|
68
|
+
# HTTP providers (imgproxy, weserv, flyimg) all use this mapping.
|
|
69
|
+
def compass_to_short(position)
|
|
70
|
+
COMPASS_TO_SHORT[position.to_s] || position.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Input format extension (lowercased, no dot) for format preservation.
|
|
74
|
+
def input_format_ext(input_path)
|
|
75
|
+
File.extname(input_path).delete(".").downcase
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Convert a 0..1 opacity float to a 0..255 integer (used by all
|
|
79
|
+
# providers that encode alpha as an 8-bit value).
|
|
80
|
+
def alpha_byte_value(opacity)
|
|
81
|
+
(opacity * 255).round
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Generate a temporary file path by replacing the extension of
|
|
85
|
+
# +input_path+ with +suffix+ (e.g. temp_path("a.jpg", "tmp_base.jpg")
|
|
86
|
+
# → "a.tmp_base.jpg").
|
|
87
|
+
def temp_path(input_path, suffix)
|
|
88
|
+
input_path.gsub(/\.[^.]+$/, ".#{suffix}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Map a smartcrop +keep+ value to the libvips interestingness string
|
|
92
|
+
# used by Sharp and LibVips smartcrop/thumbnail --crop operations.
|
|
93
|
+
def smartcrop_interestingness(keep)
|
|
94
|
+
case keep.to_s
|
|
95
|
+
when "entropy" then "entropy"
|
|
96
|
+
when "center", "centre" then "centre"
|
|
97
|
+
else "attention"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check whether any of the given CLI commands is available on PATH.
|
|
102
|
+
# Used by CLI providers (sharp, libvips, imagemagick).
|
|
103
|
+
def cli_available?(*commands)
|
|
104
|
+
commands.any? do |cmd|
|
|
105
|
+
_, _, status = Open3.capture3("which", cmd)
|
|
106
|
+
status.success?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
20
110
|
# Check if this provider is available (must be implemented by subclasses)
|
|
21
111
|
def available?
|
|
22
112
|
# Default: not available unless subclass implements actual check
|
|
@@ -206,5 +296,113 @@ module JekyllImgFlow
|
|
|
206
296
|
end
|
|
207
297
|
end
|
|
208
298
|
end
|
|
299
|
+
|
|
300
|
+
# Shared base for HTTP-based providers (imgproxy, weserv, flyimg).
|
|
301
|
+
# Subclasses must implement `service_url`, `build_combined_url`, and
|
|
302
|
+
# define a `TIMEOUT` constant. They may optionally override
|
|
303
|
+
# `provider_label` for error messages.
|
|
304
|
+
class HttpBase < BaseProvider
|
|
305
|
+
def available?
|
|
306
|
+
url = service_url
|
|
307
|
+
return false unless url
|
|
308
|
+
|
|
309
|
+
check_http_service(url)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def execute(input_path, output_path)
|
|
313
|
+
return if @operations.empty?
|
|
314
|
+
|
|
315
|
+
url = build_combined_url(input_path)
|
|
316
|
+
fetch_and_save(url, output_path)
|
|
317
|
+
output_path
|
|
318
|
+
ensure
|
|
319
|
+
reset_operations
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
protected
|
|
323
|
+
|
|
324
|
+
# Subclasses override to return the configured service URL or nil.
|
|
325
|
+
def service_url
|
|
326
|
+
raise NotImplementedError
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Subclasses override to build the full request URL.
|
|
330
|
+
def build_combined_url(_input_path)
|
|
331
|
+
raise NotImplementedError
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Label used in error messages (defaults to class name).
|
|
335
|
+
def provider_label
|
|
336
|
+
self.class.name.split("::").last
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def timeout
|
|
340
|
+
self.class::TIMEOUT
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def fetch_and_save(url, output_path)
|
|
344
|
+
response = fetch_with_timeout(url)
|
|
345
|
+
raise "#{provider_label} request failed" if response.empty?
|
|
346
|
+
|
|
347
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
348
|
+
File.write(output_path, response)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def fetch_with_timeout(url)
|
|
352
|
+
uri = URI(url)
|
|
353
|
+
Net::HTTP.start(uri.host, uri.port,
|
|
354
|
+
use_ssl: uri.scheme == "https",
|
|
355
|
+
open_timeout: timeout,
|
|
356
|
+
read_timeout: timeout) do |http|
|
|
357
|
+
request = Net::HTTP::Get.new(uri)
|
|
358
|
+
response = http.request(request)
|
|
359
|
+
|
|
360
|
+
raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
361
|
+
|
|
362
|
+
response.body
|
|
363
|
+
end
|
|
364
|
+
rescue StandardError => e
|
|
365
|
+
raise "#{provider_label} request failed: #{e.message}"
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Shared base for CLI-based providers (sharp, imagemagick, libvips).
|
|
370
|
+
# Subclasses implement `build_commands(input_path, output_path)` to
|
|
371
|
+
# return an array of command entries (strings or arrays), and
|
|
372
|
+
# `run_command(cmd)` to execute a single entry.
|
|
373
|
+
class CliBase < BaseProvider
|
|
374
|
+
def execute(input_path, output_path)
|
|
375
|
+
return if @operations.empty?
|
|
376
|
+
|
|
377
|
+
before_execute(input_path)
|
|
378
|
+
commands = build_commands(input_path, output_path)
|
|
379
|
+
Array(commands).each { |cmd| run_command(cmd) }
|
|
380
|
+
output_path
|
|
381
|
+
ensure
|
|
382
|
+
reset_operations
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
protected
|
|
386
|
+
|
|
387
|
+
# Hook for subclasses to perform pre-execution work (e.g. SVG warnings).
|
|
388
|
+
def before_execute(_input_path); end
|
|
389
|
+
|
|
390
|
+
# Subclasses override to return command(s) to execute.
|
|
391
|
+
# May return a single command or an array of commands.
|
|
392
|
+
def build_commands(_input_path, _output_path)
|
|
393
|
+
raise NotImplementedError
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Execute a single command. Default uses the shell-based
|
|
397
|
+
# `execute_command` from BaseProvider. Subclasses that use
|
|
398
|
+
# argument arrays (no shell) should override this.
|
|
399
|
+
def run_command(cmd)
|
|
400
|
+
if cmd.is_a?(Array) && cmd.first == :cleanup
|
|
401
|
+
FileUtils.rm_f(cmd[1])
|
|
402
|
+
return
|
|
403
|
+
end
|
|
404
|
+
execute_command(cmd)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
209
407
|
end
|
|
210
408
|
end
|