png_conform 0.1.1 → 0.1.3

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +82 -42
  3. data/Gemfile +2 -0
  4. data/README.adoc +3 -2
  5. data/benchmarks/README.adoc +570 -0
  6. data/benchmarks/config/default.yml +35 -0
  7. data/benchmarks/config/full.yml +32 -0
  8. data/benchmarks/config/quick.yml +32 -0
  9. data/benchmarks/direct_validation.rb +18 -0
  10. data/benchmarks/lib/benchmark_runner.rb +204 -0
  11. data/benchmarks/lib/metrics_collector.rb +193 -0
  12. data/benchmarks/lib/png_conform_runner.rb +68 -0
  13. data/benchmarks/lib/pngcheck_runner.rb +67 -0
  14. data/benchmarks/lib/report_generator.rb +301 -0
  15. data/benchmarks/lib/tool_runner.rb +104 -0
  16. data/benchmarks/profile_loading.rb +12 -0
  17. data/benchmarks/profile_validation.rb +18 -0
  18. data/benchmarks/results/.gitkeep +0 -0
  19. data/benchmarks/run_benchmark.rb +159 -0
  20. data/config/validation_profiles.yml +105 -0
  21. data/docs/CHUNK_TYPES.adoc +42 -0
  22. data/examples/README.md +282 -0
  23. data/lib/png_conform/analyzers/comparison_analyzer.rb +41 -7
  24. data/lib/png_conform/analyzers/metrics_analyzer.rb +6 -9
  25. data/lib/png_conform/analyzers/optimization_analyzer.rb +30 -24
  26. data/lib/png_conform/analyzers/resolution_analyzer.rb +31 -32
  27. data/lib/png_conform/cli.rb +12 -0
  28. data/lib/png_conform/commands/check_command.rb +118 -52
  29. data/lib/png_conform/configuration.rb +147 -0
  30. data/lib/png_conform/container.rb +113 -0
  31. data/lib/png_conform/models/decoded_chunk_data.rb +33 -0
  32. data/lib/png_conform/models/validation_result.rb +30 -4
  33. data/lib/png_conform/pipelines/pipeline_result.rb +39 -0
  34. data/lib/png_conform/pipelines/stages/analysis_stage.rb +35 -0
  35. data/lib/png_conform/pipelines/stages/base_stage.rb +23 -0
  36. data/lib/png_conform/pipelines/stages/chunk_validation_stage.rb +74 -0
  37. data/lib/png_conform/pipelines/stages/sequence_validation_stage.rb +77 -0
  38. data/lib/png_conform/pipelines/stages/signature_validation_stage.rb +41 -0
  39. data/lib/png_conform/pipelines/validation_pipeline.rb +90 -0
  40. data/lib/png_conform/readers/full_load_reader.rb +13 -4
  41. data/lib/png_conform/readers/streaming_reader.rb +27 -2
  42. data/lib/png_conform/reporters/color_reporter.rb +17 -14
  43. data/lib/png_conform/reporters/reporter_factory.rb +18 -11
  44. data/lib/png_conform/reporters/visual_elements.rb +22 -16
  45. data/lib/png_conform/services/analysis_manager.rb +120 -0
  46. data/lib/png_conform/services/chunk_processor.rb +195 -0
  47. data/lib/png_conform/services/file_signature.rb +226 -0
  48. data/lib/png_conform/services/file_strategy.rb +78 -0
  49. data/lib/png_conform/services/lru_cache.rb +170 -0
  50. data/lib/png_conform/services/parallel_validator.rb +118 -0
  51. data/lib/png_conform/services/profile_manager.rb +41 -12
  52. data/lib/png_conform/services/result_builder.rb +299 -0
  53. data/lib/png_conform/services/validation_cache.rb +210 -0
  54. data/lib/png_conform/services/validation_orchestrator.rb +188 -0
  55. data/lib/png_conform/services/validation_service.rb +82 -321
  56. data/lib/png_conform/services/validator_pool.rb +142 -0
  57. data/lib/png_conform/utils/colorizer.rb +149 -0
  58. data/lib/png_conform/validators/ancillary/idot_validator.rb +102 -0
  59. data/lib/png_conform/validators/chunk_registry.rb +143 -128
  60. data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
  61. data/lib/png_conform/version.rb +1 -1
  62. data/lib/png_conform.rb +7 -46
  63. data/png_conform.gemspec +1 -0
  64. metadata +55 -2
@@ -1,22 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../configuration"
4
+
3
5
  module PngConform
4
6
  module Analyzers
5
7
  # Analyzes PNG resolution and DPI for various use cases
6
8
  class ResolutionAnalyzer
7
- # Standard DPI values
8
- SCREEN_DPI = 72
9
- PRINT_DPI_LOW = 150
10
- PRINT_DPI_STANDARD = 300
11
- PRINT_DPI_HIGH = 600
12
-
13
- # Retina display densities
14
- RETINA_1X = 1.0
15
- RETINA_2X = 2.0
16
- RETINA_3X = 3.0
17
-
18
- def initialize(result)
9
+ def initialize(result, config: Configuration.instance)
19
10
  @result = result
11
+ @config = config
20
12
  ihdr = result.ihdr_chunk
21
13
  @width = ihdr ? get_width(ihdr) : 0
22
14
  @height = ihdr ? get_height(ihdr) : 0
@@ -63,9 +55,9 @@ module PngConform
63
55
  def retina_analysis
64
56
  analysis = {
65
57
  is_retina_ready: check_retina_ready,
66
- at_1x: calculate_physical_size(RETINA_1X),
67
- at_2x: calculate_physical_size(RETINA_2X),
68
- at_3x: calculate_physical_size(RETINA_3X),
58
+ at_1x: calculate_physical_size(@config.retina_scalers["1x"]),
59
+ at_2x: calculate_physical_size(@config.retina_scalers["2x"]),
60
+ at_3x: calculate_physical_size(@config.retina_scalers["3x"]),
69
61
  }
70
62
 
71
63
  analysis[:recommended_density] = recommend_density
@@ -82,7 +74,7 @@ module PngConform
82
74
  height_inches = @height.to_f / @dpi
83
75
 
84
76
  {
85
- capable: @dpi >= PRINT_DPI_LOW,
77
+ capable: @dpi >= @config.print_dpi_thresholds[:minimum],
86
78
  dpi: @dpi,
87
79
  physical_size: {
88
80
  width_inches: width_inches.round(2),
@@ -97,9 +89,11 @@ module PngConform
97
89
 
98
90
  def web_analysis
99
91
  {
100
- suitable_for_web: @width <= 4096 && @height <= 4096,
92
+ suitable_for_web: @width <= @config.size_thresholds[:large_for_web] &&
93
+ @height <= @config.size_thresholds[:large_for_web],
101
94
  typical_screen_size: calculate_screen_coverage,
102
- mobile_friendly: @width <= 1920 && @height <= 1920,
95
+ mobile_friendly: @width <= @config.size_thresholds[:large_for_mobile] &&
96
+ @height <= @config.size_thresholds[:large_for_mobile],
103
97
  retina_optimized: @width >= 1000 && @height >= 1000,
104
98
  load_time_estimate: estimate_load_time,
105
99
  }
@@ -119,15 +113,15 @@ module PngConform
119
113
  end
120
114
 
121
115
  def check_retina_ready
122
- @width >= 88 && @height >= 88
116
+ @width >= @config.size_thresholds[:retina_ready_min] &&
117
+ @height >= @config.size_thresholds[:retina_ready_min]
123
118
  end
124
119
 
125
120
  def calculate_physical_size(density)
126
- css_reference_dpi = 163
127
- effective_dpi = css_reference_dpi * density
121
+ effective_dpi = @config.css_reference_dpi * density
128
122
 
129
- width_points = (@width.to_f / effective_dpi * 72).round(1)
130
- height_points = (@height.to_f / effective_dpi * 72).round(1)
123
+ width_points = (@width.to_f / effective_dpi * @config.screen_dpi).round(1)
124
+ height_points = (@height.to_f / effective_dpi * @config.screen_dpi).round(1)
131
125
 
132
126
  {
133
127
  width_points: width_points,
@@ -181,15 +175,19 @@ module PngConform
181
175
  return "Unknown" unless @dpi
182
176
 
183
177
  case @dpi
184
- when 0...PRINT_DPI_LOW then "Not suitable"
185
- when PRINT_DPI_LOW...PRINT_DPI_STANDARD then "Acceptable"
186
- when PRINT_DPI_STANDARD...PRINT_DPI_HIGH then "Good"
187
- else "Excellent"
178
+ when 0...@config.print_dpi_thresholds[:minimum]
179
+ "Not suitable"
180
+ when @config.print_dpi_thresholds[:minimum]...@config.print_dpi_thresholds[:good]
181
+ "Acceptable"
182
+ when @config.print_dpi_thresholds[:good]...@config.print_dpi_thresholds[:excellent]
183
+ "Good"
184
+ else
185
+ "Excellent"
188
186
  end
189
187
  end
190
188
 
191
189
  def suitable_print_sizes
192
- return [] unless @dpi && @dpi >= PRINT_DPI_LOW
190
+ return [] unless @dpi && @dpi >= @config.print_dpi_thresholds[:minimum]
193
191
 
194
192
  width_in = @width.to_f / @dpi
195
193
  height_in = @height.to_f / @dpi
@@ -227,8 +225,7 @@ module PngConform
227
225
 
228
226
  def estimate_load_time
229
227
  file_size = @result.file_size
230
- mbps = 5
231
- bytes_per_second = (mbps * 1_000_000 / 8).to_i
228
+ bytes_per_second = @config.network_speeds[:fast]
232
229
  seconds = file_size.to_f / bytes_per_second
233
230
 
234
231
  if seconds < 0.1
@@ -243,7 +240,8 @@ module PngConform
243
240
  def generate_recommendations
244
241
  recs = []
245
242
 
246
- if @width < 100 && @height < 100
243
+ if @width < @config.size_thresholds[:retina_ready_min] &&
244
+ @height < @config.size_thresholds[:retina_ready_min]
247
245
  recs << {
248
246
  category: :retina,
249
247
  priority: :high,
@@ -259,7 +257,8 @@ module PngConform
259
257
  }
260
258
  end
261
259
 
262
- if @width > 3000 || @height > 3000
260
+ if @width > @config.size_thresholds[:very_large] ||
261
+ @height > @config.size_thresholds[:very_large]
263
262
  recs << {
264
263
  category: :web,
265
264
  priority: :high,
@@ -36,6 +36,10 @@ module PngConform
36
36
  --metrics Show detailed metrics (JSON/YAML)
37
37
  --resolution Show resolution and Retina analysis
38
38
  --mobile-ready Check mobile and Retina readiness
39
+ --batch Enable batch chunk validation (default: enabled)
40
+ --no-batch Disable batch chunk validation
41
+ --parallel, -j Enable parallel processing for multiple files
42
+ --jobs, -j NUM Number of parallel threads (default: CPU count)
39
43
 
40
44
  Examples:
41
45
  png_conform check image.png
@@ -72,6 +76,14 @@ module PngConform
72
76
  desc: "Show resolution and Retina/DPI analysis"
73
77
  option :mobile_ready, type: :boolean, default: false,
74
78
  desc: "Check mobile and Retina readiness"
79
+ option :batch, type: :boolean, default: true,
80
+ desc: "Enable batch chunk validation (faster for files with many chunks)"
81
+ option :no_batch, type: :boolean, default: false,
82
+ desc: "Disable batch chunk validation"
83
+ option :parallel, type: :boolean, default: false, aliases: "-j",
84
+ desc: "Enable parallel processing for multiple files"
85
+ option :jobs, type: :numeric, default: nil, aliases: "-j",
86
+ desc: "Number of parallel threads (default: CPU count)"
75
87
  def check(*files)
76
88
  Commands::CheckCommand.new(files, options).run
77
89
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../services/validation_service"
3
+ require_relative "../container"
4
+ require_relative "../utils/colorizer"
4
5
  require_relative "../services/profile_manager"
5
6
  require_relative "../reporters/reporter_factory"
6
- require_relative "../readers/streaming_reader"
7
7
 
8
8
  module PngConform
9
9
  module Commands
@@ -11,6 +11,7 @@ module PngConform
11
11
  #
12
12
  # Coordinates between readers, validators, and reporters to analyze
13
13
  # PNG files according to specified options and profiles.
14
+ #
14
15
  class CheckCommand
15
16
  attr_reader :files, :options
16
17
 
@@ -61,14 +62,105 @@ module PngConform
61
62
  end
62
63
 
63
64
  # Validate all specified files.
65
+ #
66
+ # Uses parallel validation if --parallel flag is set with multiple files.
67
+ # Otherwise processes files sequentially.
64
68
  def validate_files
65
69
  reporter = create_reporter
66
70
 
71
+ # Use parallel validation if enabled and multiple files
72
+ if files.length > 1 && @options[:parallel]
73
+ validate_files_parallel(reporter)
74
+ else
75
+ validate_files_sequential(reporter)
76
+ end
77
+ end
78
+
79
+ # Validate files sequentially (original behavior)
80
+ #
81
+ # @param reporter [Reporters::BaseReporter] Reporter for output
82
+ def validate_files_sequential(reporter)
67
83
  files.each do |file_path|
68
84
  validate_single_file(file_path, reporter)
69
85
  end
70
86
  end
71
87
 
88
+ # Validate files in parallel for better performance
89
+ #
90
+ # @param reporter [Reporters::BaseReporter] Reporter for output
91
+ def validate_files_parallel(reporter)
92
+ require_relative "../services/parallel_validator"
93
+
94
+ parallel_validator = Services::ParallelValidator.new(files, @options)
95
+ results = parallel_validator.validate_all
96
+
97
+ # Process results and report
98
+ results.each do |result|
99
+ if result.key?(:error)
100
+ handle_validation_error(result)
101
+ next
102
+ end
103
+
104
+ @errors_found = true unless result.valid?
105
+ reporter.report(result)
106
+
107
+ # Show additional analysis for text format
108
+ show_additional_analysis(result) if should_show_analysis?
109
+ end
110
+ rescue Interrupt
111
+ # Handle Ctrl+C gracefully
112
+ puts "\nValidation interrupted by user."
113
+ raise
114
+ rescue StandardError => e
115
+ puts "Error during parallel validation: #{e.message}"
116
+ puts e.backtrace.join("\n") if @options[:verbose]
117
+ @errors_found = true
118
+ end
119
+
120
+ # Handle validation error from parallel processing
121
+ #
122
+ # @param error_result [Hash] Error result from parallel validation
123
+ def handle_validation_error(error_result)
124
+ puts "Error: #{error_result[:error]}"
125
+ @errors_found = true
126
+
127
+ if error_result[:backtrace] && @options[:verbose]
128
+ puts "Backtrace:"
129
+ error_result[:backtrace].each { |line| puts " #{line}" }
130
+ end
131
+ end
132
+
133
+ # Check if additional analysis should be shown
134
+ #
135
+ # @return [Boolean] True if analysis should be shown
136
+ def should_show_analysis?
137
+ return false if @options[:quiet]
138
+ return true if @options[:metrics] || @options[:mobile_ready] || @options[:resolution] || @options[:optimize]
139
+
140
+ # Text format shows analysis by default (unless quiet)
141
+ @options[:format].nil? || @options[:format] == "text"
142
+ end
143
+
144
+ # Show additional analysis (resolution, optimization, metrics)
145
+ #
146
+ # @param file_analysis [FileAnalysis] File analysis results
147
+ def show_additional_analysis(file_analysis)
148
+ show_resolution_analysis(file_analysis) if @options[:resolution] || @options[:mobile_ready]
149
+ show_optimization_suggestions(file_analysis) if @options[:optimize]
150
+ show_metrics(file_analysis) if @options[:metrics]
151
+ show_mobile_readiness(file_analysis) if @options[:mobile_ready]
152
+
153
+ # Default: show resolution and optimization for text format (unless quiet)
154
+ if (@options[:format].nil? || @options[:format] == "text") &&
155
+ !@options[:quiet] &&
156
+ !@options[:resolution] &&
157
+ !@options[:mobile_ready] &&
158
+ !@options[:optimize]
159
+ show_resolution_analysis(file_analysis)
160
+ show_optimization_suggestions(file_analysis)
161
+ end
162
+ end
163
+
72
164
  # Validate a single PNG file.
73
165
  #
74
166
  # @param file_path [String] Path to the PNG file
@@ -86,11 +178,12 @@ module PngConform
86
178
  return
87
179
  end
88
180
 
89
- # Read and validate the file using streaming reader
90
- Readers::StreamingReader.open(file_path) do |reader|
91
- # Perform validation
92
- validator = Services::ValidationService.new(reader, file_path)
93
- file_analysis = validator.validate
181
+ # Use container to create reader and orchestrator
182
+ Container.open_reader(:streaming, file_path) do |reader|
183
+ options_with_path = @options.merge(filepath: file_path)
184
+ orchestrator = Container.validation_orchestrator(reader, file_path,
185
+ options_with_path)
186
+ file_analysis = orchestrator.validate
94
187
 
95
188
  # Track if any errors were found
96
189
  @errors_found = true unless file_analysis.valid?
@@ -98,15 +191,8 @@ module PngConform
98
191
  # Use reporter to output result
99
192
  reporter.report(file_analysis)
100
193
 
101
- # For text output (default), show additional analysis unless quiet
102
- if (options[:format].nil? || options[:format] == "text") && !options[:quiet]
103
- show_resolution_analysis(file_analysis)
104
- show_optimization_suggestions(file_analysis)
105
- end
106
-
107
- # Explicit flags always show
108
- show_metrics(file_analysis) if options[:metrics]
109
- show_mobile_readiness(file_analysis) if options[:mobile_ready]
194
+ # Show additional analysis
195
+ show_additional_analysis(file_analysis) if should_show_analysis?
110
196
  end
111
197
  rescue StandardError => e
112
198
  puts "Error processing #{file_path}: #{e.message}"
@@ -118,7 +204,7 @@ module PngConform
118
204
  #
119
205
  # @return [Reporters::BaseReporter] Reporter instance
120
206
  def create_reporter
121
- Reporters::ReporterFactory.create(
207
+ Container.reporter(
122
208
  format: options[:format] || "text",
123
209
  verbose: options[:verbose] || options[:very_verbose],
124
210
  quiet: options[:quiet],
@@ -135,13 +221,10 @@ module PngConform
135
221
  return unless analysis && analysis[:suggestions]
136
222
  return if analysis[:suggestions].empty?
137
223
 
138
- puts "\n#{colorize('OPTIMIZATION SUGGESTIONS:', :bold)}"
224
+ puts "\n#{Utils::Colorizer.bold('OPTIMIZATION SUGGESTIONS:')}"
139
225
  analysis[:suggestions].each_with_index do |suggestion, index|
140
- priority_color = priority_color(suggestion[:priority])
141
- priority_label = suggestion[:priority].to_s.upcase
142
-
143
- puts " #{index + 1}. [#{colorize(priority_label,
144
- priority_color)}] #{suggestion[:description]}"
226
+ puts " #{index + 1}. #{Utils::Colorizer.priority(suggestion[:description],
227
+ suggestion[:priority])}"
145
228
  if suggestion[:savings_bytes]&.positive?
146
229
  puts " Savings: #{format_bytes(suggestion[:savings_bytes])}"
147
230
  end
@@ -149,7 +232,7 @@ module PngConform
149
232
 
150
233
  total_savings = analysis[:potential_savings_bytes]
151
234
  if total_savings.positive?
152
- puts "\n #{colorize('Total Potential Savings:', :bold)} " \
235
+ puts "\n #{Utils::Colorizer.bold('Total Potential Savings:')} " \
153
236
  "#{format_bytes(total_savings)} (#{analysis[:potential_savings_percent]}%)"
154
237
  end
155
238
  end
@@ -168,7 +251,7 @@ module PngConform
168
251
  puts metrics.to_yaml
169
252
  else
170
253
  # Text format with colored output
171
- puts "\n#{colorize('METRICS:', :bold)}"
254
+ puts "\n#{Utils::Colorizer.bold('METRICS:')}"
172
255
  puts " File: #{metrics[:file][:filename]} (#{metrics[:file][:size_kb]} KB)"
173
256
  puts " Image: #{metrics[:image][:dimensions]}, #{metrics[:image][:color_type_name]}, " \
174
257
  "#{metrics[:image][:bit_depth]}-bit"
@@ -183,7 +266,7 @@ module PngConform
183
266
  analysis = file_analysis.resolution_analysis
184
267
  return unless analysis
185
268
 
186
- puts "\n#{colorize('RESOLUTION ANALYSIS:', :bold)}"
269
+ puts "\n#{Utils::Colorizer.bold('RESOLUTION ANALYSIS:')}"
187
270
 
188
271
  # Basic info
189
272
  res = analysis[:resolution]
@@ -191,7 +274,7 @@ module PngConform
191
274
  puts " DPI: #{res[:dpi] || 'Not specified'}"
192
275
 
193
276
  # Retina analysis
194
- puts "\n #{colorize('Retina Analysis:', :bold)}"
277
+ puts "\n #{Utils::Colorizer.bold('Retina Analysis:')}"
195
278
  retina = analysis[:retina]
196
279
  puts " @1x: #{retina[:at_1x][:dimensions_pt]} (#{retina[:at_1x][:suitable_for].first})"
197
280
  puts " @2x: #{retina[:at_2x][:dimensions_pt]} (#{retina[:at_2x][:suitable_for].first})"
@@ -210,7 +293,7 @@ module PngConform
210
293
  # Print analysis if DPI available
211
294
  if analysis[:print][:capable]
212
295
  print_info = analysis[:print]
213
- puts "\n #{colorize('Print Analysis:', :bold)}"
296
+ puts "\n #{Utils::Colorizer.bold('Print Analysis:')}"
214
297
  puts " Quality: #{print_info[:quality]} (#{print_info[:dpi]} DPI)"
215
298
  phys = print_info[:physical_size]
216
299
  puts " Physical Size: #{phys[:width_inches]}\" x #{phys[:height_inches]}\" " \
@@ -220,11 +303,10 @@ module PngConform
220
303
  # Recommendations
221
304
  recommendations = analysis[:recommendations]
222
305
  if recommendations && !recommendations.empty?
223
- puts "\n #{colorize('Recommendations:', :bold)}"
306
+ puts "\n #{Utils::Colorizer.bold('Recommendations:')}"
224
307
  recommendations.each do |rec|
225
- priority_color = priority_color(rec[:priority])
226
- puts " [#{colorize(rec[:priority].to_s.upcase,
227
- priority_color)}] #{rec[:message]}"
308
+ puts " #{Utils::Colorizer.priority(rec[:message],
309
+ rec[:priority])}"
228
310
  end
229
311
  end
230
312
  end
@@ -234,7 +316,7 @@ module PngConform
234
316
  analysis = file_analysis.resolution_analysis
235
317
  return unless analysis
236
318
 
237
- puts "\n#{colorize('MOBILE & RETINA READINESS:', :bold)}"
319
+ puts "\n#{Utils::Colorizer.bold('MOBILE & RETINA READINESS:')}"
238
320
 
239
321
  retina = analysis[:retina]
240
322
  web = analysis[:web]
@@ -242,10 +324,9 @@ module PngConform
242
324
  # Overall readiness
243
325
  is_ready = retina[:is_retina_ready] && web[:mobile_friendly]
244
326
  status = if is_ready
245
- colorize("✓ READY",
246
- :green)
327
+ Utils::Colorizer.success("✓ READY")
247
328
  else
248
- colorize("✗ NOT READY", :red)
329
+ Utils::Colorizer.error("✗ NOT READY")
249
330
  end
250
331
  puts " Status: #{status}"
251
332
 
@@ -275,7 +356,7 @@ module PngConform
275
356
  # Helper methods for formatting
276
357
 
277
358
  def format_check(passed)
278
- passed ? colorize("✓", :green) : colorize("✗", :red)
359
+ passed ? Utils::Colorizer.success("✓") : Utils::Colorizer.error("✗")
279
360
  end
280
361
 
281
362
  def format_bytes(bytes)
@@ -297,21 +378,6 @@ module PngConform
297
378
  end
298
379
  end
299
380
 
300
- def colorize(text, color)
301
- return text if options[:no_color]
302
-
303
- codes = {
304
- red: "\e[31m",
305
- green: "\e[32m",
306
- yellow: "\e[33m",
307
- blue: "\e[34m",
308
- bold: "\e[1m",
309
- reset: "\e[0m",
310
- }
311
-
312
- "#{codes[color]}#{text}#{codes[:reset]}"
313
- end
314
-
315
381
  # Determine the exit code based on whether errors were found.
316
382
  #
317
383
  # @return [Integer] Exit code (0 for success, 1 for errors)
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ # Application configuration
5
+ #
6
+ # Centralized configuration for all hardcoded values across the codebase.
7
+ # Provides a single source of truth for constants and settings.
8
+ #
9
+ class Configuration
10
+ attr_accessor :retina_densities, :print_dpi_thresholds,
11
+ :network_speeds, :optimization_thresholds,
12
+ :color_enabled, :screen_dpi, :css_reference_dpi,
13
+ :retina_scalers, :unnecessary_web_chunks,
14
+ :text_chunks, :metadata_chunks,
15
+ :optimization_percentages, :size_thresholds
16
+
17
+ class << self
18
+ # Get the singleton instance
19
+ #
20
+ # @return [Configuration] Configuration instance
21
+ def instance
22
+ @instance ||= new
23
+ end
24
+
25
+ # Reset configuration to defaults
26
+ #
27
+ # @return [void]
28
+ def reset!
29
+ @instance = new
30
+ end
31
+ end
32
+
33
+ # Initialize configuration with defaults
34
+ #
35
+ def initialize
36
+ # Retina display densities (Android buckets)
37
+ @retina_densities = {
38
+ mdpi: 160,
39
+ hdpi: 320,
40
+ xhdpi: 480,
41
+ xxhdpi: 640,
42
+ }
43
+
44
+ # Print DPI quality thresholds
45
+ @print_dpi_thresholds = {
46
+ minimum: 150,
47
+ good: 300,
48
+ excellent: 600,
49
+ }
50
+
51
+ # Network speeds for load time estimation (bytes per second)
52
+ @network_speeds = {
53
+ slow: 1_000_000, # 1 Mbps
54
+ fast: 10_000_000, # 10 Mbps
55
+ }
56
+
57
+ # Optimization percentage thresholds
58
+ @optimization_thresholds = {
59
+ high_savings: 30, # 30%
60
+ medium_savings: 10, # 10%
61
+ }
62
+
63
+ # Color output enabled
64
+ @color_enabled = true
65
+
66
+ # Screen DPI constants
67
+ @screen_dpi = 72
68
+
69
+ # CSS reference DPI for Retina calculations
70
+ @css_reference_dpi = 163
71
+
72
+ # Retina display scaling factors
73
+ @retina_scalers = {
74
+ "1x": 1.0,
75
+ "2x": 2.0,
76
+ "3x": 3.0,
77
+ }
78
+
79
+ # Chunks unnecessary for web/mobile use
80
+ @unnecessary_web_chunks = %w[tIME pHYs oFFs pCAL sCAL sTER].freeze
81
+
82
+ # Text chunk types
83
+ @text_chunks = %w[tEXt zTXt iTXt].freeze
84
+
85
+ # Metadata chunk types
86
+ @metadata_chunks = %w[tEXt zTXt iTXt tIME pHYs].freeze
87
+
88
+ # Optimization savings percentages
89
+ @optimization_percentages = {
90
+ bit_depth_reduction: 45, # % savings for 16->8 bit
91
+ palette_conversion: 30, # % savings for RGB->palette
92
+ interlace_removal: 15, # % savings for removing interlace
93
+ metadata_threshold: 10, # % of file size to flag excessive metadata
94
+ }
95
+
96
+ # Size thresholds
97
+ @size_thresholds = {
98
+ palette_opportunity: 10_000, # bytes - min file size to suggest palette
99
+ text_metadata: 500, # bytes - min text metadata size to flag
100
+ small_file: 100_000, # bytes - file size considered "small"
101
+ retina_ready_min: 88, # pixels - min dimension for retina ready
102
+ very_large: 3000, # pixels - dimension considered "very large"
103
+ large_for_mobile: 1920, # pixels - max dimension for mobile friendly
104
+ large_for_web: 4096, # pixels - max dimension for web suitable
105
+ }
106
+ end
107
+
108
+ # Get density value by name
109
+ #
110
+ # @param name [Symbol] Density name (:mdpi, :hdpi, :xhdpi, :xxhdpi)
111
+ # @return [Integer] DPI value
112
+ def retina_density(name)
113
+ @retina_densities[name] || @retina_densities[:hdpi]
114
+ end
115
+
116
+ # Get print DPI threshold by name
117
+ #
118
+ # @param name [Symbol] Threshold name (:minimum, :good, :excellent)
119
+ # @return [Integer] DPI value
120
+ def print_dpi_threshold(name)
121
+ @print_dpi_thresholds[name] || @print_dpi_thresholds[:good]
122
+ end
123
+
124
+ # Get network speed by name
125
+ #
126
+ # @param name [Symbol] Speed name (:slow, :fast)
127
+ # @return [Integer] Bytes per second
128
+ def network_speed(name)
129
+ @network_speeds[name] || @network_speeds[:fast]
130
+ end
131
+
132
+ # Get optimization threshold by name
133
+ #
134
+ # @param name [Symbol] Threshold name (:high_savings, :medium_savings)
135
+ # @return [Integer] Percentage threshold
136
+ def optimization_threshold(name)
137
+ @optimization_thresholds[name] || @optimization_thresholds[:medium_savings]
138
+ end
139
+
140
+ # Check if colors are enabled
141
+ #
142
+ # @return [Boolean] true if colors are enabled
143
+ def color_enabled?
144
+ @color_enabled
145
+ end
146
+ end
147
+ end