png_conform 0.1.0

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 (108) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +19 -0
  4. data/.rubocop_todo.yml +197 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/CONTRIBUTING.md +323 -0
  7. data/Gemfile +13 -0
  8. data/LICENSE +43 -0
  9. data/README.adoc +859 -0
  10. data/Rakefile +10 -0
  11. data/SECURITY.md +147 -0
  12. data/docs/ARCHITECTURE.adoc +681 -0
  13. data/docs/CHUNK_TYPES.adoc +450 -0
  14. data/docs/CLI_OPTIONS.adoc +913 -0
  15. data/docs/COMPATIBILITY.adoc +616 -0
  16. data/examples/README.adoc +398 -0
  17. data/examples/advanced_usage.rb +304 -0
  18. data/examples/basic_usage.rb +210 -0
  19. data/exe/png_conform +6 -0
  20. data/lib/png_conform/analyzers/comparison_analyzer.rb +230 -0
  21. data/lib/png_conform/analyzers/metrics_analyzer.rb +176 -0
  22. data/lib/png_conform/analyzers/optimization_analyzer.rb +190 -0
  23. data/lib/png_conform/analyzers/resolution_analyzer.rb +274 -0
  24. data/lib/png_conform/bindata/chunk_structure.rb +153 -0
  25. data/lib/png_conform/bindata/jng_file.rb +79 -0
  26. data/lib/png_conform/bindata/mng_file.rb +97 -0
  27. data/lib/png_conform/bindata/png_file.rb +162 -0
  28. data/lib/png_conform/cli.rb +116 -0
  29. data/lib/png_conform/commands/check_command.rb +323 -0
  30. data/lib/png_conform/commands/list_command.rb +67 -0
  31. data/lib/png_conform/models/chunk.rb +84 -0
  32. data/lib/png_conform/models/chunk_info.rb +71 -0
  33. data/lib/png_conform/models/compression_info.rb +49 -0
  34. data/lib/png_conform/models/decoded_chunk_data.rb +143 -0
  35. data/lib/png_conform/models/file_analysis.rb +181 -0
  36. data/lib/png_conform/models/file_info.rb +91 -0
  37. data/lib/png_conform/models/image_info.rb +52 -0
  38. data/lib/png_conform/models/validation_error.rb +89 -0
  39. data/lib/png_conform/models/validation_result.rb +137 -0
  40. data/lib/png_conform/readers/full_load_reader.rb +113 -0
  41. data/lib/png_conform/readers/streaming_reader.rb +180 -0
  42. data/lib/png_conform/reporters/base_reporter.rb +53 -0
  43. data/lib/png_conform/reporters/color_reporter.rb +65 -0
  44. data/lib/png_conform/reporters/json_reporter.rb +18 -0
  45. data/lib/png_conform/reporters/palette_reporter.rb +48 -0
  46. data/lib/png_conform/reporters/quiet_reporter.rb +18 -0
  47. data/lib/png_conform/reporters/reporter_factory.rb +108 -0
  48. data/lib/png_conform/reporters/summary_reporter.rb +65 -0
  49. data/lib/png_conform/reporters/text_reporter.rb +66 -0
  50. data/lib/png_conform/reporters/verbose_reporter.rb +87 -0
  51. data/lib/png_conform/reporters/very_verbose_reporter.rb +33 -0
  52. data/lib/png_conform/reporters/visual_elements.rb +66 -0
  53. data/lib/png_conform/reporters/yaml_reporter.rb +18 -0
  54. data/lib/png_conform/services/profile_manager.rb +242 -0
  55. data/lib/png_conform/services/validation_service.rb +457 -0
  56. data/lib/png_conform/services/zlib_validator.rb +270 -0
  57. data/lib/png_conform/validators/ancillary/bkgd_validator.rb +140 -0
  58. data/lib/png_conform/validators/ancillary/chrm_validator.rb +178 -0
  59. data/lib/png_conform/validators/ancillary/cicp_validator.rb +202 -0
  60. data/lib/png_conform/validators/ancillary/gama_validator.rb +105 -0
  61. data/lib/png_conform/validators/ancillary/hist_validator.rb +147 -0
  62. data/lib/png_conform/validators/ancillary/iccp_validator.rb +243 -0
  63. data/lib/png_conform/validators/ancillary/itxt_validator.rb +280 -0
  64. data/lib/png_conform/validators/ancillary/mdcv_validator.rb +201 -0
  65. data/lib/png_conform/validators/ancillary/offs_validator.rb +132 -0
  66. data/lib/png_conform/validators/ancillary/pcal_validator.rb +289 -0
  67. data/lib/png_conform/validators/ancillary/phys_validator.rb +107 -0
  68. data/lib/png_conform/validators/ancillary/sbit_validator.rb +176 -0
  69. data/lib/png_conform/validators/ancillary/scal_validator.rb +180 -0
  70. data/lib/png_conform/validators/ancillary/splt_validator.rb +223 -0
  71. data/lib/png_conform/validators/ancillary/srgb_validator.rb +117 -0
  72. data/lib/png_conform/validators/ancillary/ster_validator.rb +111 -0
  73. data/lib/png_conform/validators/ancillary/text_validator.rb +129 -0
  74. data/lib/png_conform/validators/ancillary/time_validator.rb +132 -0
  75. data/lib/png_conform/validators/ancillary/trns_validator.rb +154 -0
  76. data/lib/png_conform/validators/ancillary/ztxt_validator.rb +173 -0
  77. data/lib/png_conform/validators/apng/actl_validator.rb +81 -0
  78. data/lib/png_conform/validators/apng/fctl_validator.rb +155 -0
  79. data/lib/png_conform/validators/apng/fdat_validator.rb +117 -0
  80. data/lib/png_conform/validators/base_validator.rb +241 -0
  81. data/lib/png_conform/validators/chunk_registry.rb +219 -0
  82. data/lib/png_conform/validators/critical/idat_validator.rb +77 -0
  83. data/lib/png_conform/validators/critical/iend_validator.rb +68 -0
  84. data/lib/png_conform/validators/critical/ihdr_validator.rb +160 -0
  85. data/lib/png_conform/validators/critical/plte_validator.rb +120 -0
  86. data/lib/png_conform/validators/jng/jdat_validator.rb +66 -0
  87. data/lib/png_conform/validators/jng/jhdr_validator.rb +116 -0
  88. data/lib/png_conform/validators/jng/jsep_validator.rb +66 -0
  89. data/lib/png_conform/validators/mng/back_validator.rb +87 -0
  90. data/lib/png_conform/validators/mng/clip_validator.rb +65 -0
  91. data/lib/png_conform/validators/mng/clon_validator.rb +45 -0
  92. data/lib/png_conform/validators/mng/defi_validator.rb +104 -0
  93. data/lib/png_conform/validators/mng/dhdr_validator.rb +104 -0
  94. data/lib/png_conform/validators/mng/disc_validator.rb +44 -0
  95. data/lib/png_conform/validators/mng/endl_validator.rb +65 -0
  96. data/lib/png_conform/validators/mng/fram_validator.rb +91 -0
  97. data/lib/png_conform/validators/mng/loop_validator.rb +75 -0
  98. data/lib/png_conform/validators/mng/mend_validator.rb +31 -0
  99. data/lib/png_conform/validators/mng/mhdr_validator.rb +69 -0
  100. data/lib/png_conform/validators/mng/move_validator.rb +61 -0
  101. data/lib/png_conform/validators/mng/save_validator.rb +39 -0
  102. data/lib/png_conform/validators/mng/seek_validator.rb +42 -0
  103. data/lib/png_conform/validators/mng/show_validator.rb +52 -0
  104. data/lib/png_conform/validators/mng/term_validator.rb +84 -0
  105. data/lib/png_conform/version.rb +5 -0
  106. data/lib/png_conform.rb +101 -0
  107. data/png_conform.gemspec +43 -0
  108. metadata +201 -0
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module BinData
5
+ # MNG (Multiple-image Network Graphics) file structure
6
+ # An MNG file consists of:
7
+ # - 8-byte signature: 138 77 78 71 13 10 26 10
8
+ # - MHDR chunk (must be first)
9
+ # - Series of chunks (can include PNG chunks)
10
+ # - MEND chunk (must be last)
11
+ class MngFile < ::BinData::Record
12
+ # MNG signature (magic number)
13
+ MNG_SIGNATURE = [138, 77, 78, 71, 13, 10, 26, 10].pack("C*").freeze
14
+
15
+ string :signature, length: 8
16
+ array :chunks, type: :chunk_structure, read_until: :eof
17
+
18
+ # Validate MNG signature
19
+ def valid_signature?
20
+ signature == MNG_SIGNATURE
21
+ end
22
+
23
+ # Get signature as hex string for display
24
+ def signature_hex
25
+ signature.unpack1("H*")
26
+ end
27
+
28
+ # Find chunks by type
29
+ def chunks_by_type(type)
30
+ chunks.select { |chunk| chunk.type == type }
31
+ end
32
+
33
+ # Get MHDR chunk (must be first chunk)
34
+ def mhdr_chunk
35
+ chunks.first if chunks.first&.type == "MHDR"
36
+ end
37
+
38
+ # Get MEND chunk (must be last chunk)
39
+ def mend_chunk
40
+ chunks.last if chunks.last&.type == "MEND"
41
+ end
42
+
43
+ # Get all PNG image chunks (IHDR-IEND sequences)
44
+ def png_images
45
+ images = []
46
+ current_image = []
47
+
48
+ chunks.each do |chunk|
49
+ if chunk.type == "IHDR"
50
+ current_image = [chunk]
51
+ elsif !current_image.empty?
52
+ current_image << chunk
53
+ if chunk.type == "IEND"
54
+ images << current_image
55
+ current_image = []
56
+ end
57
+ end
58
+ end
59
+
60
+ images
61
+ end
62
+
63
+ # Get all JNG image chunks (JHDR-IEND sequences)
64
+ def jng_images
65
+ images = []
66
+ current_image = []
67
+
68
+ chunks.each do |chunk|
69
+ if chunk.type == "JHDR"
70
+ current_image = [chunk]
71
+ elsif !current_image.empty?
72
+ current_image << chunk
73
+ if chunk.type == "IEND"
74
+ images << current_image
75
+ current_image = []
76
+ end
77
+ end
78
+ end
79
+
80
+ images
81
+ end
82
+
83
+ # Check if file has proper chunk ordering
84
+ def proper_chunk_order?
85
+ return false unless mhdr_chunk
86
+ return false unless mend_chunk
87
+
88
+ true
89
+ end
90
+
91
+ # Get total file size
92
+ def total_size
93
+ 8 + chunks.sum(&:total_size)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require_relative "chunk_structure"
5
+
6
+ module PngConform
7
+ module BinData
8
+ # Binary structure for PNG files
9
+ #
10
+ # PNG file format (from PNG spec):
11
+ # Signature: 8 bytes (0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A)
12
+ # Chunks: variable number of chunks until IEND
13
+ #
14
+ # The PNG signature is always:
15
+ # - 0x89: High bit set to detect transmission as text
16
+ # - "PNG": Format identifier
17
+ # - 0x0D 0x0A: DOS-style line ending (CRLF)
18
+ # - 0x1A: DOS end-of-file character
19
+ # - 0x0A: Unix-style line ending (LF)
20
+ #
21
+ # @example Reading a PNG file
22
+ # File.open("image.png", "rb") do |f|
23
+ # png = PngFile.read(f)
24
+ # puts "Valid PNG" if png.valid_signature?
25
+ # png.chunks.each { |chunk| puts chunk.type }
26
+ # end
27
+ #
28
+ class PngFile < ::BinData::Record
29
+ # PNG signature (8 bytes)
30
+ # Must be: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A
31
+ string :signature, length: 8
32
+
33
+ # Array of chunks (read until IEND)
34
+ array :chunks, type: :chunk_structure, read_until: :eof
35
+
36
+ # Expected PNG signature bytes
37
+ PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
38
+ .pack("C*")
39
+ .freeze
40
+
41
+ # Check if the signature is valid
42
+ #
43
+ # @return [Boolean] true if signature matches PNG specification
44
+ def valid_signature?
45
+ signature == PNG_SIGNATURE
46
+ end
47
+
48
+ # Get signature as hex string for debugging
49
+ #
50
+ # @return [String] signature in hex format
51
+ def signature_hex
52
+ signature.bytes.map { |b| format("%02x", b) }.join(" ")
53
+ end
54
+
55
+ # Find a specific chunk by type
56
+ #
57
+ # @param type [String, Symbol] chunk type (e.g., "IHDR" or :IHDR)
58
+ # @return [ChunkStructure, nil] the first matching chunk or nil
59
+ def find_chunk(type)
60
+ type_str = type.to_s
61
+ chunks.find { |chunk| chunk.type == type_str }
62
+ end
63
+
64
+ # Find all chunks of a specific type
65
+ #
66
+ # @param type [String, Symbol] chunk type
67
+ # @return [Array<ChunkStructure>] all matching chunks
68
+ def find_chunks(type)
69
+ type_str = type.to_s
70
+ chunks.select { |chunk| chunk.type == type_str }
71
+ end
72
+
73
+ # Get the IHDR chunk (image header)
74
+ #
75
+ # @return [ChunkStructure, nil] the IHDR chunk
76
+ def ihdr
77
+ find_chunk("IHDR")
78
+ end
79
+
80
+ # Get the IEND chunk (image end)
81
+ #
82
+ # @return [ChunkStructure, nil] the IEND chunk
83
+ def iend
84
+ find_chunk("IEND")
85
+ end
86
+
87
+ # Get all IDAT chunks (image data)
88
+ #
89
+ # @return [Array<ChunkStructure>] all IDAT chunks
90
+ def idats
91
+ find_chunks("IDAT")
92
+ end
93
+
94
+ # Get the PLTE chunk (palette)
95
+ #
96
+ # @return [ChunkStructure, nil] the PLTE chunk
97
+ def plte
98
+ find_chunk("PLTE")
99
+ end
100
+
101
+ # Check if this appears to be a valid PNG structure
102
+ #
103
+ # Basic validation:
104
+ # - Valid signature
105
+ # - Has IHDR as first chunk
106
+ # - Has IEND as last chunk
107
+ #
108
+ # @return [Boolean] true if basic structure is valid
109
+ def structurally_valid?
110
+ return false unless valid_signature?
111
+ return false if chunks.empty?
112
+ return false unless chunks.first.type == "IHDR"
113
+ return false unless chunks.last.type == "IEND"
114
+
115
+ true
116
+ end
117
+
118
+ # Get chunk sequence as array of types
119
+ #
120
+ # @return [Array<String>] array of chunk type codes
121
+ def chunk_sequence
122
+ chunks.map(&:type)
123
+ end
124
+
125
+ # Count chunks by type
126
+ #
127
+ # @return [Hash<String, Integer>] chunk types and their counts
128
+ def chunk_counts
129
+ chunks.each_with_object(Hash.new(0)) do |chunk, counts|
130
+ counts[chunk.type] += 1
131
+ end
132
+ end
133
+
134
+ # Check if all chunks have valid CRCs
135
+ #
136
+ # @return [Boolean] true if all CRCs are valid
137
+ def all_crcs_valid?
138
+ chunks.all?(&:crc_valid?)
139
+ end
140
+
141
+ # Get chunks with invalid CRCs
142
+ #
143
+ # @return [Array<ChunkStructure>] chunks with CRC errors
144
+ def invalid_crc_chunks
145
+ chunks.reject(&:crc_valid?)
146
+ end
147
+
148
+ # Get file summary information
149
+ #
150
+ # @return [Hash] summary of file structure
151
+ def summary
152
+ {
153
+ signature_valid: valid_signature?,
154
+ chunk_count: chunks.length,
155
+ chunk_types: chunk_counts,
156
+ structurally_valid: structurally_valid?,
157
+ all_crcs_valid: all_crcs_valid?,
158
+ }
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "commands/check_command"
5
+ require_relative "commands/list_command"
6
+
7
+ module PngConform
8
+ # Main CLI application class using Thor framework.
9
+ #
10
+ # Provides the command-line interface for PNG validation and analysis.
11
+ # Delegates to command classes for implementation.
12
+ class Cli < Thor
13
+ class_option :verbose,
14
+ type: :boolean,
15
+ default: false,
16
+ desc: "Enable verbose output"
17
+
18
+ desc "check FILES", "Validate PNG files"
19
+ long_desc <<~DESC
20
+ Validate one or more PNG files and report any errors or warnings.
21
+
22
+ Options:
23
+ -f, --format FORMAT Output format: text, yaml, json (default: text)
24
+ -v, --verbose Print detailed chunk information
25
+ -vv, --very-verbose Print very detailed information including \
26
+ line filters
27
+ -q, --quiet Only output errors (suppress success messages)
28
+ --no-color Disable colored output (colors enabled by default)
29
+ -p, --palette Print palette and histogram chunks
30
+ -t, --text Print text chunk contents
31
+ -7, --seven-bit Escape characters >= 128 for 7-bit terminals
32
+ --profile PROFILE Validate against a specific profile \
33
+ (minimal, web, print, archive, strict, default)
34
+ --strict Enable strict validation mode
35
+ --optimize Show optimization suggestions
36
+ --metrics Show detailed metrics (JSON/YAML)
37
+ --resolution Show resolution and Retina analysis
38
+ --mobile-ready Check mobile and Retina readiness
39
+
40
+ Examples:
41
+ png_conform check image.png
42
+ png_conform check -v image.png
43
+ png_conform check --optimize image.png
44
+ png_conform check --resolution icon@2x.png
45
+ png_conform check --metrics --format json image.png
46
+ png_conform check --mobile-ready app-icon.png
47
+ png_conform check --profile web *.png
48
+ DESC
49
+ option :format, aliases: :f, type: :string, default: "text",
50
+ desc: "Output format (text, yaml, json)"
51
+ option :very_verbose, aliases: :vv, type: :boolean, default: false,
52
+ desc: "Print very detailed information"
53
+ option :quiet, aliases: :q, type: :boolean, default: false,
54
+ desc: "Only output errors"
55
+ option :no_color, type: :boolean, default: false,
56
+ desc: "Disable colored output"
57
+ option :palette, aliases: :p, type: :boolean, default: false,
58
+ desc: "Print palette and histogram chunks"
59
+ option :text, aliases: :t, type: :boolean, default: false,
60
+ desc: "Print text chunk contents"
61
+ option :seven_bit, aliases: :"7", type: :boolean, default: false,
62
+ desc: "Escape chars >= 128"
63
+ option :profile, type: :string, default: nil,
64
+ desc: "Validation profile"
65
+ option :strict, type: :boolean, default: false,
66
+ desc: "Strict validation mode"
67
+ option :optimize, type: :boolean, default: false,
68
+ desc: "Show file size optimization suggestions"
69
+ option :metrics, type: :boolean, default: false,
70
+ desc: "Show comprehensive metrics"
71
+ option :resolution, type: :boolean, default: false,
72
+ desc: "Show resolution and Retina/DPI analysis"
73
+ option :mobile_ready, type: :boolean, default: false,
74
+ desc: "Check mobile and Retina readiness"
75
+ def check(*files)
76
+ Commands::CheckCommand.new(files, options).run
77
+ end
78
+
79
+ desc "list", "List available validation profiles"
80
+ long_desc <<~DESC
81
+ Display all available validation profiles and their requirements.
82
+
83
+ Profiles define different validation rules for different use cases:
84
+ - minimal: Basic PNG structure validation
85
+ - web: Browser-optimized validation
86
+ - print: Print-ready validation with physical dimensions
87
+ - archive: Long-term preservation with full metadata
88
+ - strict: Strictest possible validation
89
+ - default: Balanced validation for general use
90
+
91
+ Examples:
92
+ png_conform list
93
+ DESC
94
+ def list
95
+ Commands::ListCommand.new(options).run
96
+ end
97
+
98
+ desc "version", "Display version information"
99
+ def version
100
+ puts "png_conform version #{PngConform::VERSION}"
101
+ puts "Ruby version #{RUBY_VERSION}"
102
+ end
103
+
104
+ # Override default error handling to provide helpful messages
105
+ def self.exit_on_failure?
106
+ true
107
+ end
108
+
109
+ # Provide helpful error message for unknown commands
110
+ def method_missing(method, *_args)
111
+ puts "Error: Unknown command '#{method}'"
112
+ puts "Run 'png_conform help' for usage information"
113
+ exit(1)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../services/validation_service"
4
+ require_relative "../services/profile_manager"
5
+ require_relative "../reporters/reporter_factory"
6
+ require_relative "../readers/streaming_reader"
7
+
8
+ module PngConform
9
+ module Commands
10
+ # Command to validate PNG files and report results.
11
+ #
12
+ # Coordinates between readers, validators, and reporters to analyze
13
+ # PNG files according to specified options and profiles.
14
+ class CheckCommand
15
+ attr_reader :files, :options
16
+
17
+ # @param files [Array<String>] List of file paths to validate
18
+ # @param options [Hash] Command-line options
19
+ def initialize(files, options = {})
20
+ @files = files
21
+ @options = options
22
+ @errors_found = false
23
+ end
24
+
25
+ # Execute the validation command.
26
+ #
27
+ # @return [Integer] Exit code (0 for success, 1 for errors)
28
+ def run
29
+ validate_inputs
30
+ validate_files
31
+ exit_code
32
+ end
33
+
34
+ private
35
+
36
+ # Validate command inputs and options.
37
+ def validate_inputs
38
+ if files.empty?
39
+ puts "Error: No files specified"
40
+ puts "Usage: png_conform check [OPTIONS] FILES"
41
+ exit(1)
42
+ end
43
+
44
+ # Check if profile exists (if specified)
45
+ if options[:profile] && !Services::ProfileManager.profile_exists?(options[:profile])
46
+ puts "Error: Unknown profile '#{options[:profile]}'"
47
+ puts "Available profiles: #{Services::ProfileManager.available_profiles.join(', ')}"
48
+ exit(1)
49
+ end
50
+
51
+ # Check for conflicting options
52
+ if options[:quiet] && options[:verbose]
53
+ puts "Warning: --quiet and --verbose are mutually exclusive, using --quiet"
54
+ options[:verbose] = false
55
+ end
56
+
57
+ return unless options[:quiet] && options[:very_verbose]
58
+
59
+ puts "Warning: --quiet and --very-verbose are mutually exclusive, using --quiet"
60
+ options[:very_verbose] = false
61
+ end
62
+
63
+ # Validate all specified files.
64
+ def validate_files
65
+ reporter = create_reporter
66
+
67
+ files.each do |file_path|
68
+ validate_single_file(file_path, reporter)
69
+ end
70
+ end
71
+
72
+ # Validate a single PNG file.
73
+ #
74
+ # @param file_path [String] Path to the PNG file
75
+ # @param reporter [Reporters::BaseReporter] Reporter for output
76
+ def validate_single_file(file_path, reporter)
77
+ unless File.exist?(file_path)
78
+ puts "Error: File not found: #{file_path}"
79
+ @errors_found = true
80
+ return
81
+ end
82
+
83
+ unless File.file?(file_path)
84
+ puts "Error: Not a file: #{file_path}"
85
+ @errors_found = true
86
+ return
87
+ end
88
+
89
+ # Read and validate the file using streaming reader
90
+ Readers::StreamingReader.open(file_path) do |reader|
91
+ # Perform validation
92
+ validator = Services::ValidationService.new(reader, file_path)
93
+ file_analysis = validator.validate
94
+
95
+ # Track if any errors were found
96
+ @errors_found = true unless file_analysis.valid?
97
+
98
+ # Use reporter to output result
99
+ reporter.report(file_analysis)
100
+
101
+ # For text output (default), show additional analysis unless quiet
102
+ if (options[:format].nil? || options[:format] == "text") && !options[:quiet]
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]
110
+ end
111
+ rescue StandardError => e
112
+ puts "Error processing #{file_path}: #{e.message}"
113
+ puts e.backtrace.join("\n") if options[:verbose]
114
+ @errors_found = true
115
+ end
116
+
117
+ # Create the appropriate reporter based on options.
118
+ #
119
+ # @return [Reporters::BaseReporter] Reporter instance
120
+ def create_reporter
121
+ Reporters::ReporterFactory.create(
122
+ format: options[:format] || "text",
123
+ verbose: options[:verbose] || options[:very_verbose],
124
+ quiet: options[:quiet],
125
+ colorize: !options[:no_color],
126
+ show_palette: options[:palette],
127
+ show_text: options[:text],
128
+ seven_bit: options[:seven_bit],
129
+ )
130
+ end
131
+
132
+ # Show optimization suggestions for the file
133
+ def show_optimization_suggestions(file_analysis)
134
+ analysis = file_analysis.optimization_analysis
135
+ return unless analysis && analysis[:suggestions]
136
+ return if analysis[:suggestions].empty?
137
+
138
+ puts "\n#{colorize('OPTIMIZATION SUGGESTIONS:', :bold)}"
139
+ analysis[:suggestions].each_with_index do |suggestion, index|
140
+ priority_color = priority_color(suggestion[:priority])
141
+ priority_label = suggestion[:priority].to_s.upcase
142
+
143
+ puts " #{index + 1}. [#{colorize(priority_label,
144
+ priority_color)}] #{suggestion[:description]}"
145
+ if suggestion[:savings_bytes]&.positive?
146
+ puts " Savings: #{format_bytes(suggestion[:savings_bytes])}"
147
+ end
148
+ end
149
+
150
+ total_savings = analysis[:potential_savings_bytes]
151
+ if total_savings.positive?
152
+ puts "\n #{colorize('Total Potential Savings:', :bold)} " \
153
+ "#{format_bytes(total_savings)} (#{analysis[:potential_savings_percent]}%)"
154
+ end
155
+ end
156
+
157
+ # Show comprehensive metrics
158
+ def show_metrics(file_analysis)
159
+ metrics = file_analysis.metrics
160
+ return unless metrics
161
+
162
+ case options[:format]
163
+ when "json"
164
+ require "json"
165
+ puts JSON.pretty_generate(metrics)
166
+ when "yaml"
167
+ require "yaml"
168
+ puts metrics.to_yaml
169
+ else
170
+ # Text format with colored output
171
+ puts "\n#{colorize('METRICS:', :bold)}"
172
+ puts " File: #{metrics[:file][:filename]} (#{metrics[:file][:size_kb]} KB)"
173
+ puts " Image: #{metrics[:image][:dimensions]}, #{metrics[:image][:color_type_name]}, " \
174
+ "#{metrics[:image][:bit_depth]}-bit"
175
+ puts " Chunks: #{metrics[:chunks][:total_count]} (#{metrics[:chunks][:types].join(', ')})"
176
+ puts " Validation: #{metrics[:validation][:error_count]} errors, " \
177
+ "#{metrics[:validation][:warning_count]} warnings"
178
+ end
179
+ end
180
+
181
+ # Show resolution and Retina analysis
182
+ def show_resolution_analysis(file_analysis)
183
+ analysis = file_analysis.resolution_analysis
184
+ return unless analysis
185
+
186
+ puts "\n#{colorize('RESOLUTION ANALYSIS:', :bold)}"
187
+
188
+ # Basic info
189
+ res = analysis[:resolution]
190
+ puts " Dimensions: #{res[:dimensions]} (#{res[:megapixels]} megapixels)"
191
+ puts " DPI: #{res[:dpi] || 'Not specified'}"
192
+
193
+ # Retina analysis
194
+ puts "\n #{colorize('Retina Analysis:', :bold)}"
195
+ retina = analysis[:retina]
196
+ puts " @1x: #{retina[:at_1x][:dimensions_pt]} (#{retina[:at_1x][:suitable_for].first})"
197
+ puts " @2x: #{retina[:at_2x][:dimensions_pt]} (#{retina[:at_2x][:suitable_for].first})"
198
+ puts " @3x: #{retina[:at_3x][:dimensions_pt]} (#{retina[:at_3x][:suitable_for].first})"
199
+ puts " Recommended: #{retina[:recommended_density]}"
200
+
201
+ # iOS suggestions
202
+ ios = retina[:ios_asset_catalog]
203
+ if ios && !ios.empty?
204
+ puts " iOS: #{ios.join(', ')}"
205
+ end
206
+
207
+ # Android
208
+ puts " Android: #{retina[:android_density]}"
209
+
210
+ # Print analysis if DPI available
211
+ if analysis[:print][:capable]
212
+ print_info = analysis[:print]
213
+ puts "\n #{colorize('Print Analysis:', :bold)}"
214
+ puts " Quality: #{print_info[:quality]} (#{print_info[:dpi]} DPI)"
215
+ phys = print_info[:physical_size]
216
+ puts " Physical Size: #{phys[:width_inches]}\" x #{phys[:height_inches]}\" " \
217
+ "(#{phys[:width_cm]} x #{phys[:height_cm]} cm)"
218
+ end
219
+
220
+ # Recommendations
221
+ recommendations = analysis[:recommendations]
222
+ if recommendations && !recommendations.empty?
223
+ puts "\n #{colorize('Recommendations:', :bold)}"
224
+ recommendations.each do |rec|
225
+ priority_color = priority_color(rec[:priority])
226
+ puts " [#{colorize(rec[:priority].to_s.upcase,
227
+ priority_color)}] #{rec[:message]}"
228
+ end
229
+ end
230
+ end
231
+
232
+ # Show mobile and Retina readiness
233
+ def show_mobile_readiness(file_analysis)
234
+ analysis = file_analysis.resolution_analysis
235
+ return unless analysis
236
+
237
+ puts "\n#{colorize('MOBILE & RETINA READINESS:', :bold)}"
238
+
239
+ retina = analysis[:retina]
240
+ web = analysis[:web]
241
+
242
+ # Overall readiness
243
+ is_ready = retina[:is_retina_ready] && web[:mobile_friendly]
244
+ status = if is_ready
245
+ colorize("✓ READY",
246
+ :green)
247
+ else
248
+ colorize("✗ NOT READY", :red)
249
+ end
250
+ puts " Status: #{status}"
251
+
252
+ # Specific checks
253
+ puts "\n Checks:"
254
+ puts " Retina Ready: #{format_check(retina[:is_retina_ready])}"
255
+ puts " Mobile Friendly: #{format_check(web[:mobile_friendly])}"
256
+ puts " Web Suitable: #{format_check(web[:suitable_for_web])}"
257
+
258
+ # Retina densities
259
+ puts "\n Retina Densities:"
260
+ puts " @1x: #{retina[:at_1x][:dimensions_pt]}"
261
+ puts " @2x: #{retina[:at_2x][:dimensions_pt]}"
262
+ puts " @3x: #{retina[:at_3x][:dimensions_pt]}"
263
+ puts " Recommended: #{retina[:recommended_density]}"
264
+
265
+ # Screen coverage
266
+ puts "\n Screen Coverage:"
267
+ web[:typical_screen_size].each do |screen, coverage|
268
+ puts " #{screen}: #{coverage}"
269
+ end
270
+
271
+ # Load time
272
+ puts "\n Load Time: #{web[:load_time_estimate]}"
273
+ end
274
+
275
+ # Helper methods for formatting
276
+
277
+ def format_check(passed)
278
+ passed ? colorize("✓", :green) : colorize("✗", :red)
279
+ end
280
+
281
+ def format_bytes(bytes)
282
+ if bytes < 1024
283
+ "#{bytes} bytes"
284
+ elsif bytes < 1024 * 1024
285
+ "#{(bytes / 1024.0).round(2)} KB"
286
+ else
287
+ "#{(bytes / 1024.0 / 1024.0).round(2)} MB"
288
+ end
289
+ end
290
+
291
+ def priority_color(priority)
292
+ case priority
293
+ when :high then :red
294
+ when :medium then :yellow
295
+ when :low then :blue
296
+ else :default
297
+ end
298
+ end
299
+
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
+ # Determine the exit code based on whether errors were found.
316
+ #
317
+ # @return [Integer] Exit code (0 for success, 1 for errors)
318
+ def exit_code
319
+ @errors_found ? 1 : 0
320
+ end
321
+ end
322
+ end
323
+ end