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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "filename_generator"
|
|
4
|
+
|
|
5
|
+
module JekyllImgFlow
|
|
6
|
+
# OperationProcessor - processes image operations using providers
|
|
7
|
+
# Handles both single operations and batch operations
|
|
8
|
+
class OperationProcessor
|
|
9
|
+
def initialize(provider, path_resolver, manifest = nil, config = nil)
|
|
10
|
+
@provider = provider
|
|
11
|
+
@path_resolver = path_resolver
|
|
12
|
+
@filename_generator = FilenameGenerator.new
|
|
13
|
+
@manifest = manifest
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Process a single operation on an image
|
|
18
|
+
# @param operation_type [Symbol] Type of operation (:resize, :crop, :quality, etc.)
|
|
19
|
+
# @param input_path [String] Path to input image
|
|
20
|
+
# @param output_path [String] Path to output image
|
|
21
|
+
# @param params [Hash] Operation parameters
|
|
22
|
+
# @return [String] Path to processed image
|
|
23
|
+
def process_single_operation(operation_type, input_path, output_path, params)
|
|
24
|
+
# Get the appropriate tag class for validation
|
|
25
|
+
tag_class = JekyllImgFlow::Tags::TagRegistry.get_tag(operation_type)
|
|
26
|
+
raise "Unknown operation: #{operation_type}" unless tag_class
|
|
27
|
+
|
|
28
|
+
# Create tag instance and process
|
|
29
|
+
tag = tag_class.new(@provider)
|
|
30
|
+
tag.process(input_path, output_path, params)
|
|
31
|
+
|
|
32
|
+
output_path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Process an operation and return the output path
|
|
36
|
+
# @param original_name [String] Original image filename
|
|
37
|
+
# @param operation [Hash] Operation structure { type: :resize, params: { width: 800 } }
|
|
38
|
+
# @param input_path [String] Path to input image
|
|
39
|
+
# @param page_path [String] Optional page path for manifest tracking
|
|
40
|
+
# @return [String] Path to processed image
|
|
41
|
+
def process_operation(original_name, operation, input_path, page_path = nil)
|
|
42
|
+
type = operation[:type]
|
|
43
|
+
params = operation[:params]
|
|
44
|
+
|
|
45
|
+
# Generate filename using FilenameGenerator (JPT compatible)
|
|
46
|
+
filename = @filename_generator.generate_filename(input_path, params)
|
|
47
|
+
# Write to _site during build (after Jekyll copies assets)
|
|
48
|
+
actual_output_path = @path_resolver.resolve_output_path(filename)
|
|
49
|
+
|
|
50
|
+
# Determine version type
|
|
51
|
+
version_type = determine_version_type(params)
|
|
52
|
+
|
|
53
|
+
# Ensure output directory exists before processing
|
|
54
|
+
FileUtils.mkdir_p(File.dirname(actual_output_path))
|
|
55
|
+
|
|
56
|
+
# Process the operation (create the image)
|
|
57
|
+
process_single_operation(type, input_path, actual_output_path, params)
|
|
58
|
+
|
|
59
|
+
# Register in manifest
|
|
60
|
+
if @manifest
|
|
61
|
+
# Convert absolute path to relative for manifest storage
|
|
62
|
+
relative_path = actual_output_path.sub(@path_resolver.site_dest, "")
|
|
63
|
+
|
|
64
|
+
provider_name = @provider.class.provider_name
|
|
65
|
+
@manifest.register_version(
|
|
66
|
+
original_name,
|
|
67
|
+
relative_path,
|
|
68
|
+
params,
|
|
69
|
+
version_type,
|
|
70
|
+
page_path,
|
|
71
|
+
nil, # file_digest
|
|
72
|
+
provider_name
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
actual_output_path
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Process multiple operations on an image in sequence (batch)
|
|
80
|
+
# @param operations [Array<Hash>] Array of operations to process
|
|
81
|
+
# @param input_path [String] Path to input image
|
|
82
|
+
# @param final_output_path [String] Path to final output image
|
|
83
|
+
# @return [String] Path to final processed image
|
|
84
|
+
def process_batch_operations(operations, input_path, final_output_path)
|
|
85
|
+
return input_path if operations.empty?
|
|
86
|
+
|
|
87
|
+
current_input = input_path
|
|
88
|
+
temp_files = []
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
# Process each operation in sequence
|
|
92
|
+
operations.each_with_index do |operation, index|
|
|
93
|
+
operation_type = operation[:type]
|
|
94
|
+
params = operation[:params] || {}
|
|
95
|
+
|
|
96
|
+
# Determine output path
|
|
97
|
+
if index == operations.length - 1
|
|
98
|
+
# Last operation - use final output path
|
|
99
|
+
output_path = final_output_path
|
|
100
|
+
else
|
|
101
|
+
# Intermediate operation - use temp file
|
|
102
|
+
format = params[:format] || File.extname(input_path).delete(".")
|
|
103
|
+
output_path = @path_resolver.temp_output_path(format)
|
|
104
|
+
temp_files << output_path
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Process the operation
|
|
108
|
+
current_input = process_single_operation(operation_type, current_input,
|
|
109
|
+
output_path, params)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
current_input
|
|
113
|
+
ensure
|
|
114
|
+
# Cleanup temp files even on failure
|
|
115
|
+
temp_files.each { |f| FileUtils.rm_f(f) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check if operation needs to be processed (cache check)
|
|
120
|
+
# @param input_path [String] Path to input image
|
|
121
|
+
# @param output_path [String] Path to output image
|
|
122
|
+
# @param operations [Hash] Operations to apply
|
|
123
|
+
# @return [Boolean] True if processing needed
|
|
124
|
+
def needs_processing?(input_path, output_path, operations)
|
|
125
|
+
# If output doesn't exist, needs processing
|
|
126
|
+
return true unless File.exist?(output_path)
|
|
127
|
+
|
|
128
|
+
# If input is newer than output, needs processing
|
|
129
|
+
return true if File.mtime(input_path) > File.mtime(output_path)
|
|
130
|
+
|
|
131
|
+
# Check if operations changed by comparing cache key
|
|
132
|
+
# stored alongside the output file
|
|
133
|
+
cache_key = @filename_generator.generate_cache_key(operations)
|
|
134
|
+
cache_file = "#{output_path}.cache_key"
|
|
135
|
+
|
|
136
|
+
return true unless File.exist?(cache_file)
|
|
137
|
+
|
|
138
|
+
stored_key = File.read(cache_file).strip
|
|
139
|
+
return true if stored_key != cache_key
|
|
140
|
+
|
|
141
|
+
# No cache key file - needs processing to create it
|
|
142
|
+
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Build operation structure from params hash
|
|
147
|
+
# Combines all params into a single operation with flat params
|
|
148
|
+
# @param params [Hash] Parameters hash
|
|
149
|
+
# @return [Hash] Operation structure { type: :resize, params: { ... } }
|
|
150
|
+
def build_operation_from_params(params)
|
|
151
|
+
# Determine primary operation type
|
|
152
|
+
type = if params[:width] || params[:height]
|
|
153
|
+
:resize
|
|
154
|
+
elsif params[:crop]
|
|
155
|
+
:crop
|
|
156
|
+
elsif params[:watermark]
|
|
157
|
+
:watermark
|
|
158
|
+
else
|
|
159
|
+
:format
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Return unified operation structure with all params flattened
|
|
163
|
+
{
|
|
164
|
+
type: type,
|
|
165
|
+
params: params.compact
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Build operations array from tag parameters
|
|
170
|
+
# @param tag_params [Hash] Parameters from tag
|
|
171
|
+
# @return [Array<Hash>] Array of operation hashes
|
|
172
|
+
def build_operations_from_params(tag_params)
|
|
173
|
+
[build_operation_from_params(tag_params)]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Determine if operations represent default or specialized version
|
|
177
|
+
# @param params [Hash] Operation parameters
|
|
178
|
+
# @return [Symbol] :default or :specialized
|
|
179
|
+
def determine_version_type(params)
|
|
180
|
+
@config&.determine_version_type(params) || :specialized
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module JekyllImgFlow
|
|
6
|
+
# Central parser for all ImgFlow markup (tags, presets, etc.)
|
|
7
|
+
# Optimized version with reduced duplication and complexity
|
|
8
|
+
class Parser
|
|
9
|
+
# Define valid operation parameters and attribute types
|
|
10
|
+
OPERATION_PARAMS = %i[width height ratio aspect_ratio quality format formats
|
|
11
|
+
optimize level watermark opacity position keep preset].freeze
|
|
12
|
+
HTML_ATTRIBUTES = %i[alt class title loading].freeze
|
|
13
|
+
|
|
14
|
+
# Simple operation mappings for single-parameter operations
|
|
15
|
+
SIMPLE_OPERATIONS = {
|
|
16
|
+
quality: ->(val) { { type: :quality, params: { quality: val } } },
|
|
17
|
+
watermark: ->(val) { { type: :watermark, params: { text: val } } },
|
|
18
|
+
opacity: ->(val) { { type: :alpha_opacity, params: { opacity: val } } }
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def self.parse(markup, _context = nil)
|
|
22
|
+
# For KISS architecture: expect structured data from TagScanner
|
|
23
|
+
return validate_structured_data(markup) if markup.is_a?(Hash) && markup.key?(:image_path)
|
|
24
|
+
|
|
25
|
+
# Parse simple markup (backward compatibility)
|
|
26
|
+
raw_options = parse_markup_to_hash(markup)
|
|
27
|
+
operations = detect_operations(raw_options)
|
|
28
|
+
html_attributes = extract_html_attributes(raw_options)
|
|
29
|
+
image_path = extract_image_path(raw_options, markup)
|
|
30
|
+
|
|
31
|
+
# Early return if no image path
|
|
32
|
+
return build_error_response(raw_options, "No image path found in markup") unless image_path
|
|
33
|
+
|
|
34
|
+
# File validation handled by PathResolver when resolving paths
|
|
35
|
+
build_success_response(image_path, operations, html_attributes, raw_options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.parse_markup_to_hash(markup)
|
|
39
|
+
options = {}
|
|
40
|
+
|
|
41
|
+
# Handle different markup formats
|
|
42
|
+
if markup.include?("=")
|
|
43
|
+
parse_key_value_format(markup, options)
|
|
44
|
+
else
|
|
45
|
+
parse_positional_format(markup, options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
options
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.parse_key_value_format(markup, options)
|
|
52
|
+
parse_key_value_pairs(markup.split, "=", result: options, handle_arrays: false)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.parse_positional_format(markup, options)
|
|
56
|
+
# Use Shellwords for proper quote handling
|
|
57
|
+
parts = Shellwords.split(markup)
|
|
58
|
+
|
|
59
|
+
parse_key_value_pairs(
|
|
60
|
+
parts,
|
|
61
|
+
":",
|
|
62
|
+
result: options,
|
|
63
|
+
handle_arrays: true,
|
|
64
|
+
image_path_handler: lambda { |part, result|
|
|
65
|
+
result[:image_path] = part unless result[:image_path]
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.parse_key_value_pairs(pairs, separator, options = {})
|
|
71
|
+
pairs.each do |pair|
|
|
72
|
+
if pair.include?(separator)
|
|
73
|
+
key, value = pair.split(separator, 2)
|
|
74
|
+
options[:result][key.to_sym] = clean_and_convert_value(value, options)
|
|
75
|
+
elsif options[:image_path_handler]
|
|
76
|
+
options[:image_path_handler].call(pair, options[:result])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.clean_and_convert_value(value, options = {})
|
|
82
|
+
value = value.tr("'\"", "")
|
|
83
|
+
value = convert_numeric_value(value)
|
|
84
|
+
|
|
85
|
+
# Handle comma-separated arrays
|
|
86
|
+
value = value.split(",").map(&:strip) if options[:handle_arrays] && value.is_a?(String) && value.include?(",")
|
|
87
|
+
|
|
88
|
+
value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.convert_numeric_value(value)
|
|
92
|
+
return value.to_i if value.match?(/^\d+$/)
|
|
93
|
+
return value.to_f if value.match?(/^\d+\.\d+$/)
|
|
94
|
+
|
|
95
|
+
value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.validate_structured_data(data)
|
|
99
|
+
image_path = data[:image_path]
|
|
100
|
+
operations = data[:operations] || {}
|
|
101
|
+
|
|
102
|
+
return build_error_response(data, "No image path found in structured data") unless image_path
|
|
103
|
+
|
|
104
|
+
# Operations validation handled by Tags - Parser only parses structure
|
|
105
|
+
# File validation handled by PathResolver when resolving paths
|
|
106
|
+
build_success_response(image_path, operations, data[:html_attributes] || {}, data)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.detect_operations(options)
|
|
110
|
+
operations = []
|
|
111
|
+
operation_options = options.slice(*OPERATION_PARAMS)
|
|
112
|
+
|
|
113
|
+
# Handle crop+resize combination (needs sequential processing)
|
|
114
|
+
has_crop = operation_options[:ratio] || operation_options[:aspect_ratio]
|
|
115
|
+
has_resize = operation_options[:width] || operation_options[:height]
|
|
116
|
+
|
|
117
|
+
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
|
+
operations << { type: :crop, params: crop_params }
|
|
121
|
+
operations << { type: :resize, params: resize_params }
|
|
122
|
+
elsif has_crop
|
|
123
|
+
crop_params = operation_options.slice(:ratio, :aspect_ratio, :keep, :position)
|
|
124
|
+
operations << { type: :crop, params: crop_params }
|
|
125
|
+
elsif has_resize
|
|
126
|
+
operations << { type: :resize, params: operation_options }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Add simple operations using mappings
|
|
130
|
+
SIMPLE_OPERATIONS.each do |key, builder|
|
|
131
|
+
operations << builder.call(operation_options[key]) if operation_options[key]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Handle format operation (can be array or single value)
|
|
135
|
+
if operation_options[:format] || operation_options[:formats]
|
|
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
|
|
143
|
+
|
|
144
|
+
# Handle optimize operation
|
|
145
|
+
if operation_options[:optimize] || operation_options[:level]
|
|
146
|
+
operations << { type: :optimize,
|
|
147
|
+
params: { level: operation_options[:level] || :medium } }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
operations
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.extract_html_attributes(options)
|
|
154
|
+
options.slice(*HTML_ATTRIBUTES)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.extract_image_path(options, markup)
|
|
158
|
+
return options[:image_path] if options[:image_path]
|
|
159
|
+
return if markup.strip.empty?
|
|
160
|
+
|
|
161
|
+
# Extract first word (before space or =)
|
|
162
|
+
markup.split(/[\s=]/, 2).first&.strip
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Convert operations array from TagScanner to structured hash
|
|
166
|
+
def self.convert_operations_array(operations_array)
|
|
167
|
+
operations_array.each_with_object({}) do |op_str, hash|
|
|
168
|
+
next unless op_str.include?(":")
|
|
169
|
+
|
|
170
|
+
key, value = op_str.split(":", 2)
|
|
171
|
+
hash[key.strip.to_sym] = clean_and_convert_value(value.strip, handle_arrays: true)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.build_error_response(raw_options, error_message)
|
|
176
|
+
{
|
|
177
|
+
image_path: nil,
|
|
178
|
+
operations: [],
|
|
179
|
+
html_attributes: {},
|
|
180
|
+
raw_options: raw_options,
|
|
181
|
+
preset: nil,
|
|
182
|
+
error: error_message
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.build_success_response(image_path, operations, html_attributes, raw_options)
|
|
187
|
+
{
|
|
188
|
+
image_path: image_path,
|
|
189
|
+
operations: operations,
|
|
190
|
+
html_attributes: html_attributes,
|
|
191
|
+
raw_options: raw_options,
|
|
192
|
+
preset: raw_options[:preset]
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# File validation removed - handled by PathResolver when resolving paths
|
|
197
|
+
# Operations validation removed - handled by Tags
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module JekyllImgFlow
|
|
7
|
+
# PathResolver - centralized path management for all image operations
|
|
8
|
+
class PathResolver
|
|
9
|
+
def initialize(config)
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Get path information for an image
|
|
14
|
+
# @param image_name [String] Name of the image file
|
|
15
|
+
# @return [Hash] Path information for different use cases
|
|
16
|
+
def paths_for(image_name)
|
|
17
|
+
{
|
|
18
|
+
cli: File.join(@config.site.source, @config.originals, image_name), # Full filesystem path
|
|
19
|
+
http: File.join(@config.originals, image_name), # Relative path for URLs
|
|
20
|
+
name: image_name # Just the filename
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get CLI path (convenience method)
|
|
25
|
+
def cli_path(image_name)
|
|
26
|
+
paths_for(image_name)[:cli]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get HTTP path (convenience method)
|
|
30
|
+
def http_path(image_name)
|
|
31
|
+
paths_for(image_name)[:http]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Resolve original path with validation
|
|
35
|
+
# @param image_name [String] Name of the image file
|
|
36
|
+
# @return [String] Full path to original image file
|
|
37
|
+
# @raise [ArgumentError] If image cannot be resolved or doesn't exist
|
|
38
|
+
def resolve_original_path(image_name)
|
|
39
|
+
full_path = cli_path(image_name)
|
|
40
|
+
|
|
41
|
+
# Validate path exists and is readable
|
|
42
|
+
raise ArgumentError, "Cannot resolve image path: #{image_name} (not found: #{full_path})" unless File.exist?(full_path)
|
|
43
|
+
|
|
44
|
+
raise ArgumentError, "Image path is not a file: #{image_name} (path: #{full_path})" unless File.file?(full_path)
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, "Image path not readable: #{image_name} (path: #{full_path})" unless File.readable?(full_path)
|
|
47
|
+
|
|
48
|
+
# Validate input file format against config
|
|
49
|
+
file_extension = File.extname(full_path).delete(".").downcase
|
|
50
|
+
input_formats = @config.input_formats
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "No input_formats configured in unified config" unless input_formats
|
|
53
|
+
|
|
54
|
+
unless input_formats.include?(file_extension)
|
|
55
|
+
raise ArgumentError,
|
|
56
|
+
"Invalid input format: '#{file_extension}'. Must be one of: #{input_formats.join(', ')}."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
full_path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Resolve output path for generated filename
|
|
63
|
+
# @param filename [String] Generated filename
|
|
64
|
+
# @return [String] Full path to output file in _site (for specialized versions during rendering)
|
|
65
|
+
def resolve_output_path(filename)
|
|
66
|
+
# Resolve output path relative to site destination
|
|
67
|
+
File.join(@config.site.dest, @config.output, filename)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Resolve output path to source directory for default images
|
|
71
|
+
# @param filename [String] Generated filename
|
|
72
|
+
# @return [String] Full path to output file in source (for default versions during pre_render)
|
|
73
|
+
def resolve_source_output_path(filename)
|
|
74
|
+
# Default images go to source directory so Jekyll can copy them to _site
|
|
75
|
+
File.join(@config.site.source, @config.output, filename)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resolve relative output path for manifest storage
|
|
79
|
+
# @param filename [String] Generated filename
|
|
80
|
+
# @return [String] Relative path from site root
|
|
81
|
+
def resolve_relative_output_path(filename)
|
|
82
|
+
# Return path relative to site root (without leading /)
|
|
83
|
+
File.join(@config.output, filename)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get site destination directory
|
|
87
|
+
# @return [String] Site destination path
|
|
88
|
+
def site_dest
|
|
89
|
+
@config.site.dest
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generate temporary output path
|
|
93
|
+
# @param format [String] File format extension
|
|
94
|
+
# @return [String] Path to temporary file
|
|
95
|
+
def temp_output_path(format)
|
|
96
|
+
File.join(Dir.tmpdir, "imgflow-out-#{SecureRandom.hex}.#{format}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Generate temporary input path
|
|
100
|
+
# @param extension [String] File extension
|
|
101
|
+
# @return [String] Path to temporary file
|
|
102
|
+
def temp_input_path(extension = "tmp")
|
|
103
|
+
File.join(Dir.tmpdir, "imgflow-in-#{SecureRandom.hex}.#{extension}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Build public URL for image
|
|
107
|
+
# @param relative_path [String] Relative path from site root
|
|
108
|
+
# @param site [Jekyll::Site] Jekyll site object
|
|
109
|
+
# @return [String] Public URL
|
|
110
|
+
def build_url(relative_path, site)
|
|
111
|
+
base_url = site.config["url"] || "http://localhost:4000"
|
|
112
|
+
baseurl = site.config["baseurl"] || ""
|
|
113
|
+
"#{base_url}#{baseurl}/#{relative_path}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get relative path from site destination
|
|
117
|
+
# @param full_path [String] Full filesystem path
|
|
118
|
+
# @param site [Jekyll::Site] Jekyll site object
|
|
119
|
+
# @return [String] Relative path
|
|
120
|
+
def relative_from_dest(full_path, site)
|
|
121
|
+
# Simple path manipulation - remove site.dest prefix and leading slash
|
|
122
|
+
if full_path.start_with?(site.dest)
|
|
123
|
+
relative = full_path[site.dest.length..]
|
|
124
|
+
relative.start_with?("/") ? relative[1..] : relative
|
|
125
|
+
else
|
|
126
|
+
full_path
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|