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
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_stage"
4
+
5
+ module PngConform
6
+ module Pipelines
7
+ module Stages
8
+ # Analysis enrichment stage
9
+ #
10
+ # Runs conditional analyzers based on options.
11
+ #
12
+ class AnalysisStage < BaseStage
13
+ # Initialize analysis stage
14
+ #
15
+ # @param context [ValidationContext] Validation context
16
+ # @param options [Hash] Validation options
17
+ def initialize(context, options)
18
+ @context = context
19
+ @options = options
20
+ end
21
+
22
+ # Execute analysis enrichment
23
+ #
24
+ # @param result [PipelineResult] Current pipeline result
25
+ # @return [PipelineResult] Updated pipeline result
26
+ def execute(result)
27
+ # Analysis is performed later during FileAnalysis building
28
+ # This stage just marks the pipeline as ready for analysis
29
+ result.ready_for_analysis = true
30
+ result
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Pipelines
5
+ module Stages
6
+ # Base class for validation pipeline stages
7
+ #
8
+ # All pipeline stages inherit from this class and implement
9
+ # the execute method. This provides a consistent interface
10
+ # for pipeline execution.
11
+ #
12
+ class BaseStage
13
+ # Execute the stage
14
+ #
15
+ # @param result [PipelineResult] Current pipeline result
16
+ # @return [PipelineResult] Updated pipeline result
17
+ def execute(result)
18
+ raise NotImplementedError, "Subclasses must implement #execute"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_stage"
4
+ require_relative "../../services/chunk_processor"
5
+ require_relative "../../models/pipeline_result"
6
+
7
+ module PngConform
8
+ module Pipelines
9
+ module Stages
10
+ # Chunk validation stage
11
+ #
12
+ # Processes all chunks through validation using ChunkProcessor.
13
+ # Caches CRC calculations during initial read to avoid recomputation.
14
+ #
15
+ class ChunkValidationStage < BaseStage
16
+ # Initialize chunk validation stage
17
+ #
18
+ # @param reader [Object] File reader
19
+ # @param context [ValidationContext] Validation context
20
+ # @param options [Hash] Validation options
21
+ def initialize(reader, context, options = {})
22
+ @reader = reader
23
+ @context = context
24
+ @options = options
25
+ @crc_cache = {} # Cache for CRC calculations
26
+ end
27
+
28
+ # Execute chunk validation
29
+ #
30
+ # @param result [PipelineResult] Current pipeline result
31
+ # @return [PipelineResult] Updated pipeline result
32
+ def execute(result)
33
+ processor = Services::ChunkProcessor.new(@reader, @context, @options)
34
+ processor.process do |chunk|
35
+ # Cache CRC during initial read to avoid recalculation
36
+ cache_crc(chunk)
37
+ result.chunks << chunk
38
+ end
39
+
40
+ result
41
+ end
42
+
43
+ private
44
+
45
+ # Cache CRC calculation for a chunk
46
+ #
47
+ # Performance optimization: calculates CRC once during initial read
48
+ # and stores it for later use in result building.
49
+ #
50
+ # @param chunk [Object] Chunk to cache CRC for
51
+ # @return [void]
52
+ def cache_crc(chunk)
53
+ return unless chunk.respond_to?(:chunk_type) && chunk.respond_to?(:data)
54
+
55
+ key = "#{chunk.chunk_type}_#{chunk.data.hash}"
56
+ @crc_cache[key] ||= calculate_crc(chunk)
57
+
58
+ # Store cached CRC on chunk for later access
59
+ chunk.instance_variable_set(:@cached_crc, @crc_cache[key])
60
+ end
61
+
62
+ # Calculate CRC32 for a chunk
63
+ #
64
+ # @param chunk [Object] BinData chunk
65
+ # @return [Integer] CRC32 value
66
+ def calculate_crc(chunk)
67
+ require "zlib"
68
+ # CRC is calculated over chunk type + chunk data
69
+ Zlib.crc32(chunk.chunk_type.to_s + chunk.data.to_s)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_stage"
4
+
5
+ module PngConform
6
+ module Pipelines
7
+ module Stages
8
+ # Chunk sequence validation stage
9
+ #
10
+ # Validates high-level chunk sequence requirements:
11
+ # - IHDR must be first chunk
12
+ # - IEND must be last chunk
13
+ # - At least one IDAT chunk required
14
+ #
15
+ class SequenceValidationStage < BaseStage
16
+ # Initialize sequence validation stage
17
+ #
18
+ # @param context [ValidationContext] Validation context
19
+ def initialize(context)
20
+ @context = context
21
+ end
22
+
23
+ # Execute sequence validation
24
+ #
25
+ # @param result [PipelineResult] Current pipeline result
26
+ # @return [PipelineResult] Updated pipeline result
27
+ def execute(result)
28
+ validate_ihdr_first
29
+ validate_iend_last
30
+ validate_idat_present
31
+ result
32
+ end
33
+
34
+ private
35
+
36
+ # Check that IHDR is the first chunk
37
+ #
38
+ # @return [void]
39
+ def validate_ihdr_first
40
+ return if @context.seen?("IHDR")
41
+
42
+ @context.add_error(
43
+ chunk_type: "IHDR",
44
+ message: "Missing IHDR chunk (must be first)",
45
+ severity: :error,
46
+ )
47
+ end
48
+
49
+ # Check that IEND is the last chunk
50
+ #
51
+ # @return [void]
52
+ def validate_iend_last
53
+ return if @context.seen?("IEND")
54
+
55
+ @context.add_error(
56
+ chunk_type: "IEND",
57
+ message: "Missing IEND chunk (must be last)",
58
+ severity: :error,
59
+ )
60
+ end
61
+
62
+ # Check that at least one IDAT chunk exists
63
+ #
64
+ # @return [void]
65
+ def validate_idat_present
66
+ return if @context.seen?("IDAT")
67
+
68
+ @context.add_error(
69
+ chunk_type: "IDAT",
70
+ message: "Missing IDAT chunk (at least one required)",
71
+ severity: :error,
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_stage"
4
+
5
+ module PngConform
6
+ module Pipelines
7
+ module Stages
8
+ # Signature validation stage
9
+ #
10
+ # Validates that the PNG file starts with the correct signature.
11
+ #
12
+ class SignatureValidationStage < BaseStage
13
+ # Initialize signature validation stage
14
+ #
15
+ # @param reader [Object] File reader
16
+ def initialize(reader)
17
+ @reader = reader
18
+ end
19
+
20
+ # Execute signature validation
21
+ #
22
+ # @param result [PipelineResult] Current pipeline result
23
+ # @return [PipelineResult] Updated pipeline result
24
+ def execute(result)
25
+ sig = @reader.signature
26
+ expected = [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
27
+
28
+ if sig != expected
29
+ result.context.add_error(
30
+ chunk_type: "SIGNATURE",
31
+ message: "Invalid PNG signature",
32
+ severity: :error,
33
+ )
34
+ end
35
+
36
+ result
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pipeline_result"
4
+ require_relative "stages/base_stage"
5
+ require_relative "stages/signature_validation_stage"
6
+ require_relative "stages/chunk_validation_stage"
7
+ require_relative "stages/sequence_validation_stage"
8
+ require_relative "stages/analysis_stage"
9
+ require_relative "../services/result_builder"
10
+ require_relative "../services/analysis_manager"
11
+
12
+ module PngConform
13
+ module Pipelines
14
+ # Validation pipeline for PNG files
15
+ #
16
+ # Executes validation in a series of stages:
17
+ # 1. Signature validation
18
+ # 2. Chunk validation
19
+ # 3. Sequence validation
20
+ # 4. Analysis preparation
21
+ #
22
+ class ValidationPipeline
23
+ # Initialize validation pipeline
24
+ #
25
+ # @param reader [Object] File reader
26
+ # @param options [Hash] Validation options
27
+ def initialize(reader, options = {})
28
+ @reader = reader
29
+ @options = options
30
+ @context = Validators::ValidationContext.new
31
+ @chunks = []
32
+ build_stages
33
+ end
34
+
35
+ # Execute the validation pipeline
36
+ #
37
+ # Runs all stages in order. Stops early if fail_fast is enabled
38
+ # and critical errors are encountered.
39
+ #
40
+ # @return [FileAnalysis] Complete file analysis
41
+ def execute
42
+ result = PipelineResult.new(context: @context, chunks: @chunks)
43
+
44
+ @stages.each do |stage|
45
+ result = stage.execute(result)
46
+
47
+ # Early termination for critical errors
48
+ break if result.should_terminate?(fail_fast: @options[:fail_fast])
49
+ end
50
+
51
+ build_file_analysis(result)
52
+ end
53
+
54
+ private
55
+
56
+ # Build the pipeline stages
57
+ #
58
+ # @return [void]
59
+ def build_stages
60
+ @stages = [
61
+ Stages::SignatureValidationStage.new(@reader),
62
+ Stages::ChunkValidationStage.new(@reader, @context, @options),
63
+ Stages::SequenceValidationStage.new(@context),
64
+ Stages::AnalysisStage.new(@context, @options),
65
+ ]
66
+ end
67
+
68
+ # Build complete FileAnalysis from pipeline result
69
+ #
70
+ # @param result [PipelineResult] Pipeline result
71
+ # @return [FileAnalysis] Complete file analysis
72
+ def build_file_analysis(_result)
73
+ result_builder = Services::ResultBuilder.new(
74
+ @reader,
75
+ @options[:filepath],
76
+ @context,
77
+ @chunks,
78
+ @options,
79
+ )
80
+ file_analysis = result_builder.build
81
+
82
+ # Run analyzers through AnalysisManager
83
+ analysis_manager = Services::AnalysisManager.new(@options)
84
+ analysis_manager.enrich(file_analysis)
85
+
86
+ file_analysis
87
+ end
88
+ end
89
+ end
90
+ end
@@ -21,12 +21,14 @@ module PngConform
21
21
  # end
22
22
  #
23
23
  class FullLoadReader
24
- attr_reader :io, :png
24
+ attr_reader :io, :png, :total_bytes_read
25
25
 
26
26
  # Initialize a new full-load reader
27
27
  #
28
28
  # @param filepath_or_io [String, IO] File path or IO object to read from
29
- def initialize(filepath_or_io)
29
+ # @param options [Hash] Options for reading behavior
30
+ # @option options [Boolean] :validate_crc (true) Calculate CRC during reading
31
+ def initialize(filepath_or_io, options = {})
30
32
  if filepath_or_io.is_a?(String)
31
33
  # File path provided
32
34
  @filepath = filepath_or_io
@@ -38,6 +40,8 @@ module PngConform
38
40
  @owns_io = false
39
41
  end
40
42
  @png = nil
43
+ @total_bytes_read = 0
44
+ @validate_crc = options.fetch(:validate_crc, true)
41
45
  end
42
46
 
43
47
  # Read the entire PNG file structure
@@ -48,6 +52,11 @@ module PngConform
48
52
 
49
53
  @io.rewind
50
54
  @png = BinData::PngFile.read(@io)
55
+
56
+ # Calculate total bytes from chunks
57
+ @total_bytes_read = @png.chunks.sum { |c| 12 + c.data_length }
58
+
59
+ @png
51
60
  end
52
61
 
53
62
  # Get PNG signature
@@ -75,9 +84,9 @@ module PngConform
75
84
  elsif @io.respond_to?(:stat)
76
85
  @io.stat.size
77
86
  else
78
- # Fallback: calculate from PNG structure
87
+ # Use cached total bytes from chunks
79
88
  read unless @png
80
- 8 + @png.chunks.sum { |c| 12 + c.length }
89
+ 8 + @total_bytes_read
81
90
  end
82
91
  end
83
92
 
@@ -27,15 +27,19 @@ module PngConform
27
27
  .pack("C*")
28
28
  .freeze
29
29
 
30
- attr_reader :io, :signature
30
+ attr_reader :io, :signature, :total_bytes_read
31
31
 
32
32
  # Initialize a new streaming reader
33
33
  #
34
34
  # @param io [IO] IO object to read from (must be opened in binary mode)
35
- def initialize(io)
35
+ # @param options [Hash] Options for reading behavior
36
+ # @option options [Boolean] :validate_crc (true) Calculate CRC during reading
37
+ def initialize(io, options = {})
36
38
  @io = io
37
39
  @signature = nil
38
40
  @chunks_read = 0
41
+ @total_bytes_read = 0
42
+ @validate_crc = options.fetch(:validate_crc, true)
39
43
  end
40
44
 
41
45
  # Read and validate the PNG signature
@@ -71,6 +75,15 @@ module PngConform
71
75
  return nil if @io.eof?
72
76
 
73
77
  chunk = BinData::ChunkStructure.read(@io)
78
+
79
+ # Track total bytes read (8 byte header + data + 4 byte CRC)
80
+ @total_bytes_read += (12 + chunk.data_length)
81
+
82
+ # Validate CRC during reading if enabled and cache result
83
+ if @validate_crc
84
+ chunk.instance_variable_set(:@_crc_valid, chunk.crc_valid?)
85
+ end
86
+
74
87
  @chunks_read += 1
75
88
  chunk
76
89
  rescue EOFError
@@ -135,6 +148,18 @@ module PngConform
135
148
  @io.rewind
136
149
  @signature = nil
137
150
  @chunks_read = 0
151
+ @total_bytes_read = 0
152
+ end
153
+
154
+ # Get file size from total bytes tracked during reading
155
+ #
156
+ # Returns the total file size including signature and all chunks.
157
+ # This is cached during reading to avoid O(n) recalculation.
158
+ #
159
+ # @return [Integer] File size in bytes (0 if no chunks read yet)
160
+ def file_size
161
+ # 8 bytes signature + total chunk bytes
162
+ 8 + @total_bytes_read
138
163
  end
139
164
 
140
165
  # Get the current position in the file
@@ -1,23 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base_reporter"
4
+ require_relative "../utils/colorizer"
4
5
 
5
6
  module PngConform
6
7
  module Reporters
7
8
  # Color reporter - wraps another reporter to add ANSI color support (-c flag)
8
9
  # Decorator pattern to add color to any reporter
9
10
  class ColorReporter < BaseReporter
10
- # ANSI color codes
11
- COLORS = {
12
- red: "\e[31m",
13
- green: "\e[32m",
14
- yellow: "\e[33m",
15
- blue: "\e[34m",
16
- magenta: "\e[35m",
17
- cyan: "\e[36m",
18
- reset: "\e[0m",
19
- }.freeze
20
-
21
11
  attr_reader :wrapped_reporter
22
12
 
23
13
  # @param wrapped_reporter [BaseReporter] The reporter to wrap with colors
@@ -41,12 +31,25 @@ module PngConform
41
31
 
42
32
  protected
43
33
 
44
- # Override colorize to actually apply colors
34
+ # Override colorize to use Colorizer class
45
35
  def colorize(text, color)
46
36
  return text unless @colorize_enabled
47
- return text unless COLORS.key?(color)
48
37
 
49
- "#{COLORS[color]}#{text}#{COLORS[:reset]}"
38
+ case color
39
+ when :red
40
+ Utils::Colorizer.error(text, bold: false)
41
+ when :green
42
+ Utils::Colorizer.success(text, bold: false)
43
+ when :yellow
44
+ Utils::Colorizer.warning(text, bold: false)
45
+ when :blue
46
+ Utils::Colorizer.info(text, bold: false)
47
+ when :cyan, :magenta
48
+ # Use cyan as a neutral color for other cases
49
+ Utils::Colorizer.colorize(text, :cyan, bold: false)
50
+ else
51
+ text
52
+ end
50
53
  end
51
54
 
52
55
  # Determine color based on validation status
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../utils/colorizer"
4
+
3
5
  module PngConform
4
6
  module Reporters
5
7
  # Module providing visual elements (emojis and colors) for CLI output
@@ -20,27 +22,31 @@ module PngConform
20
22
  image: "🖼️",
21
23
  }.freeze
22
24
 
23
- # ANSI color codes for terminal output
24
- COLORS = {
25
- green: "\e[32m",
26
- red: "\e[31m",
27
- yellow: "\e[33m",
28
- blue: "\e[34m",
29
- cyan: "\e[36m",
30
- gray: "\e[90m",
31
- reset: "\e[0m",
32
- bold: "\e[1m",
33
- }.freeze
34
-
35
- # Colorize text with ANSI color codes
25
+ # Colorize text with Colorizer class
36
26
  # @param text [String] The text to colorize
37
- # @param color [Symbol] The color name from COLORS
27
+ # @param color [Symbol] The color name
38
28
  # @return [String] Colorized text or original if colorization disabled
39
29
  def colorize(text, color)
40
30
  return text unless @colorize
41
- return text unless COLORS.key?(color)
42
31
 
43
- "#{COLORS[color]}#{text}#{COLORS[:reset]}"
32
+ case color
33
+ when :green
34
+ Utils::Colorizer.success(text, bold: false)
35
+ when :red
36
+ Utils::Colorizer.error(text, bold: false)
37
+ when :yellow
38
+ Utils::Colorizer.warning(text, bold: false)
39
+ when :blue
40
+ Utils::Colorizer.info(text, bold: false)
41
+ when :cyan
42
+ Utils::Colorizer.colorize(text, :cyan, bold: false)
43
+ when :gray
44
+ Utils::Colorizer.colorize(text, :gray, bold: false)
45
+ when :bold
46
+ Utils::Colorizer.bold(text)
47
+ else
48
+ text
49
+ end
44
50
  end
45
51
 
46
52
  # Get emoji for a given name
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../configuration"
4
+
5
+ module PngConform
6
+ module Services
7
+ # Manages conditional analysis execution
8
+ #
9
+ # The AnalysisManager handles:
10
+ # - Running resolution analysis (conditional)
11
+ # - Running optimization analysis (conditional)
12
+ # - Running metrics analysis (conditional)
13
+ #
14
+ # Analyzers are only run when needed based on options to improve performance.
15
+ # This class extracts analysis logic from ValidationService following
16
+ # Single Responsibility Principle.
17
+ #
18
+ class AnalysisManager
19
+ # Initialize analysis manager
20
+ #
21
+ # @param options [Hash] CLI options for controlling behavior
22
+ # @param config [Configuration] Configuration instance (optional)
23
+ def initialize(options = {}, config: Configuration.instance)
24
+ @options = options
25
+ @config = config
26
+ end
27
+
28
+ # Enrich FileAnalysis with conditional analyzer results
29
+ #
30
+ # Runs analyzers only when needed based on options:
31
+ # - Resolution analysis: unless quiet mode, or if --resolution or --mobile-ready
32
+ # - Optimization analysis: unless quiet mode, or if --optimize
33
+ # - Metrics analysis: if yaml/json format, or if --metrics
34
+ #
35
+ # @param file_analysis [FileAnalysis] File analysis to enrich
36
+ # @return [FileAnalysis] Enriched file analysis
37
+ def enrich(file_analysis)
38
+ validation_result = file_analysis.validation_result
39
+
40
+ if need_resolution_analysis?
41
+ file_analysis.resolution_analysis =
42
+ run_resolution_analysis(validation_result)
43
+ end
44
+
45
+ if need_optimization_analysis?
46
+ file_analysis.optimization_analysis =
47
+ run_optimization_analysis(validation_result)
48
+ end
49
+
50
+ if need_metrics_analysis?
51
+ file_analysis.metrics = run_metrics_analysis(validation_result)
52
+ end
53
+
54
+ file_analysis
55
+ end
56
+
57
+ private
58
+
59
+ # Run resolution analyzer
60
+ #
61
+ # @param result [ValidationResult] Validation result
62
+ # @return [Hash] Resolution analysis results
63
+ def run_resolution_analysis(result)
64
+ require_relative "../analyzers/resolution_analyzer" unless defined?(Analyzers::ResolutionAnalyzer)
65
+ Analyzers::ResolutionAnalyzer.new(result, config: @config).analyze
66
+ rescue StandardError => e
67
+ { error: "Resolution analysis failed: #{e.message}" }
68
+ end
69
+
70
+ # Run optimization analyzer
71
+ #
72
+ # @param result [ValidationResult] Validation result
73
+ # @return [Hash] Optimization analysis results
74
+ def run_optimization_analysis(result)
75
+ require_relative "../analyzers/optimization_analyzer" unless defined?(Analyzers::OptimizationAnalyzer)
76
+ Analyzers::OptimizationAnalyzer.new(result, config: @config).analyze
77
+ rescue StandardError => e
78
+ { error: "Optimization analysis failed: #{e.message}" }
79
+ end
80
+
81
+ # Run metrics analyzer
82
+ #
83
+ # @param result [ValidationResult] Validation result
84
+ # @return [Hash] Metrics analysis results
85
+ def run_metrics_analysis(result)
86
+ require_relative "../analyzers/metrics_analyzer" unless defined?(Analyzers::MetricsAnalyzer)
87
+ Analyzers::MetricsAnalyzer.new(result, config: @config).analyze
88
+ rescue StandardError => e
89
+ { error: "Metrics analysis failed: #{e.message}" }
90
+ end
91
+
92
+ # Check if resolution analysis is needed
93
+ #
94
+ # @return [Boolean] True if resolution analysis should be run
95
+ def need_resolution_analysis?
96
+ return true unless @options[:quiet]
97
+
98
+ @options[:resolution] || @options[:mobile_ready]
99
+ end
100
+
101
+ # Check if optimization analysis is needed
102
+ #
103
+ # @return [Boolean] True if optimization analysis should be run
104
+ def need_optimization_analysis?
105
+ return true unless @options[:quiet]
106
+
107
+ @options[:optimize]
108
+ end
109
+
110
+ # Check if metrics analysis is needed
111
+ #
112
+ # @return [Boolean] True if metrics analysis should be run
113
+ def need_metrics_analysis?
114
+ return true if ["yaml", "json"].include?(@options[:format])
115
+
116
+ @options[:metrics]
117
+ end
118
+ end
119
+ end
120
+ end