jekyll-imgflow 0.3.3 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d59e73749066617a2c71322c3c80c5161ea847872a7864cf65c9133a8a315e24
4
- data.tar.gz: 861a4df0428da9bdd3221b89838ebb4d85d598172465569b5cdc28e14f243ede
3
+ metadata.gz: e314c5d37485b9d821e58bd4614798c9e61e3b944c60f6bbb948fb50a6fe9c2b
4
+ data.tar.gz: f9b64d4bc5119727cda256d7e1b1fb77e9ee6c9a1b19e97175290cf8f23df1c9
5
5
  SHA512:
6
- metadata.gz: aae0d3bb14c346d432881a29844b1221225599dd77767d087faccc14023f0d527fcc36c4eeb0e5a1df4fa9a87f86276ea42388e2364f7158f12af8ba0512d459
7
- data.tar.gz: ba868a71465128a9f6f7259ad2852b7b16093b30496fd9a2e9d9f259e811908b2541b3f14a002475201a26576236921f76dbd52782797facb2d4a27e496c81de
6
+ metadata.gz: 9102a522a856e6fe440eb207b31c18d350e81a68055b13745ef835e51d257920e115b0054af3494534f9116e89616b13d99fd57c9aed132250bce6e98f280497
7
+ data.tar.gz: e6039b8fe325d7604fea5267c2f9fdfa40b8990647a8e0dfa936c7c2add4376b66cca4a28d8f548f6c0799321660f076902a27cba4dc3e734c424a683b7c0fdd
data/README.md CHANGED
@@ -8,6 +8,7 @@ A [Jekyll](https://jekyllrb.com/) plugin for automatic image optimization with m
8
8
  [![Status](https://img.shields.io/badge/status-active-success)](https://github.com/gundestrup/jekyll-imgflow)
9
9
  [![Semgrep](https://img.shields.io/badge/Semgrep-scanned-1B2B34?logo=semgrep&logoColor=white)](https://semgrep.dev/)
10
10
  [![CodeFactor](https://www.codefactor.io/repository/github/gundestrup/jekyll-imgflow/badge)](https://www.codefactor.io/repository/github/gundestrup/jekyll-imgflow)
11
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gundestrup_jekyll-imgflow&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=gundestrup_jekyll-imgflow)
11
12
  [![codecov](https://codecov.io/gh/gundestrup/jekyll-imgflow/branch/main/graph/badge.svg)](https://codecov.io/gh/gundestrup/jekyll-imgflow)
12
13
  [![VS Code Extension](https://img.shields.io/badge/VS%20Code%20Companion-jekyll--imgflow-blue?logo=visual-studio-code)](https://github.com/gundestrup/jekyll-imgflow-vscode)
13
14
  [![License](https://img.shields.io/github/license/gundestrup/jekyll-imgflow)](LICENSE)
@@ -98,6 +99,28 @@ ImgFlow has two processing flows: **Build-Time** (pre-generation) and **Runtime*
98
99
 
99
100
  **See:** [providers.md](docs/providers.md) for detailed provider comparison and setup
100
101
 
102
+ ### SVG File Handling
103
+
104
+ SVG files are supported as input by all providers. Since SVGs are vector
105
+ graphics, they are rasterized before format conversion (WebP, AVIF, JPG, PNG).
106
+
107
+ - **Weserv** (via librsvg): When an SVG has no explicit pixel dimensions
108
+ (e.g. `width="100%"` with a large `viewBox`), ImgFlow automatically
109
+ caps the rasterization at 2000px wide to prevent memory exhaustion.
110
+ If a resize or crop operation is present, the target dimensions are used
111
+ directly.
112
+ - **Sharp, LibVips, Imgproxy, Flyimg**: Handle SVGs via their respective
113
+ rasterization backends. SVGs with very large viewBox dimensions may be
114
+ slow or fail depending on the provider's memory limits.
115
+ - **ImageMagick**: SVG processing is slower than other providers without
116
+ the `librsvg` delegate installed. Set `FULL_SVG_TEST=true` to include
117
+ SVGs in ImageMagick test runs.
118
+
119
+ SVGs with explicit `width` and `height` attributes (in pixels, not
120
+ percentages) are always processed at the specified size first, then resized
121
+ to the target dimensions — this is the recommended way to author SVGs for
122
+ image processing.
123
+
101
124
  ## 🚀 Quick Commands
102
125
 
103
126
  ```bash
@@ -44,35 +44,11 @@ module JekyllImgFlow
44
44
  original_paths = find_original_images
45
45
  Jekyll.logger.info "📸 Found #{original_paths.length} original images"
46
46
 
47
- originals_dir = File.join(@site.source, @config.originals)
48
-
49
47
  # 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?
48
+ prepare_manifest(original_paths)
54
49
 
55
50
  # Build tasks for each original image
56
- original_paths.reject { |path| AnimatedGifDetector.animated?(path) }.each do |path|
57
- original_name = path.sub("#{originals_dir}/", "")
58
- file_digest = @filename_generator.file_digest(path)
59
-
60
- # Check if needs processing (file changed or provider changed)
61
- if needs_processing?(original_name, path, file_digest)
62
- Jekyll.logger.info "🔧 Queuing default versions for: #{original_name}"
63
-
64
- # 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 }
70
- @batch_manager.add_tasks(tasks)
71
- else
72
- expected_default_operations.length.times { @operation_processor.stats.record_cache_hit }
73
- Jekyll.logger.debug "⏭️ Skipping #{original_name} - already up-to-date"
74
- end
75
- end
51
+ queue_default_tasks(original_paths)
76
52
 
77
53
  # Process all queued tasks
78
54
  results = @batch_manager.process_all
@@ -89,6 +65,39 @@ module JekyllImgFlow
89
65
 
90
66
  private
91
67
 
68
+ def prepare_manifest(original_paths)
69
+ originals_dir = File.join(@site.source, @config.originals)
70
+ current_originals = original_paths.map { |path| path.sub("#{originals_dir}/", "") }
71
+ @manifest.cleanup_deleted_originals(current_originals)
72
+ @manifest.cleanup_obsolete_defaults(expected_default_operations)
73
+ @manifest.reset_page_usage unless incremental_build?
74
+ end
75
+
76
+ def queue_default_tasks(original_paths)
77
+ originals_dir = File.join(@site.source, @config.originals)
78
+ original_paths.reject { |path| AnimatedGifDetector.animated?(path) }.each do |path|
79
+ queue_tasks_for(path, path.sub("#{originals_dir}/", ""))
80
+ end
81
+ end
82
+
83
+ def queue_tasks_for(path, original_name)
84
+ file_digest = @filename_generator.file_digest(path)
85
+ unless needs_processing?(original_name, path, file_digest)
86
+ expected_default_operations.length.times { @operation_processor.stats.record_cache_hit }
87
+ Jekyll.logger.debug "⏭️ Skipping #{original_name} - already up-to-date"
88
+ return
89
+ end
90
+
91
+ Jekyll.logger.info "🔧 Queuing default versions for: #{original_name}"
92
+
93
+ tasks = BatchManager.build_default_tasks(
94
+ original_name, path, @config, @site, file_digest: file_digest
95
+ )
96
+ force_processing = source_digest_changed?(original_name, file_digest)
97
+ tasks.each { |task| task[:force_processing] = force_processing }
98
+ @batch_manager.add_tasks(tasks)
99
+ end
100
+
92
101
  def register_skipped_tasks(completed_tasks)
93
102
  provider_name = @registry.current_provider&.class&.provider_name || "unknown"
94
103
  completed_tasks.each do |completed|
@@ -127,13 +136,17 @@ module JekyllImgFlow
127
136
  current_provider = @registry.current_provider&.class&.provider_name || "unknown"
128
137
 
129
138
  expected_default_operations.any? do |operations|
130
- version = default_versions.find do |candidate|
131
- @manifest.same_operations?(candidate["operations"], operations)
132
- end
139
+ version = find_default_version(default_versions, operations)
133
140
  !current_default_version?(version, current_provider, file_digest)
134
141
  end
135
142
  end
136
143
 
144
+ def find_default_version(default_versions, operations)
145
+ default_versions.find do |candidate|
146
+ @manifest.same_operations?(candidate["operations"], operations)
147
+ end
148
+ end
149
+
137
150
  def expected_default_operations
138
151
  @config.sizes.each_value.flat_map do |width|
139
152
  @config.formats.map do |format|
@@ -22,6 +22,18 @@ module JekyllImgFlow
22
22
  DEFAULT_FALLBACK_FORMAT = "jpg"
23
23
  DEFAULT_IMAGE_MODAL = true
24
24
 
25
+ # Core config keys mapped to their defaults (assigned as @ivar per key).
26
+ CORE_CONFIG_DEFAULTS = {
27
+ "originals" => DEFAULT_ORIGINALS,
28
+ "output" => DEFAULT_OUTPUT,
29
+ "input_formats" => DEFAULT_INPUT_FORMATS,
30
+ "sizes" => DEFAULT_SIZES,
31
+ "formats" => DEFAULT_FORMATS,
32
+ "quality" => DEFAULT_QUALITY,
33
+ "backend_priority" => DEFAULT_BACKEND_PRIORITY,
34
+ "fallback_format" => DEFAULT_FALLBACK_FORMAT
35
+ }.freeze
36
+
25
37
  attr_reader :site, :originals, :output, :input_formats, :sizes, :formats,
26
38
  :quality, :backend_priority, :imgproxy_url,
27
39
  :weserv_url, :flyimg_url,
@@ -37,14 +49,9 @@ module JekyllImgFlow
37
49
  end
38
50
 
39
51
  def assign_core_config(cfg)
40
- @originals = cfg["originals"] || DEFAULT_ORIGINALS
41
- @output = cfg["output"] || DEFAULT_OUTPUT
42
- @input_formats = cfg["input_formats"] || DEFAULT_INPUT_FORMATS
43
- @sizes = cfg["sizes"] || DEFAULT_SIZES
44
- @formats = cfg["formats"] || DEFAULT_FORMATS
45
- @quality = cfg["quality"] || DEFAULT_QUALITY
46
- @backend_priority = cfg["backend_priority"] || DEFAULT_BACKEND_PRIORITY
47
- @fallback_format = cfg["fallback_format"] || DEFAULT_FALLBACK_FORMAT
52
+ CORE_CONFIG_DEFAULTS.each do |key, default|
53
+ instance_variable_set("@#{key}", cfg[key] || default)
54
+ end
48
55
  @image_modal = cfg.fetch("image_modal", DEFAULT_IMAGE_MODAL)
49
56
  end
50
57
 
@@ -135,17 +135,23 @@ module JekyllImgFlow
135
135
 
136
136
  max_width = @config.sizes.values.max
137
137
  Dir.children(directory).filter_map do |filename|
138
- candidate = JekyllImgFlow::FilenameGenerator.new.parse_filename(filename)
139
- next unless candidate[:base_name] == parsed[:base_name]
140
- next unless candidate[:hash] == parsed[:hash]
141
- next if max_width && candidate[:width] > max_width
142
-
143
- File.join(File.dirname(result), filename)
138
+ matching_variant_path(filename, parsed, max_width, result)
144
139
  end
145
140
  rescue Errno::ENOENT
146
141
  []
147
142
  end
148
143
 
144
+ # Return the output path for +filename+ when it is a variant of the same
145
+ # image (same base name and content hash) within the max configured width.
146
+ def matching_variant_path(filename, parsed, max_width, result)
147
+ candidate = JekyllImgFlow::FilenameGenerator.new.parse_filename(filename)
148
+ return unless candidate[:base_name] == parsed[:base_name]
149
+ return unless candidate[:hash] == parsed[:hash]
150
+ return if max_width && candidate[:width] > max_width
151
+
152
+ File.join(File.dirname(result), filename)
153
+ end
154
+
149
155
  def modal_width(result)
150
156
  JekyllImgFlow::FilenameGenerator.new.parse_filename(File.basename(result))[:width] || 0
151
157
  end
@@ -110,44 +110,51 @@ module JekyllImgFlow
110
110
  operations = []
111
111
  operation_options = options.slice(*OPERATION_PARAMS)
112
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]
113
+ add_crop_resize_operations(operations, operation_options)
114
+ add_simple_operations(operations, operation_options)
115
+ add_format_operation(operations, operation_options)
116
+ add_optimize_operation(operations, operation_options)
117
+
118
+ operations
119
+ end
120
+
121
+ def self.add_crop_resize_operations(operations, opts)
122
+ has_crop = opts[:ratio] || opts[:aspect_ratio]
123
+ has_resize = opts[:width] || opts[:height]
124
+ crop_params = opts.slice(:ratio, :aspect_ratio, :keep, :position)
116
125
 
117
126
  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
127
  operations << { type: :crop, params: crop_params }
121
- operations << { type: :resize, params: resize_params }
128
+ operations << { type: :resize, params: opts.except(:ratio, :aspect_ratio, :keep, :position) }
122
129
  elsif has_crop
123
- crop_params = operation_options.slice(:ratio, :aspect_ratio, :keep, :position)
124
130
  operations << { type: :crop, params: crop_params }
125
131
  elsif has_resize
126
- operations << { type: :resize, params: operation_options }
132
+ operations << { type: :resize, params: opts }
127
133
  end
134
+ end
128
135
 
129
- # Add simple operations using mappings
136
+ def self.add_simple_operations(operations, opts)
130
137
  SIMPLE_OPERATIONS.each do |key, builder|
131
- operations << builder.call(operation_options[key]) if operation_options[key]
138
+ operations << builder.call(opts[key]) if opts[key]
132
139
  end
140
+ end
133
141
 
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
142
+ def self.add_format_operation(operations, opts)
143
+ return unless opts[:format] || opts[:formats]
143
144
 
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
145
+ format_value = opts[:format] || opts[:formats]
146
+ operations << if format_value.is_a?(Array)
147
+ { type: :format, params: { formats: format_value } }
148
+ else
149
+ { type: :format, params: { format: format_value } }
150
+ end
151
+ end
149
152
 
150
- operations
153
+ def self.add_optimize_operation(operations, opts)
154
+ return unless opts[:optimize] || opts[:level]
155
+
156
+ operations << { type: :optimize,
157
+ params: { level: opts[:level] || :medium } }
151
158
  end
152
159
 
153
160
  def self.extract_html_attributes(options)
@@ -99,61 +99,11 @@ module JekyllImgFlow
99
99
  # @return [Hash, nil] ImgFlow preset data or nil if not convertible
100
100
  def convert_preset_to_imgflow(preset_name, preset_data)
101
101
  operations = []
102
-
103
- # Handle multi-width presets - use the largest width as primary
104
- if preset_data["widths"]
105
- max_width = Array(preset_data["widths"]).max
106
- operations << { "resize" => { "width" => max_width } }
107
- elsif preset_data["base_width"]
108
- # For pixel-ratio presets, use base_width
109
- operations << { "resize" => { "width" => preset_data["base_width"] } }
110
- elsif preset_data["width"]
111
- # Direct width setting
112
- operations << { "resize" => { "width" => preset_data["width"] } }
113
- end
114
-
115
- # Handle height (less common in Picture Tag)
116
- if preset_data["height"]
117
- if operations.any? && operations.last["resize"]
118
- # Add height to existing resize operation
119
- operations.last["resize"]["height"] = preset_data["height"]
120
- else
121
- operations << { "resize" => { "height" => preset_data["height"] } }
122
- end
123
- end
124
-
125
- # Convert quality (if specified)
126
- operations << { "quality" => { "quality" => preset_data["quality"] } } if preset_data["quality"]
127
-
128
- # Convert formats - Picture Tag uses 'original', ImgFlow uses actual format
129
- if preset_data["formats"]
130
- formats = Array(preset_data["formats"]).map do |format|
131
- case format
132
- when "original"
133
- "jpg" # Default to jpg for 'original'
134
- else
135
- format
136
- end
137
- end
138
- operations << { "format" => { "formats" => formats } }
139
- end
140
-
141
- # Convert crop (aspect ratio) - handle both ratio and aspect_ratio with precedence
142
- ratio_value = preset_data["ratio"] || preset_data["aspect_ratio"] || preset_data["crop"]
143
- operations << { "crop" => { "ratio" => ratio_value } } if ratio_value
144
-
145
- # Convert gravity/position - handle both position and gravity with precedence
146
- position_value = preset_data["position"] || preset_data["gravity"]
147
- if position_value
148
- if operations.any? { |op| op["crop"] }
149
- # Add to existing crop operation
150
- crop_op = operations.find { |op| op["crop"] }
151
- crop_op["crop"]["position"] = position_value
152
- else
153
- # Create new crop operation
154
- operations << { "crop" => { "position" => position_value } }
155
- end
156
- end
102
+ add_resize_operation(operations, preset_data)
103
+ add_quality_operation(operations, preset_data)
104
+ add_format_operation(operations, preset_data)
105
+ add_crop_operation(operations, preset_data)
106
+ add_position_operation(operations, preset_data)
157
107
 
158
108
  return if operations.empty?
159
109
 
@@ -171,30 +121,84 @@ module JekyllImgFlow
171
121
  }
172
122
  end
173
123
 
124
+ def add_resize_operation(operations, preset_data)
125
+ width = preset_width(preset_data)
126
+ operations << { "resize" => { "width" => width } } if width
127
+
128
+ return unless preset_data["height"]
129
+
130
+ if operations.any? && operations.last["resize"]
131
+ operations.last["resize"]["height"] = preset_data["height"]
132
+ else
133
+ operations << { "resize" => { "height" => preset_data["height"] } }
134
+ end
135
+ end
136
+
137
+ def preset_width(preset_data)
138
+ return Array(preset_data["widths"]).max if preset_data["widths"]
139
+ return preset_data["base_width"] if preset_data["base_width"]
140
+
141
+ preset_data["width"] if preset_data["width"]
142
+ end
143
+
144
+ def add_quality_operation(operations, preset_data)
145
+ return unless preset_data["quality"]
146
+
147
+ operations << { "quality" => { "quality" => preset_data["quality"] } }
148
+ end
149
+
150
+ def add_format_operation(operations, preset_data)
151
+ return unless preset_data["formats"]
152
+
153
+ formats = Array(preset_data["formats"]).map do |format|
154
+ format == "original" ? "jpg" : format
155
+ end
156
+ operations << { "format" => { "formats" => formats } }
157
+ end
158
+
159
+ def add_crop_operation(operations, preset_data)
160
+ ratio_value = preset_data["ratio"] || preset_data["aspect_ratio"] || preset_data["crop"]
161
+ operations << { "crop" => { "ratio" => ratio_value } } if ratio_value
162
+ end
163
+
164
+ def add_position_operation(operations, preset_data)
165
+ position_value = preset_data["position"] || preset_data["gravity"]
166
+ return unless position_value
167
+
168
+ crop_op = operations.find { |op| op["crop"] }
169
+ if crop_op
170
+ crop_op["crop"]["position"] = position_value
171
+ else
172
+ operations << { "crop" => { "position" => position_value } }
173
+ end
174
+ end
175
+
174
176
  # Generate description for ImgFlow preset
175
177
  # @param preset_name [String] Preset name
176
178
  # @param preset_data [Hash] Original preset data
177
179
  # @return [String] Description
178
180
  def generate_description(preset_name, preset_data)
179
- operations = []
181
+ parts = []
182
+ add_width_description(parts, preset_data)
183
+ parts << "height: #{preset_data['height']}" if preset_data["height"]
184
+ parts << "quality: #{preset_data['quality']}" if preset_data["quality"]
185
+ parts << "formats: #{Array(preset_data['formats']).join(',')}" if preset_data["formats"]
186
+ parts << "crop: #{preset_data['crop']}" if preset_data["crop"]
187
+ parts << "gravity: #{preset_data['gravity']}" if preset_data["gravity"]
188
+
189
+ "Migrated from Picture Tag preset '#{preset_name}'. #{parts.join(', ')}"
190
+ end
180
191
 
192
+ def add_width_description(parts, preset_data)
181
193
  if preset_data["widths"]
182
- operations << "widths: #{Array(preset_data['widths']).join(',')}"
183
- operations << "using max width: #{Array(preset_data['widths']).max}"
194
+ parts << "widths: #{Array(preset_data['widths']).join(',')}"
195
+ parts << "using max width: #{Array(preset_data['widths']).max}"
184
196
  elsif preset_data["base_width"]
185
- operations << "base_width: #{preset_data['base_width']}"
186
- operations << "pixel_ratios: #{Array(preset_data['pixel_ratios']).join(',')}"
197
+ parts << "base_width: #{preset_data['base_width']}"
198
+ parts << "pixel_ratios: #{Array(preset_data['pixel_ratios']).join(',')}"
187
199
  elsif preset_data["width"]
188
- operations << "width: #{preset_data['width']}"
200
+ parts << "width: #{preset_data['width']}"
189
201
  end
190
-
191
- operations << "height: #{preset_data['height']}" if preset_data["height"]
192
- operations << "quality: #{preset_data['quality']}" if preset_data["quality"]
193
- operations << "formats: #{Array(preset_data['formats']).join(',')}" if preset_data["formats"]
194
- operations << "crop: #{preset_data['crop']}" if preset_data["crop"]
195
- operations << "gravity: #{preset_data['gravity']}" if preset_data["gravity"]
196
-
197
- "Migrated from Picture Tag preset '#{preset_name}'. #{operations.join(', ')}"
198
202
  end
199
203
 
200
204
  # Save ImgFlow preset to YAML file
@@ -132,55 +132,65 @@ module JekyllImgFlow
132
132
  # @return [Hash] Tags in key => value format
133
133
  def yaml_to_tags(preset)
134
134
  tags = {}
135
- preset_operations = preset["operations"] || []
136
-
137
- preset_operations.each do |op|
135
+ (preset["operations"] || []).each do |op|
138
136
  next unless op.is_a?(Hash)
139
137
 
140
138
  op.each do |op_type, params|
141
139
  next unless params.is_a?(Hash)
142
140
 
143
- # Store operation type for operations that don't have specific params
144
- # This allows Parser to detect the operation type
145
- case op_type.to_s
146
- when "resize"
147
- # Resize params: width, height (both optional, at least one required)
148
- tags[:width] = params["width"] if params["width"]
149
- tags[:height] = params["height"] if params["height"]
150
- when "crop"
151
- # Crop params: ratio or width/height
152
- tags[:ratio] = params["ratio"] if params["ratio"]
153
- tags[:aspect_ratio] = params["aspect_ratio"] if params["aspect_ratio"]
154
- tags[:width] = params["width"] if params["width"] && !tags[:width]
155
- tags[:height] = params["height"] if params["height"] && !tags[:height]
156
- when "format"
157
- # Format params: format or formats
158
- if params["formats"]
159
- tag_value = params["formats"].is_a?(Array) ? params["formats"].join(",") : params["formats"]
160
- tags[:formats] = tag_value
161
- elsif params["format"]
162
- tags[:format] = params["format"]
163
- end
164
- when "quality"
165
- tags[:quality] = params["quality"] if params["quality"]
166
- when "optimize"
167
- tags[:optimize] = true
168
- tags[:level] = params["level"] if params["level"]
169
- when "opacity"
170
- tags[:opacity] = params["opacity"] if params["opacity"]
171
- else
172
- # Generic handling for unknown operations
173
- params.each do |key, value|
174
- tag_value = value.is_a?(Array) ? value.join(",") : value
175
- tags[key.to_sym] = tag_value
176
- end
177
- end
141
+ merge_operation_tags(tags, op_type.to_s, params)
178
142
  end
179
143
  end
180
144
 
181
145
  tags
182
146
  end
183
147
 
148
+ def merge_operation_tags(tags, op_type, params)
149
+ case op_type
150
+ when "resize" then merge_resize_tags(tags, params)
151
+ when "crop" then merge_crop_tags(tags, params)
152
+ when "format" then merge_format_tag(tags, params)
153
+ when "quality" then tags[:quality] = params["quality"] if params["quality"]
154
+ when "optimize" then merge_optimize_tags(tags, params)
155
+ when "opacity" then tags[:opacity] = params["opacity"] if params["opacity"]
156
+ else
157
+ merge_generic_tags(tags, params)
158
+ end
159
+ end
160
+
161
+ def merge_resize_tags(tags, params)
162
+ tags[:width] = params["width"] if params["width"]
163
+ tags[:height] = params["height"] if params["height"]
164
+ end
165
+
166
+ def merge_crop_tags(tags, params)
167
+ tags[:ratio] = params["ratio"] if params["ratio"]
168
+ tags[:aspect_ratio] = params["aspect_ratio"] if params["aspect_ratio"]
169
+ tags[:width] = params["width"] if params["width"] && !tags[:width]
170
+ tags[:height] = params["height"] if params["height"] && !tags[:height]
171
+ end
172
+
173
+ def merge_optimize_tags(tags, params)
174
+ tags[:optimize] = true
175
+ tags[:level] = params["level"] if params["level"]
176
+ end
177
+
178
+ def merge_format_tag(tags, params)
179
+ if params["formats"]
180
+ tag_value = params["formats"].is_a?(Array) ? params["formats"].join(",") : params["formats"]
181
+ tags[:formats] = tag_value
182
+ elsif params["format"]
183
+ tags[:format] = params["format"]
184
+ end
185
+ end
186
+
187
+ def merge_generic_tags(tags, params)
188
+ params.each do |key, value|
189
+ tag_value = value.is_a?(Array) ? value.join(",") : value
190
+ tags[key.to_sym] = tag_value
191
+ end
192
+ end
193
+
184
194
  # Merge preset tags with user options (user options override)
185
195
  # @param preset_tags [Hash] Tags from preset
186
196
  # @param user_options [Hash] User-provided options