jekyll-imgflow 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +33 -2
- data/lib/jekyll-imgflow/build_time_processor.rb +3 -2
- data/lib/jekyll-imgflow/config.rb +25 -18
- data/lib/jekyll-imgflow/filename_generator.rb +2 -0
- data/lib/jekyll-imgflow/html_generator.rb +6 -6
- data/lib/jekyll-imgflow/imgflow_tag.rb +86 -52
- data/lib/jekyll-imgflow/manifest_manager.rb +64 -57
- data/lib/jekyll-imgflow/operation_processor.rb +44 -64
- data/lib/jekyll-imgflow/parser.rb +32 -25
- data/lib/jekyll-imgflow/picture_tag_adaptor.rb +113 -106
- data/lib/jekyll-imgflow/picture_tag_preset_migrator.rb +73 -69
- data/lib/jekyll-imgflow/preset_manager.rb +42 -38
- data/lib/jekyll-imgflow/providers/base_provider.rb +199 -0
- data/lib/jekyll-imgflow/providers/flyimg.rb +72 -142
- data/lib/jekyll-imgflow/providers/imagemagick.rb +57 -97
- data/lib/jekyll-imgflow/providers/imgproxy.rb +53 -126
- data/lib/jekyll-imgflow/providers/libvips.rb +105 -138
- data/lib/jekyll-imgflow/providers/sharp.rb +57 -100
- data/lib/jekyll-imgflow/providers/weserv.rb +69 -124
- data/lib/jekyll-imgflow/tag_scanner.rb +19 -24
- data/lib/jekyll-imgflow/tags/base_tag.rb +33 -0
- data/lib/jekyll-imgflow/tags/crop_tag.rb +24 -47
- data/lib/jekyll-imgflow/tags/opacity_tag.rb +1 -20
- data/lib/jekyll-imgflow/tags/optimize_tag.rb +0 -9
- data/lib/jekyll-imgflow/tags/quality_tag.rb +0 -11
- data/lib/jekyll-imgflow/tags/resize_tag.rb +18 -11
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +0 -17
- data/lib/jekyll-imgflow/version.rb +1 -1
- metadata +1 -1
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
|
@@ -6,6 +6,8 @@ A [Jekyll](https://jekyllrb.com/) plugin for automatic image optimization with m
|
|
|
6
6
|
|
|
7
7
|
[](https://deepwiki.com/gundestrup/jekyll-imgflow)
|
|
8
8
|
[](https://github.com/gundestrup/jekyll-imgflow)
|
|
9
|
+
[](https://semgrep.dev/)
|
|
10
|
+
[](https://www.codefactor.io/repository/github/gundestrup/jekyll-imgflow)
|
|
9
11
|
[](https://codecov.io/gh/gundestrup/jekyll-imgflow)
|
|
10
12
|
[](https://github.com/gundestrup/jekyll-imgflow-vscode)
|
|
11
13
|
[](LICENSE)
|
|
@@ -96,6 +98,28 @@ ImgFlow has two processing flows: **Build-Time** (pre-generation) and **Runtime*
|
|
|
96
98
|
|
|
97
99
|
**See:** [providers.md](docs/providers.md) for detailed provider comparison and setup
|
|
98
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
|
+
|
|
99
123
|
## 🚀 Quick Commands
|
|
100
124
|
|
|
101
125
|
```bash
|
|
@@ -202,8 +226,15 @@ bundle install
|
|
|
202
226
|
# Download test images
|
|
203
227
|
rake download_test_images
|
|
204
228
|
|
|
205
|
-
# Run
|
|
206
|
-
rake
|
|
229
|
+
# Run the CI-equivalent checks locally
|
|
230
|
+
rake ci
|
|
231
|
+
|
|
232
|
+
# Run the local Semgrep Pro rules directly
|
|
233
|
+
semgrep scan --pro --config .semgrep.yml --error lib/
|
|
234
|
+
|
|
235
|
+
# Sync the repository scan and findings with the Semgrep dashboard
|
|
236
|
+
# (requires semgrep login or SEMGREP_APP_TOKEN)
|
|
237
|
+
semgrep ci
|
|
207
238
|
```
|
|
208
239
|
|
|
209
240
|
### Key Files
|
|
@@ -78,8 +78,9 @@ module JekyllImgFlow
|
|
|
78
78
|
results = @batch_manager.process_all
|
|
79
79
|
register_skipped_tasks(@batch_manager.completed)
|
|
80
80
|
|
|
81
|
-
#
|
|
82
|
-
|
|
81
|
+
# Persist newly processed defaults; unchanged builds keep page-usage resets in memory
|
|
82
|
+
# until post_write saves the final stable manifest.
|
|
83
|
+
@manifest.save unless results[:completed].zero? && results[:failed].zero?
|
|
83
84
|
|
|
84
85
|
Jekyll.logger.info "✅ BuildTimeProcessor complete: #{results[:completed]} completed, #{results[:failed]} failed"
|
|
85
86
|
|
|
@@ -28,33 +28,40 @@ module JekyllImgFlow
|
|
|
28
28
|
:optimize_qualities, :fallback_format, :image_modal
|
|
29
29
|
|
|
30
30
|
def initialize(site)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cfg
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@
|
|
41
|
-
@
|
|
31
|
+
@site = site
|
|
32
|
+
cfg = merged_config
|
|
33
|
+
assign_core_config(cfg)
|
|
34
|
+
assign_provider_config(cfg)
|
|
35
|
+
@optimize_qualities = cfg["optimize_qualities"] || default_optimize_qualities
|
|
36
|
+
validate_backend_priority!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def assign_core_config(cfg)
|
|
40
|
+
@originals = cfg["originals"] || DEFAULT_ORIGINALS
|
|
41
|
+
@output = cfg["output"] || DEFAULT_OUTPUT
|
|
42
|
+
@input_formats = cfg["input_formats"] || DEFAULT_INPUT_FORMATS
|
|
43
|
+
@sizes = cfg["sizes"] || DEFAULT_SIZES
|
|
44
|
+
@formats = cfg["formats"] || DEFAULT_FORMATS
|
|
45
|
+
@quality = cfg["quality"] || DEFAULT_QUALITY
|
|
42
46
|
@backend_priority = cfg["backend_priority"] || DEFAULT_BACKEND_PRIORITY
|
|
43
|
-
@fallback_format
|
|
44
|
-
@image_modal
|
|
47
|
+
@fallback_format = cfg["fallback_format"] || DEFAULT_FALLBACK_FORMAT
|
|
48
|
+
@image_modal = cfg.fetch("image_modal", DEFAULT_IMAGE_MODAL)
|
|
49
|
+
end
|
|
45
50
|
|
|
51
|
+
def assign_provider_config(cfg)
|
|
46
52
|
@imgproxy_url = cfg["imgproxy_url"]
|
|
47
|
-
@weserv_url
|
|
48
|
-
@flyimg_url
|
|
49
|
-
|
|
53
|
+
@weserv_url = cfg["weserv_url"]
|
|
54
|
+
@flyimg_url = cfg["flyimg_url"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def default_optimize_qualities
|
|
58
|
+
{
|
|
50
59
|
"low" => 30,
|
|
51
60
|
"medium" => 50, # Uses default quality
|
|
52
61
|
"high" => 85, # Uses default quality
|
|
53
62
|
"maximum" => 95,
|
|
54
63
|
"default" => 75
|
|
55
64
|
}
|
|
56
|
-
|
|
57
|
-
validate_backend_priority!
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
def cache_dir
|
|
@@ -83,6 +83,7 @@ module JekyllImgFlow
|
|
|
83
83
|
def generate_jpt_hash(original_path, operations)
|
|
84
84
|
# Get filename digest (MD5 of filename) - exactly like JPT
|
|
85
85
|
filename = File.basename(original_path)
|
|
86
|
+
# nosemgrep: ruby.lang.security.weak-hashes-md5.weak-hashes-md5 -- Jekyll Picture Tag filename compatibility, not security.
|
|
86
87
|
file_digest = Digest::MD5.hexdigest(filename)
|
|
87
88
|
|
|
88
89
|
# Build settings array (like Jekyll Picture Tag)
|
|
@@ -94,6 +95,7 @@ module JekyllImgFlow
|
|
|
94
95
|
]
|
|
95
96
|
|
|
96
97
|
# Generate MD5 hash of settings (9 chars like JPT)
|
|
98
|
+
# nosemgrep: ruby.lang.security.weak-hashes-md5.weak-hashes-md5 -- Jekyll Picture Tag filename compatibility, not security.
|
|
97
99
|
Digest::MD5.hexdigest(settings.join)[0..8]
|
|
98
100
|
end
|
|
99
101
|
end
|
|
@@ -76,15 +76,15 @@ module JekyllImgFlow
|
|
|
76
76
|
return default_attributes if attributes.nil? || attributes.empty?
|
|
77
77
|
|
|
78
78
|
{
|
|
79
|
-
img: attributes
|
|
80
|
-
picture: attributes
|
|
81
|
-
source: attributes
|
|
82
|
-
a: attributes
|
|
83
|
-
parent: attributes
|
|
79
|
+
img: attributes.fetch(:img, {}),
|
|
80
|
+
picture: attributes.fetch(:picture, {}),
|
|
81
|
+
source: attributes.fetch(:source, {}),
|
|
82
|
+
a: attributes.fetch(:a, {}),
|
|
83
|
+
parent: attributes.fetch(:parent, {}),
|
|
84
84
|
alt: attributes[:alt],
|
|
85
85
|
link: attributes[:link],
|
|
86
86
|
modal: attributes[:modal],
|
|
87
|
-
modal_results: attributes
|
|
87
|
+
modal_results: attributes.fetch(:modal_results, [])
|
|
88
88
|
}
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -73,46 +73,48 @@ module Jekyll
|
|
|
73
73
|
return "" unless parsed[:image_path]
|
|
74
74
|
|
|
75
75
|
site = context.registers[:site]
|
|
76
|
-
|
|
76
|
+
input_path = resolve_input_path(parsed[:image_path], site, components[:config])
|
|
77
|
+
original_name = original_image_name(input_path, site, components[:config])
|
|
78
|
+
page_path = page_identifier(context.registers[:page])
|
|
79
|
+
results, modal_results = process_image_variants(components, parsed, original_name, input_path,
|
|
80
|
+
page_path)
|
|
81
|
+
render_processed_results(results, modal_results, parsed, site, context)
|
|
82
|
+
end
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
input_path = resolve_image_path(
|
|
84
|
+
def resolve_input_path(image_path, site, config)
|
|
85
|
+
input_path = resolve_image_path(image_path, site, config)
|
|
80
86
|
raise ArgumentError, "Input image file not found: #{input_path}" unless File.file?(input_path)
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
originals_dir = File.join(site.source, components[:config].originals)
|
|
85
|
-
original_name = input_path.sub("#{originals_dir}/", "")
|
|
88
|
+
input_path
|
|
89
|
+
end
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else
|
|
92
|
-
"unknown"
|
|
93
|
-
end
|
|
91
|
+
def original_image_name(input_path, site, config)
|
|
92
|
+
originals_dir = File.join(site.source, config.originals)
|
|
93
|
+
input_path.sub("#{originals_dir}/", "")
|
|
94
|
+
end
|
|
94
95
|
|
|
96
|
+
def page_identifier(page)
|
|
97
|
+
(page && (page["path"] || page["url"] || page["name"])) || "unknown"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def process_image_variants(components, parsed, original_name, input_path, page_path)
|
|
95
101
|
operations = parsed[:operations]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
process_modal_variants(components, operations.first, parsed, original_name,
|
|
107
|
-
input_path, page_path)
|
|
108
|
-
end
|
|
102
|
+
return [[input_path], []] if operations.empty?
|
|
103
|
+
|
|
104
|
+
operation = operations.first
|
|
105
|
+
results = process_variants(components, operation, original_name, input_path, page_path)
|
|
106
|
+
modal_results = process_modal_variants(components, operation, parsed, original_name,
|
|
107
|
+
input_path, page_path)
|
|
108
|
+
[results, modal_results]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_processed_results(results, modal_results, parsed, site, context)
|
|
109
112
|
relative_results = results.uniq.map { |result| relative_result_path(result, site) }
|
|
110
113
|
relative_modal_results = modal_results.uniq.map do |result|
|
|
111
114
|
relative_result_path(result, site)
|
|
112
115
|
end
|
|
113
116
|
parsed = parsed.merge(markup_format: "picture") if relative_results.length > 1
|
|
114
|
-
|
|
115
|
-
generate_html(relative_results, parsed, context)
|
|
117
|
+
generate_html(relative_results, parsed.merge(modal_results: relative_modal_results), context)
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
def process_modal_variants(components, operation, parsed, original_name, input_path, page_path)
|
|
@@ -123,18 +125,22 @@ module Jekyll
|
|
|
123
125
|
return [] unless original_width
|
|
124
126
|
|
|
125
127
|
params = operation[:params].dup
|
|
126
|
-
formats =
|
|
127
|
-
|
|
128
|
+
formats = modal_formats(params, config)
|
|
129
|
+
params.delete(:formats)
|
|
128
130
|
modal_width = [original_width, config.sizes.values.max].min
|
|
129
|
-
|
|
130
131
|
formats.map do |format|
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
process_variant(components, operation, params.merge(width: modal_width, format: format),
|
|
133
|
+
original_name, input_path, page_path)
|
|
133
134
|
end
|
|
134
135
|
rescue FastImage::ImageFetchError, FastImage::UnknownImageType
|
|
135
136
|
[]
|
|
136
137
|
end
|
|
137
138
|
|
|
139
|
+
def modal_formats(params, config)
|
|
140
|
+
formats = Array(params[:formats] || params[:format])
|
|
141
|
+
formats.empty? ? config.formats : formats
|
|
142
|
+
end
|
|
143
|
+
|
|
138
144
|
def modal_requested?(parsed, config)
|
|
139
145
|
return false if %w[direct_url naked_srcset].include?(parsed[:markup_format])
|
|
140
146
|
|
|
@@ -164,35 +170,63 @@ module Jekyll
|
|
|
164
170
|
|
|
165
171
|
def process_variant(components, operation, params, original_name, input_path, page_path)
|
|
166
172
|
config = components[:config]
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
apply_default_params(params, config)
|
|
174
|
+
variant = variant_context(components, params, original_name, input_path, config)
|
|
175
|
+
|
|
176
|
+
if variant_cached?(components, original_name, params, variant[:version_type],
|
|
177
|
+
variant[:digest], variant[:output_path])
|
|
178
|
+
return cached_variant(components, original_name, params, variant[:version_type],
|
|
179
|
+
variant[:digest], variant[:output_path], page_path)
|
|
170
180
|
end
|
|
171
181
|
|
|
182
|
+
process_variant_output(components, operation, params, variant, original_name, input_path,
|
|
183
|
+
page_path)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def variant_context(components, params, original_name, input_path, config)
|
|
172
187
|
digest = components[:filename_generator].file_digest(input_path)
|
|
173
|
-
variant = operation.merge(params: params, file_digest: digest)
|
|
174
|
-
subdir = File.dirname(original_name)
|
|
175
|
-
subdir = nil if subdir == "."
|
|
176
188
|
filename = components[:filename_generator].generate_filename(input_path, params)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return output_path
|
|
185
|
-
end
|
|
189
|
+
subdir = output_subdir(original_name)
|
|
190
|
+
{
|
|
191
|
+
digest: digest,
|
|
192
|
+
output_path: components[:path_resolver].resolve_source_output_path(filename, subdir),
|
|
193
|
+
version_type: determine_version_type(params, config)
|
|
194
|
+
}
|
|
195
|
+
end
|
|
186
196
|
|
|
197
|
+
def process_variant_output(components, operation, params, variant, original_name, input_path,
|
|
198
|
+
page_path)
|
|
187
199
|
unless components[:operation_processor]
|
|
188
200
|
Jekyll.logger.warn "ImgFlow:", "No image provider available — " \
|
|
189
201
|
"rendering original without optimization."
|
|
190
202
|
return input_path
|
|
191
203
|
end
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
205
|
+
operation_params = operation.merge(params: params, file_digest: variant[:digest],
|
|
206
|
+
force_processing: true)
|
|
207
|
+
components[:operation_processor].process_operation(original_name, operation_params, input_path,
|
|
208
|
+
page_path)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def apply_default_params(params, config)
|
|
212
|
+
return unless determine_version_type(params, config) == :default
|
|
213
|
+
|
|
214
|
+
params[:format] ||= config.formats.first
|
|
215
|
+
params[:quality] ||= config.quality
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def output_subdir(original_name)
|
|
219
|
+
File.dirname(original_name).then { |dir| dir == "." ? nil : dir }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def variant_cached?(components, original_name, params, version_type, digest, output_path)
|
|
223
|
+
components[:manifest].version_exists?(original_name, params, version_type, digest) && File.file?(output_path)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def cached_variant(components, original_name, params, version_type, _digest, output_path, page_path)
|
|
227
|
+
components[:stats]&.record_cache_hit
|
|
228
|
+
components[:manifest].update_page_usage(original_name, params, version_type, page_path)
|
|
229
|
+
output_path
|
|
196
230
|
end
|
|
197
231
|
|
|
198
232
|
def relative_result_path(result, site)
|
|
@@ -25,32 +25,35 @@ module JekyllImgFlow
|
|
|
25
25
|
|
|
26
26
|
# Load existing manifest or create new one
|
|
27
27
|
def load_manifest(path = @manifest_path)
|
|
28
|
-
|
|
29
|
-
begin
|
|
30
|
-
data = JSON.parse(File.read(path))
|
|
31
|
-
|
|
32
|
-
# Handle new format with provider at top level
|
|
33
|
-
if data.is_a?(Hash) && data.key?("images")
|
|
34
|
-
@cached_provider = data["provider"]
|
|
35
|
-
images = data["images"] || {}
|
|
36
|
-
else
|
|
37
|
-
# Old format - just image data
|
|
38
|
-
@cached_provider = nil
|
|
39
|
-
images = data
|
|
40
|
-
end
|
|
28
|
+
return empty_manifest unless path && File.exist?(path)
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
load_manifest_file(path)
|
|
31
|
+
rescue JSON::ParserError => e
|
|
32
|
+
Jekyll.logger.warn "ImgFlow: Corrupt manifest file, starting fresh: #{e.message}"
|
|
33
|
+
empty_manifest
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_manifest_file(path)
|
|
37
|
+
data = JSON.parse(File.read(path))
|
|
38
|
+
images = manifest_images(data)
|
|
39
|
+
migrate_legacy_manifest(images)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def manifest_images(data)
|
|
43
|
+
if data.is_a?(Hash) && data.key?("images")
|
|
44
|
+
@cached_provider = data["provider"]
|
|
45
|
+
data["images"] || {}
|
|
48
46
|
else
|
|
49
47
|
@cached_provider = nil
|
|
50
|
-
|
|
48
|
+
data
|
|
51
49
|
end
|
|
52
50
|
end
|
|
53
51
|
|
|
52
|
+
def empty_manifest
|
|
53
|
+
@cached_provider = nil
|
|
54
|
+
{}
|
|
55
|
+
end
|
|
56
|
+
|
|
54
57
|
# Detect and migrate legacy manifest entries (pre-0.1.11 format).
|
|
55
58
|
# Old entries have operations like {"width": 400} without format/quality
|
|
56
59
|
# and no file_digest. Clearing them lets the build re-register existing
|
|
@@ -232,51 +235,55 @@ module JekyllImgFlow
|
|
|
232
235
|
# Register a new image version
|
|
233
236
|
def register_version(original_name, output_path, operations, type, page_path,
|
|
234
237
|
file_digest = nil, provider = nil)
|
|
238
|
+
image = manifest_image(original_name, file_digest)
|
|
239
|
+
image["file_digest"] = file_digest if file_digest
|
|
240
|
+
versions = image["versions"][type.to_s] ||= []
|
|
241
|
+
normalized = normalize_operations(operations)
|
|
242
|
+
page_paths = normalize_page_paths(page_path)
|
|
243
|
+
version = versions.find { |entry| same_operations?(entry["operations"], normalized) }
|
|
244
|
+
|
|
245
|
+
if version
|
|
246
|
+
update_version(version, output_path, page_paths, file_digest, provider)
|
|
247
|
+
else
|
|
248
|
+
versions << new_version(output_path, normalized, type, page_paths, file_digest, provider)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def manifest_image(original_name, file_digest)
|
|
235
253
|
@manifest[original_name] ||= {
|
|
236
254
|
"versions" => { "default" => [], "specialized" => [] },
|
|
237
|
-
"file_digest" => file_digest
|
|
255
|
+
"file_digest" => file_digest
|
|
238
256
|
}
|
|
257
|
+
end
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
versions = @manifest[original_name]["versions"][type.to_s] ||= []
|
|
244
|
-
normalized_operations = normalize_operations(operations)
|
|
245
|
-
|
|
246
|
-
# Find or create version entry
|
|
247
|
-
version = versions.find { |v| same_operations?(v["operations"], normalized_operations) }
|
|
248
|
-
|
|
249
|
-
# Normalize page_path to array
|
|
250
|
-
page_paths = if page_path.is_a?(Array)
|
|
251
|
-
page_path
|
|
252
|
-
else
|
|
253
|
-
(page_path ? [page_path] : [])
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
if version
|
|
257
|
-
# Add pages to used_on list if not already present
|
|
258
|
-
version["used_on"] ||= []
|
|
259
|
-
page_paths.each do |path|
|
|
260
|
-
version["used_on"] << path if path && !version["used_on"].include?(path)
|
|
261
|
-
end
|
|
262
|
-
version["output"] = output_path
|
|
263
|
-
version["file_digest"] = file_digest if file_digest
|
|
264
|
-
# Update provider if provided
|
|
265
|
-
version["provider"] = provider if provider
|
|
259
|
+
def normalize_page_paths(page_path)
|
|
260
|
+
if page_path.is_a?(Array)
|
|
261
|
+
page_path
|
|
266
262
|
else
|
|
267
|
-
|
|
268
|
-
versions << {
|
|
269
|
-
"output" => output_path,
|
|
270
|
-
"operations" => normalized_operations,
|
|
271
|
-
"type" => type.to_s,
|
|
272
|
-
"used_on" => page_paths,
|
|
273
|
-
"created_at" => Time.now.to_i,
|
|
274
|
-
"file_digest" => file_digest,
|
|
275
|
-
"provider" => provider
|
|
276
|
-
}
|
|
263
|
+
(page_path ? [page_path] : [])
|
|
277
264
|
end
|
|
278
265
|
end
|
|
279
266
|
|
|
267
|
+
def update_version(version, output_path, page_paths, file_digest, provider)
|
|
268
|
+
version["used_on"] ||= []
|
|
269
|
+
page_paths.each { |path| version["used_on"] << path if path && !version["used_on"].include?(path) }
|
|
270
|
+
version["output"] = output_path
|
|
271
|
+
version["file_digest"] = file_digest if file_digest
|
|
272
|
+
version["provider"] = provider if provider
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def new_version(output_path, operations, type, page_paths, file_digest, provider)
|
|
276
|
+
{
|
|
277
|
+
"output" => output_path,
|
|
278
|
+
"operations" => operations,
|
|
279
|
+
"type" => type.to_s,
|
|
280
|
+
"used_on" => page_paths,
|
|
281
|
+
"created_at" => Time.now.to_i,
|
|
282
|
+
"file_digest" => file_digest,
|
|
283
|
+
"provider" => provider
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
|
|
280
287
|
# Check if image version is default type
|
|
281
288
|
def default_version?(original_name, operations)
|
|
282
289
|
return false unless @manifest[original_name]
|
|
@@ -43,84 +43,64 @@ module JekyllImgFlow
|
|
|
43
43
|
# @param page_path [String] Optional page path for manifest tracking
|
|
44
44
|
# @return [String] Path to processed image
|
|
45
45
|
def process_operation(original_name, operation, input_path, page_path = nil)
|
|
46
|
-
type = operation[:type]
|
|
47
46
|
params = operation[:params]
|
|
48
47
|
file_digest = operation[:file_digest] || @filename_generator.file_digest(input_path)
|
|
49
|
-
|
|
50
|
-
# Determine version type
|
|
51
48
|
version_type = determine_version_type(params)
|
|
52
|
-
|
|
53
|
-
# Preserve original directory structure under output
|
|
54
|
-
subdir = File.dirname(original_name)
|
|
55
|
-
subdir = nil if subdir == "."
|
|
56
|
-
|
|
57
|
-
# Generate filename using FilenameGenerator (JPT compatible)
|
|
58
49
|
filename = @filename_generator.generate_filename(input_path, params)
|
|
50
|
+
subdir = output_subdir(original_name)
|
|
51
|
+
output_path = @path_resolver.resolve_source_output_path(filename, subdir)
|
|
52
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
process_output(operation, original_name, input_path, output_path, params)
|
|
55
|
+
register_manifest(original_name, filename, params, version_type, page_path, file_digest, subdir)
|
|
56
|
+
register_jekyll_static_file(output_path)
|
|
57
|
+
output_path
|
|
58
|
+
end
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
def output_subdir(original_name)
|
|
61
|
+
subdir = File.dirname(original_name)
|
|
62
|
+
subdir == "." ? nil : subdir
|
|
63
|
+
end
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# version is registered in the manifest without re-running the provider.
|
|
70
|
-
if !operation[:force_processing] && output_up_to_date?(input_path, actual_output_path)
|
|
71
|
-
Jekyll.logger.debug "⏭️ ImgFlow: Output exists and up-to-date, " \
|
|
72
|
-
"skipping provider call for #{original_name}"
|
|
65
|
+
def process_output(operation, original_name, input_path, output_path, params)
|
|
66
|
+
if !operation[:force_processing] && output_up_to_date?(input_path, output_path)
|
|
67
|
+
Jekyll.logger.debug "⏭️ ImgFlow: Output exists and up-to-date, skipping provider call for #{original_name}"
|
|
73
68
|
@stats.record_cache_hit
|
|
74
69
|
elsif AnimatedGifDetector.animated?(input_path)
|
|
75
|
-
|
|
76
|
-
# would destroy the animation. Copy the original file as-is so the
|
|
77
|
-
# manifest can track it and HTML references stay valid.
|
|
78
|
-
Jekyll.logger.warn "🖼️ ImgFlow: Skipping resize for animated GIF " \
|
|
79
|
-
"'#{original_name}' — copying original as-is to " \
|
|
80
|
-
"preserve animation."
|
|
81
|
-
FileUtils.cp(input_path, actual_output_path)
|
|
82
|
-
@stats.record_cache_miss
|
|
70
|
+
copy_animated_image(original_name, input_path, output_path)
|
|
83
71
|
else
|
|
84
|
-
|
|
85
|
-
elapsed = Benchmark.measure do
|
|
86
|
-
process_single_operation(type, input_path, actual_output_path, params)
|
|
87
|
-
end
|
|
88
|
-
@stats.record_operation_time(type, elapsed.real)
|
|
89
|
-
@stats.record_cache_miss
|
|
90
|
-
|
|
91
|
-
# Record compression ratio if we have the original size
|
|
92
|
-
if File.file?(input_path) && File.file?(actual_output_path)
|
|
93
|
-
format = params[:format] || File.extname(input_path).delete(".")
|
|
94
|
-
@stats.record_compression_ratio(format.to_s, File.size(input_path),
|
|
95
|
-
File.size(actual_output_path))
|
|
96
|
-
end
|
|
72
|
+
process_image(operation[:type], input_path, output_path, params)
|
|
97
73
|
end
|
|
74
|
+
end
|
|
98
75
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
76
|
+
def copy_animated_image(original_name, input_path, output_path)
|
|
77
|
+
Jekyll.logger.warn "🖼️ ImgFlow: Skipping resize for animated GIF '#{original_name}' — " \
|
|
78
|
+
"copying original as-is to preserve animation."
|
|
79
|
+
FileUtils.cp(input_path, output_path)
|
|
80
|
+
@stats.record_cache_miss
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def process_image(type, input_path, output_path, params)
|
|
84
|
+
elapsed = Benchmark.measure { process_single_operation(type, input_path, output_path, params) }
|
|
85
|
+
@stats.record_operation_time(type, elapsed.real)
|
|
86
|
+
@stats.record_cache_miss
|
|
87
|
+
record_compression_ratio(input_path, output_path, params)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def record_compression_ratio(input_path, output_path, params)
|
|
91
|
+
return unless File.file?(input_path) && File.file?(output_path)
|
|
92
|
+
|
|
93
|
+
format = params[:format] || File.extname(input_path).delete(".")
|
|
94
|
+
@stats.record_compression_ratio(format.to_s, File.size(input_path), File.size(output_path))
|
|
95
|
+
end
|
|
115
96
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# during render via imgflow tags) are not picked up by Jekyll's static
|
|
119
|
-
# file reader, which runs before pre_render. This would leave _site
|
|
120
|
-
# without optimized images until a second build.
|
|
121
|
-
register_jekyll_static_file(actual_output_path)
|
|
97
|
+
def register_manifest(original_name, filename, params, version_type, page_path, file_digest, subdir)
|
|
98
|
+
return unless @manifest
|
|
122
99
|
|
|
123
|
-
|
|
100
|
+
relative_path = "/#{@path_resolver.resolve_relative_output_path(filename, subdir)}"
|
|
101
|
+
provider_name = @provider&.class&.provider_name || "unknown"
|
|
102
|
+
@manifest.register_version(original_name, relative_path, params, version_type, page_path,
|
|
103
|
+
file_digest, provider_name)
|
|
124
104
|
end
|
|
125
105
|
|
|
126
106
|
# Register a generated file as a Jekyll::StaticFile so Jekyll copies it
|