jekyll-imgflow 0.1.9 → 0.2.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 +24 -13
- data/lib/jekyll-imgflow/batch_manager.rb +10 -7
- data/lib/jekyll-imgflow/build_time_processor.rb +70 -61
- data/lib/jekyll-imgflow/config.rb +3 -6
- data/lib/jekyll-imgflow/filename_generator.rb +8 -10
- data/lib/jekyll-imgflow/generated_file_cleaner.rb +81 -0
- data/lib/jekyll-imgflow/hooks.rb +13 -6
- data/lib/jekyll-imgflow/html_generator.rb +14 -19
- data/lib/jekyll-imgflow/imgflow_tag.rb +84 -69
- data/lib/jekyll-imgflow/manifest_manager.rb +132 -53
- data/lib/jekyll-imgflow/operation_processor.rb +43 -22
- data/lib/jekyll-imgflow/picture_tag_adaptor.rb +1 -1
- data/lib/jekyll-imgflow/preset_manager.rb +58 -7
- data/lib/jekyll-imgflow/presets/gallery.yml +7 -0
- data/lib/jekyll-imgflow/presets/hero.yml +7 -0
- data/lib/jekyll-imgflow/presets/thumbnail.yml +7 -0
- data/lib/jekyll-imgflow/processing_stats.rb +80 -0
- data/lib/jekyll-imgflow/provider_registry.rb +18 -3
- data/lib/jekyll-imgflow/providers/base_provider.rb +12 -1
- data/lib/jekyll-imgflow/providers/flyimg.rb +2 -1
- data/lib/jekyll-imgflow/providers/imagemagick.rb +72 -3
- data/lib/jekyll-imgflow/providers/imgproxy.rb +2 -1
- data/lib/jekyll-imgflow/providers/libvips.rb +152 -100
- data/lib/jekyll-imgflow/providers/sharp.rb +2 -1
- data/lib/jekyll-imgflow/providers/weserv.rb +3 -2
- data/lib/jekyll-imgflow/tags/opacity_tag.rb +8 -1
- data/lib/jekyll-imgflow/tags/resize_tag.rb +2 -0
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +8 -1
- data/lib/jekyll-imgflow/tasks.rb +129 -0
- data/lib/jekyll-imgflow/version.rb +1 -1
- data/lib/jekyll-imgflow.rb +2 -0
- metadata +24 -156
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b95efe6605d712e14895c4152a68b4b751eea6954416e428acbe96518bd4284
|
|
4
|
+
data.tar.gz: 434fc1985138599142a94d32dda9c986c63893c4acfc587e085d8e3486d700d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f588617884137bcf8054e2a1bce6489b9aebfe4829ce9a279bb65654871dd13d41df6e6f0e0a15ddeb7fb39b9f4f9776206a2f6ff16b83db6e11c82b76dd78ef
|
|
7
|
+
data.tar.gz: 480ba9208f2952793bff054b24b8f7e79a47de1b83d322d0f0f261242cbd8d01f4e532b0571397d4f0efb18c31f27867546ba987f51add3a664fb5b7049bcfb1
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# ImgFlow
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
A [Jekyll](https://jekyllrb.com/) plugin for automatic image optimization with multiple providers and formats.
|
|
4
6
|
|
|
5
7
|
[](https://deepwiki.com/gundestrup/jekyll-imgflow)
|
|
@@ -27,10 +29,9 @@ imgflow:
|
|
|
27
29
|
{% imgflow photo.jpg resize width:800 %}
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
> **
|
|
31
|
-
>
|
|
32
|
-
> [Installation Guide](docs/installation.md#4-add-to-_configyml)
|
|
33
|
-
> and [Configuration Reference](docs/ARCHITECTURE.md).
|
|
32
|
+
> **Configuration:** Only `originals` and `output` are required in `_config.yml`
|
|
33
|
+
> — everything else has sensible defaults. See the
|
|
34
|
+
> [Installation Guide](docs/installation.md#4-add-to-_configyml) for full options.
|
|
34
35
|
|
|
35
36
|
> **Note:** Image references require exact filenames (or paths relative to
|
|
36
37
|
> `originals`). There is no fuzzy matching during the build yet — if you type
|
|
@@ -84,12 +85,12 @@ ImgFlow has two processing flows: **Build-Time** (pre-generation) and **Runtime*
|
|
|
84
85
|
## 🎯 Provider Support
|
|
85
86
|
|
|
86
87
|
### HTTP API Services (Recommended)
|
|
87
|
-
- **[Imgproxy](https://github.com/imgproxy/imgproxy)** - Fast, reliable (Port
|
|
88
|
-
- **[Weserv](https://github.com/weserv/images)** - Battle-tested (Port
|
|
89
|
-
- **[Flyimg](https://github.com/flyimg/flyimg)** - On-the-fly processing (Port
|
|
88
|
+
- **[Imgproxy](https://github.com/imgproxy/imgproxy)** - Fast, reliable (Port 4022)
|
|
89
|
+
- **[Weserv](https://github.com/weserv/images)** - Battle-tested (Port 4026)
|
|
90
|
+
- **[Flyimg](https://github.com/flyimg/flyimg)** - On-the-fly processing (Port 4030)
|
|
90
91
|
|
|
91
92
|
### CLI Tools (Local)
|
|
92
|
-
- **[Sharp](https://github.com/lovell/sharp)** -
|
|
93
|
+
- **[Sharp](https://github.com/lovell/sharp)** - Node.js/libvips processing
|
|
93
94
|
- **[ImageMagick](https://github.com/ImageMagick/ImageMagick)** - Feature-rich
|
|
94
95
|
- **[LibVips](https://github.com/libvips/libvips)** - Memory efficient
|
|
95
96
|
|
|
@@ -108,7 +109,7 @@ rake test # All tests
|
|
|
108
109
|
rake parallel:test # Parallel testing (faster)
|
|
109
110
|
|
|
110
111
|
# Docker Services
|
|
111
|
-
|
|
112
|
+
rake start_services # Start services (pulls latest pinned images)
|
|
112
113
|
rake check_services # Verify services
|
|
113
114
|
```
|
|
114
115
|
|
|
@@ -128,25 +129,35 @@ bundle install
|
|
|
128
129
|
imgflow:
|
|
129
130
|
originals: "assets/images/originals"
|
|
130
131
|
output: "assets/images/optimized"
|
|
132
|
+
# cache_dir defaults to ".cache/imgflow" (stores manifest, survives _site wipes)
|
|
131
133
|
|
|
132
134
|
# Use in templates
|
|
133
135
|
{% imgflow photo.jpg resize width:800 %}
|
|
136
|
+
|
|
137
|
+
# Or use built-in presets:
|
|
138
|
+
{% imgflow photo.jpg preset:gallery %}
|
|
134
139
|
```
|
|
135
140
|
|
|
141
|
+
**Built-in presets:** `gallery` (400px, avif/webp/jpg, Q80), `hero` (800px, avif/webp/jpg, Q85), `thumbnail` (150px, webp/jpg, Q75). They work without installation. To copy editable versions, add `require "jekyll-imgflow/tasks"` to the site's `Rakefile`, then run `rake imgflow:install_presets`.
|
|
142
|
+
|
|
136
143
|
**See:** [installation.md](docs/installation.md) for detailed installation and
|
|
137
|
-
[all configuration options](docs/installation.md#4-add-to-_configyml)
|
|
144
|
+
[all configuration options](docs/installation.md#4-add-to-_configyml),
|
|
145
|
+
[presets.md](docs/usage/presets.md) for the preset system
|
|
138
146
|
|
|
139
147
|
## 🐳 Docker Setup (Recommended)
|
|
140
148
|
|
|
141
149
|
```bash
|
|
142
|
-
# Start all services
|
|
143
|
-
|
|
150
|
+
# Start all services (pulls latest pinned images first)
|
|
151
|
+
rake start_services
|
|
144
152
|
|
|
145
153
|
# Check services
|
|
146
154
|
rake check_services
|
|
147
155
|
|
|
156
|
+
# Check if pinned images are outdated (run before releases)
|
|
157
|
+
rake check_docker_images
|
|
158
|
+
|
|
148
159
|
# Stop services
|
|
149
|
-
|
|
160
|
+
rake stop_services
|
|
150
161
|
```
|
|
151
162
|
|
|
152
163
|
**See:** [docker.md](docs/docker.md) for detailed Docker configuration and services
|
|
@@ -91,11 +91,12 @@ module JekyllImgFlow
|
|
|
91
91
|
def process_task(task)
|
|
92
92
|
# Check if processing is needed
|
|
93
93
|
# Check if output is up-to-date
|
|
94
|
-
if task[:skip_if_exists] && File.exist?(task[:output_path]) &&
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
if !task[:force_processing] && task[:skip_if_exists] && File.exist?(task[:output_path]) &&
|
|
95
|
+
!@operation_processor.needs_processing?(
|
|
96
|
+
task[:input_path],
|
|
97
|
+
task[:output_path],
|
|
98
|
+
task[:params]
|
|
99
|
+
)
|
|
99
100
|
Jekyll.logger.debug "⏭️ Skipping #{task[:original_name]} - already up-to-date"
|
|
100
101
|
mark_completed(task, :skipped)
|
|
101
102
|
return
|
|
@@ -104,7 +105,8 @@ module JekyllImgFlow
|
|
|
104
105
|
# Process the operation
|
|
105
106
|
operation = {
|
|
106
107
|
type: task[:operation_type],
|
|
107
|
-
params: task[:params]
|
|
108
|
+
params: task[:params],
|
|
109
|
+
file_digest: task[:file_digest]
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
Jekyll.logger.debug "🔍 BatchManager: Processing task - input: #{task[:input_path]}, original: #{task[:original_name]}"
|
|
@@ -201,7 +203,7 @@ module JekyllImgFlow
|
|
|
201
203
|
# @param config [Config] ImgFlow configuration
|
|
202
204
|
# @param site [Jekyll::Site] Jekyll site object
|
|
203
205
|
# @return [Array<Hash>] Array of tasks
|
|
204
|
-
def self.build_default_tasks(original_name, input_path, config, _site)
|
|
206
|
+
def self.build_default_tasks(original_name, input_path, config, _site, file_digest: nil)
|
|
205
207
|
tasks = []
|
|
206
208
|
|
|
207
209
|
# Use FilenameGenerator for consistent naming
|
|
@@ -233,6 +235,7 @@ module JekyllImgFlow
|
|
|
233
235
|
},
|
|
234
236
|
version_type: :default,
|
|
235
237
|
page_path: nil, # Default versions not tied to specific page
|
|
238
|
+
file_digest: file_digest,
|
|
236
239
|
skip_if_exists: true
|
|
237
240
|
}
|
|
238
241
|
end
|
|
@@ -13,29 +13,31 @@ module JekyllImgFlow
|
|
|
13
13
|
|
|
14
14
|
# Initialize operation processor with current provider
|
|
15
15
|
provider = @registry.current_provider
|
|
16
|
-
@operation_processor = OperationProcessor.new(provider, @path_resolver, @manifest, @config)
|
|
16
|
+
@operation_processor = OperationProcessor.new(provider, @path_resolver, @manifest, @config) if provider
|
|
17
17
|
|
|
18
18
|
# Initialize batch manager
|
|
19
19
|
@batch_manager = BatchManager.new(@operation_processor)
|
|
20
20
|
|
|
21
21
|
# Store all components in site for sharing with ImgFlow tag
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
filename_generator = JekyllImgFlow::FilenameGenerator.new
|
|
22
|
+
@filename_generator = JekyllImgFlow::FilenameGenerator.new
|
|
25
23
|
|
|
26
24
|
@site.imgflow_components = {
|
|
27
25
|
config: @config,
|
|
28
26
|
manifest: @manifest,
|
|
29
27
|
path_resolver: @path_resolver,
|
|
30
|
-
filename_generator: filename_generator,
|
|
28
|
+
filename_generator: @filename_generator,
|
|
31
29
|
registry: @registry,
|
|
32
30
|
provider: provider,
|
|
33
|
-
operation_processor: @operation_processor
|
|
31
|
+
operation_processor: @operation_processor,
|
|
32
|
+
stats: @operation_processor&.stats,
|
|
33
|
+
preset_manager: JekyllImgFlow::PresetManager.new(@site, @config)
|
|
34
34
|
}
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Process all changed images in originals folder
|
|
38
38
|
def process_changed_images
|
|
39
|
+
return { completed: 0, failed: 0 } unless @operation_processor
|
|
40
|
+
|
|
39
41
|
Jekyll.logger.info "🔄 BuildTimeProcessor: Processing changed images"
|
|
40
42
|
|
|
41
43
|
# Get all original images
|
|
@@ -44,27 +46,37 @@ module JekyllImgFlow
|
|
|
44
46
|
|
|
45
47
|
originals_dir = File.join(@site.source, @config.originals)
|
|
46
48
|
|
|
49
|
+
# Clean up manifest entries for deleted originals
|
|
50
|
+
current_originals = original_paths.map { |path| path.sub("#{originals_dir}/", "") }
|
|
51
|
+
@manifest.cleanup_deleted_originals(current_originals)
|
|
52
|
+
@manifest.cleanup_obsolete_defaults(expected_default_operations)
|
|
53
|
+
@manifest.reset_page_usage unless incremental_build?
|
|
54
|
+
|
|
47
55
|
# Build tasks for each original image
|
|
48
|
-
original_paths.each do |path|
|
|
56
|
+
original_paths.reject { |path| AnimatedGifDetector.animated?(path) }.each do |path|
|
|
49
57
|
original_name = path.sub("#{originals_dir}/", "")
|
|
58
|
+
file_digest = @filename_generator.file_digest(path)
|
|
50
59
|
|
|
51
60
|
# Check if needs processing (file changed or provider changed)
|
|
52
|
-
if needs_processing?(original_name, path)
|
|
61
|
+
if needs_processing?(original_name, path, file_digest)
|
|
53
62
|
Jekyll.logger.info "🔧 Queuing default versions for: #{original_name}"
|
|
54
63
|
|
|
55
64
|
# Build default tasks
|
|
56
|
-
tasks = BatchManager.build_default_tasks(
|
|
65
|
+
tasks = BatchManager.build_default_tasks(
|
|
66
|
+
original_name, path, @config, @site, file_digest: file_digest
|
|
67
|
+
)
|
|
68
|
+
force_processing = source_digest_changed?(original_name, file_digest)
|
|
69
|
+
tasks.each { |task| task[:force_processing] = force_processing }
|
|
57
70
|
@batch_manager.add_tasks(tasks)
|
|
58
71
|
else
|
|
72
|
+
expected_default_operations.length.times { @operation_processor.stats.record_cache_hit }
|
|
59
73
|
Jekyll.logger.debug "⏭️ Skipping #{original_name} - already up-to-date"
|
|
60
74
|
end
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
# Process all queued tasks
|
|
64
78
|
results = @batch_manager.process_all
|
|
65
|
-
|
|
66
|
-
# Register completed tasks in manifest
|
|
67
|
-
register_completed_tasks(@batch_manager.completed)
|
|
79
|
+
register_skipped_tasks(@batch_manager.completed)
|
|
68
80
|
|
|
69
81
|
# Save manifest so it's available when ImgflowTag runs
|
|
70
82
|
@manifest.save
|
|
@@ -76,34 +88,19 @@ module JekyllImgFlow
|
|
|
76
88
|
|
|
77
89
|
private
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
next unless task[:task] && task[:result]
|
|
84
|
-
|
|
85
|
-
# Get provider name
|
|
86
|
-
provider_name = @registry.current_provider.class.name.split("::").last.downcase
|
|
91
|
+
def register_skipped_tasks(completed_tasks)
|
|
92
|
+
provider_name = @registry.current_provider&.class&.provider_name || "unknown"
|
|
93
|
+
completed_tasks.each do |completed|
|
|
94
|
+
next unless completed[:status] == :skipped
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
absolute_path = task[:result]
|
|
92
|
-
relative_path = absolute_path.sub(@site.source, "")
|
|
93
|
-
|
|
94
|
-
# Register the version in manifest
|
|
96
|
+
task = completed[:task]
|
|
97
|
+
@operation_processor.stats.record_cache_hit
|
|
98
|
+
relative_path = task[:output_path].sub(@site.source, "")
|
|
95
99
|
@manifest.register_version(
|
|
96
|
-
task[:task
|
|
97
|
-
|
|
98
|
-
task[:task][:params],
|
|
99
|
-
:default, # BuildTimeProcessor creates default versions
|
|
100
|
-
nil, # page_path (default versions aren't tied to specific pages)
|
|
101
|
-
nil, # file_digest
|
|
102
|
-
provider_name
|
|
100
|
+
task[:original_name], relative_path, task[:params], :default, nil,
|
|
101
|
+
task[:file_digest], provider_name
|
|
103
102
|
)
|
|
104
103
|
end
|
|
105
|
-
|
|
106
|
-
Jekyll.logger.debug "📝 Registered #{completed_tasks.length} completed tasks in manifest"
|
|
107
104
|
end
|
|
108
105
|
|
|
109
106
|
# Find all original images in originals folder
|
|
@@ -113,48 +110,60 @@ module JekyllImgFlow
|
|
|
113
110
|
input_formats = @config.input_formats
|
|
114
111
|
|
|
115
112
|
Dir.glob(File.join(originals_dir, "**", "*.{#{input_formats.join(',')}}"))
|
|
116
|
-
.reject { |path| AnimatedGifDetector.animated?(path) }
|
|
117
113
|
end
|
|
118
114
|
|
|
119
115
|
# Check if image needs processing
|
|
120
116
|
# @param original_name [String] Path to original image relative to originals directory
|
|
121
117
|
# @param path [String, nil] Full path to original image (defaults to original_name)
|
|
122
118
|
# @return [Boolean] True if processing needed
|
|
123
|
-
def needs_processing?(original_name, path = nil)
|
|
119
|
+
def needs_processing?(original_name, path = nil, file_digest = nil)
|
|
124
120
|
path ||= original_name
|
|
125
|
-
# For single-argument calls, derive a flat original_name from the full path
|
|
126
121
|
original_name = File.basename(original_name) if original_name == path
|
|
122
|
+
return true unless File.file?(path)
|
|
127
123
|
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
file_digest ||= @filename_generator.file_digest(path)
|
|
125
|
+
default_versions = @manifest.get_versions(original_name)["default"] || []
|
|
126
|
+
current_provider = @registry.current_provider&.class&.provider_name || "unknown"
|
|
130
127
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
expected_default_operations.any? do |operations|
|
|
129
|
+
version = default_versions.find do |candidate|
|
|
130
|
+
@manifest.same_operations?(candidate["operations"], operations)
|
|
131
|
+
end
|
|
132
|
+
!current_default_version?(version, current_provider, file_digest)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
def expected_default_operations
|
|
137
|
+
@config.sizes.each_value.flat_map do |width|
|
|
138
|
+
@config.formats.map do |format|
|
|
139
|
+
{ width: width, format: format, quality: @config.quality }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
def source_digest_changed?(original_name, file_digest)
|
|
145
|
+
digests = (@manifest.get_versions(original_name)["default"] || []).filter_map do |version|
|
|
146
|
+
version["file_digest"]
|
|
147
|
+
end
|
|
148
|
+
digests.any? && digests.any? { |digest| digest != file_digest }
|
|
149
|
+
end
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
def incremental_build?
|
|
152
|
+
@site.is_a?(Jekyll::Site) && @site.incremental?
|
|
153
|
+
end
|
|
146
154
|
|
|
147
|
-
|
|
155
|
+
def current_default_version?(version, current_provider, file_digest)
|
|
156
|
+
return false unless version && version["provider"] == current_provider
|
|
157
|
+
return false unless version["file_digest"] == file_digest
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
version_providers = default_versions.map { |v| v["provider"] }
|
|
152
|
-
version_providers.uniq!
|
|
159
|
+
output_path = version["output"]
|
|
160
|
+
return false unless output_path
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
absolute_path = File.expand_path(output_path.delete_prefix("/"), @site.source)
|
|
163
|
+
output_root = File.expand_path(@config.output, @site.source)
|
|
164
|
+
return false unless absolute_path.start_with?("#{output_root}#{File::SEPARATOR}")
|
|
155
165
|
|
|
156
|
-
|
|
157
|
-
false
|
|
166
|
+
File.file?(absolute_path)
|
|
158
167
|
end
|
|
159
168
|
end
|
|
160
169
|
end
|
|
@@ -23,7 +23,7 @@ module JekyllImgFlow
|
|
|
23
23
|
|
|
24
24
|
attr_reader :site, :originals, :output, :input_formats, :sizes, :formats,
|
|
25
25
|
:quality, :backend_priority, :imgproxy_url,
|
|
26
|
-
:
|
|
26
|
+
:weserv_url, :flyimg_url,
|
|
27
27
|
:optimize_qualities, :fallback_format
|
|
28
28
|
|
|
29
29
|
def initialize(site)
|
|
@@ -42,7 +42,6 @@ module JekyllImgFlow
|
|
|
42
42
|
@fallback_format = cfg["fallback_format"] || DEFAULT_FALLBACK_FORMAT
|
|
43
43
|
|
|
44
44
|
@imgproxy_url = cfg["imgproxy_url"]
|
|
45
|
-
@image_compressor_url = cfg["image_compressor_url"]
|
|
46
45
|
@weserv_url = cfg["weserv_url"]
|
|
47
46
|
@flyimg_url = cfg["flyimg_url"]
|
|
48
47
|
@optimize_qualities = cfg["optimize_qualities"] || {
|
|
@@ -64,12 +63,10 @@ module JekyllImgFlow
|
|
|
64
63
|
{
|
|
65
64
|
enabled: merged_config.key?("imgproxy_url") ||
|
|
66
65
|
merged_config.key?("weserv_url") ||
|
|
67
|
-
merged_config.key?("flyimg_url")
|
|
68
|
-
merged_config.key?("image_compressor_url"),
|
|
66
|
+
merged_config.key?("flyimg_url"),
|
|
69
67
|
imgproxy_url: @imgproxy_url,
|
|
70
68
|
weserv_url: @weserv_url,
|
|
71
|
-
flyimg_url: @flyimg_url
|
|
72
|
-
image_compressor_url: @image_compressor_url
|
|
69
|
+
flyimg_url: @flyimg_url
|
|
73
70
|
}
|
|
74
71
|
end
|
|
75
72
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
|
-
require "json"
|
|
5
4
|
|
|
6
5
|
module JekyllImgFlow
|
|
7
6
|
# Generates unique filenames for processed images
|
|
@@ -11,10 +10,18 @@ module JekyllImgFlow
|
|
|
11
10
|
# @param original_name [String] Original image filename
|
|
12
11
|
# @param operations [Hash] Operations applied to image
|
|
13
12
|
# @return [String] Generated filename (without path)
|
|
13
|
+
# Formats that are valid for output (raster only).
|
|
14
|
+
# SVG and other vector formats are input-only — image providers
|
|
15
|
+
# always produce raster output (webp, avif, jpg, png).
|
|
16
|
+
VECTOR_FORMATS = %w[svg].freeze
|
|
17
|
+
|
|
14
18
|
def generate_filename(original_name, operations)
|
|
15
19
|
base_name = File.basename(original_name, ".*")
|
|
16
20
|
width = operations[:width]
|
|
17
21
|
format = operations[:format] || File.extname(original_name).delete(".")
|
|
22
|
+
# Vector formats (e.g. SVG) are input-only — never use them as
|
|
23
|
+
# output format. Fall back to jpg (universal <img> fallback).
|
|
24
|
+
format = "jpg" if VECTOR_FORMATS.include?(format.to_s.downcase)
|
|
18
25
|
|
|
19
26
|
# Generate hash using Jekyll Picture Tag approach
|
|
20
27
|
hash = generate_jpt_hash(original_name, operations)
|
|
@@ -22,15 +29,6 @@ module JekyllImgFlow
|
|
|
22
29
|
"#{base_name}-#{width}-#{hash}.#{format}"
|
|
23
30
|
end
|
|
24
31
|
|
|
25
|
-
# Generate cache key for operations (SHA256 for manifest tracking)
|
|
26
|
-
# @param operations [Hash] Operations hash
|
|
27
|
-
# @return [String] Cache key
|
|
28
|
-
def generate_cache_key(operations)
|
|
29
|
-
# Sort keys to ensure consistent hashing
|
|
30
|
-
sorted_operations = operations.sort.to_h
|
|
31
|
-
Digest::SHA256.hexdigest(sorted_operations.to_json)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
32
|
# Get file digest for manifest tracking
|
|
35
33
|
# @param original_path [String] Path to original image
|
|
36
34
|
# @return [String] SHA256 digest of file
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module JekyllImgFlow
|
|
6
|
+
class GeneratedFileCleaner
|
|
7
|
+
def initialize(site, config)
|
|
8
|
+
@site = site
|
|
9
|
+
@config = config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def delete(output_path)
|
|
13
|
+
return false unless output_path
|
|
14
|
+
|
|
15
|
+
unregister_static_file(output_path)
|
|
16
|
+
deleted = false
|
|
17
|
+
[@site.source, @site.dest].each do |base_path|
|
|
18
|
+
path = safe_generated_path(base_path, output_path)
|
|
19
|
+
next unless path && File.file?(path)
|
|
20
|
+
|
|
21
|
+
File.delete(path)
|
|
22
|
+
remove_empty_output_dirs(File.dirname(path), output_root(base_path))
|
|
23
|
+
deleted = true
|
|
24
|
+
end
|
|
25
|
+
deleted
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clear
|
|
29
|
+
roots = [@site.source, @site.dest].map { |base_path| output_root(base_path) }
|
|
30
|
+
valid = roots.zip([@site.source, @site.dest]).all? do |directory, base_path|
|
|
31
|
+
base = File.expand_path(base_path)
|
|
32
|
+
directory.start_with?("#{base}#{File::SEPARATOR}")
|
|
33
|
+
end
|
|
34
|
+
raise "Refusing to delete optimized directory outside configured roots" unless valid
|
|
35
|
+
|
|
36
|
+
unregister_output_tree(roots.first)
|
|
37
|
+
files = roots.flat_map do |directory|
|
|
38
|
+
Dir.glob(File.join(directory, "**", "*")).select { |path| File.file?(path) }
|
|
39
|
+
end
|
|
40
|
+
files = files.uniq
|
|
41
|
+
files.each { |path| File.delete(path) }
|
|
42
|
+
files.length
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def unregister_static_file(output_path)
|
|
48
|
+
return unless @site.is_a?(Jekyll::Site)
|
|
49
|
+
|
|
50
|
+
source_path = safe_generated_path(@site.source, output_path)
|
|
51
|
+
@site.static_files.reject! do |static_file|
|
|
52
|
+
source_path && File.expand_path(static_file.path) == source_path
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def unregister_output_tree(root)
|
|
57
|
+
return unless @site.is_a?(Jekyll::Site)
|
|
58
|
+
|
|
59
|
+
@site.static_files.reject! do |static_file|
|
|
60
|
+
File.expand_path(static_file.path).start_with?("#{root}#{File::SEPARATOR}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def safe_generated_path(base_path, output_path)
|
|
65
|
+
root = output_root(base_path)
|
|
66
|
+
candidate = File.expand_path(output_path.delete_prefix("/"), base_path)
|
|
67
|
+
candidate if candidate.start_with?("#{root}#{File::SEPARATOR}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def output_root(base_path)
|
|
71
|
+
File.expand_path(@config.output, base_path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def remove_empty_output_dirs(directory, root)
|
|
75
|
+
while directory.start_with?("#{root}#{File::SEPARATOR}") && Dir.empty?(directory)
|
|
76
|
+
Dir.rmdir(directory)
|
|
77
|
+
directory = File.dirname(directory)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/jekyll-imgflow/hooks.rb
CHANGED
|
@@ -11,6 +11,12 @@ module JekyllImgFlow
|
|
|
11
11
|
site.define_singleton_method(:imgflow_components=) { |value| @imgflow_components = value }
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
if site.respond_to?(:config)
|
|
15
|
+
cache_dir = Config.new(site).cache_dir
|
|
16
|
+
site.config["exclude"] = Array(site.config["exclude"])
|
|
17
|
+
site.config["exclude"] << cache_dir unless site.config["exclude"].include?(cache_dir)
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
# Components will be initialized on first use (lazy loading)
|
|
15
21
|
Jekyll.logger.info "✅ ImgFlow: Components ready for initialization"
|
|
16
22
|
end
|
|
@@ -31,22 +37,23 @@ module JekyllImgFlow
|
|
|
31
37
|
Jekyll.logger.info "🔌 ImgFlow post_write hook - saving final manifest with page usage"
|
|
32
38
|
|
|
33
39
|
if site.imgflow_components && site.imgflow_components[:manifest]
|
|
34
|
-
site.imgflow_components[:manifest]
|
|
40
|
+
manifest = site.imgflow_components[:manifest]
|
|
41
|
+
cleanup_orphaned_images(site, manifest) if Jekyll.env != "development" &&
|
|
42
|
+
!(site.is_a?(Jekyll::Site) && site.incremental?)
|
|
43
|
+
manifest.save
|
|
35
44
|
Jekyll.logger.info "✅ ImgFlow: Final manifest saved with all page usage data"
|
|
36
45
|
else
|
|
37
46
|
Jekyll.logger.warn "⚠️ ImgFlow: No manifest found to save"
|
|
38
47
|
end
|
|
39
|
-
|
|
40
|
-
# Clean up orphaned specialized images in production
|
|
41
|
-
cleanup_orphaned_images(site) if Jekyll.env != "development"
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
def self.cleanup_orphaned_images(site)
|
|
50
|
+
def self.cleanup_orphaned_images(site, manifest = nil)
|
|
45
51
|
Jekyll.logger.info "🧹 ImgFlow: Checking for orphaned specialized images..."
|
|
46
52
|
|
|
47
53
|
# Use ManifestManager to find and cleanup orphans
|
|
48
|
-
manifest
|
|
54
|
+
manifest ||= ManifestManager.new(site)
|
|
49
55
|
cleaned = manifest.cleanup_orphans
|
|
56
|
+
manifest.save
|
|
50
57
|
|
|
51
58
|
if cleaned.any?
|
|
52
59
|
Jekyll.logger.info "✅ ImgFlow: Removed #{cleaned.length} orphaned specialized images"
|
|
@@ -4,9 +4,6 @@ module JekyllImgFlow
|
|
|
4
4
|
# Unified HTML Generator for all markup formats
|
|
5
5
|
# Supports Picture Tag compatibility with multiple markup formats and attributes
|
|
6
6
|
class HtmlGenerator
|
|
7
|
-
MARKUP_FORMATS = %w[auto picture img data_auto data_picture data_img direct_url
|
|
8
|
-
naked_srcset].freeze
|
|
9
|
-
|
|
10
7
|
# Generate HTML based on markup format and attributes
|
|
11
8
|
# @param results [Array<String>] Processed image paths
|
|
12
9
|
# @param attributes [Hash] HTML attributes by element
|
|
@@ -332,11 +329,20 @@ module JekyllImgFlow
|
|
|
332
329
|
.gsub(">", ">")
|
|
333
330
|
end
|
|
334
331
|
|
|
335
|
-
# Get correct path for HTML generation
|
|
332
|
+
# Get correct path for HTML generation.
|
|
336
333
|
def html_path(relative_path)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
334
|
+
path = "/#{relative_path.delete_prefix('/')}"
|
|
335
|
+
baseurl = html_baseurl
|
|
336
|
+
return path if baseurl.empty? || path == baseurl || path.start_with?("#{baseurl}/")
|
|
337
|
+
|
|
338
|
+
"#{baseurl}#{path}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def html_baseurl
|
|
342
|
+
site = @context&.registers&.dig(:site) || @config&.site
|
|
343
|
+
baseurl = site&.config&.fetch("baseurl", "").to_s
|
|
344
|
+
normalized = baseurl.gsub(%r{\A/+|/+$}, "")
|
|
345
|
+
normalized.empty? ? "" : "/#{normalized}"
|
|
340
346
|
end
|
|
341
347
|
|
|
342
348
|
# Get absolute URL when needed (for direct_url format)
|
|
@@ -345,18 +351,7 @@ module JekyllImgFlow
|
|
|
345
351
|
|
|
346
352
|
site = @context.registers[:site]
|
|
347
353
|
base_url = site.config["url"] || "http://localhost:4000"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
# Ensure base_url ends without trailing slash and baseurl starts without slash
|
|
351
|
-
base_url = base_url.chomp("/")
|
|
352
|
-
baseurl = baseurl[1..] if baseurl.start_with?("/")
|
|
353
|
-
|
|
354
|
-
# Build URL parts carefully to avoid double slashes
|
|
355
|
-
url_parts = [base_url]
|
|
356
|
-
url_parts << baseurl unless baseurl.empty?
|
|
357
|
-
url_parts << html_path(relative_path)
|
|
358
|
-
|
|
359
|
-
url_parts.join("/")
|
|
354
|
+
"#{base_url.chomp('/')}#{html_path(relative_path)}"
|
|
360
355
|
end
|
|
361
356
|
end
|
|
362
357
|
end
|