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,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Jng
8
+ # Validates JNG JDAT (JPEG image data) chunks
9
+ #
10
+ # The JDAT chunk contains JPEG-compressed image data. Multiple JDAT chunks
11
+ # may be present in a JNG file and must be concatenated in sequence to
12
+ # form the complete JPEG datastream.
13
+ #
14
+ # Validation rules:
15
+ # - Must appear after JHDR
16
+ # - Contains JPEG compressed data
17
+ # - Multiple JDAT chunks allowed (concatenated in sequence)
18
+ # - Must appear before IEND
19
+ class JdatValidator < BaseValidator
20
+ # Validate JDAT chunk
21
+ #
22
+ # @return [Boolean] True if validation passed
23
+ def validate
24
+ return false unless check_crc
25
+
26
+ valid = true
27
+
28
+ # JDAT must appear after JHDR
29
+ unless context.retrieve(:jhdr_present)
30
+ add_error("JDAT must appear after JHDR")
31
+ valid = false
32
+ end
33
+
34
+ # JDAT must appear before IEND
35
+ if context.seen?("IEND")
36
+ add_error("JDAT must appear before IEND")
37
+ valid = false
38
+ end
39
+
40
+ # Check minimum length (at least 1 byte of JPEG data)
41
+ data = chunk.chunk_data
42
+ if data.empty?
43
+ add_error("JDAT chunk too short (#{data.length} bytes, " \
44
+ "minimum 1)")
45
+ valid = false
46
+ end
47
+
48
+ if valid
49
+ # Store JDAT count for tracking
50
+ jdat_count = context.retrieve(:jdat_count) || 0
51
+ context.store(:jdat_count, jdat_count + 1)
52
+
53
+ # Track total JDAT data length
54
+ total_jdat_length = context.retrieve(:jdat_data_length) || 0
55
+ context.store(:jdat_data_length, total_jdat_length + data.length)
56
+ end
57
+
58
+ # NOTE: We don't validate JPEG structure here - that would require
59
+ # JPEG parsing which is beyond the scope of JNG chunk validation
60
+
61
+ valid
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Jng
8
+ # Validator for JHDR (JNG Header) chunk
9
+ #
10
+ # The JHDR chunk is the first chunk in a JNG (JPEG Network Graphics)
11
+ # datastream and contains information about the JPEG image.
12
+ #
13
+ # Structure (16 bytes):
14
+ # - width (4 bytes): Image width in pixels
15
+ # - height (4 bytes): Image height in pixels
16
+ # - color_type (1 byte): Color type (8, 10, 12, 14)
17
+ # - image_sample_depth (1 byte): JPEG sample depth (8 or 12)
18
+ # - image_compression_method (1 byte): JPEG compression (8)
19
+ # - interlace_method (1 byte): Interlace method (0)
20
+ # - alpha_sample_depth (1 byte): Alpha channel sample depth
21
+ # - alpha_compression_method (1 byte): Alpha compression method
22
+ # - alpha_filter_method (1 byte): Alpha filter method
23
+ # - alpha_interlace_method (1 byte): Alpha interlace method
24
+ # - 4 reserved bytes
25
+ #
26
+ class JhdrValidator < BaseValidator
27
+ VALID_COLOR_TYPES = [8, 10, 12, 14].freeze
28
+ VALID_SAMPLE_DEPTHS = [8, 12].freeze
29
+
30
+ # Validate JHDR chunk
31
+ #
32
+ # @return [Boolean] True if validation passed
33
+ def validate
34
+ return false unless check_crc
35
+ return false unless check_length(16)
36
+
37
+ # JHDR must be first chunk in JNG
38
+ unless context.chunks_seen.empty?
39
+ add_error("JHDR must be the first chunk in JNG datastream")
40
+ return false
41
+ end
42
+
43
+ data = chunk.chunk_data
44
+
45
+ # Extract fields
46
+ width = data[0, 4].unpack1("N")
47
+ height = data[4, 4].unpack1("N")
48
+ color_type = data[8].unpack1("C")
49
+ image_sample_depth = data[9].unpack1("C")
50
+ image_compression = data[10].unpack1("C")
51
+ interlace = data[11].unpack1("C")
52
+
53
+ valid = true
54
+
55
+ # Validate dimensions
56
+ valid &= check_dimensions(width, height)
57
+
58
+ # Validate color type
59
+ valid &= check_enum(color_type, VALID_COLOR_TYPES, "color type")
60
+
61
+ # Validate sample depth
62
+ valid &= check_enum(image_sample_depth, VALID_SAMPLE_DEPTHS,
63
+ "image sample depth")
64
+
65
+ # Validate compression method (must be 8 for baseline JPEG)
66
+ if image_compression != 8
67
+ add_error("invalid image compression method (#{image_compression}, " \
68
+ "must be 8)")
69
+ valid = false
70
+ end
71
+
72
+ # Validate interlace method (must be 0)
73
+ if interlace != 0
74
+ add_error("invalid interlace method (#{interlace}, must be 0)")
75
+ valid = false
76
+ end
77
+
78
+ if valid
79
+ store_jhdr_info(width, height, color_type,
80
+ image_sample_depth)
81
+ end
82
+
83
+ valid
84
+ end
85
+
86
+ private
87
+
88
+ # Check image dimensions
89
+ def check_dimensions(width, height)
90
+ valid = true
91
+
92
+ if width.zero?
93
+ add_error("invalid image width (0)")
94
+ valid = false
95
+ end
96
+
97
+ if height.zero?
98
+ add_error("invalid image height (0)")
99
+ valid = false
100
+ end
101
+
102
+ valid
103
+ end
104
+
105
+ # Store JHDR information in context for use by other validators
106
+ def store_jhdr_info(width, height, color_type, image_sample_depth)
107
+ context.store(:jhdr_width, width)
108
+ context.store(:jhdr_height, height)
109
+ context.store(:jhdr_color_type, color_type)
110
+ context.store(:jhdr_image_sample_depth, image_sample_depth)
111
+ context.store(:jhdr_present, true)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_validator"
4
+
5
+ module PngConform
6
+ module Validators
7
+ module Jng
8
+ # Validates JNG JSEP (8-bit/12-bit image separator) chunks
9
+ #
10
+ # The JSEP chunk separates 8-bit and 12-bit JPEG image data in a JNG file.
11
+ # It is only present when the sample depth is 12 bits and both 8-bit and
12
+ # 12-bit JPEG data are included.
13
+ #
14
+ # Validation rules:
15
+ # - Must appear after JHDR
16
+ # - Length must be 0 (no data)
17
+ # - Only valid when sample depth is 12 bits
18
+ # - Separates 8-bit JDAT chunks from 12-bit JDAT chunks
19
+ # - Must appear before IEND
20
+ class JsepValidator < BaseValidator
21
+ # Validate JSEP chunk
22
+ #
23
+ # @return [Boolean] True if validation passed
24
+ def validate
25
+ return false unless check_crc
26
+ return false unless check_length(0)
27
+
28
+ valid = true
29
+
30
+ # JSEP must appear after JHDR
31
+ unless context.retrieve(:jhdr_present)
32
+ add_error("JSEP must appear after JHDR")
33
+ valid = false
34
+ end
35
+
36
+ # JSEP must appear before IEND
37
+ if context.seen?("IEND")
38
+ add_error("JSEP must appear before IEND")
39
+ valid = false
40
+ end
41
+
42
+ # JSEP is only valid with 12-bit sample depth
43
+ sample_depth = context.retrieve(:jhdr_image_sample_depth)
44
+ if sample_depth && sample_depth != 12
45
+ add_error("JSEP is only valid with 12-bit sample depth, " \
46
+ "got #{sample_depth}-bit")
47
+ valid = false
48
+ end
49
+
50
+ # JSEP should appear after at least one JDAT (8-bit data)
51
+ # and before the 12-bit JDAT chunks
52
+ unless context.retrieve(:jdat_count).to_i.positive?
53
+ add_warning("JSEP should appear after 8-bit JDAT chunks")
54
+ end
55
+
56
+ if valid
57
+ # Mark that JSEP was seen
58
+ context.store(:jsep_present, true)
59
+ end
60
+
61
+ valid
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG BACK (Background color) chunks
7
+ #
8
+ # The BACK chunk specifies the background color and image for the MNG
9
+ # frame.
10
+ #
11
+ # Validation rules:
12
+ # - Must appear after MHDR
13
+ # - Must appear before MEND
14
+ # - Length must be 6, 7, or 10 bytes
15
+ # - Mandatory background flag: 0 or 1
16
+ # - Tile mode: 0-3
17
+ class BackValidator < BaseValidator
18
+ VALID_LENGTHS = [6, 7, 10].freeze
19
+
20
+ def validate
21
+ return false unless check_crc
22
+
23
+ unless context.retrieve(:mhdr_present)
24
+ add_error("BACK must appear after MHDR")
25
+ return false
26
+ end
27
+
28
+ if context.seen?("MEND")
29
+ add_error("BACK must appear before MEND")
30
+ return false
31
+ end
32
+
33
+ data = chunk.chunk_data
34
+
35
+ unless VALID_LENGTHS.include?(data.length)
36
+ add_error(
37
+ "BACK chunk must be 6, 7, or 10 bytes, " \
38
+ "got #{data.length}",
39
+ )
40
+ return false
41
+ end
42
+
43
+ # Red, Green, Blue (2 bytes each)
44
+ red, green, blue = data[0, 6].unpack("nnn")
45
+ context.store(:back_red, red)
46
+ context.store(:back_green, green)
47
+ context.store(:back_blue, blue)
48
+
49
+ if data.length >= 7
50
+ # Mandatory background flag (1 byte)
51
+ mandatory = data.getbyte(6)
52
+
53
+ unless [0, 1].include?(mandatory)
54
+ add_error(
55
+ "BACK mandatory flag must be 0 or 1, got #{mandatory}",
56
+ )
57
+ return false
58
+ end
59
+
60
+ context.store(:back_mandatory, mandatory)
61
+ end
62
+
63
+ if data.length == 10
64
+ # Background image ID (2 bytes)
65
+ bg_image_id = data[7, 2].unpack1("n")
66
+
67
+ # Background tile mode (1 byte)
68
+ tile_mode = data.getbyte(9)
69
+
70
+ unless (0..3).cover?(tile_mode)
71
+ add_error(
72
+ "BACK tile mode must be 0-3, got #{tile_mode}",
73
+ )
74
+ return false
75
+ end
76
+
77
+ context.store(:back_image_id, bg_image_id)
78
+ context.store(:back_tile_mode, tile_mode)
79
+ end
80
+
81
+ context.store(:back_present, true)
82
+ true
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG CLIP (Clip object) chunks
7
+ #
8
+ # The CLIP chunk defines clipping boundaries for subsequent objects.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Must appear before MEND
13
+ # - Length must be 21 bytes
14
+ # - Contains first/last object IDs, clip type, and clipping boundaries
15
+ class ClipValidator < BaseValidator
16
+ EXPECTED_LENGTH = 21
17
+
18
+ def validate
19
+ return false unless check_crc
20
+
21
+ unless context.retrieve(:mhdr_present)
22
+ add_error("CLIP must appear after MHDR")
23
+ return false
24
+ end
25
+
26
+ if context.seen?("MEND")
27
+ add_error("CLIP must appear before MEND")
28
+ return false
29
+ end
30
+
31
+ data = chunk.chunk_data
32
+
33
+ unless data.length == EXPECTED_LENGTH
34
+ add_error(
35
+ "CLIP chunk must be #{EXPECTED_LENGTH} bytes, " \
36
+ "got #{data.length}",
37
+ )
38
+ return false
39
+ end
40
+
41
+ # Parse CLIP data
42
+ # First object ID (2 bytes)
43
+ # Last object ID (2 bytes)
44
+ # Clip type (1 byte)
45
+ # Left delta (4 bytes, signed)
46
+ # Right delta (4 bytes, signed)
47
+ # Top delta (4 bytes, signed)
48
+ # Bottom delta (4 bytes, signed)
49
+ first_id, last_id, clip_type, left, right, top, bottom =
50
+ data.unpack("nnCl>l>l>l>")
51
+
52
+ context.store(:clip_first_id, first_id)
53
+ context.store(:clip_last_id, last_id)
54
+ context.store(:clip_type, clip_type)
55
+ context.store(:clip_left, left)
56
+ context.store(:clip_right, right)
57
+ context.store(:clip_top, top)
58
+ context.store(:clip_bottom, bottom)
59
+ context.store(:clip_present, true)
60
+ true
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG CLON (Clone object) chunks
7
+ #
8
+ # The CLON chunk creates a copy of an existing object.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Length must be 4 or 16 bytes
13
+ # - Must appear before MEND
14
+ class ClonValidator < BaseValidator
15
+ VALID_LENGTHS = [4, 16].freeze
16
+
17
+ def validate
18
+ return false unless check_crc
19
+
20
+ unless context.retrieve(:mhdr_present)
21
+ add_error("CLON must appear after MHDR")
22
+ return false
23
+ end
24
+
25
+ if context.seen?("MEND")
26
+ add_error("CLON must appear before MEND")
27
+ return false
28
+ end
29
+
30
+ data = chunk.chunk_data
31
+
32
+ unless VALID_LENGTHS.include?(data.length)
33
+ add_error(
34
+ "CLON chunk must be 4 or 16 bytes, got #{data.length}",
35
+ )
36
+ return false
37
+ end
38
+
39
+ context.store(:clon_present, true)
40
+ true
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG DEFI (Object definition) chunks
7
+ #
8
+ # The DEFI chunk defines an object and its location within the MNG frame.
9
+ # It specifies the object ID, clipping boundaries, and concrete flag.
10
+ #
11
+ # Validation rules:
12
+ # - Must appear after MHDR
13
+ # - Length must be 2, 3, 4, 12, or 28 bytes
14
+ # - Object ID >= 0
15
+ # - Clipping boundaries must be valid if specified
16
+ # - Must appear before MEND
17
+ class DefiValidator < BaseValidator
18
+ VALID_LENGTHS = [2, 3, 4, 12, 28].freeze
19
+
20
+ def validate
21
+ return false unless check_crc
22
+
23
+ unless context.retrieve(:mhdr_present)
24
+ add_error("DEFI must appear after MHDR")
25
+ return false
26
+ end
27
+
28
+ if context.seen?("MEND")
29
+ add_error("DEFI must appear before MEND")
30
+ return false
31
+ end
32
+
33
+ data = chunk.chunk_data
34
+ unless VALID_LENGTHS.include?(data.length)
35
+ add_error(
36
+ "DEFI chunk must be 2, 3, 4, 12, or 28 bytes, " \
37
+ "got #{data.length}",
38
+ )
39
+ return false
40
+ end
41
+
42
+ pos = 0
43
+
44
+ # Object ID (2 bytes)
45
+ object_id = data.unpack1("n")
46
+ pos += 2
47
+ context.store(:defi_object_id, object_id)
48
+
49
+ if data.length >= 3
50
+ # Do-not-show flag (1 byte)
51
+ do_not_show = data.getbyte(pos)
52
+ pos += 1
53
+ context.store(:defi_do_not_show, do_not_show)
54
+ end
55
+
56
+ if data.length >= 4
57
+ # Concrete flag (1 byte)
58
+ concrete = data.getbyte(pos)
59
+ pos += 1
60
+ context.store(:defi_concrete, concrete)
61
+ end
62
+
63
+ if data.length >= 12
64
+ # X and Y location (4 bytes each)
65
+ x_location, y_location = data[pos, 8].unpack("NN")
66
+ pos += 8
67
+ context.store(:defi_x_location, x_location)
68
+ context.store(:defi_y_location, y_location)
69
+ end
70
+
71
+ if data.length == 28
72
+ # Clipping boundaries (4 x 4 bytes)
73
+ left, right, top, bottom = data[pos, 16].unpack("NNNN")
74
+
75
+ # Validate clipping boundaries
76
+ if left > right
77
+ add_error(
78
+ "DEFI left clipping (#{left}) must not exceed " \
79
+ "right clipping (#{right})",
80
+ )
81
+ return false
82
+ end
83
+
84
+ if top > bottom
85
+ add_error(
86
+ "DEFI top clipping (#{top}) must not exceed " \
87
+ "bottom clipping (#{bottom})",
88
+ )
89
+ return false
90
+ end
91
+
92
+ context.store(:defi_clip_left, left)
93
+ context.store(:defi_clip_right, right)
94
+ context.store(:defi_clip_top, top)
95
+ context.store(:defi_clip_bottom, bottom)
96
+ end
97
+
98
+ context.store(:defi_present, true)
99
+ true
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG DHDR (Delta-PNG header) chunks
7
+ #
8
+ # The DHDR chunk marks the beginning of an embedded delta-PNG (difference
9
+ # image) in an MNG file. It has a similar structure to IHDR but is used
10
+ # for delta frames.
11
+ #
12
+ # Validation rules:
13
+ # - Must appear after MHDR
14
+ # - Length must be 4, 12, or 20 bytes
15
+ # - Object ID must be >= 0
16
+ # - Image type: 0 (PNG), 2 (JNG), or 4 (PNG with alpha separation)
17
+ # - Delta type: 0 (entire replacement), 1-7 (various delta types)
18
+ # - Block width/height must be > 0 if specified
19
+ class DhdrValidator < BaseValidator
20
+ VALID_LENGTHS = [4, 12, 20].freeze
21
+ VALID_IMAGE_TYPES = [0, 2, 4].freeze
22
+ VALID_DELTA_TYPES = (0..7).to_a.freeze
23
+
24
+ def validate
25
+ return false unless check_crc
26
+
27
+ unless context.retrieve(:mhdr_present)
28
+ add_error("DHDR must appear after MHDR")
29
+ return false
30
+ end
31
+
32
+ if context.seen?("MEND")
33
+ add_error("DHDR must appear before MEND")
34
+ return false
35
+ end
36
+
37
+ data = chunk.chunk_data
38
+ unless VALID_LENGTHS.include?(data.length)
39
+ add_error(
40
+ "DHDR chunk must be 4, 12, or 20 bytes, " \
41
+ "got #{data.length}",
42
+ )
43
+ return false
44
+ end
45
+
46
+ values = data.unpack("N*")
47
+
48
+ # All formats have object ID
49
+ object_id = values[0]
50
+ context.store(:dhdr_object_id, object_id)
51
+
52
+ if data.length >= 12
53
+ # 12 or 20 byte format includes image type and delta type
54
+ image_type = values[1]
55
+ delta_type = values[2]
56
+
57
+ unless VALID_IMAGE_TYPES.include?(image_type)
58
+ add_error(
59
+ "Invalid DHDR image type: #{image_type} " \
60
+ "(must be 0, 2, or 4)",
61
+ )
62
+ return false
63
+ end
64
+
65
+ unless VALID_DELTA_TYPES.include?(delta_type)
66
+ add_error(
67
+ "Invalid DHDR delta type: #{delta_type} " \
68
+ "(must be 0-7)",
69
+ )
70
+ return false
71
+ end
72
+
73
+ context.store(:dhdr_image_type, image_type)
74
+ context.store(:dhdr_delta_type, delta_type)
75
+ end
76
+
77
+ if data.length == 20
78
+ # 20 byte format includes block dimensions
79
+ block_width = values[3]
80
+ block_height = values[4]
81
+
82
+ if block_width.zero?
83
+ add_error("DHDR block width must be greater than 0")
84
+ return false
85
+ end
86
+
87
+ if block_height.zero?
88
+ add_error("DHDR block height must be greater than 0")
89
+ return false
90
+ end
91
+
92
+ context.store(:dhdr_block_width, block_width)
93
+ context.store(:dhdr_block_height, block_height)
94
+ end
95
+
96
+ # DHDR begins a new object definition
97
+ context.store(:in_dhdr_section, true)
98
+ context.store(:dhdr_present, true)
99
+ true
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG DISC (Discard objects) chunks
7
+ #
8
+ # The DISC chunk discards one or more objects from the MNG object buffer.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Length must be a multiple of 2 bytes (list of object IDs)
13
+ # - Must appear before MEND
14
+ class DiscValidator < BaseValidator
15
+ def validate
16
+ return false unless check_crc
17
+
18
+ unless context.retrieve(:mhdr_present)
19
+ add_error("DISC must appear after MHDR")
20
+ return false
21
+ end
22
+
23
+ if context.seen?("MEND")
24
+ add_error("DISC must appear before MEND")
25
+ return false
26
+ end
27
+
28
+ data = chunk.chunk_data
29
+
30
+ if data.length.odd?
31
+ add_error(
32
+ "DISC chunk length must be a multiple of 2, " \
33
+ "got #{data.length}",
34
+ )
35
+ return false
36
+ end
37
+
38
+ context.store(:disc_present, true)
39
+ true
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end