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,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Ancillary
8
+ # Validator for PNG pCAL (Pixel Calibration) chunk
9
+ #
10
+ # pCAL maps pixel values to physical values:
11
+ # - Calibration name (1-79 bytes, Latin-1)
12
+ # - Null separator (1 byte)
13
+ # - Original zero (4 bytes, signed)
14
+ # - Original max (4 bytes, signed)
15
+ # - Equation type (1 byte, 0-3)
16
+ # - Number of parameters (1 byte)
17
+ # - Unit name (null-terminated Latin-1)
18
+ # - Parameters (null-terminated ASCII floating point strings)
19
+ #
20
+ # Validation rules from PNG spec:
21
+ # - Calibration name must be 1-79 characters, Latin-1 printable
22
+ # - Calibration name must not have leading/trailing/consecutive spaces
23
+ # - Equation type must be 0-3
24
+ # - Number of parameters must match equation type requirements
25
+ # - Parameters must be valid ASCII floating point numbers
26
+ # - Must appear before IDAT chunk
27
+ # - Only one pCAL chunk allowed
28
+ class PcalValidator < BaseValidator
29
+ # Maximum calibration name length
30
+ MAX_CALIBRATION_NAME_LENGTH = 79
31
+
32
+ # Latin-1 printable characters (space to tilde + high ASCII)
33
+ PRINTABLE_LATIN1 = (32..126).to_a + (161..255).to_a
34
+
35
+ # Valid equation types and required parameter counts
36
+ EQUATION_LINEAR = 0 # p0 + p1*x (2 params)
37
+ EQUATION_BASE_E = 1 # p0 + p1*e^(p2*x) (3 params)
38
+ EQUATION_BASE_10 = 2 # p0 + p1*10^(p2*x) (3 params)
39
+ EQUATION_ARBITRARY = 3 # p0 + p1*n^(p2*x) (4 params)
40
+
41
+ EQUATION_PARAM_COUNTS = {
42
+ EQUATION_LINEAR => 2,
43
+ EQUATION_BASE_E => 3,
44
+ EQUATION_BASE_10 => 3,
45
+ EQUATION_ARBITRARY => 4,
46
+ }.freeze
47
+
48
+ # ASCII floating point regex
49
+ FLOAT_REGEX = /\A[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\z/
50
+
51
+ # Validate pCAL chunk
52
+ #
53
+ # @return [Boolean] True if validation passed
54
+ def validate
55
+ return false unless check_crc
56
+ return false unless check_uniqueness
57
+ return false unless check_position
58
+ return false unless check_structure
59
+ return false unless check_calibration_name
60
+ return false unless check_equation_type
61
+ return false unless check_parameters
62
+
63
+ store_calibration_info
64
+ true
65
+ end
66
+
67
+ private
68
+
69
+ # Check that only one pCAL chunk exists
70
+ def check_uniqueness
71
+ if context.retrieve(:has_calibration)
72
+ add_error("duplicate pCAL chunk (only one allowed)")
73
+ return false
74
+ end
75
+
76
+ true
77
+ end
78
+
79
+ # Check that pCAL appears before IDAT
80
+ def check_position
81
+ if context.seen?("IDAT")
82
+ add_error("pCAL chunk after IDAT chunk")
83
+ return false
84
+ end
85
+
86
+ true
87
+ end
88
+
89
+ # Check pCAL chunk structure
90
+ def check_structure
91
+ data = chunk.chunk_data
92
+
93
+ # Must contain at least name + nulls + int fields + equation type + param count
94
+ if data.length < 12
95
+ add_error("pCAL chunk too short (minimum 12 bytes)")
96
+ return false
97
+ end
98
+
99
+ # Must contain null separator after name
100
+ null_pos = data.index("\0")
101
+ unless null_pos
102
+ add_error("pCAL chunk missing null separator")
103
+ return false
104
+ end
105
+
106
+ true
107
+ end
108
+
109
+ # Check calibration name validity
110
+ def check_calibration_name
111
+ data = chunk.chunk_data
112
+ null_pos = data.index("\0")
113
+ cal_name = data[0, null_pos]
114
+
115
+ # Check calibration name length
116
+ if cal_name.empty?
117
+ add_error("pCAL chunk has empty calibration name")
118
+ return false
119
+ end
120
+
121
+ if cal_name.length > MAX_CALIBRATION_NAME_LENGTH
122
+ add_error("pCAL calibration name too long " \
123
+ "(#{cal_name.length}, max #{MAX_CALIBRATION_NAME_LENGTH})")
124
+ return false
125
+ end
126
+
127
+ # Check for Latin-1 printable characters
128
+ cal_name.bytes.each do |byte|
129
+ next if PRINTABLE_LATIN1.include?(byte)
130
+
131
+ add_error("pCAL calibration name contains non-printable " \
132
+ "character (0x#{byte.to_s(16)})")
133
+ return false
134
+ end
135
+
136
+ # Check for leading/trailing spaces
137
+ if cal_name.start_with?(" ")
138
+ add_error("pCAL calibration name has leading space")
139
+ return false
140
+ end
141
+
142
+ if cal_name.end_with?(" ")
143
+ add_error("pCAL calibration name has trailing space")
144
+ return false
145
+ end
146
+
147
+ # Check for consecutive spaces
148
+ if cal_name.include?(" ")
149
+ add_error("pCAL calibration name has consecutive spaces")
150
+ return false
151
+ end
152
+
153
+ true
154
+ end
155
+
156
+ # Check equation type
157
+ def check_equation_type
158
+ data = chunk.chunk_data
159
+ null_pos = data.index("\0")
160
+ equation_type = data[null_pos + 9].ord
161
+
162
+ unless EQUATION_PARAM_COUNTS.key?(equation_type)
163
+ add_error("pCAL invalid equation type (#{equation_type}, " \
164
+ "must be 0-3)")
165
+ return false
166
+ end
167
+
168
+ true
169
+ end
170
+
171
+ # Check parameters
172
+ def check_parameters
173
+ data = chunk.chunk_data
174
+ null_pos = data.index("\0")
175
+ equation_type = data[null_pos + 9].ord
176
+ num_params = data[null_pos + 10].ord
177
+
178
+ expected_params = EQUATION_PARAM_COUNTS[equation_type]
179
+ unless num_params == expected_params
180
+ add_error("pCAL parameter count mismatch (#{num_params}, " \
181
+ "expected #{expected_params} for equation type #{equation_type})")
182
+ return false
183
+ end
184
+
185
+ # Find unit name and parameters
186
+ unit_start = null_pos + 11
187
+ unit_null = data.index("\0", unit_start)
188
+ unless unit_null
189
+ add_error("pCAL missing unit name null terminator")
190
+ return false
191
+ end
192
+
193
+ # Parse parameters
194
+ param_start = unit_null + 1
195
+ params_found = 0
196
+ pos = param_start
197
+
198
+ while params_found < num_params && pos < data.length
199
+ param_end = data.index("\0", pos)
200
+ unless param_end
201
+ add_error("pCAL missing parameter #{params_found + 1} " \
202
+ "null terminator")
203
+ return false
204
+ end
205
+
206
+ param_str = data[pos, param_end - pos]
207
+ unless valid_float_string?(param_str)
208
+ add_error("pCAL invalid parameter #{params_found + 1} " \
209
+ "format: '#{param_str}'")
210
+ return false
211
+ end
212
+
213
+ params_found += 1
214
+ pos = param_end + 1
215
+ end
216
+
217
+ unless params_found == num_params
218
+ add_error("pCAL found #{params_found} parameters, " \
219
+ "expected #{num_params}")
220
+ return false
221
+ end
222
+
223
+ true
224
+ end
225
+
226
+ # Validate floating point string format
227
+ def valid_float_string?(str)
228
+ return false if str.empty?
229
+
230
+ str.match?(FLOAT_REGEX)
231
+ end
232
+
233
+ # Store calibration information in context
234
+ def store_calibration_info
235
+ data = chunk.chunk_data
236
+ null_pos = data.index("\0")
237
+ cal_name = data[0, null_pos]
238
+
239
+ # Parse original zero and max (signed 32-bit big-endian)
240
+ orig_zero_bytes = data[null_pos + 1, 4].bytes
241
+ orig_zero = (orig_zero_bytes[0] << 24) |
242
+ (orig_zero_bytes[1] << 16) |
243
+ (orig_zero_bytes[2] << 8) |
244
+ orig_zero_bytes[3]
245
+ orig_zero -= (1 << 32) if orig_zero >= (1 << 31)
246
+
247
+ orig_max_bytes = data[null_pos + 5, 4].bytes
248
+ orig_max = (orig_max_bytes[0] << 24) |
249
+ (orig_max_bytes[1] << 16) |
250
+ (orig_max_bytes[2] << 8) |
251
+ orig_max_bytes[3]
252
+ orig_max -= (1 << 32) if orig_max >= (1 << 31)
253
+
254
+ equation_type = data[null_pos + 9].ord
255
+ num_params = data[null_pos + 10].ord
256
+
257
+ # Parse unit name
258
+ unit_start = null_pos + 11
259
+ unit_null = data.index("\0", unit_start)
260
+ unit_name = data[unit_start, unit_null - unit_start]
261
+
262
+ # Parse parameters
263
+ params = []
264
+ pos = unit_null + 1
265
+ num_params.times do
266
+ param_end = data.index("\0", pos)
267
+ param_str = data[pos, param_end - pos]
268
+ params << param_str.to_f
269
+ pos = param_end + 1
270
+ end
271
+
272
+ # Store in context
273
+ context.store(:has_calibration, true)
274
+ context.store(:calibration_name, cal_name)
275
+ context.store(:calibration_equation_type, equation_type)
276
+ context.store(:calibration_parameters, params)
277
+
278
+ # Add info about the calibration
279
+ eq_type_name = %w[linear base-e base-10
280
+ arbitrary][equation_type]
281
+ add_info("pCAL: \"#{cal_name}\" #{eq_type_name} equation, " \
282
+ "range #{orig_zero}..#{orig_max}, " \
283
+ "unit \"#{unit_name}\", " \
284
+ "params: #{params.join(', ')}")
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Ancillary
8
+ # Validator for PNG pHYs (Physical Pixel Dimensions) chunk
9
+ #
10
+ # pHYs specifies the intended pixel size or aspect ratio:
11
+ # - Pixels per unit, X axis (4 bytes)
12
+ # - Pixels per unit, Y axis (4 bytes)
13
+ # - Unit specifier (1 byte): 0 = unknown, 1 = meters
14
+ #
15
+ # Validation rules from PNG spec:
16
+ # - Must be exactly 9 bytes
17
+ # - Must appear before IDAT
18
+ # - Only one pHYs chunk allowed
19
+ # - Unit specifier must be 0 or 1
20
+ class PhysValidator < BaseValidator
21
+ # Unit specifiers
22
+ UNIT_UNKNOWN = 0
23
+ UNIT_METER = 1
24
+
25
+ # Common DPI values (pixels per inch)
26
+ DPI_72 = 2835 # 72 DPI in pixels per meter
27
+ DPI_96 = 3780 # 96 DPI in pixels per meter
28
+ DPI_150 = 5906 # 150 DPI in pixels per meter
29
+ DPI_300 = 11_811 # 300 DPI in pixels per meter
30
+
31
+ # Validate pHYs chunk
32
+ #
33
+ # @return [Boolean] True if validation passed
34
+ def validate
35
+ return false unless check_crc
36
+ return false unless check_length(9)
37
+ return false unless check_position
38
+ return false unless check_uniqueness
39
+ return false unless check_unit
40
+
41
+ store_phys_info
42
+ true
43
+ end
44
+
45
+ private
46
+
47
+ # Check pHYs position relative to other chunks
48
+ def check_position
49
+ # pHYs should appear before IDAT
50
+ if context.seen?("IDAT")
51
+ add_error("pHYs chunk after IDAT (must be before)")
52
+ return false
53
+ end
54
+ true
55
+ end
56
+
57
+ # Check that only one pHYs chunk is present
58
+ def check_uniqueness
59
+ if context.seen?("pHYs")
60
+ add_error("duplicate pHYs chunk")
61
+ return false
62
+ end
63
+ true
64
+ end
65
+
66
+ # Check unit specifier
67
+ def check_unit
68
+ data = chunk.chunk_data
69
+ unit = data[8].unpack1("C")
70
+
71
+ check_enum(unit, [UNIT_UNKNOWN, UNIT_METER], "unit specifier")
72
+ end
73
+
74
+ # Store physical dimensions in context
75
+ def store_phys_info
76
+ data = chunk.chunk_data
77
+ pixels_per_unit_x = data[0, 4].unpack1("N")
78
+ pixels_per_unit_y = data[4, 4].unpack1("N")
79
+ unit = data[8].unpack1("C")
80
+
81
+ context.store(:phys_x, pixels_per_unit_x)
82
+ context.store(:phys_y, pixels_per_unit_y)
83
+ context.store(:phys_unit, unit)
84
+
85
+ # Calculate aspect ratio
86
+ if pixels_per_unit_x.positive? && pixels_per_unit_y.positive?
87
+ aspect = pixels_per_unit_x.to_f / pixels_per_unit_y
88
+ context.store(:pixel_aspect_ratio, aspect)
89
+
90
+ if (aspect - 1.0).abs > 0.01
91
+ add_info("pHYs: non-square pixels " \
92
+ "(aspect ratio #{format('%.3f', aspect)})")
93
+ end
94
+ end
95
+
96
+ # Provide DPI information if unit is meters
97
+ unless unit == UNIT_METER && pixels_per_unit_x == pixels_per_unit_y
98
+ return
99
+ end
100
+
101
+ dpi = (pixels_per_unit_x * 0.0254).round
102
+ add_info("pHYs: #{dpi} DPI")
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Ancillary
8
+ # Validator for PNG sBIT (Significant Bits) chunk
9
+ #
10
+ # sBIT indicates the number of significant bits in the original image:
11
+ # - For grayscale: 1 byte (significant grayscale bits)
12
+ # - For truecolor: 3 bytes (R, G, B significant bits)
13
+ # - For indexed-color: 3 bytes (R, G, B significant bits)
14
+ # - For grayscale+alpha: 2 bytes (gray, alpha significant bits)
15
+ # - For truecolor+alpha: 4 bytes (R, G, B, alpha significant bits)
16
+ #
17
+ # Validation rules from PNG spec:
18
+ # - Length depends on color type
19
+ # - Must appear before PLTE and IDAT
20
+ # - Only one sBIT chunk allowed
21
+ # - Each value must be > 0 and <= sample depth
22
+ class SbitValidator < BaseValidator
23
+ # Validate sBIT chunk
24
+ #
25
+ # @return [Boolean] True if validation passed
26
+ def validate
27
+ return false unless check_crc
28
+ return false unless check_position
29
+ return false unless check_uniqueness
30
+ return false unless check_length_for_color_type
31
+ return false unless check_values
32
+
33
+ store_sbit_info
34
+ true
35
+ end
36
+
37
+ private
38
+
39
+ # Check sBIT position relative to other chunks
40
+ def check_position
41
+ valid = true
42
+
43
+ # sBIT should appear before PLTE and IDAT
44
+ if context.seen?("PLTE")
45
+ add_warning("sBIT chunk after PLTE (should be before)")
46
+ end
47
+
48
+ if context.seen?("IDAT")
49
+ add_error("sBIT chunk after IDAT (must be before)")
50
+ valid = false
51
+ end
52
+
53
+ valid
54
+ end
55
+
56
+ # Check that only one sBIT chunk is present
57
+ def check_uniqueness
58
+ if context.seen?("sBIT")
59
+ add_error("duplicate sBIT chunk")
60
+ return false
61
+ end
62
+ true
63
+ end
64
+
65
+ # Check sBIT length for color type
66
+ def check_length_for_color_type
67
+ color_type = context.retrieve(:color_type)
68
+ return true unless color_type
69
+
70
+ expected_length = case color_type
71
+ when 0 then 1 # Grayscale
72
+ when 2 then 3 # Truecolor
73
+ when 3 then 3 # Indexed-color
74
+ when 4 then 2 # Grayscale + alpha
75
+ when 6 then 4 # Truecolor + alpha
76
+ else
77
+ return true
78
+ end
79
+
80
+ check_length(expected_length)
81
+ end
82
+
83
+ # Check significant bit values
84
+ def check_values
85
+ color_type = context.retrieve(:color_type)
86
+ bit_depth = context.retrieve(:bit_depth)
87
+ return true unless color_type && bit_depth
88
+
89
+ data = chunk.chunk_data
90
+ valid = true
91
+
92
+ case color_type
93
+ when 0
94
+ # Grayscale
95
+ gray_bits = data[0].unpack1("C")
96
+ valid &= check_sbit_value(gray_bits, bit_depth, "gray")
97
+ when 2
98
+ # Truecolor
99
+ red, green, blue = data.unpack("CCC")
100
+ valid &= check_sbit_value(red, bit_depth, "red")
101
+ valid &= check_sbit_value(green, bit_depth, "green")
102
+ valid &= check_sbit_value(blue, bit_depth, "blue")
103
+ when 3
104
+ # Indexed-color (refers to palette sample depth, which is always 8)
105
+ red, green, blue = data.unpack("CCC")
106
+ valid &= check_sbit_value(red, 8, "red")
107
+ valid &= check_sbit_value(green, 8, "green")
108
+ valid &= check_sbit_value(blue, 8, "blue")
109
+ when 4
110
+ # Grayscale + alpha
111
+ gray, alpha = data.unpack("CC")
112
+ valid &= check_sbit_value(gray, bit_depth, "gray")
113
+ valid &= check_sbit_value(alpha, bit_depth, "alpha")
114
+ when 6
115
+ # Truecolor + alpha
116
+ red, green, blue, alpha = data.unpack("CCCC")
117
+ valid &= check_sbit_value(red, bit_depth, "red")
118
+ valid &= check_sbit_value(green, bit_depth, "green")
119
+ valid &= check_sbit_value(blue, bit_depth, "blue")
120
+ valid &= check_sbit_value(alpha, bit_depth, "alpha")
121
+ end
122
+
123
+ valid
124
+ end
125
+
126
+ # Check individual significant bit value
127
+ def check_sbit_value(value, max_depth, name)
128
+ if value.zero?
129
+ add_error("#{name} significant bits cannot be 0")
130
+ return false
131
+ end
132
+
133
+ if value > max_depth
134
+ add_error("#{name} significant bits (#{value}) exceeds " \
135
+ "sample depth (#{max_depth})")
136
+ return false
137
+ end
138
+
139
+ true
140
+ end
141
+
142
+ # Store significant bit information in context
143
+ def store_sbit_info
144
+ color_type = context.retrieve(:color_type)
145
+ data = chunk.chunk_data
146
+
147
+ case color_type
148
+ when 0
149
+ gray = data[0].unpack1("C")
150
+ context.store(:significant_bits, { gray: gray })
151
+ add_info("sBIT: gray=#{gray}")
152
+ when 2
153
+ red, green, blue = data.unpack("CCC")
154
+ context.store(:significant_bits,
155
+ { red: red, green: green, blue: blue })
156
+ add_info("sBIT: R=#{red}, G=#{green}, B=#{blue}")
157
+ when 3
158
+ red, green, blue = data.unpack("CCC")
159
+ context.store(:significant_bits,
160
+ { red: red, green: green, blue: blue })
161
+ add_info("sBIT: R=#{red}, G=#{green}, B=#{blue}")
162
+ when 4
163
+ gray, alpha = data.unpack("CC")
164
+ context.store(:significant_bits, { gray: gray, alpha: alpha })
165
+ add_info("sBIT: gray=#{gray}, alpha=#{alpha}")
166
+ when 6
167
+ red, green, blue, alpha = data.unpack("CCCC")
168
+ context.store(:significant_bits,
169
+ { red: red, green: green, blue: blue, alpha: alpha })
170
+ add_info("sBIT: R=#{red}, G=#{green}, B=#{blue}, A=#{alpha}")
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end