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
@@ -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
@@ -139,5 +139,38 @@ module PngConform
139
139
  year, month, day, hour, minute, second)
140
140
  end
141
141
  end
142
+
143
+ # iDOT chunk decoded data
144
+ class IdotData < DecodedChunkData
145
+ attribute :display_scale, :integer
146
+ attribute :pixel_format, :integer
147
+ attribute :color_space, :integer
148
+ attribute :backing_scale_factor, :integer
149
+ attribute :flags, :integer
150
+ attribute :reserved1, :integer
151
+ attribute :reserved2, :integer
152
+
153
+ def summary
154
+ parts = []
155
+ parts << "display scale: #{display_scale}" if display_scale
156
+ parts << "pixel format: #{pixel_format}" if pixel_format
157
+ parts << "color space: #{color_space}" if color_space
158
+ parts << "backing scale: #{backing_scale_factor}" if backing_scale_factor
159
+ parts.join(", ")
160
+ end
161
+
162
+ # Format all seven values for detailed display
163
+ def detailed_info
164
+ [
165
+ display_scale,
166
+ pixel_format,
167
+ color_space,
168
+ backing_scale_factor,
169
+ flags,
170
+ reserved1,
171
+ reserved2,
172
+ ].join(", ")
173
+ end
174
+ end
142
175
  end
143
176
  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
@@ -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