png_conform 0.1.1 → 0.1.2

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.
@@ -0,0 +1,282 @@
1
+ # PngConform Examples
2
+
3
+ This directory contains example scripts demonstrating how to use PngConform in various scenarios.
4
+
5
+ ## Quick Start
6
+
7
+ All examples are executable Ruby scripts. Make sure you have PngConform installed:
8
+
9
+ ```bash
10
+ gem install png_conform
11
+ # or
12
+ bundle install
13
+ ```
14
+
15
+ ## Available Examples
16
+
17
+ ### Basic Usage ([`basic_usage.rb`](basic_usage.rb))
18
+
19
+ Demonstrates fundamental PngConform operations:
20
+
21
+ - Basic file validation
22
+ - Profile-based validation
23
+ - Detailed chunk inspection
24
+ - Batch validation of multiple files
25
+ - Exporting results to YAML/JSON
26
+
27
+ **Run it:**
28
+ ```bash
29
+ ruby examples/basic_usage.rb path/to/image.png
30
+ ruby examples/basic_usage.rb path/to/image.png path/to/png_directory
31
+ ```
32
+
33
+ **What you'll learn:**
34
+ - How to validate a single PNG file
35
+ - How to use different validation profiles
36
+ - How to inspect chunk data
37
+ - How to process multiple files efficiently
38
+ - How to export validation results
39
+
40
+ ### Advanced Usage ([`advanced_usage.rb`](advanced_usage.rb))
41
+
42
+ Demonstrates advanced integration patterns:
43
+
44
+ - Creating custom reporters
45
+ - Working with validators directly
46
+ - Comparing streaming vs full-load modes
47
+ - Profile comparison across all profiles
48
+ - Error handling best practices
49
+ - Extracting metadata from chunks
50
+ - Performance monitoring
51
+
52
+ **Run it:**
53
+ ```bash
54
+ ruby examples/advanced_usage.rb path/to/image.png
55
+ ruby examples/advanced_usage.rb file1.png file2.png file3.png
56
+ ```
57
+
58
+ **What you'll learn:**
59
+ - How to create custom output formats
60
+ - How to handle errors properly
61
+ - How to optimize for large files
62
+ - How to extract chunk metadata
63
+ - How to monitor performance
64
+
65
+ ## Common Use Cases
66
+
67
+ ### Validate a Single File
68
+
69
+ ```ruby
70
+ require "png_conform"
71
+
72
+ service = PngConform::Services::ValidationService.new
73
+ result = service.validate_file("image.png")
74
+
75
+ puts result.valid? ? "Valid PNG" : "Invalid PNG"
76
+ ```
77
+
78
+ ### Batch Validation
79
+
80
+ ```ruby
81
+ Dir.glob("images/*.png").each do |file|
82
+ result = service.validate_file(file)
83
+ puts "#{file}: #{result.valid? ? '✓' : '✗'}"
84
+ end
85
+ ```
86
+
87
+ ### Profile Validation
88
+
89
+ ```ruby
90
+ profile_manager = PngConform::Services::ProfileManager.new
91
+ profile = profile_manager.load_profile("web")
92
+
93
+ result = service.validate_file("image.png", profile: profile)
94
+ ```
95
+
96
+ ### Extract Metadata
97
+
98
+ ```ruby
99
+ result = service.validate_file("image.png")
100
+
101
+ # Get image dimensions
102
+ puts "#{result.image_info.width}x#{result.image_info.height}"
103
+
104
+ # Get chunk information
105
+ result.chunks.each do |chunk|
106
+ puts "#{chunk.type}: #{chunk.length} bytes"
107
+ end
108
+ ```
109
+
110
+ ### Handle Errors
111
+
112
+ ```ruby
113
+ begin
114
+ result = service.validate_file("image.png")
115
+ rescue PngConform::ParseError => e
116
+ puts "File is corrupted: #{e.message}"
117
+ rescue PngConform::Error => e
118
+ puts "Validation error: #{e.message}"
119
+ end
120
+ ```
121
+
122
+ ## Testing the Examples
123
+
124
+ You can test the examples with files from the PngSuite test fixture:
125
+
126
+ ```bash
127
+ # Using a test fixture
128
+ ruby examples/basic_usage.rb spec/fixtures/pngsuite/background/bgwn6a08.png
129
+
130
+ # Using multiple test files
131
+ ruby examples/advanced_usage.rb spec/fixtures/pngsuite/background/*.png
132
+ ```
133
+
134
+ ## Integration Patterns
135
+
136
+ ### Web Application
137
+
138
+ ```ruby
139
+ # In a Rails/Sinatra controller
140
+ def validate_upload
141
+ uploaded_file = params[:file]
142
+
143
+ # Save to temporary location
144
+ temp_path = "/tmp/#{SecureRandom.hex}.png"
145
+ File.write(temp_path, uploaded_file.read)
146
+
147
+ # Validate
148
+ service = PngConform::Services::ValidationService.new
149
+ result = service.validate_file(temp_path)
150
+
151
+ # Clean up
152
+ File.delete(temp_path)
153
+
154
+ # Return result
155
+ render json: {
156
+ valid: result.valid?,
157
+ errors: result.errors.map(&:message)
158
+ }
159
+ end
160
+ ```
161
+
162
+ ### Background Job
163
+
164
+ ```ruby
165
+ # In a Sidekiq/ActiveJob worker
166
+ class PngValidationJob < ApplicationJob
167
+ def perform(file_path)
168
+ service = PngConform::Services::ValidationService.new
169
+ result = service.validate_file(file_path, streaming: true)
170
+
171
+ if result.valid?
172
+ # Process valid file
173
+ ProcessImageJob.perform_later(file_path)
174
+ else
175
+ # Handle invalid file
176
+ NotifyUserJob.perform_later(user_id, result.errors)
177
+ end
178
+ end
179
+ end
180
+ ```
181
+
182
+ ### Command Line Tool
183
+
184
+ ```ruby
185
+ #!/usr/bin/env ruby
186
+ # Custom validation script
187
+
188
+ require "png_conform"
189
+
190
+ ARGV.each do |file|
191
+ service = PngConform::Services::ValidationService.new
192
+ result = service.validate_file(file)
193
+
194
+ status = result.valid? ? "PASS" : "FAIL"
195
+ puts "#{status}: #{file}"
196
+
197
+ unless result.valid?
198
+ result.errors.each do |error|
199
+ puts " #{error.severity}: #{error.message}"
200
+ end
201
+ end
202
+ end
203
+ ```
204
+
205
+ ## Performance Tips
206
+
207
+ ### Large Files
208
+
209
+ For files larger than 50MB, use streaming mode:
210
+
211
+ ```ruby
212
+ result = service.validate_file("large.png", streaming: true)
213
+ ```
214
+
215
+ ### Batch Processing
216
+
217
+ Process files in parallel using threads:
218
+
219
+ ```ruby
220
+ require "concurrent"
221
+
222
+ files = Dir.glob("images/*.png")
223
+ pool = Concurrent::FixedThreadPool.new(4)
224
+
225
+ files.each do |file|
226
+ pool.post do
227
+ result = service.validate_file(file)
228
+ # Process result...
229
+ end
230
+ end
231
+
232
+ pool.shutdown
233
+ pool.wait_for_termination
234
+ ```
235
+
236
+ ### Memory Management
237
+
238
+ For production systems, set resource limits:
239
+
240
+ ```ruby
241
+ MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB
242
+ MAX_PROCESSING_TIME = 30 # seconds
243
+
244
+ if File.size(file_path) > MAX_FILE_SIZE
245
+ raise "File too large"
246
+ end
247
+
248
+ Timeout.timeout(MAX_PROCESSING_TIME) do
249
+ result = service.validate_file(file_path)
250
+ end
251
+ ```
252
+
253
+ ## Troubleshooting
254
+
255
+ ### Common Issues
256
+
257
+ **"File not found" error:**
258
+ - Check file path is correct
259
+ - Use absolute paths if relative paths don't work
260
+ - Ensure file has read permissions
261
+
262
+ **Memory issues with large files:**
263
+ - Use streaming mode: `streaming: true`
264
+ - Process files one at a time
265
+ - Set memory limits in your environment
266
+
267
+ **Slow validation:**
268
+ - Use streaming mode for large files
269
+ - Consider caching results
270
+ - Run validation in background jobs
271
+
272
+ ## Additional Resources
273
+
274
+ - [API Documentation](../README.adoc)
275
+ - [Architecture Guide](../ARCHITECTURE.md)
276
+ - [Contributing Guide](../CONTRIBUTING.md)
277
+ - [Security Policy](../SECURITY.md)
278
+
279
+ ## Questions?
280
+
281
+ - Open an issue: https://github.com/claricle/png_conform/issues
282
+ - Read the documentation: https://github.com/claricle/png_conform
@@ -88,8 +88,9 @@ module PngConform
88
88
 
89
89
  # Read and validate the file using streaming reader
90
90
  Readers::StreamingReader.open(file_path) do |reader|
91
- # Perform validation
92
- validator = Services::ValidationService.new(reader, file_path)
91
+ # Perform validation - pass options for conditional analyzers (Phase 1.1 + 1.2)
92
+ validator = Services::ValidationService.new(reader, file_path,
93
+ options)
93
94
  file_analysis = validator.validate
94
95
 
95
96
  # Track if any errors were found
@@ -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
@@ -1,21 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "summary_reporter"
4
- require_relative "verbose_reporter"
5
- require_relative "very_verbose_reporter"
6
- require_relative "quiet_reporter"
7
- require_relative "palette_reporter"
8
- require_relative "text_reporter"
9
- require_relative "color_reporter"
10
- require_relative "yaml_reporter"
11
- require_relative "json_reporter"
12
-
13
3
  module PngConform
14
4
  module Reporters
15
5
  # Factory for creating reporter instances based on options.
16
6
  #
17
7
  # Implements the Factory pattern to provide a clean interface for
18
8
  # creating reporters with various combinations of options.
9
+ # Uses lazy loading to only require reporters when actually needed.
19
10
  class ReporterFactory
20
11
  # Create a reporter based on the specified options.
21
12
  #
@@ -34,30 +25,43 @@ module PngConform
34
25
  # Format takes priority over verbosity
35
26
  case format
36
27
  when "yaml"
28
+ require_relative "yaml_reporter" unless defined?(YamlReporter)
37
29
  return YamlReporter.new
38
30
  when "json"
31
+ require_relative "json_reporter" unless defined?(JsonReporter)
39
32
  return JsonReporter.new
40
33
  end
41
34
 
42
- # Text reporters with verbosity levels
35
+ # Text reporters with verbosity levels - lazy load base and visual elements
36
+ require_relative "visual_elements" unless defined?(VisualElements)
37
+ require_relative "base_reporter" unless defined?(BaseReporter)
38
+
43
39
  reporter = if verbosity
44
40
  case verbosity
45
41
  when :quiet
42
+ require_relative "quiet_reporter" unless defined?(QuietReporter)
46
43
  QuietReporter.new($stdout, colorize: colorize)
47
44
  when :verbose
45
+ require_relative "verbose_reporter" unless defined?(VerboseReporter)
48
46
  VerboseReporter.new($stdout, colorize: colorize)
49
47
  when :very_verbose
48
+ require_relative "very_verbose_reporter" unless defined?(VeryVerboseReporter)
50
49
  VeryVerboseReporter.new($stdout, colorize: colorize)
51
50
  when :summary
51
+ require_relative "summary_reporter" unless defined?(SummaryReporter)
52
52
  SummaryReporter.new($stdout, colorize: colorize)
53
53
  else
54
+ require_relative "summary_reporter" unless defined?(SummaryReporter)
54
55
  SummaryReporter.new($stdout, colorize: colorize)
55
56
  end
56
57
  elsif quiet
58
+ require_relative "quiet_reporter" unless defined?(QuietReporter)
57
59
  QuietReporter.new($stdout, colorize: colorize)
58
60
  elsif verbose
61
+ require_relative "verbose_reporter" unless defined?(VerboseReporter)
59
62
  VerboseReporter.new($stdout, colorize: colorize)
60
63
  else
64
+ require_relative "summary_reporter" unless defined?(SummaryReporter)
61
65
  SummaryReporter.new($stdout, colorize: colorize)
62
66
  end
63
67
 
@@ -76,6 +80,7 @@ module PngConform
76
80
  # @param reporter [BaseReporter] Reporter to wrap
77
81
  # @return [PaletteReporter] Wrapped reporter
78
82
  def self.wrap_with_palette(reporter)
83
+ require_relative "palette_reporter" unless defined?(PaletteReporter)
79
84
  PaletteReporter.new(reporter)
80
85
  end
81
86
 
@@ -86,6 +91,7 @@ module PngConform
86
91
  # @param escape_mode [Symbol] Explicit escape mode setting
87
92
  # @return [TextReporter] Wrapped reporter
88
93
  def self.wrap_with_text(reporter, seven_bit, escape_mode)
94
+ require_relative "text_reporter" unless defined?(TextReporter)
89
95
  mode = if escape_mode == :none
90
96
  (seven_bit ? :seven_bit : :none)
91
97
  else
@@ -99,6 +105,7 @@ module PngConform
99
105
  # @param reporter [BaseReporter] Reporter to wrap
100
106
  # @return [ColorReporter] Wrapped reporter
101
107
  def self.wrap_with_color(reporter)
108
+ require_relative "color_reporter" unless defined?(ColorReporter)
102
109
  ColorReporter.new(reporter)
103
110
  end
104
111
 
@@ -5,9 +5,10 @@ require_relative "../models/validation_result"
5
5
  require_relative "../models/file_analysis"
6
6
  require_relative "../models/image_info"
7
7
  require_relative "../models/compression_info"
8
- require_relative "../analyzers/resolution_analyzer"
9
- require_relative "../analyzers/optimization_analyzer"
10
- require_relative "../analyzers/metrics_analyzer"
8
+ # Lazy load analyzers - only require when needed (Phase 2: Lazy Loading)
9
+ # require_relative "../analyzers/resolution_analyzer"
10
+ # require_relative "../analyzers/optimization_analyzer"
11
+ # require_relative "../analyzers/metrics_analyzer"
11
12
 
12
13
  module PngConform
13
14
  module Services
@@ -28,11 +29,12 @@ module PngConform
28
29
  # Convenience method to validate a file by path
29
30
  #
30
31
  # @param filepath [String] Path to PNG file
32
+ # @param options [Hash] Optional CLI options
31
33
  # @return [ValidationResult] Validation results
32
- def self.validate_file(filepath)
34
+ def self.validate_file(filepath, options = {})
33
35
  require_relative "../readers/full_load_reader"
34
36
  reader = Readers::FullLoadReader.new(filepath)
35
- service = new(reader, filepath)
37
+ service = new(reader, filepath, options)
36
38
  service.validate
37
39
  end
38
40
 
@@ -40,9 +42,11 @@ module PngConform
40
42
  #
41
43
  # @param reader [Object] File reader (StreamingReader or FullLoadReader)
42
44
  # @param filepath [String, nil] Optional file path (for reporting)
43
- def initialize(reader, filepath = nil)
45
+ # @param options [Hash] CLI options for controlling behavior
46
+ def initialize(reader, filepath = nil, options = {})
44
47
  @reader = reader
45
48
  @filepath = filepath
49
+ @options = options
46
50
  @context = Validators::ValidationContext.new
47
51
  @results = []
48
52
  @chunks = [] # Store chunks as we read them
@@ -184,7 +188,7 @@ module PngConform
184
188
  # Proper Model → Formatter pattern
185
189
  # - Builds ValidationResult (legacy)
186
190
  # - Extracts ImageInfo and CompressionInfo
187
- # - Runs all analyzers HERE (not in reporters)
191
+ # - Runs analyzers conditionally (Phase 1.1 optimization)
188
192
  # - Returns complete FileAnalysis model
189
193
  #
190
194
  # @return [FileAnalysis] Complete analysis model
@@ -205,10 +209,18 @@ module PngConform
205
209
  analysis.image_info = image_info
206
210
  analysis.compression_info = extract_compression_info(validation_result)
207
211
 
208
- # Run analyzers HERE (proper Model Formatter pattern)
209
- analysis.resolution_analysis = run_resolution_analysis(validation_result)
210
- analysis.optimization_analysis = run_optimization_analysis(validation_result)
211
- analysis.metrics = run_metrics_analysis(validation_result)
212
+ # Run analyzers conditionally (Phase 1.1: saves ~80ms)
213
+ if need_resolution_analysis?
214
+ analysis.resolution_analysis = run_resolution_analysis(validation_result)
215
+ end
216
+
217
+ if need_optimization_analysis?
218
+ analysis.optimization_analysis = run_optimization_analysis(validation_result)
219
+ end
220
+
221
+ if need_metrics_analysis?
222
+ analysis.metrics = run_metrics_analysis(validation_result)
223
+ end
212
224
  end
213
225
  end
214
226
 
@@ -248,10 +260,12 @@ module PngConform
248
260
 
249
261
  result.crc_errors_count = crc_error_count
250
262
 
251
- # Calculate compression ratio for PNG files
252
- if result.file_type == "PNG"
253
- result.compression_ratio = calculate_compression_ratio(result.chunks)
254
- end
263
+ # Calculate compression ratio for PNG files (Phase 1.2: lazy calculation)
264
+ result.compression_ratio = if result.file_type == "PNG" && need_compression_ratio?
265
+ calculate_compression_ratio(result.chunks)
266
+ else
267
+ 0.0
268
+ end
255
269
 
256
270
  # Add errors from service (@results)
257
271
  @results.select { |r| r[:type] == :error }.each do |r|
@@ -422,6 +436,7 @@ module PngConform
422
436
 
423
437
  # Run resolution analyzer
424
438
  def run_resolution_analysis(result)
439
+ require_relative "../analyzers/resolution_analyzer" unless defined?(Analyzers::ResolutionAnalyzer)
425
440
  Analyzers::ResolutionAnalyzer.new(result).analyze
426
441
  rescue StandardError => e
427
442
  { error: "Resolution analysis failed: #{e.message}" }
@@ -429,6 +444,7 @@ module PngConform
429
444
 
430
445
  # Run optimization analyzer
431
446
  def run_optimization_analysis(result)
447
+ require_relative "../analyzers/optimization_analyzer" unless defined?(Analyzers::OptimizationAnalyzer)
432
448
  Analyzers::OptimizationAnalyzer.new(result).analyze
433
449
  rescue StandardError => e
434
450
  { error: "Optimization analysis failed: #{e.message}" }
@@ -436,6 +452,7 @@ module PngConform
436
452
 
437
453
  # Run metrics analyzer
438
454
  def run_metrics_analysis(result)
455
+ require_relative "../analyzers/metrics_analyzer" unless defined?(Analyzers::MetricsAnalyzer)
439
456
  Analyzers::MetricsAnalyzer.new(result).analyze
440
457
  rescue StandardError => e
441
458
  { error: "Metrics analysis failed: #{e.message}" }
@@ -452,6 +469,34 @@ module PngConform
452
469
  else "unknown"
453
470
  end
454
471
  end
472
+
473
+ # Check if resolution analysis is needed (Phase 1.1)
474
+ def need_resolution_analysis?
475
+ return true unless @options[:quiet]
476
+
477
+ @options[:resolution] || @options[:mobile_ready]
478
+ end
479
+
480
+ # Check if optimization analysis is needed (Phase 1.1)
481
+ def need_optimization_analysis?
482
+ return true unless @options[:quiet]
483
+
484
+ @options[:optimize]
485
+ end
486
+
487
+ # Check if metrics analysis is needed (Phase 1.1)
488
+ def need_metrics_analysis?
489
+ return true if ["yaml", "json"].include?(@options[:format])
490
+
491
+ @options[:metrics]
492
+ end
493
+
494
+ # Check if compression ratio calculation is needed (Phase 1.2)
495
+ def need_compression_ratio?
496
+ return true if ["yaml", "json"].include?(@options[:format])
497
+
498
+ !@options[:quiet]
499
+ end
455
500
  end
456
501
  end
457
502
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Ancillary
8
+ # Validator for PNG iDOT (Apple Display Optimization) chunk
9
+ #
10
+ # iDOT is an Apple-specific chunk found in screenshots and images saved
11
+ # from macOS/iOS devices. It contains display optimization data for
12
+ # Retina displays and multi-core decoding performance.
13
+ #
14
+ # Structure (28 bytes - seven 32-bit little-endian integers):
15
+ # - Display scale factor (4 bytes)
16
+ # - Pixel format information (4 bytes)
17
+ # - Color space information (4 bytes)
18
+ # - Backing scale factor (4 bytes)
19
+ # - Flags (4 bytes)
20
+ # - Reserved field 1 (4 bytes)
21
+ # - Reserved field 2 (4 bytes)
22
+ #
23
+ # Validation rules:
24
+ # - Chunk must be exactly 28 bytes
25
+ # - Only one iDOT chunk allowed
26
+ # - Must appear before IDAT chunk
27
+ #
28
+ # References:
29
+ # - Apple's proprietary display optimization format
30
+ # - Used in macOS/iOS screenshot PNG files
31
+ class IdotValidator < BaseValidator
32
+ # Expected chunk length (7 x 4-byte integers)
33
+ EXPECTED_LENGTH = 28
34
+
35
+ # Validate iDOT chunk
36
+ #
37
+ # @return [Boolean] True if validation passed
38
+ def validate
39
+ return false unless check_crc
40
+ return false unless check_length(EXPECTED_LENGTH)
41
+ return false unless check_uniqueness
42
+ return false unless check_position
43
+
44
+ decode_and_store_data
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ # Check that only one iDOT chunk exists
51
+ def check_uniqueness
52
+ if context.seen?("iDOT")
53
+ add_error("duplicate iDOT chunk (only one allowed)")
54
+ return false
55
+ end
56
+
57
+ true
58
+ end
59
+
60
+ # Check that iDOT appears before IDAT
61
+ def check_position
62
+ if context.seen?("IDAT")
63
+ add_error("iDOT chunk after IDAT (must be before)")
64
+ return false
65
+ end
66
+
67
+ true
68
+ end
69
+
70
+ # Decode iDOT data and store in context
71
+ def decode_and_store_data
72
+ values = chunk.chunk_data.unpack("V7")
73
+
74
+ # Create IdotData model
75
+ idot_data = create_idot_data(values)
76
+
77
+ # Store in context for later use
78
+ context.store(:idot_data, idot_data)
79
+
80
+ # Add info message with decoded data
81
+ add_info("iDOT: Apple display optimization (#{idot_data.detailed_info})")
82
+ end
83
+
84
+ # Create IdotData model from parsed values
85
+ #
86
+ # @param values [Array<Integer>] Seven 32-bit integers
87
+ # @return [Models::IdotData] The decoded data model
88
+ def create_idot_data(values)
89
+ Models::IdotData.new(
90
+ display_scale: values[0],
91
+ pixel_format: values[1],
92
+ color_space: values[2],
93
+ backing_scale_factor: values[3],
94
+ flags: values[4],
95
+ reserved1: values[5],
96
+ reserved2: values[6],
97
+ )
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end