png_conform 0.1.2 → 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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +116 -6
  3. data/Gemfile +1 -1
  4. data/config/validation_profiles.yml +105 -0
  5. data/lib/png_conform/analyzers/comparison_analyzer.rb +41 -7
  6. data/lib/png_conform/analyzers/metrics_analyzer.rb +6 -9
  7. data/lib/png_conform/analyzers/optimization_analyzer.rb +30 -24
  8. data/lib/png_conform/analyzers/resolution_analyzer.rb +31 -32
  9. data/lib/png_conform/cli.rb +12 -0
  10. data/lib/png_conform/commands/check_command.rb +118 -53
  11. data/lib/png_conform/configuration.rb +147 -0
  12. data/lib/png_conform/container.rb +113 -0
  13. data/lib/png_conform/models/validation_result.rb +30 -4
  14. data/lib/png_conform/pipelines/pipeline_result.rb +39 -0
  15. data/lib/png_conform/pipelines/stages/analysis_stage.rb +35 -0
  16. data/lib/png_conform/pipelines/stages/base_stage.rb +23 -0
  17. data/lib/png_conform/pipelines/stages/chunk_validation_stage.rb +74 -0
  18. data/lib/png_conform/pipelines/stages/sequence_validation_stage.rb +77 -0
  19. data/lib/png_conform/pipelines/stages/signature_validation_stage.rb +41 -0
  20. data/lib/png_conform/pipelines/validation_pipeline.rb +90 -0
  21. data/lib/png_conform/readers/full_load_reader.rb +13 -4
  22. data/lib/png_conform/readers/streaming_reader.rb +27 -2
  23. data/lib/png_conform/reporters/color_reporter.rb +17 -14
  24. data/lib/png_conform/reporters/visual_elements.rb +22 -16
  25. data/lib/png_conform/services/analysis_manager.rb +120 -0
  26. data/lib/png_conform/services/chunk_processor.rb +195 -0
  27. data/lib/png_conform/services/file_signature.rb +226 -0
  28. data/lib/png_conform/services/file_strategy.rb +78 -0
  29. data/lib/png_conform/services/lru_cache.rb +170 -0
  30. data/lib/png_conform/services/parallel_validator.rb +118 -0
  31. data/lib/png_conform/services/profile_manager.rb +41 -12
  32. data/lib/png_conform/services/result_builder.rb +299 -0
  33. data/lib/png_conform/services/validation_cache.rb +210 -0
  34. data/lib/png_conform/services/validation_orchestrator.rb +188 -0
  35. data/lib/png_conform/services/validation_service.rb +53 -337
  36. data/lib/png_conform/services/validator_pool.rb +142 -0
  37. data/lib/png_conform/utils/colorizer.rb +149 -0
  38. data/lib/png_conform/validators/chunk_registry.rb +12 -0
  39. data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
  40. data/lib/png_conform/version.rb +1 -1
  41. data/png_conform.gemspec +1 -0
  42. metadata +38 -2
@@ -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,12 +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 - pass options for conditional analyzers (Phase 1.1 + 1.2)
92
- validator = Services::ValidationService.new(reader, file_path,
93
- options)
94
- 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
95
187
 
96
188
  # Track if any errors were found
97
189
  @errors_found = true unless file_analysis.valid?
@@ -99,15 +191,8 @@ module PngConform
99
191
  # Use reporter to output result
100
192
  reporter.report(file_analysis)
101
193
 
102
- # For text output (default), show additional analysis unless quiet
103
- if (options[:format].nil? || options[:format] == "text") && !options[:quiet]
104
- show_resolution_analysis(file_analysis)
105
- show_optimization_suggestions(file_analysis)
106
- end
107
-
108
- # Explicit flags always show
109
- show_metrics(file_analysis) if options[:metrics]
110
- show_mobile_readiness(file_analysis) if options[:mobile_ready]
194
+ # Show additional analysis
195
+ show_additional_analysis(file_analysis) if should_show_analysis?
111
196
  end
112
197
  rescue StandardError => e
113
198
  puts "Error processing #{file_path}: #{e.message}"
@@ -119,7 +204,7 @@ module PngConform
119
204
  #
120
205
  # @return [Reporters::BaseReporter] Reporter instance
121
206
  def create_reporter
122
- Reporters::ReporterFactory.create(
207
+ Container.reporter(
123
208
  format: options[:format] || "text",
124
209
  verbose: options[:verbose] || options[:very_verbose],
125
210
  quiet: options[:quiet],
@@ -136,13 +221,10 @@ module PngConform
136
221
  return unless analysis && analysis[:suggestions]
137
222
  return if analysis[:suggestions].empty?
138
223
 
139
- puts "\n#{colorize('OPTIMIZATION SUGGESTIONS:', :bold)}"
224
+ puts "\n#{Utils::Colorizer.bold('OPTIMIZATION SUGGESTIONS:')}"
140
225
  analysis[:suggestions].each_with_index do |suggestion, index|
141
- priority_color = priority_color(suggestion[:priority])
142
- priority_label = suggestion[:priority].to_s.upcase
143
-
144
- puts " #{index + 1}. [#{colorize(priority_label,
145
- priority_color)}] #{suggestion[:description]}"
226
+ puts " #{index + 1}. #{Utils::Colorizer.priority(suggestion[:description],
227
+ suggestion[:priority])}"
146
228
  if suggestion[:savings_bytes]&.positive?
147
229
  puts " Savings: #{format_bytes(suggestion[:savings_bytes])}"
148
230
  end
@@ -150,7 +232,7 @@ module PngConform
150
232
 
151
233
  total_savings = analysis[:potential_savings_bytes]
152
234
  if total_savings.positive?
153
- puts "\n #{colorize('Total Potential Savings:', :bold)} " \
235
+ puts "\n #{Utils::Colorizer.bold('Total Potential Savings:')} " \
154
236
  "#{format_bytes(total_savings)} (#{analysis[:potential_savings_percent]}%)"
155
237
  end
156
238
  end
@@ -169,7 +251,7 @@ module PngConform
169
251
  puts metrics.to_yaml
170
252
  else
171
253
  # Text format with colored output
172
- puts "\n#{colorize('METRICS:', :bold)}"
254
+ puts "\n#{Utils::Colorizer.bold('METRICS:')}"
173
255
  puts " File: #{metrics[:file][:filename]} (#{metrics[:file][:size_kb]} KB)"
174
256
  puts " Image: #{metrics[:image][:dimensions]}, #{metrics[:image][:color_type_name]}, " \
175
257
  "#{metrics[:image][:bit_depth]}-bit"
@@ -184,7 +266,7 @@ module PngConform
184
266
  analysis = file_analysis.resolution_analysis
185
267
  return unless analysis
186
268
 
187
- puts "\n#{colorize('RESOLUTION ANALYSIS:', :bold)}"
269
+ puts "\n#{Utils::Colorizer.bold('RESOLUTION ANALYSIS:')}"
188
270
 
189
271
  # Basic info
190
272
  res = analysis[:resolution]
@@ -192,7 +274,7 @@ module PngConform
192
274
  puts " DPI: #{res[:dpi] || 'Not specified'}"
193
275
 
194
276
  # Retina analysis
195
- puts "\n #{colorize('Retina Analysis:', :bold)}"
277
+ puts "\n #{Utils::Colorizer.bold('Retina Analysis:')}"
196
278
  retina = analysis[:retina]
197
279
  puts " @1x: #{retina[:at_1x][:dimensions_pt]} (#{retina[:at_1x][:suitable_for].first})"
198
280
  puts " @2x: #{retina[:at_2x][:dimensions_pt]} (#{retina[:at_2x][:suitable_for].first})"
@@ -211,7 +293,7 @@ module PngConform
211
293
  # Print analysis if DPI available
212
294
  if analysis[:print][:capable]
213
295
  print_info = analysis[:print]
214
- puts "\n #{colorize('Print Analysis:', :bold)}"
296
+ puts "\n #{Utils::Colorizer.bold('Print Analysis:')}"
215
297
  puts " Quality: #{print_info[:quality]} (#{print_info[:dpi]} DPI)"
216
298
  phys = print_info[:physical_size]
217
299
  puts " Physical Size: #{phys[:width_inches]}\" x #{phys[:height_inches]}\" " \
@@ -221,11 +303,10 @@ module PngConform
221
303
  # Recommendations
222
304
  recommendations = analysis[:recommendations]
223
305
  if recommendations && !recommendations.empty?
224
- puts "\n #{colorize('Recommendations:', :bold)}"
306
+ puts "\n #{Utils::Colorizer.bold('Recommendations:')}"
225
307
  recommendations.each do |rec|
226
- priority_color = priority_color(rec[:priority])
227
- puts " [#{colorize(rec[:priority].to_s.upcase,
228
- priority_color)}] #{rec[:message]}"
308
+ puts " #{Utils::Colorizer.priority(rec[:message],
309
+ rec[:priority])}"
229
310
  end
230
311
  end
231
312
  end
@@ -235,7 +316,7 @@ module PngConform
235
316
  analysis = file_analysis.resolution_analysis
236
317
  return unless analysis
237
318
 
238
- puts "\n#{colorize('MOBILE & RETINA READINESS:', :bold)}"
319
+ puts "\n#{Utils::Colorizer.bold('MOBILE & RETINA READINESS:')}"
239
320
 
240
321
  retina = analysis[:retina]
241
322
  web = analysis[:web]
@@ -243,10 +324,9 @@ module PngConform
243
324
  # Overall readiness
244
325
  is_ready = retina[:is_retina_ready] && web[:mobile_friendly]
245
326
  status = if is_ready
246
- colorize("✓ READY",
247
- :green)
327
+ Utils::Colorizer.success("✓ READY")
248
328
  else
249
- colorize("✗ NOT READY", :red)
329
+ Utils::Colorizer.error("✗ NOT READY")
250
330
  end
251
331
  puts " Status: #{status}"
252
332
 
@@ -276,7 +356,7 @@ module PngConform
276
356
  # Helper methods for formatting
277
357
 
278
358
  def format_check(passed)
279
- passed ? colorize("✓", :green) : colorize("✗", :red)
359
+ passed ? Utils::Colorizer.success("✓") : Utils::Colorizer.error("✗")
280
360
  end
281
361
 
282
362
  def format_bytes(bytes)
@@ -298,21 +378,6 @@ module PngConform
298
378
  end
299
379
  end
300
380
 
301
- def colorize(text, color)
302
- return text if options[:no_color]
303
-
304
- codes = {
305
- red: "\e[31m",
306
- green: "\e[32m",
307
- yellow: "\e[33m",
308
- blue: "\e[34m",
309
- bold: "\e[1m",
310
- reset: "\e[0m",
311
- }
312
-
313
- "#{codes[color]}#{text}#{codes[:reset]}"
314
- end
315
-
316
381
  # Determine the exit code based on whether errors were found.
317
382
  #
318
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
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "readers/streaming_reader"
4
+ require_relative "services/profile_manager"
5
+ require_relative "services/analysis_manager"
6
+
7
+ module PngConform
8
+ # Dependency Injection Container
9
+ #
10
+ # Centralized dependency management for the application.
11
+ # Provides factory methods for creating readers, validators, reporters,
12
+ # and other services with proper dependency injection.
13
+ #
14
+ # This makes testing easier by allowing mock injection and provides
15
+ # a single source of truth for object creation.
16
+ #
17
+ class Container
18
+ class << self
19
+ # Create a reader for the given type and filepath
20
+ #
21
+ # @param type [Symbol] Reader type (:streaming or :full_load)
22
+ # @param filepath [String] Path to PNG file
23
+ # @param options [Hash] Additional options for the reader
24
+ # @return [Object] Reader instance
25
+ def reader(type, filepath, options = {})
26
+ case type
27
+ when :streaming
28
+ io = File.open(filepath, "rb")
29
+ Readers::StreamingReader.new(io, options)
30
+ when :full_load
31
+ Readers::FullLoadReader.new(filepath, options)
32
+ else
33
+ raise ArgumentError, "Unknown reader type: #{type}"
34
+ end
35
+ end
36
+
37
+ # Open a reader with automatic file closing
38
+ #
39
+ # @param type [Symbol] Reader type (:streaming or :full_load)
40
+ # @param filepath [String] Path to PNG file
41
+ # @param options [Hash] Additional options for the reader
42
+ # @yield [reader] Reader instance
43
+ # @return [Object] Result of the block
44
+ def open_reader(type, filepath, _options = {}, &block)
45
+ case type
46
+ when :streaming
47
+ Readers::StreamingReader.open(filepath, &block)
48
+ when :full_load
49
+ Readers::FullLoadReader.open(filepath, &block)
50
+ else
51
+ raise ArgumentError, "Unknown reader type: #{type}"
52
+ end
53
+ end
54
+
55
+ # Create a validator for a chunk
56
+ #
57
+ # Delegates to ChunkRegistry for validator creation
58
+ #
59
+ # @param chunk [Object] Chunk object
60
+ # @param context [ValidationContext] Validation context
61
+ # @return [Object] Validator instance or nil
62
+ def validator(chunk, context)
63
+ require_relative "validators/chunk_registry"
64
+ Validators::ChunkRegistry.create_validator(chunk, context)
65
+ end
66
+
67
+ # Create a reporter based on options
68
+ #
69
+ # Delegates to ReporterFactory for reporter creation
70
+ #
71
+ # @param options [Hash] Reporter options
72
+ # @return [Object] Reporter instance
73
+ def reporter(options)
74
+ require_relative "reporters/reporter_factory"
75
+ Reporters::ReporterFactory.create(**options)
76
+ end
77
+
78
+ # Get the profile manager singleton
79
+ #
80
+ # @return [ProfileManager] Profile manager instance
81
+ def profile_manager
82
+ @profile_manager ||= Services::ProfileManager.new
83
+ end
84
+
85
+ # Create an analysis manager
86
+ #
87
+ # @param options [Hash] Analysis options
88
+ # @return [AnalysisManager] Analysis manager instance
89
+ def analysis_manager(options = {})
90
+ Services::AnalysisManager.new(options)
91
+ end
92
+
93
+ # Create a validation orchestrator
94
+ #
95
+ # @param reader [Object] File reader
96
+ # @param filepath [String] File path
97
+ # @param options [Hash] Validation options
98
+ # @return [ValidationOrchestrator] Orchestrator instance
99
+ def validation_orchestrator(reader, filepath = nil, options = {})
100
+ require_relative "services/validation_orchestrator"
101
+ Services::ValidationOrchestrator.new(reader, filepath, options)
102
+ end
103
+ alias_method :validation_service, :validation_orchestrator
104
+
105
+ # Reset container state
106
+ #
107
+ # Clears cached instances (useful for testing)
108
+ def reset!
109
+ @profile_manager = nil
110
+ end
111
+ end
112
+ end
113
+ end
@@ -13,15 +13,26 @@ module PngConform
13
13
  attribute :compression_ratio, :float
14
14
  attribute :crc_errors_count, :integer, default: -> { 0 }
15
15
 
16
+ # Non-serialized hash map for fast chunk type lookup
17
+ attr_reader :chunks_by_type_map
18
+
16
19
  # File types
17
20
  FILE_TYPE_PNG = "PNG"
18
21
  FILE_TYPE_MNG = "MNG"
19
22
  FILE_TYPE_JNG = "JNG"
20
23
  FILE_TYPE_UNKNOWN = "UNKNOWN"
21
24
 
25
+ # Initialize with hash map for fast lookups
26
+ def initialize(*args)
27
+ super(*args)
28
+ @chunks_by_type_map = {}
29
+ rebuild_chunks_map
30
+ end
31
+
22
32
  # Add a chunk to the result
23
33
  def add_chunk(chunk)
24
34
  chunks << chunk
35
+ add_to_chunks_map(chunk)
25
36
  end
26
37
 
27
38
  # Add an error to the result
@@ -90,14 +101,14 @@ module PngConform
90
101
  chunks.count
91
102
  end
92
103
 
93
- # Find chunks by type
104
+ # Find chunks by type (O(1) hash lookup)
94
105
  def chunks_by_type(type)
95
- chunks.select { |chunk| chunk.type == type }
106
+ @chunks_by_type_map[type] || []
96
107
  end
97
108
 
98
- # Check if file has specific chunk type
109
+ # Check if file has specific chunk type (O(1) hash lookup)
99
110
  def has_chunk?(type)
100
- chunks.any? { |chunk| chunk.type == type }
111
+ @chunks_by_type_map.key?(type) && !@chunks_by_type_map[type].empty?
101
112
  end
102
113
 
103
114
  # Get IHDR chunk (PNG/JNG)
@@ -132,6 +143,21 @@ module PngConform
132
143
  end
133
144
  parts.join("\n")
134
145
  end
146
+
147
+ private
148
+
149
+ # Add chunk to hash map for fast lookup
150
+ def add_to_chunks_map(chunk)
151
+ chunk_type = chunk.type
152
+ @chunks_by_type_map[chunk_type] ||= []
153
+ @chunks_by_type_map[chunk_type] << chunk
154
+ end
155
+
156
+ # Rebuild hash map from chunks (for deserialization or external modification)
157
+ def rebuild_chunks_map
158
+ @chunks_by_type_map.clear
159
+ chunks.each { |chunk| add_to_chunks_map(chunk) }
160
+ end
135
161
  end
136
162
  end
137
163
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Pipelines
5
+ # Result object for pipeline stages
6
+ #
7
+ # Carries state through the validation pipeline.
8
+ #
9
+ class PipelineResult
10
+ attr_accessor :ready_for_analysis
11
+ attr_reader :context, :chunks
12
+
13
+ # Initialize pipeline result
14
+ #
15
+ # @param context [ValidationContext] Validation context
16
+ # @param chunks [Array] Array of chunks
17
+ def initialize(context:, chunks: [])
18
+ @context = context
19
+ @chunks = chunks
20
+ @ready_for_analysis = false
21
+ end
22
+
23
+ # Check if validation has errors
24
+ #
25
+ # @return [Boolean] true if there are errors
26
+ def has_errors?
27
+ @context.has_errors?
28
+ end
29
+
30
+ # Check if validation should terminate early
31
+ #
32
+ # @param fail_fast [Boolean] Whether fail_fast is enabled
33
+ # @return [Boolean] true if should terminate
34
+ def should_terminate?(fail_fast: false)
35
+ fail_fast && has_errors?
36
+ end
37
+ end
38
+ end
39
+ end