Class: JekyllImgFlow::OperationProcessor

Inherits:
Object
  • Object
show all
Defined in:
lib/jekyll-imgflow/operation_processor.rb

Overview

OperationProcessor - processes image operations using providers Handles both single operations and batch operations

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(provider, path_resolver, manifest = nil, config = nil) ⇒ OperationProcessor

Returns a new instance of OperationProcessor.



12
13
14
15
16
17
18
19
# File 'lib/jekyll-imgflow/operation_processor.rb', line 12

def initialize(provider, path_resolver, manifest = nil, config = nil)
  @provider = provider
  @path_resolver = path_resolver
  @filename_generator = FilenameGenerator.new
  @manifest = manifest
  @config = config
  @stats = ProcessingStats.new
end

Instance Attribute Details

#statsObject

Returns the value of attribute stats.



10
11
12
# File 'lib/jekyll-imgflow/operation_processor.rb', line 10

def stats
  @stats
end

Instance Method Details

#add_static_file(site, relative) ⇒ Object

Parameters:

  • site (Jekyll::Site)

    Jekyll site object

  • relative (String)

    Path relative to site source



127
128
129
130
131
# File 'lib/jekyll-imgflow/operation_processor.rb', line 127

def add_static_file(site, relative)
  site.static_files << Jekyll::StaticFile.new(
    site, site.source, File.dirname(relative), File.basename(relative)
  )
end

#build_operation_from_params(params) ⇒ Hash

Build operation structure from params hash Combines all params into a single operation with flat params

Parameters:

  • params (Hash)

    Parameters hash

Returns:

  • (Hash)

    Operation structure { type: :resize, params: { ... } }



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/jekyll-imgflow/operation_processor.rb', line 200

def build_operation_from_params(params)
  # Determine primary operation type
  type = if params[:width] || params[:height]
           :resize
         elsif params[:crop]
           :crop
         elsif params[:watermark]
           :watermark
         else
           :format
         end

  # Return unified operation structure with all params flattened
  {
    type: type,
    params: params.compact
  }
end

#build_operations_from_params(tag_params) ⇒ Array<Hash>

Build operations array from tag parameters

Parameters:

  • tag_params (Hash)

    Parameters from tag

Returns:

  • (Array<Hash>)

    Array of operation hashes



222
223
224
# File 'lib/jekyll-imgflow/operation_processor.rb', line 222

def build_operations_from_params(tag_params)
  [build_operation_from_params(tag_params)]
end

#copy_animated_image(original_name, input_path, output_path) ⇒ Object



76
77
78
79
80
81
# File 'lib/jekyll-imgflow/operation_processor.rb', line 76

def copy_animated_image(original_name, input_path, output_path)
  Jekyll.logger.warn "🖼️  ImgFlow: Skipping resize for animated GIF '#{original_name}' — " \
                     "copying original as-is to preserve animation."
  FileUtils.cp(input_path, output_path)
  @stats.record_cache_miss
end

#determine_version_type(params) ⇒ Symbol

Determine if operations represent default or specialized version

Parameters:

  • params (Hash)

    Operation parameters

Returns:

  • (Symbol)

    :default or :specialized



229
230
231
# File 'lib/jekyll-imgflow/operation_processor.rb', line 229

def determine_version_type(params)
  @config&.determine_version_type(params) || :specialized
end

#needs_processing?(input_path, output_path, _operations = nil) ⇒ Boolean

Check if operation needs to be processed (cache check)

Parameters:

  • input_path (String)

    Path to input image

  • output_path (String)

    Path to output image

  • operations (Hash)

    Operations to apply

Returns:

  • (Boolean)

    True if processing needed



178
179
180
181
182
183
184
# File 'lib/jekyll-imgflow/operation_processor.rb', line 178

def needs_processing?(input_path, output_path, _operations = nil)
  # If output doesn't exist, needs processing
  return true unless File.exist?(output_path)

  # If input is newer than output, needs processing
  File.mtime(input_path) > File.mtime(output_path)
end

#output_subdir(original_name) ⇒ Object



60
61
62
63
# File 'lib/jekyll-imgflow/operation_processor.rb', line 60

def output_subdir(original_name)
  subdir = File.dirname(original_name)
  subdir == "." ? nil : subdir
end

#output_up_to_date?(input_path, output_path) ⇒ Boolean

Check if the output file exists and the input is not newer than the output. Used to skip provider calls when the optimized file is already on disk (e.g. legacy manifest, manifest out of sync).

Parameters:

  • input_path (String)

    Path to input image

  • output_path (String)

    Path to output image

Returns:

  • (Boolean)

    True if output exists and is up-to-date



192
193
194
# File 'lib/jekyll-imgflow/operation_processor.rb', line 192

def output_up_to_date?(input_path, output_path)
  File.file?(output_path) && File.mtime(input_path) <= File.mtime(output_path)
end

#process_batch_operations(operations, input_path, final_output_path) ⇒ String

Process multiple operations on an image in sequence (batch)

Parameters:

  • operations (Array<Hash>)

    Array of operations to process

  • input_path (String)

    Path to input image

  • final_output_path (String)

    Path to final output image

Returns:

  • (String)

    Path to final processed image



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/jekyll-imgflow/operation_processor.rb', line 138

def process_batch_operations(operations, input_path, final_output_path)
  return input_path if operations.empty?

  current_input = input_path
  temp_files = []

  begin
    # Process each operation in sequence
    operations.each_with_index do |operation, index|
      operation_type = operation[:type]
      params = operation[:params] || {}

      # Determine output path
      if index == operations.length - 1
        # Last operation - use final output path
        output_path = final_output_path
      else
        # Intermediate operation - use temp file
        format = params[:format] || File.extname(input_path).delete(".")
        output_path = @path_resolver.temp_output_path(format)
        temp_files << output_path
      end

      # Process the operation
      current_input = process_single_operation(operation_type, current_input,
                                               output_path, params)
    end

    current_input
  ensure
    # Cleanup temp files even on failure
    temp_files.each { |f| FileUtils.rm_f(f) }
  end
end

#process_image(type, input_path, output_path, params) ⇒ Object



83
84
85
86
87
88
# File 'lib/jekyll-imgflow/operation_processor.rb', line 83

def process_image(type, input_path, output_path, params)
  elapsed = Benchmark.measure { process_single_operation(type, input_path, output_path, params) }
  @stats.record_operation_time(type, elapsed.real)
  @stats.record_cache_miss
  record_compression_ratio(input_path, output_path, params)
end

#process_operation(original_name, operation, input_path, page_path = nil) ⇒ String

Process an operation and return the output path

Parameters:

  • original_name (String)

    Original image filename

  • operation (Hash)

    Operation structure { type: :resize, params: { width: 800 } }

  • input_path (String)

    Path to input image

  • page_path (String) (defaults to: nil)

    Optional page path for manifest tracking

Returns:

  • (String)

    Path to processed image



45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/jekyll-imgflow/operation_processor.rb', line 45

def process_operation(original_name, operation, input_path, page_path = nil)
  params = operation[:params]
  file_digest = operation[:file_digest] || @filename_generator.file_digest(input_path)
  version_type = determine_version_type(params)
  filename = @filename_generator.generate_filename(input_path, params)
  subdir = output_subdir(original_name)
  output_path = @path_resolver.resolve_source_output_path(filename, subdir)
  FileUtils.mkdir_p(File.dirname(output_path))

  process_output(operation, original_name, input_path, output_path, params)
  register_manifest(original_name, filename, params, version_type, page_path, file_digest, subdir)
  register_jekyll_static_file(output_path)
  output_path
end

#process_output(operation, original_name, input_path, output_path, params) ⇒ Object



65
66
67
68
69
70
71
72
73
74
# File 'lib/jekyll-imgflow/operation_processor.rb', line 65

def process_output(operation, original_name, input_path, output_path, params)
  if !operation[:force_processing] && output_up_to_date?(input_path, output_path)
    Jekyll.logger.debug "⏭️  ImgFlow: Output exists and up-to-date, skipping provider call for #{original_name}"
    @stats.record_cache_hit
  elsif AnimatedGifDetector.animated?(input_path)
    copy_animated_image(original_name, input_path, output_path)
  else
    process_image(operation[:type], input_path, output_path, params)
  end
end

#process_single_operation(operation_type, input_path, output_path, params) ⇒ String

Process a single operation on an image

Parameters:

  • operation_type (Symbol)

    Type of operation (:resize, :crop, :quality, etc.)

  • input_path (String)

    Path to input image

  • output_path (String)

    Path to output image

  • params (Hash)

    Operation parameters

Returns:

  • (String)

    Path to processed image



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/jekyll-imgflow/operation_processor.rb', line 27

def process_single_operation(operation_type, input_path, output_path, params)
  # Get the appropriate tag class for validation
  tag_class = JekyllImgFlow::Tags::TagRegistry.get_tag(operation_type)
  raise "Unknown operation: #{operation_type}" unless tag_class

  # Create tag instance and process
  tag = tag_class.new(@provider)
  tag.process(input_path, output_path, params)

  output_path
end

#record_compression_ratio(input_path, output_path, params) ⇒ Object



90
91
92
93
94
95
# File 'lib/jekyll-imgflow/operation_processor.rb', line 90

def record_compression_ratio(input_path, output_path, params)
  return unless File.file?(input_path) && File.file?(output_path)

  format = params[:format] || File.extname(input_path).delete(".")
  @stats.record_compression_ratio(format.to_s, File.size(input_path), File.size(output_path))
end

#register_jekyll_static_file(file_path) ⇒ Object

Register a generated file as a Jekyll::StaticFile so Jekyll copies it to _site during the write phase. No-op for mock/test sites.

Parameters:

  • file_path (String)

    Absolute path to the generated file in source



109
110
111
112
113
114
115
116
117
# File 'lib/jekyll-imgflow/operation_processor.rb', line 109

def register_jekyll_static_file(file_path)
  site = @config&.site
  return unless site.is_a?(Jekyll::Site)

  relative = file_path.delete_prefix("#{site.source}/")
  return if relative == file_path # not under site source

  add_static_file(site, relative) unless static_file_registered?(site, relative)
end

#register_manifest(original_name, filename, params, version_type, page_path, file_digest, subdir) ⇒ Object



97
98
99
100
101
102
103
104
# File 'lib/jekyll-imgflow/operation_processor.rb', line 97

def register_manifest(original_name, filename, params, version_type, page_path, file_digest, subdir)
  return unless @manifest

  relative_path = "/#{@path_resolver.resolve_relative_output_path(filename, subdir)}"
  provider_name = @provider&.class&.provider_name || "unknown"
  @manifest.register_version(original_name, relative_path, params, version_type, page_path,
                             file_digest, provider_name)
end

#static_file_registered?(site, relative) ⇒ Boolean

Parameters:

  • site (Jekyll::Site)

    Jekyll site object

  • relative (String)

    Path relative to site source

Returns:

  • (Boolean)


121
122
123
# File 'lib/jekyll-imgflow/operation_processor.rb', line 121

def static_file_registered?(site, relative)
  site.static_files.any? { |sf| sf.relative_path == "/#{relative}" }
end