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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +116 -6
- data/Gemfile +1 -1
- data/config/validation_profiles.yml +105 -0
- data/lib/png_conform/analyzers/comparison_analyzer.rb +41 -7
- data/lib/png_conform/analyzers/metrics_analyzer.rb +6 -9
- data/lib/png_conform/analyzers/optimization_analyzer.rb +30 -24
- data/lib/png_conform/analyzers/resolution_analyzer.rb +31 -32
- data/lib/png_conform/cli.rb +12 -0
- data/lib/png_conform/commands/check_command.rb +118 -53
- data/lib/png_conform/configuration.rb +147 -0
- data/lib/png_conform/container.rb +113 -0
- data/lib/png_conform/models/validation_result.rb +30 -4
- data/lib/png_conform/pipelines/pipeline_result.rb +39 -0
- data/lib/png_conform/pipelines/stages/analysis_stage.rb +35 -0
- data/lib/png_conform/pipelines/stages/base_stage.rb +23 -0
- data/lib/png_conform/pipelines/stages/chunk_validation_stage.rb +74 -0
- data/lib/png_conform/pipelines/stages/sequence_validation_stage.rb +77 -0
- data/lib/png_conform/pipelines/stages/signature_validation_stage.rb +41 -0
- data/lib/png_conform/pipelines/validation_pipeline.rb +90 -0
- data/lib/png_conform/readers/full_load_reader.rb +13 -4
- data/lib/png_conform/readers/streaming_reader.rb +27 -2
- data/lib/png_conform/reporters/color_reporter.rb +17 -14
- data/lib/png_conform/reporters/visual_elements.rb +22 -16
- data/lib/png_conform/services/analysis_manager.rb +120 -0
- data/lib/png_conform/services/chunk_processor.rb +195 -0
- data/lib/png_conform/services/file_signature.rb +226 -0
- data/lib/png_conform/services/file_strategy.rb +78 -0
- data/lib/png_conform/services/lru_cache.rb +170 -0
- data/lib/png_conform/services/parallel_validator.rb +118 -0
- data/lib/png_conform/services/profile_manager.rb +41 -12
- data/lib/png_conform/services/result_builder.rb +299 -0
- data/lib/png_conform/services/validation_cache.rb +210 -0
- data/lib/png_conform/services/validation_orchestrator.rb +188 -0
- data/lib/png_conform/services/validation_service.rb +53 -337
- data/lib/png_conform/services/validator_pool.rb +142 -0
- data/lib/png_conform/utils/colorizer.rb +149 -0
- data/lib/png_conform/validators/chunk_registry.rb +12 -0
- data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
- data/lib/png_conform/version.rb +1 -1
- data/png_conform.gemspec +1 -0
- 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
|
-
|
|
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
|
-
#
|
|
87
|
+
# Use cached total bytes from chunks
|
|
79
88
|
read unless @png
|
|
80
|
-
8 + @
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|