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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +82 -42
- 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/config/validation_profiles.yml +105 -0
- data/docs/CHUNK_TYPES.adoc +42 -0
- data/examples/README.md +282 -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 -52
- data/lib/png_conform/configuration.rb +147 -0
- data/lib/png_conform/container.rb +113 -0
- data/lib/png_conform/models/decoded_chunk_data.rb +33 -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/reporter_factory.rb +18 -11
- 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 +82 -321
- data/lib/png_conform/services/validator_pool.rb +142 -0
- data/lib/png_conform/utils/colorizer.rb +149 -0
- data/lib/png_conform/validators/ancillary/idot_validator.rb +102 -0
- data/lib/png_conform/validators/chunk_registry.rb +143 -128
- data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
- data/lib/png_conform/version.rb +1 -1
- data/lib/png_conform.rb +7 -46
- data/png_conform.gemspec +1 -0
- metadata +55 -2
data/docs/CHUNK_TYPES.adoc
CHANGED
|
@@ -240,6 +240,48 @@ Complete reference of all PNG, MNG, JNG, and APNG chunk types validated by PngCo
|
|
|
240
240
|
**Size**: 1 byte
|
|
241
241
|
**Values**: 0=cross-fuse, 1=diverging-fuse
|
|
242
242
|
|
|
243
|
+
== Apple Extensions
|
|
244
|
+
|
|
245
|
+
=== iDOT (Apple Display Optimization)
|
|
246
|
+
|
|
247
|
+
**Purpose**: Apple-specific display optimization data for Retina displays
|
|
248
|
+
**Size**: 28 bytes
|
|
249
|
+
**Optional**: Only present in screenshots and images saved from macOS/iOS devices
|
|
250
|
+
|
|
251
|
+
**Contents**:
|
|
252
|
+
|
|
253
|
+
* Display scale factor (4 bytes, little-endian)
|
|
254
|
+
* Pixel format information (4 bytes, little-endian)
|
|
255
|
+
* Color space information (4 bytes, little-endian)
|
|
256
|
+
* Backing scale factor (4 bytes, little-endian)
|
|
257
|
+
* Flags (4 bytes, little-endian)
|
|
258
|
+
* Reserved field 1 (4 bytes, little-endian)
|
|
259
|
+
* Reserved field 2 (4 bytes, little-endian)
|
|
260
|
+
|
|
261
|
+
**Validation**:
|
|
262
|
+
|
|
263
|
+
* Chunk must be exactly 28 bytes
|
|
264
|
+
* Only one iDOT chunk allowed per file
|
|
265
|
+
* Must appear before IDAT chunk
|
|
266
|
+
* CRC must be valid
|
|
267
|
+
|
|
268
|
+
**Usage**:
|
|
269
|
+
|
|
270
|
+
The iDOT chunk is automatically added by macOS and iOS when saving screenshots or images through system APIs. It contains display optimization data for:
|
|
271
|
+
|
|
272
|
+
* Retina display rendering
|
|
273
|
+
* Multi-core decoding performance
|
|
274
|
+
* Display color space information
|
|
275
|
+
* Backing store scale factors
|
|
276
|
+
|
|
277
|
+
This chunk is safe to ignore for standard PNG decoders, as it follows the ancillary chunk naming convention (lowercase first letter).
|
|
278
|
+
|
|
279
|
+
**References**:
|
|
280
|
+
|
|
281
|
+
* Apple proprietary format
|
|
282
|
+
* Found in PNG files generated by macOS 10.7+ and iOS 5+
|
|
283
|
+
* Commonly seen in screenshot files
|
|
284
|
+
|
|
243
285
|
== APNG (Animated PNG) Chunks
|
|
244
286
|
|
|
245
287
|
=== acTL (Animation Control)
|
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
|
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration"
|
|
4
|
+
require_relative "../services/file_signature"
|
|
5
|
+
|
|
3
6
|
module PngConform
|
|
4
7
|
module Analyzers
|
|
5
8
|
# Compares two PNG files and reports differences
|
|
6
9
|
class ComparisonAnalyzer
|
|
7
|
-
|
|
8
|
-
METADATA_CHUNKS = %w[tEXt zTXt iTXt tIME].freeze
|
|
9
|
-
|
|
10
|
-
def initialize(result1, result2)
|
|
10
|
+
def initialize(result1, result2, config: Configuration.instance)
|
|
11
11
|
@result1 = result1
|
|
12
12
|
@result2 = result2
|
|
13
|
+
@config = config
|
|
14
|
+
|
|
15
|
+
# Fast path: compute signatures for quick equality check
|
|
16
|
+
@sig1 = Services::FileSignature.from_result(result1).compute_signature
|
|
17
|
+
@sig2 = Services::FileSignature.from_result(result2).compute_signature
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
def analyze
|
|
21
|
+
# Fast return if signatures are identical
|
|
22
|
+
return identical_result if @sig1 == @sig2
|
|
23
|
+
|
|
24
|
+
# Full comparison for different files
|
|
25
|
+
full_comparison
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Return result for identical files
|
|
31
|
+
#
|
|
32
|
+
# @return [Hash] Analysis result for identical files
|
|
33
|
+
def identical_result
|
|
34
|
+
{
|
|
35
|
+
files: {
|
|
36
|
+
file1: @result1.filename,
|
|
37
|
+
file2: @result2.filename,
|
|
38
|
+
identical: true,
|
|
39
|
+
signature: @sig1.short_signature,
|
|
40
|
+
},
|
|
41
|
+
summary: ["Files are binary identical"],
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Full comparison for different files
|
|
46
|
+
#
|
|
47
|
+
# @return [Hash] Complete comparison analysis
|
|
48
|
+
def full_comparison
|
|
16
49
|
{
|
|
17
50
|
files: file_comparison,
|
|
18
51
|
image: image_comparison,
|
|
@@ -23,8 +56,6 @@ module PngConform
|
|
|
23
56
|
}
|
|
24
57
|
end
|
|
25
58
|
|
|
26
|
-
private
|
|
27
|
-
|
|
28
59
|
def file_comparison
|
|
29
60
|
size1 = @result1.file_size
|
|
30
61
|
size2 = @result2.file_size
|
|
@@ -166,7 +197,10 @@ module PngConform
|
|
|
166
197
|
end
|
|
167
198
|
|
|
168
199
|
def metadata_count(result)
|
|
169
|
-
|
|
200
|
+
# Use only text and time chunks from metadata (excluding pHYs which is physical)
|
|
201
|
+
result.chunks.count do |c|
|
|
202
|
+
@config.text_chunks.include?(c.type) || c.type == "tIME"
|
|
203
|
+
end
|
|
170
204
|
end
|
|
171
205
|
|
|
172
206
|
def format_size_change(diff, percent)
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration"
|
|
4
|
+
|
|
3
5
|
module PngConform
|
|
4
6
|
module Analyzers
|
|
5
7
|
# Generates comprehensive metrics for CI/CD and automation
|
|
6
8
|
class MetricsAnalyzer
|
|
7
|
-
|
|
8
|
-
TEXT_CHUNKS = %w[tEXt zTXt iTXt].freeze
|
|
9
|
-
|
|
10
|
-
# Metadata chunk types including time
|
|
11
|
-
METADATA_CHUNKS = %w[tEXt zTXt iTXt tIME].freeze
|
|
12
|
-
|
|
13
|
-
def initialize(result)
|
|
9
|
+
def initialize(result, config: Configuration.instance)
|
|
14
10
|
@result = result
|
|
11
|
+
@config = config
|
|
15
12
|
ihdr = result.ihdr_chunk
|
|
16
13
|
@width = ihdr ? get_width(ihdr) : 0
|
|
17
14
|
@height = ihdr ? get_height(ihdr) : 0
|
|
@@ -149,10 +146,10 @@ module PngConform
|
|
|
149
146
|
has_iccp: @result.has_chunk?("iCCP"),
|
|
150
147
|
has_transparency: @result.has_chunk?("tRNS"),
|
|
151
148
|
has_metadata: @result.chunks.any? do |c|
|
|
152
|
-
|
|
149
|
+
@config.text_chunks.include?(c.type)
|
|
153
150
|
end,
|
|
154
151
|
metadata_chunks_count: @result.chunks.count do |c|
|
|
155
|
-
|
|
152
|
+
@config.metadata_chunks.include?(c.type)
|
|
156
153
|
end,
|
|
157
154
|
bytes_per_pixel: calculate_bytes_per_pixel,
|
|
158
155
|
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration"
|
|
4
|
+
|
|
3
5
|
module PngConform
|
|
4
6
|
module Analyzers
|
|
5
7
|
# Analyzes PNG files for optimization opportunities
|
|
6
8
|
class OptimizationAnalyzer
|
|
7
|
-
|
|
8
|
-
UNNECESSARY_FOR_WEB = %w[tIME pHYs oFFs pCAL sCAL sTER].freeze
|
|
9
|
-
|
|
10
|
-
# Text chunk types
|
|
11
|
-
TEXT_CHUNKS = %w[tEXt zTXt iTXt].freeze
|
|
12
|
-
|
|
13
|
-
# Metadata chunk types
|
|
14
|
-
METADATA_CHUNKS = %w[tEXt zTXt iTXt tIME pHYs].freeze
|
|
15
|
-
|
|
16
|
-
def initialize(result)
|
|
9
|
+
def initialize(result, config: Configuration.instance)
|
|
17
10
|
@result = result
|
|
11
|
+
@config = config
|
|
18
12
|
@suggestions = []
|
|
19
13
|
end
|
|
20
14
|
|
|
@@ -37,7 +31,7 @@ module PngConform
|
|
|
37
31
|
|
|
38
32
|
def check_unnecessary_chunks
|
|
39
33
|
unnecessary = @result.chunks.select do |c|
|
|
40
|
-
|
|
34
|
+
@config.unnecessary_web_chunks.include?(c.type)
|
|
41
35
|
end
|
|
42
36
|
return if unnecessary.empty?
|
|
43
37
|
|
|
@@ -61,14 +55,17 @@ module PngConform
|
|
|
61
55
|
# Estimate if 8-bit would be sufficient
|
|
62
56
|
if could_use_8_bit?
|
|
63
57
|
current_size = @result.file_size
|
|
64
|
-
estimated_savings = (
|
|
58
|
+
estimated_savings = (
|
|
59
|
+
current_size *
|
|
60
|
+
@config.optimization_percentages[:bit_depth_reduction] / 100.0
|
|
61
|
+
).to_i
|
|
65
62
|
|
|
66
63
|
@suggestions << {
|
|
67
64
|
type: :reduce_bit_depth,
|
|
68
65
|
priority: :high,
|
|
69
66
|
savings_bytes: estimated_savings,
|
|
70
67
|
description: "Convert from 16-bit to 8-bit depth " \
|
|
71
|
-
"(estimated
|
|
68
|
+
"(estimated ~#{@config.optimization_percentages[:bit_depth_reduction]}% file size reduction)",
|
|
72
69
|
current: "16-bit",
|
|
73
70
|
recommended: "8-bit",
|
|
74
71
|
}
|
|
@@ -79,15 +76,18 @@ module PngConform
|
|
|
79
76
|
# Get color type from IHDR
|
|
80
77
|
ihdr = @result.ihdr_chunk
|
|
81
78
|
return unless ihdr && get_color_type(ihdr) == 2 # RGB
|
|
82
|
-
return if @result.file_size <
|
|
79
|
+
return if @result.file_size < @config.size_thresholds[:palette_opportunity]
|
|
83
80
|
|
|
84
81
|
# If it's RGB but could be palette
|
|
85
82
|
@suggestions << {
|
|
86
83
|
type: :convert_to_palette,
|
|
87
84
|
priority: :medium,
|
|
88
|
-
savings_bytes: (
|
|
85
|
+
savings_bytes: (
|
|
86
|
+
@result.file_size *
|
|
87
|
+
@config.optimization_percentages[:palette_conversion] / 100.0
|
|
88
|
+
).to_i,
|
|
89
89
|
description: "Consider converting to palette mode if using limited colors " \
|
|
90
|
-
"(potential
|
|
90
|
+
"(potential ~#{@config.optimization_percentages[:palette_conversion]}% reduction)",
|
|
91
91
|
current: "RGB (Truecolor)",
|
|
92
92
|
recommended: "Indexed (Palette)",
|
|
93
93
|
}
|
|
@@ -99,25 +99,30 @@ module PngConform
|
|
|
99
99
|
return unless ihdr && get_interlace_method(ihdr) == 1
|
|
100
100
|
|
|
101
101
|
# Interlaced PNGs are larger
|
|
102
|
-
savings = (
|
|
102
|
+
savings = (
|
|
103
|
+
@result.file_size *
|
|
104
|
+
@config.optimization_percentages[:interlace_removal] / 100.0
|
|
105
|
+
).to_i
|
|
103
106
|
|
|
104
107
|
@suggestions << {
|
|
105
108
|
type: :remove_interlacing,
|
|
106
109
|
priority: :low,
|
|
107
110
|
savings_bytes: savings,
|
|
108
111
|
description: "Remove interlacing for smaller file size " \
|
|
109
|
-
"(
|
|
112
|
+
"(~#{@config.optimization_percentages[:interlace_removal]}% reduction, but slower initial display)",
|
|
110
113
|
current: "Adam7 interlaced",
|
|
111
114
|
recommended: "Non-interlaced",
|
|
112
115
|
}
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
def check_text_chunks
|
|
116
|
-
text_chunks = @result.chunks.select
|
|
119
|
+
text_chunks = @result.chunks.select do |c|
|
|
120
|
+
@config.text_chunks.include?(c.type)
|
|
121
|
+
end
|
|
117
122
|
return if text_chunks.empty?
|
|
118
123
|
|
|
119
124
|
total_text_size = text_chunks.sum { |c| c.length + 12 }
|
|
120
|
-
return if total_text_size <
|
|
125
|
+
return if total_text_size < @config.size_thresholds[:text_metadata]
|
|
121
126
|
|
|
122
127
|
@suggestions << {
|
|
123
128
|
type: :reduce_metadata,
|
|
@@ -131,14 +136,15 @@ module PngConform
|
|
|
131
136
|
|
|
132
137
|
def check_metadata_size
|
|
133
138
|
metadata_chunks = @result.chunks.select do |c|
|
|
134
|
-
|
|
139
|
+
@config.metadata_chunks.include?(c.type)
|
|
135
140
|
end
|
|
136
141
|
|
|
137
142
|
total_metadata = metadata_chunks.sum { |c| c.length + 12 }
|
|
138
143
|
file_size = @result.file_size
|
|
139
144
|
|
|
140
|
-
# If metadata is more than
|
|
141
|
-
|
|
145
|
+
# If metadata is more than threshold percent of file size
|
|
146
|
+
threshold = @config.optimization_percentages[:metadata_threshold]
|
|
147
|
+
return unless total_metadata > file_size * threshold / 100.0
|
|
142
148
|
|
|
143
149
|
@suggestions << {
|
|
144
150
|
type: :excessive_metadata,
|
|
@@ -173,7 +179,7 @@ module PngConform
|
|
|
173
179
|
def could_use_8_bit?
|
|
174
180
|
# Conservative heuristic: suggest 8-bit for smaller files
|
|
175
181
|
# Without pixel analysis, we're conservative
|
|
176
|
-
@result.file_size <
|
|
182
|
+
@result.file_size < @config.size_thresholds[:small_file]
|
|
177
183
|
end
|
|
178
184
|
|
|
179
185
|
def calculate_total_savings
|