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,210 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # Basic Usage Examples for PngConform
6
+ #
7
+ # This file demonstrates the most common ways to use PngConform
8
+ # for validating PNG files programmatically.
9
+ #
10
+
11
+ require "png_conform"
12
+
13
+ # Example 1: Basic validation
14
+ def basic_validation(file_path)
15
+ puts "=" * 60
16
+ puts "Example 1: Basic Validation"
17
+ puts "=" * 60
18
+
19
+ result = PngConform::Services::ValidationService.validate_file(file_path)
20
+
21
+ if result.valid?
22
+ puts "✓ File is valid!"
23
+ puts " Dimensions: #{result.image_info.width}x#{result.image_info.height}"
24
+ puts " Color type: #{result.image_info.color_type}"
25
+ puts " Bit depth: #{result.image_info.bit_depth}"
26
+ puts " Chunks: #{result.chunks.count}"
27
+ else
28
+ puts "✗ File has errors:"
29
+ result.errors.each do |error|
30
+ puts " #{error.severity.upcase}: #{error.message}"
31
+ end
32
+ end
33
+ puts
34
+ end
35
+
36
+ # Example 2: Validation with specific profile
37
+ def profile_validation(file_path, profile_name)
38
+ puts "=" * 60
39
+ puts "Example 2: Profile-Based Validation (#{profile_name})"
40
+ puts "=" * 60
41
+
42
+ profile = PngConform::Services::ProfileManager.get_profile(profile_name)
43
+
44
+ if profile
45
+ puts "Profile: #{profile[:name]} - #{profile[:description]}"
46
+ puts "Required chunks: #{profile[:required_chunks].join(', ')}"
47
+ puts
48
+
49
+ result = PngConform::Services::ValidationService.validate_file(file_path)
50
+ chunk_types = result.chunks.map(&:type)
51
+
52
+ # Validate against profile
53
+ profile_result = PngConform::Services::ProfileManager.validate_file_against_profile(
54
+ chunk_types, profile_name
55
+ )
56
+
57
+ if profile_result[:valid]
58
+ puts "✓ File conforms to #{profile_name} profile"
59
+ else
60
+ puts "✗ Profile violations:"
61
+ profile_result[:errors].each do |error|
62
+ puts " #{error}"
63
+ end
64
+ end
65
+ else
66
+ puts "✗ Profile not found: #{profile_name}"
67
+ end
68
+ puts
69
+ end
70
+
71
+ # Example 3: Detailed chunk inspection
72
+ def inspect_chunks(file_path)
73
+ puts "=" * 60
74
+ puts "Example 3: Detailed Chunk Inspection"
75
+ puts "=" * 60
76
+
77
+ result = PngConform::Services::ValidationService.validate_file(file_path)
78
+
79
+ puts "File: #{file_path}"
80
+ puts "Chunks found: #{result.chunks.count}"
81
+ puts
82
+
83
+ result.chunks.each do |chunk|
84
+ puts "Chunk: #{chunk.type}"
85
+ puts " Offset: 0x#{chunk.offset.to_s(16).rjust(8, '0')}"
86
+ puts " Length: #{chunk.length} bytes"
87
+ puts " CRC: #{chunk.crc_expected}"
88
+ puts " CRC Valid: #{chunk.valid_crc ? '✓' : '✗'}"
89
+
90
+ # Display decoded data for specific chunks
91
+ if chunk.data
92
+ case chunk.type
93
+ when "IHDR"
94
+ # Extract IHDR data manually
95
+ width = chunk.data.bytes[0..3].pack("C*").unpack1("N")
96
+ height = chunk.data.bytes[4..7].pack("C*").unpack1("N")
97
+ bit_depth = chunk.data.bytes[8]
98
+ color_type = chunk.data.bytes[9]
99
+ puts " Width: #{width}"
100
+ puts " Height: #{height}"
101
+ puts " Bit Depth: #{bit_depth}"
102
+ puts " Color Type: #{color_type}"
103
+ when "gAMA"
104
+ gamma_value = chunk.data.bytes[0..3].pack("C*").unpack1("N")
105
+ puts " Gamma: #{gamma_value / 100_000.0}"
106
+ when "tEXt"
107
+ text = chunk.data.to_s
108
+ null_pos = text.index("\x00")
109
+ if null_pos
110
+ keyword = text[0...null_pos]
111
+ content = text[(null_pos + 1)..]
112
+ puts " Keyword: #{keyword}"
113
+ puts " Text: #{content[0..50]}..."
114
+ end
115
+ end
116
+ end
117
+ puts
118
+ end
119
+ end
120
+
121
+ # Example 4: Batch validation
122
+ def batch_validation(directory)
123
+ puts "=" * 60
124
+ puts "Example 4: Batch Validation"
125
+ puts "=" * 60
126
+
127
+ results = {
128
+ valid: [],
129
+ invalid: [],
130
+ errors: [],
131
+ }
132
+
133
+ Dir.glob(File.join(directory, "*.png")).each do |file|
134
+ result = PngConform::Services::ValidationService.validate_file(file)
135
+ if result.valid?
136
+ results[:valid] << file
137
+ else
138
+ results[:invalid] << { file: file, errors: result.errors }
139
+ end
140
+ rescue StandardError => e
141
+ results[:errors] << { file: file, error: e.message }
142
+ end
143
+
144
+ puts "Processed #{results[:valid].count + results[:invalid].count + results[:errors].count} files"
145
+ puts " Valid: #{results[:valid].count}"
146
+ puts " Invalid: #{results[:invalid].count}"
147
+ puts " Errors: #{results[:errors].count}"
148
+ puts
149
+
150
+ unless results[:invalid].empty?
151
+ puts "Invalid files:"
152
+ results[:invalid].each do |item|
153
+ puts " #{File.basename(item[:file])}: #{item[:errors].count} errors"
154
+ end
155
+ end
156
+ puts
157
+ end
158
+
159
+ # Example 5: Export validation results
160
+ def export_results(file_path, format = :yaml)
161
+ puts "=" * 60
162
+ puts "Example 5: Export Results (#{format.upcase})"
163
+ puts "=" * 60
164
+
165
+ result = PngConform::Services::ValidationService.validate_file(file_path)
166
+
167
+ case format
168
+ when :yaml
169
+ require "yaml"
170
+ puts result.to_yaml
171
+ when :json
172
+ require "json"
173
+ puts JSON.pretty_generate(result.to_h)
174
+ else
175
+ puts "Unknown format: #{format}"
176
+ end
177
+ puts
178
+ end
179
+
180
+ # Main execution
181
+ if __FILE__ == $PROGRAM_NAME
182
+ # Check if a file path was provided
183
+ if ARGV.empty?
184
+ puts "Usage: ruby #{$PROGRAM_NAME} PATH_TO_PNG_FILE [DIRECTORY_FOR_BATCH]"
185
+ puts
186
+ puts "Examples:"
187
+ puts " ruby #{$PROGRAM_NAME} image.png"
188
+ puts " ruby #{$PROGRAM_NAME} image.png ./png_directory"
189
+ exit 1
190
+ end
191
+
192
+ file_path = ARGV[0]
193
+ directory = ARGV[1]
194
+
195
+ unless File.exist?(file_path)
196
+ puts "Error: File not found: #{file_path}"
197
+ exit 1
198
+ end
199
+
200
+ # Run examples
201
+ basic_validation(file_path)
202
+ profile_validation(file_path, "web")
203
+ inspect_chunks(file_path)
204
+ export_results(file_path, :yaml)
205
+
206
+ # Run batch validation if directory provided
207
+ if directory && Dir.exist?(directory)
208
+ batch_validation(directory)
209
+ end
210
+ end
data/exe/png_conform ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/png_conform"
5
+
6
+ PngConform::Cli.start(ARGV)
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Analyzers
5
+ # Compares two PNG files and reports differences
6
+ class ComparisonAnalyzer
7
+ # Metadata chunk types
8
+ METADATA_CHUNKS = %w[tEXt zTXt iTXt tIME].freeze
9
+
10
+ def initialize(result1, result2)
11
+ @result1 = result1
12
+ @result2 = result2
13
+ end
14
+
15
+ def analyze
16
+ {
17
+ files: file_comparison,
18
+ image: image_comparison,
19
+ chunks: chunk_comparison,
20
+ validation: validation_comparison,
21
+ quality: quality_comparison,
22
+ summary: generate_summary,
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def file_comparison
29
+ size1 = @result1.file_size
30
+ size2 = @result2.file_size
31
+ diff = size2 - size1
32
+ percent = size1.zero? ? 0 : ((diff.to_f / size1) * 100).round(2)
33
+
34
+ {
35
+ file1: @result1.filename,
36
+ file2: @result2.filename,
37
+ size_bytes: {
38
+ file1: size1,
39
+ file2: size2,
40
+ difference: diff,
41
+ change_percent: percent,
42
+ },
43
+ size_change: format_size_change(diff, percent),
44
+ }
45
+ end
46
+
47
+ def image_comparison
48
+ ihdr1 = @result1.ihdr_chunk
49
+ ihdr2 = @result2.ihdr_chunk
50
+
51
+ w1 = ihdr1 ? get_width(ihdr1) : 0
52
+ h1 = ihdr1 ? get_height(ihdr1) : 0
53
+ w2 = ihdr2 ? get_width(ihdr2) : 0
54
+ h2 = ihdr2 ? get_height(ihdr2) : 0
55
+
56
+ {
57
+ dimensions: {
58
+ file1: "#{w1}x#{h1}",
59
+ file2: "#{w2}x#{h2}",
60
+ same: w1 == w2 && h1 == h2,
61
+ },
62
+ }
63
+ end
64
+
65
+ def chunk_comparison
66
+ chunks1 = @result1.chunks.map(&:type)
67
+ chunks2 = @result2.chunks.map(&:type)
68
+
69
+ {
70
+ count: {
71
+ file1: chunks1.count,
72
+ file2: chunks2.count,
73
+ difference: chunks2.count - chunks1.count,
74
+ },
75
+ added: chunks2 - chunks1,
76
+ removed: chunks1 - chunks2,
77
+ common: chunks1 & chunks2,
78
+ changed: detect_chunk_changes,
79
+ }
80
+ end
81
+
82
+ def validation_comparison
83
+ {
84
+ validity: {
85
+ file1: @result1.valid?,
86
+ file2: @result2.valid?,
87
+ same: @result1.valid? == @result2.valid?,
88
+ },
89
+ errors: {
90
+ file1: @result1.error_count,
91
+ file2: @result2.error_count,
92
+ difference: @result2.error_count - @result1.error_count,
93
+ },
94
+ warnings: {
95
+ file1: @result1.warning_count,
96
+ file2: @result2.warning_count,
97
+ difference: @result2.warning_count - @result1.warning_count,
98
+ },
99
+ new_errors: new_errors,
100
+ resolved_errors: resolved_errors,
101
+ }
102
+ end
103
+
104
+ def quality_comparison
105
+ {
106
+ compression_ratio: {
107
+ file1: @result1.compression_ratio,
108
+ file2: @result2.compression_ratio,
109
+ improved: compression_improved?,
110
+ },
111
+ has_color_profile: {
112
+ file1: has_color_profile?(@result1),
113
+ file2: has_color_profile?(@result2),
114
+ same: has_color_profile?(@result1) == has_color_profile?(@result2),
115
+ },
116
+ metadata_count: {
117
+ file1: metadata_count(@result1),
118
+ file2: metadata_count(@result2),
119
+ difference: metadata_count(@result2) - metadata_count(@result1),
120
+ },
121
+ }
122
+ end
123
+
124
+ def detect_chunk_changes
125
+ common_types = @result1.chunks.map(&:type) & @result2.chunks.map(&:type)
126
+ changed = []
127
+
128
+ common_types.each do |type|
129
+ chunk1 = @result1.chunks.find { |c| c.type == type }
130
+ chunk2 = @result2.chunks.find { |c| c.type == type }
131
+
132
+ next unless chunk1 && chunk2
133
+
134
+ if chunk1.length != chunk2.length || chunk1.crc != chunk2.crc
135
+ changed << {
136
+ type: type,
137
+ size_changed: chunk1.length != chunk2.length,
138
+ data_changed: chunk1.crc != chunk2.crc,
139
+ }
140
+ end
141
+ end
142
+
143
+ changed
144
+ end
145
+
146
+ def new_errors
147
+ errors1 = @result1.errors.map(&:message).to_set
148
+ errors2 = @result2.errors.map(&:message).to_set
149
+ (errors2 - errors1).to_a
150
+ end
151
+
152
+ def resolved_errors
153
+ errors1 = @result1.errors.map(&:message).to_set
154
+ errors2 = @result2.errors.map(&:message).to_set
155
+ (errors1 - errors2).to_a
156
+ end
157
+
158
+ def compression_improved?
159
+ return nil unless @result1.compression_ratio && @result2.compression_ratio
160
+
161
+ @result2.compression_ratio > @result1.compression_ratio
162
+ end
163
+
164
+ def has_color_profile?(result)
165
+ result.has_chunk?("gAMA") || result.has_chunk?("sRGB") || result.has_chunk?("iCCP")
166
+ end
167
+
168
+ def metadata_count(result)
169
+ result.chunks.count { |c| METADATA_CHUNKS.include?(c.type) }
170
+ end
171
+
172
+ def format_size_change(diff, percent)
173
+ if diff.positive?
174
+ "Larger by #{diff} bytes (+#{percent}%)"
175
+ elsif diff.negative?
176
+ "Smaller by #{diff.abs} bytes (#{percent}%)"
177
+ else
178
+ "Same size"
179
+ end
180
+ end
181
+
182
+ def generate_summary
183
+ summary = []
184
+
185
+ size_diff = @result2.file_size - @result1.file_size
186
+ summary << format_size_summary(size_diff) if size_diff.abs > 1024
187
+
188
+ added = chunk_comparison[:added]
189
+ removed = chunk_comparison[:removed]
190
+ summary << "Chunks added: #{added.join(', ')}" if added.any?
191
+ summary << "Chunks removed: #{removed.join(', ')}" if removed.any?
192
+
193
+ if @result1.valid? && !@result2.valid?
194
+ summary << "⚠️ File became invalid"
195
+ elsif !@result1.valid? && @result2.valid?
196
+ summary << "✓ File became valid"
197
+ end
198
+
199
+ summary
200
+ end
201
+
202
+ def format_size_summary(diff)
203
+ diff.positive? ? "File grew by #{format_bytes(diff)}" : "File reduced by #{format_bytes(diff.abs)} ✓"
204
+ end
205
+
206
+ def format_bytes(bytes)
207
+ if bytes < 1024
208
+ "#{bytes} bytes"
209
+ elsif bytes < 1024 * 1024
210
+ "#{(bytes / 1024.0).round(2)} KB"
211
+ else
212
+ "#{(bytes / 1024.0 / 1024.0).round(2)} MB"
213
+ end
214
+ end
215
+
216
+ # Helper methods to extract IHDR data
217
+ def get_width(ihdr_chunk)
218
+ return 0 unless ihdr_chunk.data && ihdr_chunk.data.bytesize >= 4
219
+
220
+ ihdr_chunk.data.bytes[0..3].pack("C*").unpack1("N")
221
+ end
222
+
223
+ def get_height(ihdr_chunk)
224
+ return 0 unless ihdr_chunk.data && ihdr_chunk.data.bytesize >= 8
225
+
226
+ ihdr_chunk.data.bytes[4..7].pack("C*").unpack1("N")
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Analyzers
5
+ # Generates comprehensive metrics for CI/CD and automation
6
+ 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)
14
+ @result = result
15
+ ihdr = result.ihdr_chunk
16
+ @width = ihdr ? get_width(ihdr) : 0
17
+ @height = ihdr ? get_height(ihdr) : 0
18
+ @bit_depth = ihdr ? get_bit_depth(ihdr) : 0
19
+ @color_type = ihdr ? get_color_type(ihdr) : 0
20
+ end
21
+
22
+ def analyze
23
+ {
24
+ file: file_metrics,
25
+ image: image_metrics,
26
+ chunks: chunk_metrics,
27
+ validation: validation_metrics,
28
+ compression: compression_metrics,
29
+ quality: quality_metrics,
30
+ }
31
+ end
32
+
33
+ def to_json(*_args)
34
+ require "json"
35
+ JSON.pretty_generate(analyze)
36
+ end
37
+
38
+ def to_yaml
39
+ require "yaml"
40
+ analyze.to_yaml
41
+ end
42
+
43
+ private
44
+
45
+ # Extract data from IHDR chunk
46
+ def get_width(ihdr_chunk)
47
+ return 0 unless ihdr_chunk.data && ihdr_chunk.data.bytesize >= 4
48
+
49
+ ihdr_chunk.data.bytes[0..3].pack("C*").unpack1("N")
50
+ end
51
+
52
+ def get_height(ihdr_chunk)
53
+ return 0 unless ihdr_chunk.data && ihdr_chunk.data.bytesize >= 8
54
+
55
+ ihdr_chunk.data.bytes[4..7].pack("C*").unpack1("N")
56
+ end
57
+
58
+ def get_bit_depth(ihdr_chunk)
59
+ return 0 unless ihdr_chunk.data && ihdr_chunk.data.bytesize >= 9
60
+
61
+ ihdr_chunk.data.bytes[8]
62
+ end
63
+
64
+ def get_color_type(ihdr_chunk)
65
+ return 0 unless ihdr_chunk.data && ihdr_chunk.data.bytesize >= 10
66
+
67
+ ihdr_chunk.data.bytes[9]
68
+ end
69
+
70
+ def color_type_name
71
+ case @color_type
72
+ when 0 then "Grayscale"
73
+ when 2 then "RGB"
74
+ when 3 then "Indexed"
75
+ when 4 then "Grayscale+Alpha"
76
+ when 6 then "RGBA"
77
+ else "Unknown"
78
+ end
79
+ end
80
+
81
+ def file_metrics
82
+ {
83
+ filename: @result.filename,
84
+ size_bytes: @result.file_size,
85
+ size_kb: (@result.file_size / 1024.0).round(2),
86
+ size_mb: (@result.file_size / 1024.0 / 1024.0).round(4),
87
+ file_type: @result.file_type,
88
+ }
89
+ end
90
+
91
+ def image_metrics
92
+ {
93
+ width: @width,
94
+ height: @height,
95
+ dimensions: "#{@width}x#{@height}",
96
+ total_pixels: @width * @height,
97
+ megapixels: (@width * @height / 1_000_000.0).round(2),
98
+ bit_depth: @bit_depth,
99
+ color_type: @color_type,
100
+ color_type_name: color_type_name,
101
+ has_alpha: [4, 6].include?(@color_type),
102
+ has_palette: @color_type == 3,
103
+ }
104
+ end
105
+
106
+ def chunk_metrics
107
+ {
108
+ total_count: @result.chunks.count,
109
+ types: @result.chunks.map(&:type).uniq.sort,
110
+ type_counts: @result.chunks.group_by(&:type).transform_values(&:count),
111
+ critical_chunks: @result.chunks.select(&:critical?).map(&:type),
112
+ ancillary_chunks: @result.chunks.reject(&:critical?).map(&:type),
113
+ total_chunk_data_bytes: @result.chunks.sum(&:length),
114
+ total_chunk_overhead_bytes: @result.chunks.count * 12,
115
+ chunk_data_percentage: calculate_chunk_data_percentage,
116
+ }
117
+ end
118
+
119
+ def validation_metrics
120
+ {
121
+ valid: @result.valid?,
122
+ error_count: @result.error_count,
123
+ warning_count: @result.warning_count,
124
+ info_count: @result.info_count,
125
+ errors_by_severity: {
126
+ error: @result.error_count,
127
+ warning: @result.warning_count,
128
+ info: @result.info_count,
129
+ },
130
+ crc_errors: @result.crc_errors_count,
131
+ has_errors: @result.error_count.positive?,
132
+ has_warnings: @result.warning_count.positive?,
133
+ }
134
+ end
135
+
136
+ def compression_metrics
137
+ {
138
+ compression_ratio: @result.compression_ratio,
139
+ }
140
+ end
141
+
142
+ def quality_metrics
143
+ {
144
+ has_color_profile: @result.has_chunk?("gAMA") ||
145
+ @result.has_chunk?("sRGB") ||
146
+ @result.has_chunk?("iCCP"),
147
+ has_gamma: @result.has_chunk?("gAMA"),
148
+ has_srgb: @result.has_chunk?("sRGB"),
149
+ has_iccp: @result.has_chunk?("iCCP"),
150
+ has_transparency: @result.has_chunk?("tRNS"),
151
+ has_metadata: @result.chunks.any? do |c|
152
+ TEXT_CHUNKS.include?(c.type)
153
+ end,
154
+ metadata_chunks_count: @result.chunks.count do |c|
155
+ METADATA_CHUNKS.include?(c.type)
156
+ end,
157
+ bytes_per_pixel: calculate_bytes_per_pixel,
158
+ }
159
+ end
160
+
161
+ def calculate_chunk_data_percentage
162
+ return 0 if @result.file_size.zero?
163
+
164
+ total_chunk_bytes = @result.chunks.sum(&:length) + (@result.chunks.count * 12)
165
+ (total_chunk_bytes.to_f / @result.file_size * 100).round(2)
166
+ end
167
+
168
+ def calculate_bytes_per_pixel
169
+ total_pixels = @width * @height
170
+ return 0 if total_pixels.zero?
171
+
172
+ (@result.file_size.to_f / total_pixels).round(3)
173
+ end
174
+ end
175
+ end
176
+ end