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
|
@@ -1,22 +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 resolution and DPI for various use cases
|
|
6
8
|
class ResolutionAnalyzer
|
|
7
|
-
|
|
8
|
-
SCREEN_DPI = 72
|
|
9
|
-
PRINT_DPI_LOW = 150
|
|
10
|
-
PRINT_DPI_STANDARD = 300
|
|
11
|
-
PRINT_DPI_HIGH = 600
|
|
12
|
-
|
|
13
|
-
# Retina display densities
|
|
14
|
-
RETINA_1X = 1.0
|
|
15
|
-
RETINA_2X = 2.0
|
|
16
|
-
RETINA_3X = 3.0
|
|
17
|
-
|
|
18
|
-
def initialize(result)
|
|
9
|
+
def initialize(result, config: Configuration.instance)
|
|
19
10
|
@result = result
|
|
11
|
+
@config = config
|
|
20
12
|
ihdr = result.ihdr_chunk
|
|
21
13
|
@width = ihdr ? get_width(ihdr) : 0
|
|
22
14
|
@height = ihdr ? get_height(ihdr) : 0
|
|
@@ -63,9 +55,9 @@ module PngConform
|
|
|
63
55
|
def retina_analysis
|
|
64
56
|
analysis = {
|
|
65
57
|
is_retina_ready: check_retina_ready,
|
|
66
|
-
at_1x: calculate_physical_size(
|
|
67
|
-
at_2x: calculate_physical_size(
|
|
68
|
-
at_3x: calculate_physical_size(
|
|
58
|
+
at_1x: calculate_physical_size(@config.retina_scalers["1x"]),
|
|
59
|
+
at_2x: calculate_physical_size(@config.retina_scalers["2x"]),
|
|
60
|
+
at_3x: calculate_physical_size(@config.retina_scalers["3x"]),
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
analysis[:recommended_density] = recommend_density
|
|
@@ -82,7 +74,7 @@ module PngConform
|
|
|
82
74
|
height_inches = @height.to_f / @dpi
|
|
83
75
|
|
|
84
76
|
{
|
|
85
|
-
capable: @dpi >=
|
|
77
|
+
capable: @dpi >= @config.print_dpi_thresholds[:minimum],
|
|
86
78
|
dpi: @dpi,
|
|
87
79
|
physical_size: {
|
|
88
80
|
width_inches: width_inches.round(2),
|
|
@@ -97,9 +89,11 @@ module PngConform
|
|
|
97
89
|
|
|
98
90
|
def web_analysis
|
|
99
91
|
{
|
|
100
|
-
suitable_for_web: @width <=
|
|
92
|
+
suitable_for_web: @width <= @config.size_thresholds[:large_for_web] &&
|
|
93
|
+
@height <= @config.size_thresholds[:large_for_web],
|
|
101
94
|
typical_screen_size: calculate_screen_coverage,
|
|
102
|
-
mobile_friendly: @width <=
|
|
95
|
+
mobile_friendly: @width <= @config.size_thresholds[:large_for_mobile] &&
|
|
96
|
+
@height <= @config.size_thresholds[:large_for_mobile],
|
|
103
97
|
retina_optimized: @width >= 1000 && @height >= 1000,
|
|
104
98
|
load_time_estimate: estimate_load_time,
|
|
105
99
|
}
|
|
@@ -119,15 +113,15 @@ module PngConform
|
|
|
119
113
|
end
|
|
120
114
|
|
|
121
115
|
def check_retina_ready
|
|
122
|
-
@width >=
|
|
116
|
+
@width >= @config.size_thresholds[:retina_ready_min] &&
|
|
117
|
+
@height >= @config.size_thresholds[:retina_ready_min]
|
|
123
118
|
end
|
|
124
119
|
|
|
125
120
|
def calculate_physical_size(density)
|
|
126
|
-
|
|
127
|
-
effective_dpi = css_reference_dpi * density
|
|
121
|
+
effective_dpi = @config.css_reference_dpi * density
|
|
128
122
|
|
|
129
|
-
width_points = (@width.to_f / effective_dpi *
|
|
130
|
-
height_points = (@height.to_f / effective_dpi *
|
|
123
|
+
width_points = (@width.to_f / effective_dpi * @config.screen_dpi).round(1)
|
|
124
|
+
height_points = (@height.to_f / effective_dpi * @config.screen_dpi).round(1)
|
|
131
125
|
|
|
132
126
|
{
|
|
133
127
|
width_points: width_points,
|
|
@@ -181,15 +175,19 @@ module PngConform
|
|
|
181
175
|
return "Unknown" unless @dpi
|
|
182
176
|
|
|
183
177
|
case @dpi
|
|
184
|
-
when 0
|
|
185
|
-
|
|
186
|
-
when
|
|
187
|
-
|
|
178
|
+
when 0...@config.print_dpi_thresholds[:minimum]
|
|
179
|
+
"Not suitable"
|
|
180
|
+
when @config.print_dpi_thresholds[:minimum]...@config.print_dpi_thresholds[:good]
|
|
181
|
+
"Acceptable"
|
|
182
|
+
when @config.print_dpi_thresholds[:good]...@config.print_dpi_thresholds[:excellent]
|
|
183
|
+
"Good"
|
|
184
|
+
else
|
|
185
|
+
"Excellent"
|
|
188
186
|
end
|
|
189
187
|
end
|
|
190
188
|
|
|
191
189
|
def suitable_print_sizes
|
|
192
|
-
return [] unless @dpi && @dpi >=
|
|
190
|
+
return [] unless @dpi && @dpi >= @config.print_dpi_thresholds[:minimum]
|
|
193
191
|
|
|
194
192
|
width_in = @width.to_f / @dpi
|
|
195
193
|
height_in = @height.to_f / @dpi
|
|
@@ -227,8 +225,7 @@ module PngConform
|
|
|
227
225
|
|
|
228
226
|
def estimate_load_time
|
|
229
227
|
file_size = @result.file_size
|
|
230
|
-
|
|
231
|
-
bytes_per_second = (mbps * 1_000_000 / 8).to_i
|
|
228
|
+
bytes_per_second = @config.network_speeds[:fast]
|
|
232
229
|
seconds = file_size.to_f / bytes_per_second
|
|
233
230
|
|
|
234
231
|
if seconds < 0.1
|
|
@@ -243,7 +240,8 @@ module PngConform
|
|
|
243
240
|
def generate_recommendations
|
|
244
241
|
recs = []
|
|
245
242
|
|
|
246
|
-
if @width <
|
|
243
|
+
if @width < @config.size_thresholds[:retina_ready_min] &&
|
|
244
|
+
@height < @config.size_thresholds[:retina_ready_min]
|
|
247
245
|
recs << {
|
|
248
246
|
category: :retina,
|
|
249
247
|
priority: :high,
|
|
@@ -259,7 +257,8 @@ module PngConform
|
|
|
259
257
|
}
|
|
260
258
|
end
|
|
261
259
|
|
|
262
|
-
if @width >
|
|
260
|
+
if @width > @config.size_thresholds[:very_large] ||
|
|
261
|
+
@height > @config.size_thresholds[:very_large]
|
|
263
262
|
recs << {
|
|
264
263
|
category: :web,
|
|
265
264
|
priority: :high,
|
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,11 +178,12 @@ module PngConform
|
|
|
86
178
|
return
|
|
87
179
|
end
|
|
88
180
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
94
187
|
|
|
95
188
|
# Track if any errors were found
|
|
96
189
|
@errors_found = true unless file_analysis.valid?
|
|
@@ -98,15 +191,8 @@ module PngConform
|
|
|
98
191
|
# Use reporter to output result
|
|
99
192
|
reporter.report(file_analysis)
|
|
100
193
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
show_resolution_analysis(file_analysis)
|
|
104
|
-
show_optimization_suggestions(file_analysis)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Explicit flags always show
|
|
108
|
-
show_metrics(file_analysis) if options[:metrics]
|
|
109
|
-
show_mobile_readiness(file_analysis) if options[:mobile_ready]
|
|
194
|
+
# Show additional analysis
|
|
195
|
+
show_additional_analysis(file_analysis) if should_show_analysis?
|
|
110
196
|
end
|
|
111
197
|
rescue StandardError => e
|
|
112
198
|
puts "Error processing #{file_path}: #{e.message}"
|
|
@@ -118,7 +204,7 @@ module PngConform
|
|
|
118
204
|
#
|
|
119
205
|
# @return [Reporters::BaseReporter] Reporter instance
|
|
120
206
|
def create_reporter
|
|
121
|
-
|
|
207
|
+
Container.reporter(
|
|
122
208
|
format: options[:format] || "text",
|
|
123
209
|
verbose: options[:verbose] || options[:very_verbose],
|
|
124
210
|
quiet: options[:quiet],
|
|
@@ -135,13 +221,10 @@ module PngConform
|
|
|
135
221
|
return unless analysis && analysis[:suggestions]
|
|
136
222
|
return if analysis[:suggestions].empty?
|
|
137
223
|
|
|
138
|
-
puts "\n#{
|
|
224
|
+
puts "\n#{Utils::Colorizer.bold('OPTIMIZATION SUGGESTIONS:')}"
|
|
139
225
|
analysis[:suggestions].each_with_index do |suggestion, index|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
puts " #{index + 1}. [#{colorize(priority_label,
|
|
144
|
-
priority_color)}] #{suggestion[:description]}"
|
|
226
|
+
puts " #{index + 1}. #{Utils::Colorizer.priority(suggestion[:description],
|
|
227
|
+
suggestion[:priority])}"
|
|
145
228
|
if suggestion[:savings_bytes]&.positive?
|
|
146
229
|
puts " Savings: #{format_bytes(suggestion[:savings_bytes])}"
|
|
147
230
|
end
|
|
@@ -149,7 +232,7 @@ module PngConform
|
|
|
149
232
|
|
|
150
233
|
total_savings = analysis[:potential_savings_bytes]
|
|
151
234
|
if total_savings.positive?
|
|
152
|
-
puts "\n #{
|
|
235
|
+
puts "\n #{Utils::Colorizer.bold('Total Potential Savings:')} " \
|
|
153
236
|
"#{format_bytes(total_savings)} (#{analysis[:potential_savings_percent]}%)"
|
|
154
237
|
end
|
|
155
238
|
end
|
|
@@ -168,7 +251,7 @@ module PngConform
|
|
|
168
251
|
puts metrics.to_yaml
|
|
169
252
|
else
|
|
170
253
|
# Text format with colored output
|
|
171
|
-
puts "\n#{
|
|
254
|
+
puts "\n#{Utils::Colorizer.bold('METRICS:')}"
|
|
172
255
|
puts " File: #{metrics[:file][:filename]} (#{metrics[:file][:size_kb]} KB)"
|
|
173
256
|
puts " Image: #{metrics[:image][:dimensions]}, #{metrics[:image][:color_type_name]}, " \
|
|
174
257
|
"#{metrics[:image][:bit_depth]}-bit"
|
|
@@ -183,7 +266,7 @@ module PngConform
|
|
|
183
266
|
analysis = file_analysis.resolution_analysis
|
|
184
267
|
return unless analysis
|
|
185
268
|
|
|
186
|
-
puts "\n#{
|
|
269
|
+
puts "\n#{Utils::Colorizer.bold('RESOLUTION ANALYSIS:')}"
|
|
187
270
|
|
|
188
271
|
# Basic info
|
|
189
272
|
res = analysis[:resolution]
|
|
@@ -191,7 +274,7 @@ module PngConform
|
|
|
191
274
|
puts " DPI: #{res[:dpi] || 'Not specified'}"
|
|
192
275
|
|
|
193
276
|
# Retina analysis
|
|
194
|
-
puts "\n #{
|
|
277
|
+
puts "\n #{Utils::Colorizer.bold('Retina Analysis:')}"
|
|
195
278
|
retina = analysis[:retina]
|
|
196
279
|
puts " @1x: #{retina[:at_1x][:dimensions_pt]} (#{retina[:at_1x][:suitable_for].first})"
|
|
197
280
|
puts " @2x: #{retina[:at_2x][:dimensions_pt]} (#{retina[:at_2x][:suitable_for].first})"
|
|
@@ -210,7 +293,7 @@ module PngConform
|
|
|
210
293
|
# Print analysis if DPI available
|
|
211
294
|
if analysis[:print][:capable]
|
|
212
295
|
print_info = analysis[:print]
|
|
213
|
-
puts "\n #{
|
|
296
|
+
puts "\n #{Utils::Colorizer.bold('Print Analysis:')}"
|
|
214
297
|
puts " Quality: #{print_info[:quality]} (#{print_info[:dpi]} DPI)"
|
|
215
298
|
phys = print_info[:physical_size]
|
|
216
299
|
puts " Physical Size: #{phys[:width_inches]}\" x #{phys[:height_inches]}\" " \
|
|
@@ -220,11 +303,10 @@ module PngConform
|
|
|
220
303
|
# Recommendations
|
|
221
304
|
recommendations = analysis[:recommendations]
|
|
222
305
|
if recommendations && !recommendations.empty?
|
|
223
|
-
puts "\n #{
|
|
306
|
+
puts "\n #{Utils::Colorizer.bold('Recommendations:')}"
|
|
224
307
|
recommendations.each do |rec|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
priority_color)}] #{rec[:message]}"
|
|
308
|
+
puts " #{Utils::Colorizer.priority(rec[:message],
|
|
309
|
+
rec[:priority])}"
|
|
228
310
|
end
|
|
229
311
|
end
|
|
230
312
|
end
|
|
@@ -234,7 +316,7 @@ module PngConform
|
|
|
234
316
|
analysis = file_analysis.resolution_analysis
|
|
235
317
|
return unless analysis
|
|
236
318
|
|
|
237
|
-
puts "\n#{
|
|
319
|
+
puts "\n#{Utils::Colorizer.bold('MOBILE & RETINA READINESS:')}"
|
|
238
320
|
|
|
239
321
|
retina = analysis[:retina]
|
|
240
322
|
web = analysis[:web]
|
|
@@ -242,10 +324,9 @@ module PngConform
|
|
|
242
324
|
# Overall readiness
|
|
243
325
|
is_ready = retina[:is_retina_ready] && web[:mobile_friendly]
|
|
244
326
|
status = if is_ready
|
|
245
|
-
|
|
246
|
-
:green)
|
|
327
|
+
Utils::Colorizer.success("✓ READY")
|
|
247
328
|
else
|
|
248
|
-
|
|
329
|
+
Utils::Colorizer.error("✗ NOT READY")
|
|
249
330
|
end
|
|
250
331
|
puts " Status: #{status}"
|
|
251
332
|
|
|
@@ -275,7 +356,7 @@ module PngConform
|
|
|
275
356
|
# Helper methods for formatting
|
|
276
357
|
|
|
277
358
|
def format_check(passed)
|
|
278
|
-
passed ?
|
|
359
|
+
passed ? Utils::Colorizer.success("✓") : Utils::Colorizer.error("✗")
|
|
279
360
|
end
|
|
280
361
|
|
|
281
362
|
def format_bytes(bytes)
|
|
@@ -297,21 +378,6 @@ module PngConform
|
|
|
297
378
|
end
|
|
298
379
|
end
|
|
299
380
|
|
|
300
|
-
def colorize(text, color)
|
|
301
|
-
return text if options[:no_color]
|
|
302
|
-
|
|
303
|
-
codes = {
|
|
304
|
-
red: "\e[31m",
|
|
305
|
-
green: "\e[32m",
|
|
306
|
-
yellow: "\e[33m",
|
|
307
|
-
blue: "\e[34m",
|
|
308
|
-
bold: "\e[1m",
|
|
309
|
-
reset: "\e[0m",
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
"#{codes[color]}#{text}#{codes[:reset]}"
|
|
313
|
-
end
|
|
314
|
-
|
|
315
381
|
# Determine the exit code based on whether errors were found.
|
|
316
382
|
#
|
|
317
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
|