jekyll-imgflow 0.1.5
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 +7 -0
- data/LICENSE +661 -0
- data/README.md +214 -0
- data/lib/jekyll-imgflow/batch_manager.rb +263 -0
- data/lib/jekyll-imgflow/build_time_processor.rb +152 -0
- data/lib/jekyll-imgflow/config.rb +130 -0
- data/lib/jekyll-imgflow/filename_generator.rb +102 -0
- data/lib/jekyll-imgflow/helpers/http_downloader.rb +68 -0
- data/lib/jekyll-imgflow/hooks.rb +57 -0
- data/lib/jekyll-imgflow/html_generator.rb +335 -0
- data/lib/jekyll-imgflow/imgflow_tag.rb +247 -0
- data/lib/jekyll-imgflow/manifest_manager.rb +297 -0
- data/lib/jekyll-imgflow/operation_processor.rb +183 -0
- data/lib/jekyll-imgflow/parser.rb +199 -0
- data/lib/jekyll-imgflow/path_resolver.rb +130 -0
- data/lib/jekyll-imgflow/picture_tag_adaptor.rb +299 -0
- data/lib/jekyll-imgflow/picture_tag_preset_migrator.rb +210 -0
- data/lib/jekyll-imgflow/preset_manager.rb +149 -0
- data/lib/jekyll-imgflow/provider_registry.rb +106 -0
- data/lib/jekyll-imgflow/providers/base_provider.rb +198 -0
- data/lib/jekyll-imgflow/providers/flyimg.rb +187 -0
- data/lib/jekyll-imgflow/providers/imagemagick.rb +138 -0
- data/lib/jekyll-imgflow/providers/imgproxy.rb +164 -0
- data/lib/jekyll-imgflow/providers/libvips.rb +292 -0
- data/lib/jekyll-imgflow/providers/sharp.rb +213 -0
- data/lib/jekyll-imgflow/providers/weserv.rb +160 -0
- data/lib/jekyll-imgflow/tag_scanner.rb +227 -0
- data/lib/jekyll-imgflow/tags/base_tag.rb +52 -0
- data/lib/jekyll-imgflow/tags/crop_tag.rb +220 -0
- data/lib/jekyll-imgflow/tags/format_tag.rb +64 -0
- data/lib/jekyll-imgflow/tags/opacity_tag.rb +36 -0
- data/lib/jekyll-imgflow/tags/optimize_tag.rb +52 -0
- data/lib/jekyll-imgflow/tags/quality_tag.rb +31 -0
- data/lib/jekyll-imgflow/tags/resize_tag.rb +58 -0
- data/lib/jekyll-imgflow/tags/tag_registry.rb +70 -0
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +51 -0
- data/lib/jekyll-imgflow/version.rb +5 -0
- data/lib/jekyll-imgflow.rb +30 -0
- metadata +261 -0
data/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# ImgFlow
|
|
2
|
+
|
|
3
|
+
A [Jekyll](https://jekyllrb.com/) plugin for automatic image optimization with multiple providers and formats.
|
|
4
|
+
|
|
5
|
+
## 🚀 Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Add to your Gemfile
|
|
9
|
+
gem 'jekyll-imgflow'
|
|
10
|
+
|
|
11
|
+
# Install
|
|
12
|
+
bundle install
|
|
13
|
+
|
|
14
|
+
# Configure (_config.yml)
|
|
15
|
+
imgflow:
|
|
16
|
+
originals: "assets/images/originals"
|
|
17
|
+
output: "assets/images/optimized"
|
|
18
|
+
sizes: {sm: 400, md: 800, lg: 1200}
|
|
19
|
+
formats: [webp, avif, jpg]
|
|
20
|
+
|
|
21
|
+
# Use in templates
|
|
22
|
+
{% imgflow photo.jpg resize width:800 %}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## ✨ Key Features
|
|
26
|
+
|
|
27
|
+
- **Multiple Providers**: [Sharp](https://github.com/lovell/sharp), [ImageMagick](https://github.com/ImageMagick/ImageMagick), [LibVips](https://github.com/libvips/libvips), [Imgproxy](https://github.com/imgproxy/imgproxy), [Weserv](https://github.com/weserv/images), [Flyimg](https://github.com/flyimg/flyimg)
|
|
28
|
+
- **Modern Formats**: WebP, AVIF, JPG, PNG with automatic fallbacks
|
|
29
|
+
- **Picture Tag Support**: Full [Jekyll Picture Tag](https://github.com/robwierzbowski/jekyll_picture_tag) compatibility
|
|
30
|
+
- **Build-Time Processing**: Pre-generate images for optimal performance
|
|
31
|
+
- **Docker Services**: Ready-to-use image processing containers
|
|
32
|
+
- **Parallel Testing**: Fast test execution with CPU optimization
|
|
33
|
+
|
|
34
|
+
## 📚 Documentation
|
|
35
|
+
|
|
36
|
+
### Getting Started
|
|
37
|
+
- **[Installation Guide](docs/installation.md)** - Detailed setup instructions
|
|
38
|
+
- **[Basic Usage](docs/usage/)** - Core functionality and examples
|
|
39
|
+
- **[Picture Tag Migration](docs/picture_tag_migration.md)** - Migrate from Picture Tag
|
|
40
|
+
|
|
41
|
+
### Configuration
|
|
42
|
+
- **[Provider Setup](docs/providers.md)** - Configure image providers
|
|
43
|
+
- **[Docker Services](docs/docker.md)** - Using Docker containers
|
|
44
|
+
- **[Presets System](docs/usage/presets.md)** - Predefined configurations
|
|
45
|
+
|
|
46
|
+
### Development
|
|
47
|
+
- **[Development Guide](docs/development.md)** - Contributing and setup
|
|
48
|
+
- **[Testing Guide](docs/testing.md)** - Running and writing tests
|
|
49
|
+
- **[Scripts Reference](docs/scripts.md)** - Development utilities
|
|
50
|
+
|
|
51
|
+
### Performance
|
|
52
|
+
- **[Provider Comparison](docs/providers.md)** - Performance benchmarks
|
|
53
|
+
- **[Parallel Testing](docs/parallel_testing.md)** - Fast test execution
|
|
54
|
+
|
|
55
|
+
## 🏗️ Architecture Overview
|
|
56
|
+
|
|
57
|
+
ImgFlow has two processing flows: **Build-Time** (pre-generation) and **Runtime** (on-demand).
|
|
58
|
+
|
|
59
|
+
**See:** [ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed component architecture and data flow
|
|
60
|
+
|
|
61
|
+
### Quick Overview
|
|
62
|
+
- **Build-Time Flow** - Pre-generate images during Jekyll build
|
|
63
|
+
- **Runtime Flow** - Process images on-demand during template rendering
|
|
64
|
+
- **Providers** - Image processing backends (CLI tools, HTTP APIs)
|
|
65
|
+
- **Tags** - Jekyll template tags for image optimization
|
|
66
|
+
|
|
67
|
+
## 🎯 Provider Support
|
|
68
|
+
|
|
69
|
+
### HTTP API Services (Recommended)
|
|
70
|
+
- **[Imgproxy](https://github.com/imgproxy/imgproxy)** - Fast, reliable (Port 33001)
|
|
71
|
+
- **[Weserv](https://github.com/weserv/images)** - Battle-tested (Port 33007)
|
|
72
|
+
- **[Flyimg](https://github.com/flyimg/flyimg)** - On-the-fly processing (Port 33008)
|
|
73
|
+
|
|
74
|
+
### CLI Tools (Local)
|
|
75
|
+
- **[Sharp](https://github.com/lovell/sharp)** - Fastest (Node.js/libvips)
|
|
76
|
+
- **[ImageMagick](https://github.com/ImageMagick/ImageMagick)** - Feature-rich
|
|
77
|
+
- **[LibVips](https://github.com/libvips/libvips)** - Memory efficient
|
|
78
|
+
|
|
79
|
+
**See:** [providers.md](docs/providers.md) for detailed provider comparison and setup
|
|
80
|
+
|
|
81
|
+
## 🚀 Quick Commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Development
|
|
85
|
+
bundle exec jekyll serve # Start development server
|
|
86
|
+
bundle exec jekyll build # Build site
|
|
87
|
+
|
|
88
|
+
# Testing
|
|
89
|
+
rake quick # Quick checks (style + tests)
|
|
90
|
+
rake test # All tests
|
|
91
|
+
rake parallel:test # Parallel testing (faster)
|
|
92
|
+
|
|
93
|
+
# Docker Services
|
|
94
|
+
docker-compose -f docker-compose.test.yml up -d # Start services
|
|
95
|
+
rake check_services # Verify services
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**See:** [testing.md](docs/testing.md) for comprehensive testing guide, [scripts.md](docs/scripts.md) for development utilities, and [rake.md](docs/rake.md) for all available Rake tasks
|
|
99
|
+
|
|
100
|
+
## 📦 Installation
|
|
101
|
+
|
|
102
|
+
### Quick Setup
|
|
103
|
+
```bash
|
|
104
|
+
# Add to Gemfile
|
|
105
|
+
gem 'jekyll-imgflow'
|
|
106
|
+
|
|
107
|
+
# Install
|
|
108
|
+
bundle install
|
|
109
|
+
|
|
110
|
+
# Configure (_config.yml)
|
|
111
|
+
imgflow:
|
|
112
|
+
originals: "assets/images/originals"
|
|
113
|
+
output: "assets/images/optimized"
|
|
114
|
+
sizes: {sm: 400, md: 800, lg: 1200}
|
|
115
|
+
formats: [webp, avif, jpg]
|
|
116
|
+
|
|
117
|
+
# Use in templates
|
|
118
|
+
{% imgflow photo.jpg resize width:800 %}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**See:** [installation.md](docs/installation.md) for detailed installation instructions and configuration options
|
|
122
|
+
|
|
123
|
+
## 🐳 Docker Setup (Recommended)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Start all services
|
|
127
|
+
docker-compose -f docker-compose.test.yml --env-file .env.test up -d
|
|
128
|
+
|
|
129
|
+
# Check services
|
|
130
|
+
rake check_services
|
|
131
|
+
|
|
132
|
+
# Stop services
|
|
133
|
+
docker-compose -f docker-compose.test.yml down
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**See:** [docker.md](docs/docker.md) for detailed Docker configuration and services
|
|
137
|
+
|
|
138
|
+
## 🔄 Picture Tag Migration
|
|
139
|
+
|
|
140
|
+
ImgFlow provides full compatibility with [Jekyll Picture Tag](https://github.com/robwierzbowski/jekyll_picture_tag):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Preview migration
|
|
144
|
+
bin/imgflow_migrate_presets --preview
|
|
145
|
+
|
|
146
|
+
# Migrate presets
|
|
147
|
+
bin/imgflow_migrate_presets
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**See:** [Picture Tag Migration Guide](docs/picture_tag_migration.md)
|
|
151
|
+
|
|
152
|
+
## 📊 Performance
|
|
153
|
+
|
|
154
|
+
Latest benchmarks show **Sharp** as the fastest provider:
|
|
155
|
+
|
|
156
|
+
| Provider | Time (s) | Speed |
|
|
157
|
+
|----------|----------|-------|
|
|
158
|
+
| [Sharp](https://github.com/lovell/sharp) | 13.97 | 🏆 Fastest |
|
|
159
|
+
| [LibVips](https://github.com/libvips/libvips) | 21.89 | Very Fast |
|
|
160
|
+
| [Weserv](https://github.com/weserv/images) | 30.19 | Fast |
|
|
161
|
+
| [ImageMagick](https://github.com/ImageMagick/ImageMagick) | 30.62 | Fast |
|
|
162
|
+
| [Imgproxy](https://github.com/imgproxy/imgproxy) | 31.27 | Fast |
|
|
163
|
+
|
|
164
|
+
**See:** [Provider Comparison](docs/providers.md) for detailed benchmarks
|
|
165
|
+
|
|
166
|
+
## 🛠️ Development
|
|
167
|
+
|
|
168
|
+
### Quick Setup
|
|
169
|
+
```bash
|
|
170
|
+
# Clone and setup
|
|
171
|
+
git clone <repository>
|
|
172
|
+
cd jekyll-imgflow
|
|
173
|
+
bundle install
|
|
174
|
+
|
|
175
|
+
# Download test images
|
|
176
|
+
rake download_test_images
|
|
177
|
+
|
|
178
|
+
# Run tests
|
|
179
|
+
rake quick
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Key Files
|
|
183
|
+
- **`lib/jekyll-imgflow.rb`** - Main module
|
|
184
|
+
- **`lib/jekyll-imgflow/imgflow_tag.rb`** - Main tag implementation
|
|
185
|
+
- **`lib/jekyll-imgflow/html_generator.rb`** - HTML generation
|
|
186
|
+
- **`lib/jekyll-imgflow/picture_tag_adaptor.rb`** - Picture Tag compatibility
|
|
187
|
+
|
|
188
|
+
**See:** [development.md](docs/development.md) for complete development setup, scripts, and workflows
|
|
189
|
+
|
|
190
|
+
## 🤝 Contributing
|
|
191
|
+
|
|
192
|
+
1. Fork the repository
|
|
193
|
+
2. Create a feature branch
|
|
194
|
+
3. Make your changes
|
|
195
|
+
4. Add tests for new functionality
|
|
196
|
+
5. Run `rake quality` to check all quality metrics
|
|
197
|
+
6. Submit a pull request
|
|
198
|
+
|
|
199
|
+
**See:** [development.md](docs/development.md) for detailed guidelines and [scripts.md](docs/scripts.md) for development utilities
|
|
200
|
+
|
|
201
|
+
## 📄 License
|
|
202
|
+
|
|
203
|
+
This project is licensed under the AGPL-3.0-or-later License - see the [LICENSE](LICENSE) file for details.
|
|
204
|
+
|
|
205
|
+
## 🔗 Links
|
|
206
|
+
|
|
207
|
+
- **[Documentation](docs/README.md)** - Complete documentation
|
|
208
|
+
- **[GitHub Repository](https://github.com/gundestrup/jekyll-imgflow)** - Source code
|
|
209
|
+
- **[Issue Tracker](https://github.com/gundestrup/jekyll-imgflow/issues)** - Report issues
|
|
210
|
+
- **[Releases](https://github.com/gundestrup/jekyll-imgflow/releases)** - Latest versions
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
**ImgFlow** - Fast, flexible image optimization for Jekyll sites 🚀
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "filename_generator"
|
|
4
|
+
|
|
5
|
+
module JekyllImgFlow
|
|
6
|
+
# BatchManager - manages batch processing of image operations
|
|
7
|
+
# Queues tasks and processes them efficiently
|
|
8
|
+
class BatchManager
|
|
9
|
+
attr_reader :queue, :completed, :failed
|
|
10
|
+
|
|
11
|
+
def initialize(operation_processor)
|
|
12
|
+
@operation_processor = operation_processor
|
|
13
|
+
@filename_generator = FilenameGenerator.new
|
|
14
|
+
@queue = []
|
|
15
|
+
@completed = []
|
|
16
|
+
@failed = []
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Add a task to the batch queue
|
|
21
|
+
# @param task [Hash] Task definition
|
|
22
|
+
# @option task [String] :original_name Original image filename
|
|
23
|
+
# @option task [Symbol] :operation_type Type of operation
|
|
24
|
+
# @option task [String] :input_path Path to input image
|
|
25
|
+
# @option task [String] :output_path Path to output image
|
|
26
|
+
# @option task [Hash] :params Operation parameters
|
|
27
|
+
# @option task [Symbol] :version_type :default or :specialized
|
|
28
|
+
# @option task [String] :page_path Page using this image
|
|
29
|
+
def add_task(task)
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@queue << task
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Add multiple tasks to the queue
|
|
36
|
+
# @param tasks [Array<Hash>] Array of task definitions
|
|
37
|
+
def add_tasks(tasks)
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
@queue.concat(tasks)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Process all queued tasks
|
|
44
|
+
# @param parallel [Boolean] Whether to process in parallel (future enhancement)
|
|
45
|
+
# @return [Hash] Results summary
|
|
46
|
+
def process_all(parallel: false)
|
|
47
|
+
start_time = Time.now
|
|
48
|
+
|
|
49
|
+
if parallel
|
|
50
|
+
# Future: implement parallel processing
|
|
51
|
+
process_parallel
|
|
52
|
+
else
|
|
53
|
+
process_sequential
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end_time = Time.now
|
|
57
|
+
duration = end_time - start_time
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
total: @queue.length + @completed.length + @failed.length,
|
|
61
|
+
completed: @completed.length,
|
|
62
|
+
failed: @failed.length,
|
|
63
|
+
duration: duration
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Process tasks sequentially
|
|
68
|
+
def process_sequential
|
|
69
|
+
while (task = next_task)
|
|
70
|
+
process_task(task)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Process tasks in parallel (future enhancement)
|
|
75
|
+
def process_parallel
|
|
76
|
+
# Future: implement thread pool for parallel processing
|
|
77
|
+
# For now, fall back to sequential
|
|
78
|
+
process_sequential
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get the next task from the queue
|
|
82
|
+
# @return [Hash, nil] Next task or nil if queue is empty
|
|
83
|
+
def next_task
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
@queue.shift
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Process a single task
|
|
90
|
+
# @param task [Hash] Task to process
|
|
91
|
+
def process_task(task)
|
|
92
|
+
# Check if processing is needed
|
|
93
|
+
# Check if output is up-to-date
|
|
94
|
+
if task[:skip_if_exists] && File.exist?(task[:output_path]) && !@operation_processor.needs_processing?(
|
|
95
|
+
task[:input_path],
|
|
96
|
+
task[:output_path],
|
|
97
|
+
task[:params]
|
|
98
|
+
)
|
|
99
|
+
Jekyll.logger.debug "⏭️ Skipping #{task[:original_name]} - already up-to-date"
|
|
100
|
+
mark_completed(task, :skipped)
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Process the operation
|
|
105
|
+
operation = {
|
|
106
|
+
type: task[:operation_type],
|
|
107
|
+
params: task[:params]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Jekyll.logger.debug "🔍 BatchManager: Processing task - input: #{task[:input_path]}, original: #{task[:original_name]}"
|
|
111
|
+
result = @operation_processor.process_operation(
|
|
112
|
+
task[:original_name],
|
|
113
|
+
operation,
|
|
114
|
+
task[:input_path]
|
|
115
|
+
)
|
|
116
|
+
Jekyll.logger.debug "🔍 BatchManager: OperationProcessor returned - result: #{result}"
|
|
117
|
+
|
|
118
|
+
# Store result for manifest registration
|
|
119
|
+
task[:result_path] = result
|
|
120
|
+
|
|
121
|
+
mark_completed(task, :processed, result)
|
|
122
|
+
Jekyll.logger.debug "✅ Processed: #{task[:original_name]} → #{File.basename(result)}"
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
mark_failed(task, e)
|
|
125
|
+
Jekyll.logger.error "❌ Failed: #{task[:original_name]} - #{e.message}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Mark a task as completed
|
|
129
|
+
# @param task [Hash] Completed task
|
|
130
|
+
# @param status [Symbol] Completion status (:processed or :skipped)
|
|
131
|
+
# @param result [String] Result path
|
|
132
|
+
def mark_completed(task, status, result = nil)
|
|
133
|
+
@mutex.synchronize do
|
|
134
|
+
@completed << {
|
|
135
|
+
task: task,
|
|
136
|
+
status: status,
|
|
137
|
+
result: result,
|
|
138
|
+
completed_at: Time.now
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Mark a task as failed
|
|
144
|
+
# @param task [Hash] Failed task
|
|
145
|
+
# @param error [Exception] Error that occurred
|
|
146
|
+
def mark_failed(task, error)
|
|
147
|
+
@mutex.synchronize do
|
|
148
|
+
@failed << {
|
|
149
|
+
task: task,
|
|
150
|
+
error: error.message,
|
|
151
|
+
backtrace: error.backtrace&.first(5),
|
|
152
|
+
failed_at: Time.now
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get queue status
|
|
158
|
+
# @return [Hash] Current queue status
|
|
159
|
+
def status
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
{
|
|
162
|
+
queued: @queue.length,
|
|
163
|
+
completed: @completed.length,
|
|
164
|
+
failed: @failed.length,
|
|
165
|
+
total: @queue.length + @completed.length + @failed.length
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Clear all queues
|
|
171
|
+
def clear
|
|
172
|
+
@mutex.synchronize do
|
|
173
|
+
@queue.clear
|
|
174
|
+
@completed.clear
|
|
175
|
+
@failed.clear
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Get failed tasks for retry
|
|
180
|
+
# @return [Array<Hash>] Array of failed tasks
|
|
181
|
+
def failed_tasks
|
|
182
|
+
@mutex.synchronize do
|
|
183
|
+
@failed.map { |f| f[:task] }
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Retry failed tasks
|
|
188
|
+
# @return [Integer] Number of tasks added back to queue
|
|
189
|
+
def retry_failed
|
|
190
|
+
tasks = failed_tasks
|
|
191
|
+
@mutex.synchronize do
|
|
192
|
+
@failed.clear
|
|
193
|
+
@queue.concat(tasks)
|
|
194
|
+
end
|
|
195
|
+
tasks.length
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Build batch tasks for default image versions
|
|
199
|
+
# @param original_name [String] Original image filename
|
|
200
|
+
# @param input_path [String] Path to original image
|
|
201
|
+
# @param config [Config] ImgFlow configuration
|
|
202
|
+
# @param site [Jekyll::Site] Jekyll site object
|
|
203
|
+
# @return [Array<Hash>] Array of tasks
|
|
204
|
+
def self.build_default_tasks(original_name, input_path, config, _site)
|
|
205
|
+
tasks = []
|
|
206
|
+
|
|
207
|
+
# Use FilenameGenerator for consistent naming
|
|
208
|
+
filename_generator = JekyllImgFlow::FilenameGenerator.new
|
|
209
|
+
path_resolver = JekyllImgFlow::PathResolver.new(config)
|
|
210
|
+
|
|
211
|
+
config.sizes.each_value do |width|
|
|
212
|
+
config.formats.each do |format|
|
|
213
|
+
# Use FilenameGenerator to generate proper filename
|
|
214
|
+
operations = { width: width, format: format, quality: config.quality }
|
|
215
|
+
filename = filename_generator.generate_filename(original_name, operations)
|
|
216
|
+
# Write to _site during build (after Jekyll copies assets)
|
|
217
|
+
output_path = path_resolver.resolve_output_path(filename)
|
|
218
|
+
|
|
219
|
+
tasks << {
|
|
220
|
+
original_name: original_name,
|
|
221
|
+
operation_type: :resize,
|
|
222
|
+
input_path: input_path,
|
|
223
|
+
output_path: output_path,
|
|
224
|
+
params: {
|
|
225
|
+
width: width,
|
|
226
|
+
format: format,
|
|
227
|
+
quality: config.quality
|
|
228
|
+
},
|
|
229
|
+
version_type: :default,
|
|
230
|
+
page_path: nil, # Default versions not tied to specific page
|
|
231
|
+
skip_if_exists: true
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
tasks
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Build batch tasks for specialized image version
|
|
240
|
+
# @param original_name [String] Original image filename
|
|
241
|
+
# @param input_path [String] Path to original image
|
|
242
|
+
# @param operations [Array<Hash>] Operations to apply
|
|
243
|
+
# @param output_path [String] Path to output image
|
|
244
|
+
# @param page_path [String] Page using this image
|
|
245
|
+
# @return [Hash] Task definition
|
|
246
|
+
def self.build_specialized_task(original_name, input_path, operations, output_path, page_path)
|
|
247
|
+
# For now, assume single operation
|
|
248
|
+
# Future: support chaining multiple operations
|
|
249
|
+
operation = operations.first
|
|
250
|
+
|
|
251
|
+
{
|
|
252
|
+
original_name: original_name,
|
|
253
|
+
operation_type: operation[:type],
|
|
254
|
+
input_path: input_path,
|
|
255
|
+
output_path: output_path,
|
|
256
|
+
params: operation[:params],
|
|
257
|
+
version_type: :specialized,
|
|
258
|
+
page_path: page_path,
|
|
259
|
+
skip_if_exists: false
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllImgFlow
|
|
4
|
+
# BuildTimeProcessor - processes images during Jekyll build
|
|
5
|
+
# Generates default image versions for all originals
|
|
6
|
+
class BuildTimeProcessor
|
|
7
|
+
def initialize(site)
|
|
8
|
+
@site = site
|
|
9
|
+
@config = Config.new(site)
|
|
10
|
+
@manifest = ManifestManager.new(site)
|
|
11
|
+
@path_resolver = PathResolver.new(@config)
|
|
12
|
+
@registry = ProviderRegistry.new(@config)
|
|
13
|
+
|
|
14
|
+
# Initialize operation processor with current provider
|
|
15
|
+
provider = @registry.current_provider
|
|
16
|
+
@operation_processor = OperationProcessor.new(provider, @path_resolver, @manifest, @config)
|
|
17
|
+
|
|
18
|
+
# Initialize batch manager
|
|
19
|
+
@batch_manager = BatchManager.new(@operation_processor)
|
|
20
|
+
|
|
21
|
+
# Store all components in site for sharing with ImgFlow tag
|
|
22
|
+
return if @site.imgflow_components
|
|
23
|
+
|
|
24
|
+
filename_generator = JekyllImgFlow::FilenameGenerator.new
|
|
25
|
+
|
|
26
|
+
@site.imgflow_components = {
|
|
27
|
+
config: @config,
|
|
28
|
+
manifest: @manifest,
|
|
29
|
+
path_resolver: @path_resolver,
|
|
30
|
+
filename_generator: filename_generator,
|
|
31
|
+
registry: @registry,
|
|
32
|
+
provider: provider,
|
|
33
|
+
operation_processor: @operation_processor
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Process all changed images in originals folder
|
|
38
|
+
def process_changed_images
|
|
39
|
+
Jekyll.logger.info "🔄 BuildTimeProcessor: Processing changed images"
|
|
40
|
+
|
|
41
|
+
# Get all original images
|
|
42
|
+
original_paths = find_original_images
|
|
43
|
+
Jekyll.logger.info "📸 Found #{original_paths.length} original images"
|
|
44
|
+
|
|
45
|
+
# Build tasks for each original image
|
|
46
|
+
original_paths.each do |path|
|
|
47
|
+
original_name = File.basename(path)
|
|
48
|
+
|
|
49
|
+
# Check if needs processing (file changed or provider changed)
|
|
50
|
+
if needs_processing?(path)
|
|
51
|
+
Jekyll.logger.info "🔧 Queuing default versions for: #{original_name}"
|
|
52
|
+
|
|
53
|
+
# Build default tasks
|
|
54
|
+
tasks = BatchManager.build_default_tasks(original_name, path, @config, @site)
|
|
55
|
+
@batch_manager.add_tasks(tasks)
|
|
56
|
+
else
|
|
57
|
+
Jekyll.logger.debug "⏭️ Skipping #{original_name} - already up-to-date"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Process all queued tasks
|
|
62
|
+
results = @batch_manager.process_all
|
|
63
|
+
|
|
64
|
+
# Register completed tasks in manifest
|
|
65
|
+
register_completed_tasks(@batch_manager.completed)
|
|
66
|
+
|
|
67
|
+
# Save manifest so it's available when ImgflowTag runs
|
|
68
|
+
@manifest.save
|
|
69
|
+
|
|
70
|
+
Jekyll.logger.info "✅ BuildTimeProcessor complete: #{results[:completed]} completed, #{results[:failed]} failed"
|
|
71
|
+
|
|
72
|
+
results
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Register completed tasks in manifest
|
|
78
|
+
# @param completed_tasks [Array<Hash>] Array of completed tasks from BatchManager
|
|
79
|
+
def register_completed_tasks(completed_tasks)
|
|
80
|
+
completed_tasks.each do |task|
|
|
81
|
+
next unless task[:task] && task[:result]
|
|
82
|
+
|
|
83
|
+
# Get provider name
|
|
84
|
+
provider_name = @registry.current_provider.class.name.split("::").last.downcase
|
|
85
|
+
|
|
86
|
+
# Convert absolute path to relative for manifest storage
|
|
87
|
+
absolute_path = task[:result]
|
|
88
|
+
relative_path = absolute_path.sub(@site.dest, "")
|
|
89
|
+
|
|
90
|
+
# Register the version in manifest
|
|
91
|
+
@manifest.register_version(
|
|
92
|
+
task[:task][:original_name],
|
|
93
|
+
relative_path,
|
|
94
|
+
task[:task][:params],
|
|
95
|
+
:default, # BuildTimeProcessor creates default versions
|
|
96
|
+
nil, # page_path (default versions aren't tied to specific pages)
|
|
97
|
+
nil, # file_digest
|
|
98
|
+
provider_name
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Jekyll.logger.debug "📝 Registered #{completed_tasks.length} completed tasks in manifest"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Find all original images in originals folder
|
|
106
|
+
def find_original_images
|
|
107
|
+
# Use PathResolver to get the originals directory path
|
|
108
|
+
originals_dir = File.join(@site.source, @config.originals)
|
|
109
|
+
input_formats = @config.input_formats
|
|
110
|
+
|
|
111
|
+
Dir.glob(File.join(originals_dir, "**", "*.{#{input_formats.join(',')}}"))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if image needs processing
|
|
115
|
+
# @param path [String] Path to original image
|
|
116
|
+
# @return [Boolean] True if processing needed
|
|
117
|
+
def needs_processing?(path)
|
|
118
|
+
original_name = File.basename(path)
|
|
119
|
+
|
|
120
|
+
# If no versions exist, needs processing
|
|
121
|
+
return true unless @manifest.versions?(original_name)
|
|
122
|
+
|
|
123
|
+
# Check if original file was modified
|
|
124
|
+
versions = @manifest.get_versions(original_name)
|
|
125
|
+
default_versions = versions["default"] || []
|
|
126
|
+
|
|
127
|
+
return true if default_versions.empty?
|
|
128
|
+
|
|
129
|
+
# Get the most recent version creation time
|
|
130
|
+
latest_version = default_versions.max_by { |v| v["created_at"] || 0 }
|
|
131
|
+
version_time = latest_version["created_at"]
|
|
132
|
+
|
|
133
|
+
# If original is newer than versions, needs processing
|
|
134
|
+
return true unless version_time
|
|
135
|
+
|
|
136
|
+
original_mtime = File.mtime(path).to_i
|
|
137
|
+
return true if original_mtime > version_time
|
|
138
|
+
|
|
139
|
+
# No timestamp means we need to process
|
|
140
|
+
|
|
141
|
+
# Check if provider changed
|
|
142
|
+
current_provider = @registry.current_provider.class.provider_name
|
|
143
|
+
version_providers = default_versions.map { |v| v["provider"] }
|
|
144
|
+
version_providers.uniq!
|
|
145
|
+
|
|
146
|
+
return true unless version_providers.include?(current_provider)
|
|
147
|
+
|
|
148
|
+
# Up-to-date
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|