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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +14 -84
- data/Gemfile +2 -0
- data/README.adoc +3 -2
- data/benchmarks/README.adoc +570 -0
- data/benchmarks/config/default.yml +35 -0
- data/benchmarks/config/full.yml +32 -0
- data/benchmarks/config/quick.yml +32 -0
- data/benchmarks/direct_validation.rb +18 -0
- data/benchmarks/lib/benchmark_runner.rb +204 -0
- data/benchmarks/lib/metrics_collector.rb +193 -0
- data/benchmarks/lib/png_conform_runner.rb +68 -0
- data/benchmarks/lib/pngcheck_runner.rb +67 -0
- data/benchmarks/lib/report_generator.rb +301 -0
- data/benchmarks/lib/tool_runner.rb +104 -0
- data/benchmarks/profile_loading.rb +12 -0
- data/benchmarks/profile_validation.rb +18 -0
- data/benchmarks/results/.gitkeep +0 -0
- data/benchmarks/run_benchmark.rb +159 -0
- data/docs/CHUNK_TYPES.adoc +42 -0
- data/examples/README.md +282 -0
- data/lib/png_conform/commands/check_command.rb +3 -2
- data/lib/png_conform/models/decoded_chunk_data.rb +33 -0
- data/lib/png_conform/reporters/reporter_factory.rb +18 -11
- data/lib/png_conform/services/validation_service.rb +60 -15
- data/lib/png_conform/validators/ancillary/idot_validator.rb +102 -0
- data/lib/png_conform/validators/chunk_registry.rb +131 -128
- data/lib/png_conform/version.rb +1 -1
- data/lib/png_conform.rb +7 -46
- metadata +19 -2
data/examples/README.md
ADDED
|
@@ -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
|
-
|
|
9
|
-
require_relative "../analyzers/
|
|
10
|
-
require_relative "../analyzers/
|
|
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
|
-
|
|
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
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|