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,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "critical/ihdr_validator"
4
+ require_relative "critical/plte_validator"
5
+ require_relative "critical/idat_validator"
6
+ require_relative "critical/iend_validator"
7
+ require_relative "ancillary/text_validator"
8
+ require_relative "ancillary/ztxt_validator"
9
+ require_relative "ancillary/itxt_validator"
10
+ require_relative "ancillary/gama_validator"
11
+ require_relative "ancillary/chrm_validator"
12
+ require_relative "ancillary/srgb_validator"
13
+ require_relative "ancillary/sbit_validator"
14
+ require_relative "ancillary/bkgd_validator"
15
+ require_relative "ancillary/iccp_validator"
16
+ require_relative "ancillary/hist_validator"
17
+ require_relative "ancillary/splt_validator"
18
+ require_relative "ancillary/trns_validator"
19
+ require_relative "ancillary/phys_validator"
20
+ require_relative "ancillary/time_validator"
21
+ require_relative "ancillary/offs_validator"
22
+ require_relative "ancillary/pcal_validator"
23
+ require_relative "ancillary/scal_validator"
24
+ require_relative "ancillary/ster_validator"
25
+ require_relative "ancillary/cicp_validator"
26
+ require_relative "ancillary/mdcv_validator"
27
+ require_relative "apng/actl_validator"
28
+ require_relative "apng/fctl_validator"
29
+ require_relative "apng/fdat_validator"
30
+ require_relative "mng/mhdr_validator"
31
+ require_relative "mng/mend_validator"
32
+ require_relative "mng/dhdr_validator"
33
+ require_relative "mng/fram_validator"
34
+ require_relative "mng/defi_validator"
35
+ require_relative "mng/back_validator"
36
+ require_relative "mng/loop_validator"
37
+ require_relative "mng/endl_validator"
38
+ require_relative "mng/term_validator"
39
+ require_relative "mng/save_validator"
40
+ require_relative "mng/seek_validator"
41
+ require_relative "mng/move_validator"
42
+ require_relative "mng/clip_validator"
43
+ require_relative "mng/show_validator"
44
+ require_relative "mng/clon_validator"
45
+ require_relative "mng/disc_validator"
46
+ require_relative "jng/jhdr_validator"
47
+ require_relative "jng/jdat_validator"
48
+ require_relative "jng/jsep_validator"
49
+
50
+ module PngConform
51
+ module Validators
52
+ # Registry of chunk types to their corresponding validator classes
53
+ #
54
+ # This class maintains a mapping between PNG chunk type codes and
55
+ # their validator implementations. It follows the Registry pattern
56
+ # to provide centralized validator discovery and instantiation.
57
+ #
58
+ # The registry is organized by chunk categories:
59
+ # - Critical chunks (IHDR, PLTE, IDAT, IEND)
60
+ # - Text chunks (tEXt, zTXt, iTXt)
61
+ # - Color management (gAMA, cHRM, sRGB, sBIT, bKGD, iCCP)
62
+ # - Palette support (hIST, sPLT, tRNS)
63
+ # - Metadata (pHYs, tIME, oFFs, pCAL, sCAL, sTER)
64
+ # - PNG 3rd edition (cICP, mDCv)
65
+ # - APNG (acTL, fcTL, fdAT)
66
+ # - MNG (MHDR, MEND, DHDR, FRAM, DEFI, BACK, LOOP, ENDL, etc.)
67
+ # - JNG (JHDR, JDAT, JSEP)
68
+ #
69
+ class ChunkRegistry
70
+ # Map of chunk type codes to validator classes
71
+ VALIDATORS = {
72
+ # Critical chunks
73
+ "IHDR" => Critical::IhdrValidator,
74
+ "PLTE" => Critical::PlteValidator,
75
+ "IDAT" => Critical::IdatValidator,
76
+ "IEND" => Critical::IendValidator,
77
+
78
+ # Text chunks
79
+ "tEXt" => Ancillary::TextValidator,
80
+ "zTXt" => Ancillary::ZtxtValidator,
81
+ "iTXt" => Ancillary::ItxtValidator,
82
+
83
+ # Color management
84
+ "gAMA" => Ancillary::GamaValidator,
85
+ "cHRM" => Ancillary::ChrmValidator,
86
+ "sRGB" => Ancillary::SrgbValidator,
87
+ "sBIT" => Ancillary::SbitValidator,
88
+ "bKGD" => Ancillary::BkgdValidator,
89
+ "iCCP" => Ancillary::IccpValidator,
90
+
91
+ # Palette support
92
+ "hIST" => Ancillary::HistValidator,
93
+ "sPLT" => Ancillary::SpltValidator,
94
+ "tRNS" => Ancillary::TrnsValidator,
95
+
96
+ # Metadata
97
+ "pHYs" => Ancillary::PhysValidator,
98
+ "tIME" => Ancillary::TimeValidator,
99
+ "oFFs" => Ancillary::OffsValidator,
100
+ "pCAL" => Ancillary::PcalValidator,
101
+ "sCAL" => Ancillary::ScalValidator,
102
+ "sTER" => Ancillary::SterValidator,
103
+
104
+ # PNG 3rd edition
105
+ "cICP" => Ancillary::CicpValidator,
106
+ "mDCv" => Ancillary::MdcvValidator,
107
+
108
+ # APNG (Animated PNG)
109
+ "acTL" => Apng::ActlValidator,
110
+ "fcTL" => Apng::FctlValidator,
111
+ "fdAT" => Apng::FdatValidator,
112
+
113
+ # MNG (Multiple-image Network Graphics)
114
+ "MHDR" => Mng::MhdrValidator,
115
+ "MEND" => Mng::MendValidator,
116
+ "DHDR" => Mng::DhdrValidator,
117
+ "FRAM" => Mng::FramValidator,
118
+ "DEFI" => Mng::DefiValidator,
119
+ "BACK" => Mng::BackValidator,
120
+ "LOOP" => Mng::LoopValidator,
121
+ "ENDL" => Mng::EndlValidator,
122
+ "TERM" => Mng::TermValidator,
123
+ "SAVE" => Mng::SaveValidator,
124
+ "SEEK" => Mng::SeekValidator,
125
+ "MOVE" => Mng::MoveValidator,
126
+ "CLIP" => Mng::ClipValidator,
127
+ "SHOW" => Mng::ShowValidator,
128
+ "CLON" => Mng::ClonValidator,
129
+ "DISC" => Mng::DiscValidator,
130
+
131
+ # JNG (JPEG Network Graphics)
132
+ "JHDR" => Jng::JhdrValidator,
133
+ "JDAT" => Jng::JdatValidator,
134
+ "JSEP" => Jng::JsepValidator,
135
+ }.freeze
136
+
137
+ class << self
138
+ # Get validator class for a chunk type
139
+ #
140
+ # @param chunk_type [String] Four-character chunk type code
141
+ # @return [Class, nil] Validator class or nil if not found
142
+ def validator_for(chunk_type)
143
+ VALIDATORS[chunk_type]
144
+ end
145
+
146
+ # Check if a validator exists for a chunk type
147
+ #
148
+ # @param chunk_type [String] Four-character chunk type code
149
+ # @return [Boolean] True if validator exists
150
+ def validator_exists?(chunk_type)
151
+ VALIDATORS.key?(chunk_type)
152
+ end
153
+
154
+ # Get all registered chunk types
155
+ #
156
+ # @return [Array<String>] List of chunk type codes
157
+ def chunk_types
158
+ VALIDATORS.keys
159
+ end
160
+
161
+ # Get validators by category
162
+ #
163
+ # @param category [Symbol] Category name
164
+ # (:critical, :text, :color, :palette, :metadata, :png3)
165
+ # @return [Hash] Map of chunk types to validators in category
166
+ def validators_by_category(category)
167
+ case category
168
+ when :critical
169
+ VALIDATORS.select { |k, _| %w[IHDR PLTE IDAT IEND].include?(k) }
170
+ when :text
171
+ VALIDATORS.select { |k, _| %w[tEXt zTXt iTXt].include?(k) }
172
+ when :color
173
+ VALIDATORS.select do |k, _|
174
+ %w[gAMA cHRM sRGB sBIT bKGD iCCP].include?(k)
175
+ end
176
+ when :palette
177
+ VALIDATORS.select { |k, _| %w[hIST sPLT tRNS].include?(k) }
178
+ when :metadata
179
+ VALIDATORS.select do |k, _|
180
+ %w[pHYs tIME oFFs pCAL sCAL sTER].include?(k)
181
+ end
182
+ when :png3
183
+ VALIDATORS.select { |k, _| %w[cICP mDCv].include?(k) }
184
+ when :apng
185
+ VALIDATORS.select { |k, _| %w[acTL fcTL fdAT].include?(k) }
186
+ when :mng
187
+ VALIDATORS.select do |k, _|
188
+ %w[MHDR MEND DHDR FRAM DEFI BACK LOOP ENDL TERM SAVE SEEK
189
+ MOVE CLIP SHOW CLON DISC].include?(k)
190
+ end
191
+ when :jng
192
+ VALIDATORS.select { |k, _| %w[JHDR JDAT JSEP].include?(k) }
193
+ else
194
+ {}
195
+ end
196
+ end
197
+
198
+ # Get count of registered validators
199
+ #
200
+ # @return [Integer] Number of registered validators
201
+ def count
202
+ VALIDATORS.size
203
+ end
204
+
205
+ # Create validator instance for a chunk
206
+ #
207
+ # @param chunk [Object] Chunk object with type and data
208
+ # @param context [ValidationContext] Validation context
209
+ # @return [BaseValidator, nil] Validator instance or nil
210
+ def create_validator(chunk, context)
211
+ validator_class = validator_for(chunk.type)
212
+ return nil unless validator_class
213
+
214
+ validator_class.new(chunk, context)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Critical
8
+ # Validator for PNG IDAT (Image Data) chunk
9
+ #
10
+ # IDAT contains the compressed image data using zlib compression.
11
+ # Multiple IDAT chunks may be present and must be consecutive.
12
+ #
13
+ # Validation rules from PNG spec:
14
+ # - At least one IDAT chunk must be present
15
+ # - All IDAT chunks must be consecutive (no other chunks between them)
16
+ # - Must appear after IHDR chunk
17
+ # - Must appear after PLTE chunk (if present for indexed-color)
18
+ # - CRC must be valid
19
+ # - Combined data forms valid zlib stream (full validation in Phase 8)
20
+ #
21
+ # This validator performs basic structural checks.
22
+ # Full zlib decompression and filter validation is in Phase 8.
23
+ class IdatValidator < BaseValidator
24
+ # Validate IDAT chunk
25
+ #
26
+ # @return [Boolean] True if validation passed
27
+ def validate
28
+ return false unless check_crc
29
+ return false unless check_not_empty
30
+ return false unless check_position
31
+
32
+ record_idat_chunk
33
+ true
34
+ end
35
+
36
+ private
37
+
38
+ # Check that IDAT chunk is not empty
39
+ def check_not_empty
40
+ return true unless chunk.chunk_data.empty?
41
+
42
+ add_error("IDAT chunk is empty")
43
+ false
44
+ end
45
+
46
+ # Check IDAT position relative to other chunks
47
+ def check_position
48
+ valid = true
49
+
50
+ # IDAT must appear after IHDR
51
+ unless context.seen?("IHDR")
52
+ add_error("IDAT chunk before IHDR")
53
+ valid = false
54
+ end
55
+
56
+ # For indexed-color, IDAT must appear after PLTE
57
+ color_type = context.retrieve(:color_type)
58
+ if color_type == 3 && !context.seen?("PLTE")
59
+ add_error("IDAT chunk before PLTE for indexed-color image")
60
+ valid = false
61
+ end
62
+
63
+ valid
64
+ end
65
+
66
+ # Record IDAT chunk for sequence validation
67
+ def record_idat_chunk
68
+ context.record_chunk("IDAT", chunk)
69
+
70
+ # Track total IDAT size for compression ratio calculation
71
+ total_size = context.retrieve(:total_idat_size) || 0
72
+ context.store(:total_idat_size, total_size + chunk.chunk_data.length)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Critical
8
+ # Validator for PNG IEND (Image Trailer) chunk
9
+ #
10
+ # IEND marks the end of the PNG datastream.
11
+ #
12
+ # Validation rules from PNG spec:
13
+ # - Must be the last chunk in the file
14
+ # - Must be exactly 0 bytes in length
15
+ # - CRC must be valid
16
+ # - Must appear after at least one IDAT chunk
17
+ class IendValidator < BaseValidator
18
+ # Validate IEND chunk
19
+ #
20
+ # @return [Boolean] True if validation passed
21
+ def validate
22
+ return false unless check_crc
23
+ return false unless check_empty
24
+ return false unless check_position
25
+
26
+ record_iend_chunk
27
+ true
28
+ end
29
+
30
+ private
31
+
32
+ # Check that IEND chunk is empty
33
+ def check_empty
34
+ return true if chunk.chunk_data.empty?
35
+
36
+ add_error("invalid IEND chunk length " \
37
+ "(#{chunk.chunk_data.length}, should be 0)")
38
+ false
39
+ end
40
+
41
+ # Check IEND position relative to other chunks
42
+ def check_position
43
+ valid = true
44
+
45
+ # IEND must appear after IHDR
46
+ unless context.seen?("IHDR")
47
+ add_error("IEND chunk before IHDR")
48
+ valid = false
49
+ end
50
+
51
+ # IEND must appear after at least one IDAT
52
+ unless context.seen?("IDAT")
53
+ add_error("IEND chunk before IDAT")
54
+ valid = false
55
+ end
56
+
57
+ valid
58
+ end
59
+
60
+ # Record IEND chunk
61
+ def record_iend_chunk
62
+ context.record_chunk("IEND", chunk)
63
+ context.store(:has_iend, true)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Critical
8
+ # Validator for PNG IHDR (Image Header) chunk
9
+ #
10
+ # IHDR is always the first chunk in a PNG file and defines:
11
+ # - Image dimensions (width, height)
12
+ # - Bit depth
13
+ # - Color type
14
+ # - Compression method
15
+ # - Filter method
16
+ # - Interlace method
17
+ #
18
+ # Validation rules from PNG spec:
19
+ # - Must be exactly 13 bytes
20
+ # - Width and height must be non-zero
21
+ # - Bit depth must be valid for color type
22
+ # - Color type must be 0, 2, 3, 4, or 6
23
+ # - Compression method must be 0
24
+ # - Filter method must be 0
25
+ # - Interlace method must be 0 or 1
26
+ class IhdrValidator < BaseValidator
27
+ # Valid color types
28
+ COLOR_TYPES = {
29
+ 0 => "grayscale",
30
+ 2 => "truecolor",
31
+ 3 => "indexed-color",
32
+ 4 => "grayscale with alpha",
33
+ 6 => "truecolor with alpha",
34
+ }.freeze
35
+
36
+ # Valid bit depths for each color type
37
+ VALID_BIT_DEPTHS = {
38
+ 0 => [1, 2, 4, 8, 16], # Grayscale
39
+ 2 => [8, 16], # Truecolor
40
+ 3 => [1, 2, 4, 8], # Indexed-color
41
+ 4 => [8, 16], # Grayscale with alpha
42
+ 6 => [8, 16], # Truecolor with alpha
43
+ }.freeze
44
+
45
+ # Validate IHDR chunk
46
+ #
47
+ # @return [Boolean] True if validation passed
48
+ def validate
49
+ return false unless check_crc
50
+ return false unless check_length(13)
51
+
52
+ data = chunk.chunk_data
53
+ width = data[0, 4].unpack1("N")
54
+ height = data[4, 4].unpack1("N")
55
+ bit_depth = data[8].unpack1("C")
56
+ color_type = data[9].unpack1("C")
57
+ compression = data[10].unpack1("C")
58
+ filter = data[11].unpack1("C")
59
+ interlace = data[12].unpack1("C")
60
+
61
+ valid = true
62
+ valid &= check_dimensions(width, height)
63
+ valid &= check_color_type(color_type)
64
+ valid &= check_bit_depth(bit_depth, color_type)
65
+ valid &= check_compression(compression)
66
+ valid &= check_filter(filter)
67
+ valid &= check_interlace(interlace)
68
+
69
+ if valid
70
+ store_ihdr_info(width, height, bit_depth, color_type,
71
+ interlace)
72
+ end
73
+
74
+ valid
75
+ end
76
+
77
+ private
78
+
79
+ # Check image dimensions
80
+ def check_dimensions(width, height)
81
+ valid = true
82
+
83
+ if width.zero?
84
+ add_error("invalid image width (0)")
85
+ valid = false
86
+ end
87
+
88
+ if height.zero?
89
+ add_error("invalid image height (0)")
90
+ valid = false
91
+ end
92
+
93
+ if width > 2**31 - 1
94
+ add_warning("image width (#{width}) exceeds maximum " \
95
+ "recommended value")
96
+ end
97
+
98
+ if height > 2**31 - 1
99
+ add_warning("image height (#{height}) exceeds maximum " \
100
+ "recommended value")
101
+ end
102
+
103
+ valid
104
+ end
105
+
106
+ # Check color type is valid
107
+ def check_color_type(color_type)
108
+ check_enum(color_type, COLOR_TYPES.keys, "color type")
109
+ end
110
+
111
+ # Check bit depth is valid for color type
112
+ def check_bit_depth(bit_depth, color_type)
113
+ return false unless COLOR_TYPES.key?(color_type)
114
+
115
+ valid_depths = VALID_BIT_DEPTHS[color_type]
116
+ return true if valid_depths.include?(bit_depth)
117
+
118
+ add_error("invalid bit depth (#{bit_depth}) for " \
119
+ "#{COLOR_TYPES[color_type]} (must be one of " \
120
+ "#{valid_depths.join(', ')})")
121
+ false
122
+ end
123
+
124
+ # Check compression method
125
+ def check_compression(compression)
126
+ if compression != 0
127
+ add_error("invalid compression method (#{compression}, " \
128
+ "must be 0)")
129
+ return false
130
+ end
131
+ true
132
+ end
133
+
134
+ # Check filter method
135
+ def check_filter(filter)
136
+ if filter != 0
137
+ add_error("invalid filter method (#{filter}, must be 0)")
138
+ return false
139
+ end
140
+ true
141
+ end
142
+
143
+ # Check interlace method
144
+ def check_interlace(interlace)
145
+ check_enum(interlace, [0, 1], "interlace method")
146
+ end
147
+
148
+ # Store IHDR information in context for use by other validators
149
+ def store_ihdr_info(width, height, bit_depth, color_type, interlace)
150
+ context.store(:width, width)
151
+ context.store(:height, height)
152
+ context.store(:bit_depth, bit_depth)
153
+ context.store(:color_type, color_type)
154
+ context.store(:interlace, interlace)
155
+ context.store(:color_type_name, COLOR_TYPES[color_type])
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Critical
8
+ # Validator for PNG PLTE (Palette) chunk
9
+ #
10
+ # PLTE defines the color palette for indexed-color images.
11
+ # - Required for color type 3 (indexed-color)
12
+ # - Optional for color types 2 and 6 (truecolor and truecolor+alpha)
13
+ # - Forbidden for color types 0 and 4 (grayscale and grayscale+alpha)
14
+ #
15
+ # Validation rules from PNG spec:
16
+ # - Length must be divisible by 3 (RGB triplets)
17
+ # - Must contain 1-256 palette entries
18
+ # - For indexed-color, number of entries must not exceed 2^bit_depth
19
+ # - Must appear before first IDAT chunk
20
+ # - Must appear before bKGD, hIST, tRNS chunks
21
+ class PlteValidator < BaseValidator
22
+ # Maximum number of palette entries
23
+ MAX_ENTRIES = 256
24
+
25
+ # Validate PLTE chunk
26
+ #
27
+ # @return [Boolean] True if validation passed
28
+ def validate
29
+ return false unless check_crc
30
+ return false unless check_divisible_by_3
31
+ return false unless check_entry_count
32
+ return false unless check_color_type_compatibility
33
+ return false unless check_bit_depth_compatibility
34
+
35
+ store_palette_info
36
+ true
37
+ end
38
+
39
+ private
40
+
41
+ # Check that chunk length is divisible by 3
42
+ def check_divisible_by_3
43
+ length = chunk.chunk_data.length
44
+ return true if (length % 3).zero?
45
+
46
+ add_error("invalid PLTE length (#{length}, must be divisible by 3)")
47
+ false
48
+ end
49
+
50
+ # Check number of palette entries
51
+ def check_entry_count
52
+ length = chunk.chunk_data.length
53
+ entries = length / 3
54
+
55
+ if entries.zero?
56
+ add_error("invalid PLTE chunk (no entries)")
57
+ return false
58
+ end
59
+
60
+ if entries > MAX_ENTRIES
61
+ add_error("invalid PLTE chunk (#{entries} entries, " \
62
+ "maximum is #{MAX_ENTRIES})")
63
+ return false
64
+ end
65
+
66
+ true
67
+ end
68
+
69
+ # Check PLTE compatibility with color type
70
+ def check_color_type_compatibility
71
+ color_type = context.retrieve(:color_type)
72
+ return true unless color_type # IHDR not validated yet
73
+
74
+ case color_type
75
+ when 0, 4
76
+ # Grayscale and grayscale+alpha: PLTE forbidden
77
+ add_error("PLTE chunk not allowed for grayscale images")
78
+ false
79
+ when 3
80
+ # Indexed-color: PLTE required (checked elsewhere)
81
+ true
82
+ when 2, 6
83
+ # Truecolor and truecolor+alpha: PLTE optional (suggested palette)
84
+ add_info("PLTE chunk present (suggested palette)")
85
+ true
86
+ else
87
+ # Unknown color type
88
+ add_warning("PLTE chunk present but color type unknown")
89
+ true
90
+ end
91
+ end
92
+
93
+ # Check palette size vs bit depth for indexed-color images
94
+ def check_bit_depth_compatibility
95
+ color_type = context.retrieve(:color_type)
96
+ bit_depth = context.retrieve(:bit_depth)
97
+
98
+ return true unless color_type == 3 # Only for indexed-color
99
+ return true unless bit_depth # Bit depth not available
100
+
101
+ max_entries = 2**bit_depth
102
+ entries = chunk.chunk_data.length / 3
103
+
104
+ return true if entries <= max_entries
105
+
106
+ add_error("PLTE chunk has #{entries} entries but bit depth " \
107
+ "#{bit_depth} allows maximum of #{max_entries}")
108
+ false
109
+ end
110
+
111
+ # Store palette information in context
112
+ def store_palette_info
113
+ entries = chunk.chunk_data.length / 3
114
+ context.store(:palette_entries, entries)
115
+ context.store(:has_palette, true)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end