png_conform 0.1.2 → 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 +116 -6
- data/Gemfile +1 -1
- data/config/validation_profiles.yml +105 -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 -53
- data/lib/png_conform/configuration.rb +147 -0
- data/lib/png_conform/container.rb +113 -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/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 +53 -337
- data/lib/png_conform/services/validator_pool.rb +142 -0
- data/lib/png_conform/utils/colorizer.rb +149 -0
- data/lib/png_conform/validators/chunk_registry.rb +12 -0
- data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
- data/lib/png_conform/version.rb +1 -1
- data/png_conform.gemspec +1 -0
- metadata +38 -2
data/lib/png_conform/cli.rb
CHANGED
|
@@ -36,6 +36,10 @@ module PngConform
|
|
|
36
36
|
--metrics Show detailed metrics (JSON/YAML)
|
|
37
37
|
--resolution Show resolution and Retina analysis
|
|
38
38
|
--mobile-ready Check mobile and Retina readiness
|
|
39
|
+
--batch Enable batch chunk validation (default: enabled)
|
|
40
|
+
--no-batch Disable batch chunk validation
|
|
41
|
+
--parallel, -j Enable parallel processing for multiple files
|
|
42
|
+
--jobs, -j NUM Number of parallel threads (default: CPU count)
|
|
39
43
|
|
|
40
44
|
Examples:
|
|
41
45
|
png_conform check image.png
|
|
@@ -72,6 +76,14 @@ module PngConform
|
|
|
72
76
|
desc: "Show resolution and Retina/DPI analysis"
|
|
73
77
|
option :mobile_ready, type: :boolean, default: false,
|
|
74
78
|
desc: "Check mobile and Retina readiness"
|
|
79
|
+
option :batch, type: :boolean, default: true,
|
|
80
|
+
desc: "Enable batch chunk validation (faster for files with many chunks)"
|
|
81
|
+
option :no_batch, type: :boolean, default: false,
|
|
82
|
+
desc: "Disable batch chunk validation"
|
|
83
|
+
option :parallel, type: :boolean, default: false, aliases: "-j",
|
|
84
|
+
desc: "Enable parallel processing for multiple files"
|
|
85
|
+
option :jobs, type: :numeric, default: nil, aliases: "-j",
|
|
86
|
+
desc: "Number of parallel threads (default: CPU count)"
|
|
75
87
|
def check(*files)
|
|
76
88
|
Commands::CheckCommand.new(files, options).run
|
|
77
89
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../
|
|
3
|
+
require_relative "../container"
|
|
4
|
+
require_relative "../utils/colorizer"
|
|
4
5
|
require_relative "../services/profile_manager"
|
|
5
6
|
require_relative "../reporters/reporter_factory"
|
|
6
|
-
require_relative "../readers/streaming_reader"
|
|
7
7
|
|
|
8
8
|
module PngConform
|
|
9
9
|
module Commands
|
|
@@ -11,6 +11,7 @@ module PngConform
|
|
|
11
11
|
#
|
|
12
12
|
# Coordinates between readers, validators, and reporters to analyze
|
|
13
13
|
# PNG files according to specified options and profiles.
|
|
14
|
+
#
|
|
14
15
|
class CheckCommand
|
|
15
16
|
attr_reader :files, :options
|
|
16
17
|
|
|
@@ -61,14 +62,105 @@ module PngConform
|
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# Validate all specified files.
|
|
65
|
+
#
|
|
66
|
+
# Uses parallel validation if --parallel flag is set with multiple files.
|
|
67
|
+
# Otherwise processes files sequentially.
|
|
64
68
|
def validate_files
|
|
65
69
|
reporter = create_reporter
|
|
66
70
|
|
|
71
|
+
# Use parallel validation if enabled and multiple files
|
|
72
|
+
if files.length > 1 && @options[:parallel]
|
|
73
|
+
validate_files_parallel(reporter)
|
|
74
|
+
else
|
|
75
|
+
validate_files_sequential(reporter)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Validate files sequentially (original behavior)
|
|
80
|
+
#
|
|
81
|
+
# @param reporter [Reporters::BaseReporter] Reporter for output
|
|
82
|
+
def validate_files_sequential(reporter)
|
|
67
83
|
files.each do |file_path|
|
|
68
84
|
validate_single_file(file_path, reporter)
|
|
69
85
|
end
|
|
70
86
|
end
|
|
71
87
|
|
|
88
|
+
# Validate files in parallel for better performance
|
|
89
|
+
#
|
|
90
|
+
# @param reporter [Reporters::BaseReporter] Reporter for output
|
|
91
|
+
def validate_files_parallel(reporter)
|
|
92
|
+
require_relative "../services/parallel_validator"
|
|
93
|
+
|
|
94
|
+
parallel_validator = Services::ParallelValidator.new(files, @options)
|
|
95
|
+
results = parallel_validator.validate_all
|
|
96
|
+
|
|
97
|
+
# Process results and report
|
|
98
|
+
results.each do |result|
|
|
99
|
+
if result.key?(:error)
|
|
100
|
+
handle_validation_error(result)
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@errors_found = true unless result.valid?
|
|
105
|
+
reporter.report(result)
|
|
106
|
+
|
|
107
|
+
# Show additional analysis for text format
|
|
108
|
+
show_additional_analysis(result) if should_show_analysis?
|
|
109
|
+
end
|
|
110
|
+
rescue Interrupt
|
|
111
|
+
# Handle Ctrl+C gracefully
|
|
112
|
+
puts "\nValidation interrupted by user."
|
|
113
|
+
raise
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
puts "Error during parallel validation: #{e.message}"
|
|
116
|
+
puts e.backtrace.join("\n") if @options[:verbose]
|
|
117
|
+
@errors_found = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Handle validation error from parallel processing
|
|
121
|
+
#
|
|
122
|
+
# @param error_result [Hash] Error result from parallel validation
|
|
123
|
+
def handle_validation_error(error_result)
|
|
124
|
+
puts "Error: #{error_result[:error]}"
|
|
125
|
+
@errors_found = true
|
|
126
|
+
|
|
127
|
+
if error_result[:backtrace] && @options[:verbose]
|
|
128
|
+
puts "Backtrace:"
|
|
129
|
+
error_result[:backtrace].each { |line| puts " #{line}" }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if additional analysis should be shown
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean] True if analysis should be shown
|
|
136
|
+
def should_show_analysis?
|
|
137
|
+
return false if @options[:quiet]
|
|
138
|
+
return true if @options[:metrics] || @options[:mobile_ready] || @options[:resolution] || @options[:optimize]
|
|
139
|
+
|
|
140
|
+
# Text format shows analysis by default (unless quiet)
|
|
141
|
+
@options[:format].nil? || @options[:format] == "text"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Show additional analysis (resolution, optimization, metrics)
|
|
145
|
+
#
|
|
146
|
+
# @param file_analysis [FileAnalysis] File analysis results
|
|
147
|
+
def show_additional_analysis(file_analysis)
|
|
148
|
+
show_resolution_analysis(file_analysis) if @options[:resolution] || @options[:mobile_ready]
|
|
149
|
+
show_optimization_suggestions(file_analysis) if @options[:optimize]
|
|
150
|
+
show_metrics(file_analysis) if @options[:metrics]
|
|
151
|
+
show_mobile_readiness(file_analysis) if @options[:mobile_ready]
|
|
152
|
+
|
|
153
|
+
# Default: show resolution and optimization for text format (unless quiet)
|
|
154
|
+
if (@options[:format].nil? || @options[:format] == "text") &&
|
|
155
|
+
!@options[:quiet] &&
|
|
156
|
+
!@options[:resolution] &&
|
|
157
|
+
!@options[:mobile_ready] &&
|
|
158
|
+
!@options[:optimize]
|
|
159
|
+
show_resolution_analysis(file_analysis)
|
|
160
|
+
show_optimization_suggestions(file_analysis)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
72
164
|
# Validate a single PNG file.
|
|
73
165
|
#
|
|
74
166
|
# @param file_path [String] Path to the PNG file
|
|
@@ -86,12 +178,12 @@ module PngConform
|
|
|
86
178
|
return
|
|
87
179
|
end
|
|
88
180
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
file_analysis =
|
|
181
|
+
# Use container to create reader and orchestrator
|
|
182
|
+
Container.open_reader(:streaming, file_path) do |reader|
|
|
183
|
+
options_with_path = @options.merge(filepath: file_path)
|
|
184
|
+
orchestrator = Container.validation_orchestrator(reader, file_path,
|
|
185
|
+
options_with_path)
|
|
186
|
+
file_analysis = orchestrator.validate
|
|
95
187
|
|
|
96
188
|
# Track if any errors were found
|
|
97
189
|
@errors_found = true unless file_analysis.valid?
|
|
@@ -99,15 +191,8 @@ module PngConform
|
|
|
99
191
|
# Use reporter to output result
|
|
100
192
|
reporter.report(file_analysis)
|
|
101
193
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
show_resolution_analysis(file_analysis)
|
|
105
|
-
show_optimization_suggestions(file_analysis)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Explicit flags always show
|
|
109
|
-
show_metrics(file_analysis) if options[:metrics]
|
|
110
|
-
show_mobile_readiness(file_analysis) if options[:mobile_ready]
|
|
194
|
+
# Show additional analysis
|
|
195
|
+
show_additional_analysis(file_analysis) if should_show_analysis?
|
|
111
196
|
end
|
|
112
197
|
rescue StandardError => e
|
|
113
198
|
puts "Error processing #{file_path}: #{e.message}"
|
|
@@ -119,7 +204,7 @@ module PngConform
|
|
|
119
204
|
#
|
|
120
205
|
# @return [Reporters::BaseReporter] Reporter instance
|
|
121
206
|
def create_reporter
|
|
122
|
-
|
|
207
|
+
Container.reporter(
|
|
123
208
|
format: options[:format] || "text",
|
|
124
209
|
verbose: options[:verbose] || options[:very_verbose],
|
|
125
210
|
quiet: options[:quiet],
|
|
@@ -136,13 +221,10 @@ module PngConform
|
|
|
136
221
|
return unless analysis && analysis[:suggestions]
|
|
137
222
|
return if analysis[:suggestions].empty?
|
|
138
223
|
|
|
139
|
-
puts "\n#{
|
|
224
|
+
puts "\n#{Utils::Colorizer.bold('OPTIMIZATION SUGGESTIONS:')}"
|
|
140
225
|
analysis[:suggestions].each_with_index do |suggestion, index|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
puts " #{index + 1}. [#{colorize(priority_label,
|
|
145
|
-
priority_color)}] #{suggestion[:description]}"
|
|
226
|
+
puts " #{index + 1}. #{Utils::Colorizer.priority(suggestion[:description],
|
|
227
|
+
suggestion[:priority])}"
|
|
146
228
|
if suggestion[:savings_bytes]&.positive?
|
|
147
229
|
puts " Savings: #{format_bytes(suggestion[:savings_bytes])}"
|
|
148
230
|
end
|
|
@@ -150,7 +232,7 @@ module PngConform
|
|
|
150
232
|
|
|
151
233
|
total_savings = analysis[:potential_savings_bytes]
|
|
152
234
|
if total_savings.positive?
|
|
153
|
-
puts "\n #{
|
|
235
|
+
puts "\n #{Utils::Colorizer.bold('Total Potential Savings:')} " \
|
|
154
236
|
"#{format_bytes(total_savings)} (#{analysis[:potential_savings_percent]}%)"
|
|
155
237
|
end
|
|
156
238
|
end
|
|
@@ -169,7 +251,7 @@ module PngConform
|
|
|
169
251
|
puts metrics.to_yaml
|
|
170
252
|
else
|
|
171
253
|
# Text format with colored output
|
|
172
|
-
puts "\n#{
|
|
254
|
+
puts "\n#{Utils::Colorizer.bold('METRICS:')}"
|
|
173
255
|
puts " File: #{metrics[:file][:filename]} (#{metrics[:file][:size_kb]} KB)"
|
|
174
256
|
puts " Image: #{metrics[:image][:dimensions]}, #{metrics[:image][:color_type_name]}, " \
|
|
175
257
|
"#{metrics[:image][:bit_depth]}-bit"
|
|
@@ -184,7 +266,7 @@ module PngConform
|
|
|
184
266
|
analysis = file_analysis.resolution_analysis
|
|
185
267
|
return unless analysis
|
|
186
268
|
|
|
187
|
-
puts "\n#{
|
|
269
|
+
puts "\n#{Utils::Colorizer.bold('RESOLUTION ANALYSIS:')}"
|
|
188
270
|
|
|
189
271
|
# Basic info
|
|
190
272
|
res = analysis[:resolution]
|
|
@@ -192,7 +274,7 @@ module PngConform
|
|
|
192
274
|
puts " DPI: #{res[:dpi] || 'Not specified'}"
|
|
193
275
|
|
|
194
276
|
# Retina analysis
|
|
195
|
-
puts "\n #{
|
|
277
|
+
puts "\n #{Utils::Colorizer.bold('Retina Analysis:')}"
|
|
196
278
|
retina = analysis[:retina]
|
|
197
279
|
puts " @1x: #{retina[:at_1x][:dimensions_pt]} (#{retina[:at_1x][:suitable_for].first})"
|
|
198
280
|
puts " @2x: #{retina[:at_2x][:dimensions_pt]} (#{retina[:at_2x][:suitable_for].first})"
|
|
@@ -211,7 +293,7 @@ module PngConform
|
|
|
211
293
|
# Print analysis if DPI available
|
|
212
294
|
if analysis[:print][:capable]
|
|
213
295
|
print_info = analysis[:print]
|
|
214
|
-
puts "\n #{
|
|
296
|
+
puts "\n #{Utils::Colorizer.bold('Print Analysis:')}"
|
|
215
297
|
puts " Quality: #{print_info[:quality]} (#{print_info[:dpi]} DPI)"
|
|
216
298
|
phys = print_info[:physical_size]
|
|
217
299
|
puts " Physical Size: #{phys[:width_inches]}\" x #{phys[:height_inches]}\" " \
|
|
@@ -221,11 +303,10 @@ module PngConform
|
|
|
221
303
|
# Recommendations
|
|
222
304
|
recommendations = analysis[:recommendations]
|
|
223
305
|
if recommendations && !recommendations.empty?
|
|
224
|
-
puts "\n #{
|
|
306
|
+
puts "\n #{Utils::Colorizer.bold('Recommendations:')}"
|
|
225
307
|
recommendations.each do |rec|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
priority_color)}] #{rec[:message]}"
|
|
308
|
+
puts " #{Utils::Colorizer.priority(rec[:message],
|
|
309
|
+
rec[:priority])}"
|
|
229
310
|
end
|
|
230
311
|
end
|
|
231
312
|
end
|
|
@@ -235,7 +316,7 @@ module PngConform
|
|
|
235
316
|
analysis = file_analysis.resolution_analysis
|
|
236
317
|
return unless analysis
|
|
237
318
|
|
|
238
|
-
puts "\n#{
|
|
319
|
+
puts "\n#{Utils::Colorizer.bold('MOBILE & RETINA READINESS:')}"
|
|
239
320
|
|
|
240
321
|
retina = analysis[:retina]
|
|
241
322
|
web = analysis[:web]
|
|
@@ -243,10 +324,9 @@ module PngConform
|
|
|
243
324
|
# Overall readiness
|
|
244
325
|
is_ready = retina[:is_retina_ready] && web[:mobile_friendly]
|
|
245
326
|
status = if is_ready
|
|
246
|
-
|
|
247
|
-
:green)
|
|
327
|
+
Utils::Colorizer.success("✓ READY")
|
|
248
328
|
else
|
|
249
|
-
|
|
329
|
+
Utils::Colorizer.error("✗ NOT READY")
|
|
250
330
|
end
|
|
251
331
|
puts " Status: #{status}"
|
|
252
332
|
|
|
@@ -276,7 +356,7 @@ module PngConform
|
|
|
276
356
|
# Helper methods for formatting
|
|
277
357
|
|
|
278
358
|
def format_check(passed)
|
|
279
|
-
passed ?
|
|
359
|
+
passed ? Utils::Colorizer.success("✓") : Utils::Colorizer.error("✗")
|
|
280
360
|
end
|
|
281
361
|
|
|
282
362
|
def format_bytes(bytes)
|
|
@@ -298,21 +378,6 @@ module PngConform
|
|
|
298
378
|
end
|
|
299
379
|
end
|
|
300
380
|
|
|
301
|
-
def colorize(text, color)
|
|
302
|
-
return text if options[:no_color]
|
|
303
|
-
|
|
304
|
-
codes = {
|
|
305
|
-
red: "\e[31m",
|
|
306
|
-
green: "\e[32m",
|
|
307
|
-
yellow: "\e[33m",
|
|
308
|
-
blue: "\e[34m",
|
|
309
|
-
bold: "\e[1m",
|
|
310
|
-
reset: "\e[0m",
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
"#{codes[color]}#{text}#{codes[:reset]}"
|
|
314
|
-
end
|
|
315
|
-
|
|
316
381
|
# Determine the exit code based on whether errors were found.
|
|
317
382
|
#
|
|
318
383
|
# @return [Integer] Exit code (0 for success, 1 for errors)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
# Application configuration
|
|
5
|
+
#
|
|
6
|
+
# Centralized configuration for all hardcoded values across the codebase.
|
|
7
|
+
# Provides a single source of truth for constants and settings.
|
|
8
|
+
#
|
|
9
|
+
class Configuration
|
|
10
|
+
attr_accessor :retina_densities, :print_dpi_thresholds,
|
|
11
|
+
:network_speeds, :optimization_thresholds,
|
|
12
|
+
:color_enabled, :screen_dpi, :css_reference_dpi,
|
|
13
|
+
:retina_scalers, :unnecessary_web_chunks,
|
|
14
|
+
:text_chunks, :metadata_chunks,
|
|
15
|
+
:optimization_percentages, :size_thresholds
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Get the singleton instance
|
|
19
|
+
#
|
|
20
|
+
# @return [Configuration] Configuration instance
|
|
21
|
+
def instance
|
|
22
|
+
@instance ||= new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Reset configuration to defaults
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
def reset!
|
|
29
|
+
@instance = new
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initialize configuration with defaults
|
|
34
|
+
#
|
|
35
|
+
def initialize
|
|
36
|
+
# Retina display densities (Android buckets)
|
|
37
|
+
@retina_densities = {
|
|
38
|
+
mdpi: 160,
|
|
39
|
+
hdpi: 320,
|
|
40
|
+
xhdpi: 480,
|
|
41
|
+
xxhdpi: 640,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Print DPI quality thresholds
|
|
45
|
+
@print_dpi_thresholds = {
|
|
46
|
+
minimum: 150,
|
|
47
|
+
good: 300,
|
|
48
|
+
excellent: 600,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Network speeds for load time estimation (bytes per second)
|
|
52
|
+
@network_speeds = {
|
|
53
|
+
slow: 1_000_000, # 1 Mbps
|
|
54
|
+
fast: 10_000_000, # 10 Mbps
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Optimization percentage thresholds
|
|
58
|
+
@optimization_thresholds = {
|
|
59
|
+
high_savings: 30, # 30%
|
|
60
|
+
medium_savings: 10, # 10%
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Color output enabled
|
|
64
|
+
@color_enabled = true
|
|
65
|
+
|
|
66
|
+
# Screen DPI constants
|
|
67
|
+
@screen_dpi = 72
|
|
68
|
+
|
|
69
|
+
# CSS reference DPI for Retina calculations
|
|
70
|
+
@css_reference_dpi = 163
|
|
71
|
+
|
|
72
|
+
# Retina display scaling factors
|
|
73
|
+
@retina_scalers = {
|
|
74
|
+
"1x": 1.0,
|
|
75
|
+
"2x": 2.0,
|
|
76
|
+
"3x": 3.0,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Chunks unnecessary for web/mobile use
|
|
80
|
+
@unnecessary_web_chunks = %w[tIME pHYs oFFs pCAL sCAL sTER].freeze
|
|
81
|
+
|
|
82
|
+
# Text chunk types
|
|
83
|
+
@text_chunks = %w[tEXt zTXt iTXt].freeze
|
|
84
|
+
|
|
85
|
+
# Metadata chunk types
|
|
86
|
+
@metadata_chunks = %w[tEXt zTXt iTXt tIME pHYs].freeze
|
|
87
|
+
|
|
88
|
+
# Optimization savings percentages
|
|
89
|
+
@optimization_percentages = {
|
|
90
|
+
bit_depth_reduction: 45, # % savings for 16->8 bit
|
|
91
|
+
palette_conversion: 30, # % savings for RGB->palette
|
|
92
|
+
interlace_removal: 15, # % savings for removing interlace
|
|
93
|
+
metadata_threshold: 10, # % of file size to flag excessive metadata
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Size thresholds
|
|
97
|
+
@size_thresholds = {
|
|
98
|
+
palette_opportunity: 10_000, # bytes - min file size to suggest palette
|
|
99
|
+
text_metadata: 500, # bytes - min text metadata size to flag
|
|
100
|
+
small_file: 100_000, # bytes - file size considered "small"
|
|
101
|
+
retina_ready_min: 88, # pixels - min dimension for retina ready
|
|
102
|
+
very_large: 3000, # pixels - dimension considered "very large"
|
|
103
|
+
large_for_mobile: 1920, # pixels - max dimension for mobile friendly
|
|
104
|
+
large_for_web: 4096, # pixels - max dimension for web suitable
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get density value by name
|
|
109
|
+
#
|
|
110
|
+
# @param name [Symbol] Density name (:mdpi, :hdpi, :xhdpi, :xxhdpi)
|
|
111
|
+
# @return [Integer] DPI value
|
|
112
|
+
def retina_density(name)
|
|
113
|
+
@retina_densities[name] || @retina_densities[:hdpi]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get print DPI threshold by name
|
|
117
|
+
#
|
|
118
|
+
# @param name [Symbol] Threshold name (:minimum, :good, :excellent)
|
|
119
|
+
# @return [Integer] DPI value
|
|
120
|
+
def print_dpi_threshold(name)
|
|
121
|
+
@print_dpi_thresholds[name] || @print_dpi_thresholds[:good]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get network speed by name
|
|
125
|
+
#
|
|
126
|
+
# @param name [Symbol] Speed name (:slow, :fast)
|
|
127
|
+
# @return [Integer] Bytes per second
|
|
128
|
+
def network_speed(name)
|
|
129
|
+
@network_speeds[name] || @network_speeds[:fast]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get optimization threshold by name
|
|
133
|
+
#
|
|
134
|
+
# @param name [Symbol] Threshold name (:high_savings, :medium_savings)
|
|
135
|
+
# @return [Integer] Percentage threshold
|
|
136
|
+
def optimization_threshold(name)
|
|
137
|
+
@optimization_thresholds[name] || @optimization_thresholds[:medium_savings]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Check if colors are enabled
|
|
141
|
+
#
|
|
142
|
+
# @return [Boolean] true if colors are enabled
|
|
143
|
+
def color_enabled?
|
|
144
|
+
@color_enabled
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "readers/streaming_reader"
|
|
4
|
+
require_relative "services/profile_manager"
|
|
5
|
+
require_relative "services/analysis_manager"
|
|
6
|
+
|
|
7
|
+
module PngConform
|
|
8
|
+
# Dependency Injection Container
|
|
9
|
+
#
|
|
10
|
+
# Centralized dependency management for the application.
|
|
11
|
+
# Provides factory methods for creating readers, validators, reporters,
|
|
12
|
+
# and other services with proper dependency injection.
|
|
13
|
+
#
|
|
14
|
+
# This makes testing easier by allowing mock injection and provides
|
|
15
|
+
# a single source of truth for object creation.
|
|
16
|
+
#
|
|
17
|
+
class Container
|
|
18
|
+
class << self
|
|
19
|
+
# Create a reader for the given type and filepath
|
|
20
|
+
#
|
|
21
|
+
# @param type [Symbol] Reader type (:streaming or :full_load)
|
|
22
|
+
# @param filepath [String] Path to PNG file
|
|
23
|
+
# @param options [Hash] Additional options for the reader
|
|
24
|
+
# @return [Object] Reader instance
|
|
25
|
+
def reader(type, filepath, options = {})
|
|
26
|
+
case type
|
|
27
|
+
when :streaming
|
|
28
|
+
io = File.open(filepath, "rb")
|
|
29
|
+
Readers::StreamingReader.new(io, options)
|
|
30
|
+
when :full_load
|
|
31
|
+
Readers::FullLoadReader.new(filepath, options)
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError, "Unknown reader type: #{type}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Open a reader with automatic file closing
|
|
38
|
+
#
|
|
39
|
+
# @param type [Symbol] Reader type (:streaming or :full_load)
|
|
40
|
+
# @param filepath [String] Path to PNG file
|
|
41
|
+
# @param options [Hash] Additional options for the reader
|
|
42
|
+
# @yield [reader] Reader instance
|
|
43
|
+
# @return [Object] Result of the block
|
|
44
|
+
def open_reader(type, filepath, _options = {}, &block)
|
|
45
|
+
case type
|
|
46
|
+
when :streaming
|
|
47
|
+
Readers::StreamingReader.open(filepath, &block)
|
|
48
|
+
when :full_load
|
|
49
|
+
Readers::FullLoadReader.open(filepath, &block)
|
|
50
|
+
else
|
|
51
|
+
raise ArgumentError, "Unknown reader type: #{type}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create a validator for a chunk
|
|
56
|
+
#
|
|
57
|
+
# Delegates to ChunkRegistry for validator creation
|
|
58
|
+
#
|
|
59
|
+
# @param chunk [Object] Chunk object
|
|
60
|
+
# @param context [ValidationContext] Validation context
|
|
61
|
+
# @return [Object] Validator instance or nil
|
|
62
|
+
def validator(chunk, context)
|
|
63
|
+
require_relative "validators/chunk_registry"
|
|
64
|
+
Validators::ChunkRegistry.create_validator(chunk, context)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Create a reporter based on options
|
|
68
|
+
#
|
|
69
|
+
# Delegates to ReporterFactory for reporter creation
|
|
70
|
+
#
|
|
71
|
+
# @param options [Hash] Reporter options
|
|
72
|
+
# @return [Object] Reporter instance
|
|
73
|
+
def reporter(options)
|
|
74
|
+
require_relative "reporters/reporter_factory"
|
|
75
|
+
Reporters::ReporterFactory.create(**options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get the profile manager singleton
|
|
79
|
+
#
|
|
80
|
+
# @return [ProfileManager] Profile manager instance
|
|
81
|
+
def profile_manager
|
|
82
|
+
@profile_manager ||= Services::ProfileManager.new
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Create an analysis manager
|
|
86
|
+
#
|
|
87
|
+
# @param options [Hash] Analysis options
|
|
88
|
+
# @return [AnalysisManager] Analysis manager instance
|
|
89
|
+
def analysis_manager(options = {})
|
|
90
|
+
Services::AnalysisManager.new(options)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create a validation orchestrator
|
|
94
|
+
#
|
|
95
|
+
# @param reader [Object] File reader
|
|
96
|
+
# @param filepath [String] File path
|
|
97
|
+
# @param options [Hash] Validation options
|
|
98
|
+
# @return [ValidationOrchestrator] Orchestrator instance
|
|
99
|
+
def validation_orchestrator(reader, filepath = nil, options = {})
|
|
100
|
+
require_relative "services/validation_orchestrator"
|
|
101
|
+
Services::ValidationOrchestrator.new(reader, filepath, options)
|
|
102
|
+
end
|
|
103
|
+
alias_method :validation_service, :validation_orchestrator
|
|
104
|
+
|
|
105
|
+
# Reset container state
|
|
106
|
+
#
|
|
107
|
+
# Clears cached instances (useful for testing)
|
|
108
|
+
def reset!
|
|
109
|
+
@profile_manager = nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -13,15 +13,26 @@ module PngConform
|
|
|
13
13
|
attribute :compression_ratio, :float
|
|
14
14
|
attribute :crc_errors_count, :integer, default: -> { 0 }
|
|
15
15
|
|
|
16
|
+
# Non-serialized hash map for fast chunk type lookup
|
|
17
|
+
attr_reader :chunks_by_type_map
|
|
18
|
+
|
|
16
19
|
# File types
|
|
17
20
|
FILE_TYPE_PNG = "PNG"
|
|
18
21
|
FILE_TYPE_MNG = "MNG"
|
|
19
22
|
FILE_TYPE_JNG = "JNG"
|
|
20
23
|
FILE_TYPE_UNKNOWN = "UNKNOWN"
|
|
21
24
|
|
|
25
|
+
# Initialize with hash map for fast lookups
|
|
26
|
+
def initialize(*args)
|
|
27
|
+
super(*args)
|
|
28
|
+
@chunks_by_type_map = {}
|
|
29
|
+
rebuild_chunks_map
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
# Add a chunk to the result
|
|
23
33
|
def add_chunk(chunk)
|
|
24
34
|
chunks << chunk
|
|
35
|
+
add_to_chunks_map(chunk)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# Add an error to the result
|
|
@@ -90,14 +101,14 @@ module PngConform
|
|
|
90
101
|
chunks.count
|
|
91
102
|
end
|
|
92
103
|
|
|
93
|
-
# Find chunks by type
|
|
104
|
+
# Find chunks by type (O(1) hash lookup)
|
|
94
105
|
def chunks_by_type(type)
|
|
95
|
-
|
|
106
|
+
@chunks_by_type_map[type] || []
|
|
96
107
|
end
|
|
97
108
|
|
|
98
|
-
# Check if file has specific chunk type
|
|
109
|
+
# Check if file has specific chunk type (O(1) hash lookup)
|
|
99
110
|
def has_chunk?(type)
|
|
100
|
-
|
|
111
|
+
@chunks_by_type_map.key?(type) && !@chunks_by_type_map[type].empty?
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
# Get IHDR chunk (PNG/JNG)
|
|
@@ -132,6 +143,21 @@ module PngConform
|
|
|
132
143
|
end
|
|
133
144
|
parts.join("\n")
|
|
134
145
|
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Add chunk to hash map for fast lookup
|
|
150
|
+
def add_to_chunks_map(chunk)
|
|
151
|
+
chunk_type = chunk.type
|
|
152
|
+
@chunks_by_type_map[chunk_type] ||= []
|
|
153
|
+
@chunks_by_type_map[chunk_type] << chunk
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Rebuild hash map from chunks (for deserialization or external modification)
|
|
157
|
+
def rebuild_chunks_map
|
|
158
|
+
@chunks_by_type_map.clear
|
|
159
|
+
chunks.each { |chunk| add_to_chunks_map(chunk) }
|
|
160
|
+
end
|
|
135
161
|
end
|
|
136
162
|
end
|
|
137
163
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Pipelines
|
|
5
|
+
# Result object for pipeline stages
|
|
6
|
+
#
|
|
7
|
+
# Carries state through the validation pipeline.
|
|
8
|
+
#
|
|
9
|
+
class PipelineResult
|
|
10
|
+
attr_accessor :ready_for_analysis
|
|
11
|
+
attr_reader :context, :chunks
|
|
12
|
+
|
|
13
|
+
# Initialize pipeline result
|
|
14
|
+
#
|
|
15
|
+
# @param context [ValidationContext] Validation context
|
|
16
|
+
# @param chunks [Array] Array of chunks
|
|
17
|
+
def initialize(context:, chunks: [])
|
|
18
|
+
@context = context
|
|
19
|
+
@chunks = chunks
|
|
20
|
+
@ready_for_analysis = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Check if validation has errors
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean] true if there are errors
|
|
26
|
+
def has_errors?
|
|
27
|
+
@context.has_errors?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if validation should terminate early
|
|
31
|
+
#
|
|
32
|
+
# @param fail_fast [Boolean] Whether fail_fast is enabled
|
|
33
|
+
# @return [Boolean] true if should terminate
|
|
34
|
+
def should_terminate?(fail_fast: false)
|
|
35
|
+
fail_fast && has_errors?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|