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
@@ -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)
@@ -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
- # Metadata chunk types
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
- result.chunks.count { |c| METADATA_CHUNKS.include?(c.type) }
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
- # Text chunk types
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
- TEXT_CHUNKS.include?(c.type)
149
+ @config.text_chunks.include?(c.type)
153
150
  end,
154
151
  metadata_chunks_count: @result.chunks.count do |c|
155
- METADATA_CHUNKS.include?(c.type)
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
- # Chunks that are often unnecessary for web/mobile use
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
- UNNECESSARY_FOR_WEB.include?(c.type)
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 = (current_size * 0.45).to_i # ~45% reduction
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 ~45% file size reduction)",
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 < 10_000 # Skip small files
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: (@result.file_size * 0.30).to_i,
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 ~30% reduction)",
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 = (@result.file_size * 0.15).to_i
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
- "(~15% reduction, but slower initial display)",
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 { |c| TEXT_CHUNKS.include?(c.type) }
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 < 500 # Ignore small metadata
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
- METADATA_CHUNKS.include?(c.type)
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 10% of file size
141
- return unless total_metadata > file_size * 0.1
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 < 100_000
182
+ @result.file_size < @config.size_thresholds[:small_file]
177
183
  end
178
184
 
179
185
  def calculate_total_savings