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



147
148
149
150
151
# File 'lib/jekyll-imgflow/operation_processor.rb', line 147

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: { ... } }



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/jekyll-imgflow/operation_processor.rb', line 220

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



242
243
244
# File 'lib/jekyll-imgflow/operation_processor.rb', line 242

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

#determine_version_type(params) ⇒ Symbol

Determine if operations represent default or specialized version

Parameters:

  • params (Hash)

    Operation parameters

Returns:

  • (Symbol)

    :default or :specialized



249
250
251
# File 'lib/jekyll-imgflow/operation_processor.rb', line 249

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



198
199
200
201
202
203
204
# File 'lib/jekyll-imgflow/operation_processor.rb', line 198

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_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



212
213
214
# File 'lib/jekyll-imgflow/operation_processor.rb', line 212

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



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/jekyll-imgflow/operation_processor.rb', line 158

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_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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/jekyll-imgflow/operation_processor.rb', line 45

def process_operation(original_name, operation, input_path, page_path = nil)
  type = operation[:type]
  params = operation[:params]
  file_digest = operation[:file_digest] || @filename_generator.file_digest(input_path)

  # Determine version type
  version_type = determine_version_type(params)

  # Preserve original directory structure under output
  subdir = File.dirname(original_name)
  subdir = nil if subdir == "."

  # Generate filename using FilenameGenerator (JPT compatible)
  filename = @filename_generator.generate_filename(input_path, params)

  # Write to source directory so Jekyll copies files to _site during the write phase
  actual_output_path = @path_resolver.resolve_source_output_path(filename, subdir)

  # Ensure output directory exists before processing
  FileUtils.mkdir_p(File.dirname(actual_output_path))

  # Skip processing if the output file already exists and is up-to-date.
  # This handles legacy manifests, corrupted manifests, and any case where
  # optimized files exist on disk but the manifest is out of sync — the
  # version is registered in the manifest without re-running the provider.
  if !operation[:force_processing] && output_up_to_date?(input_path, actual_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)
    # Animated GIFs must not be resized or converted — the operation
    # would destroy the animation. Copy the original file as-is so the
    # manifest can track it and HTML references stay valid.
    Jekyll.logger.warn "🖼️  ImgFlow: Skipping resize for animated GIF " \
                       "'#{original_name}' — copying original as-is to " \
                       "preserve animation."
    FileUtils.cp(input_path, actual_output_path)
    @stats.record_cache_miss
  else
    # Process the operation (create the image)
    elapsed = Benchmark.measure do
      process_single_operation(type, input_path, actual_output_path, params)
    end
    @stats.record_operation_time(type, elapsed.real)
    @stats.record_cache_miss

    # Record compression ratio if we have the original size
    if File.file?(input_path) && File.file?(actual_output_path)
      format = params[:format] || File.extname(input_path).delete(".")
      @stats.record_compression_ratio(format.to_s, File.size(input_path),
                                      File.size(actual_output_path))
    end
  end

  # Register in manifest
  if @manifest
    # Store relative path (with leading /) for manifest storage
    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

  # Register as Jekyll static file so Jekyll copies it to _site during
  # the write phase. Without this, files created during pre_render (or
  # during render via imgflow tags) are not picked up by Jekyll's static
  # file reader, which runs before pre_render. This would leave _site
  # without optimized images until a second build.
  register_jekyll_static_file(actual_output_path)

  actual_output_path
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

#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



129
130
131
132
133
134
135
136
137
# File 'lib/jekyll-imgflow/operation_processor.rb', line 129

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

#static_file_registered?(site, relative) ⇒ Boolean

Parameters:

  • site (Jekyll::Site)

    Jekyll site object

  • relative (String)

    Path relative to site source

Returns:

  • (Boolean)


141
142
143
# File 'lib/jekyll-imgflow/operation_processor.rb', line 141

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