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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG ENDL (End loop) chunks
7
+ #
8
+ # The ENDL chunk marks the end of a loop started by a LOOP chunk.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Length must be 1 byte
13
+ # - Nesting level must match corresponding LOOP
14
+ # - Must have corresponding LOOP before it
15
+ # - Must appear before MEND
16
+ class EndlValidator < BaseValidator
17
+ EXPECTED_LENGTH = 1
18
+
19
+ def validate
20
+ return false unless check_crc
21
+ return false unless check_length(EXPECTED_LENGTH)
22
+
23
+ unless context.retrieve(:mhdr_present)
24
+ add_error("ENDL must appear after MHDR")
25
+ return false
26
+ end
27
+
28
+ if context.seen?("MEND")
29
+ add_error("ENDL must appear before MEND")
30
+ return false
31
+ end
32
+
33
+ unless context.retrieve(:loop_present)
34
+ add_error("ENDL must appear after LOOP")
35
+ return false
36
+ end
37
+
38
+ # Nesting level (1 byte)
39
+ nesting_level = chunk.chunk_data.getbyte(0)
40
+
41
+ # Validate loop nesting
42
+ loop_stack = context.retrieve(:loop_stack) || []
43
+
44
+ if loop_stack.empty?
45
+ add_error("ENDL without corresponding LOOP")
46
+ return false
47
+ elsif loop_stack.last != nesting_level
48
+ add_error(
49
+ "ENDL nesting level (#{nesting_level}) does not match " \
50
+ "LOOP nesting level (#{loop_stack.last})",
51
+ )
52
+ return false
53
+ else
54
+ loop_stack.pop
55
+ context.store(:loop_stack, loop_stack)
56
+ end
57
+
58
+ context.store(:endl_nesting_level, nesting_level)
59
+ context.store(:endl_present, true)
60
+ true
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG FRAM (Frame parameters) chunks
7
+ #
8
+ # The FRAM chunk sets frame parameters including framing mode, subframe
9
+ # name, change interframe delay, change timeout, change layer clipping,
10
+ # and change sync ID.
11
+ #
12
+ # Validation rules:
13
+ # - Must appear after MHDR
14
+ # - Length can be 0, 1, or variable (with parameters)
15
+ # - Framing mode: 0-4
16
+ # - All parameters optional based on change flags
17
+ # - Must appear before MEND
18
+ class FramValidator < BaseValidator
19
+ VALID_FRAMING_MODES = (0..4).to_a.freeze
20
+
21
+ def validate
22
+ return false unless check_crc
23
+
24
+ unless context.retrieve(:mhdr_present)
25
+ add_error("FRAM must appear after MHDR")
26
+ return false
27
+ end
28
+
29
+ if context.seen?("MEND")
30
+ add_error("FRAM must appear before MEND")
31
+ return false
32
+ end
33
+
34
+ data = chunk.chunk_data
35
+
36
+ # Empty FRAM is valid
37
+ if data.empty?
38
+ context.store(:fram_present, true)
39
+ return true
40
+ end
41
+
42
+ pos = 0
43
+
44
+ # Framing mode (1 byte)
45
+ if pos < data.length
46
+ framing_mode = data.getbyte(pos)
47
+ pos += 1
48
+
49
+ unless VALID_FRAMING_MODES.include?(framing_mode)
50
+ add_error(
51
+ "Invalid FRAM framing mode: #{framing_mode} (must be 0-4)",
52
+ )
53
+ return false
54
+ end
55
+
56
+ context.store(:fram_framing_mode, framing_mode)
57
+ end
58
+
59
+ # Parse optional change flags and parameters
60
+ if pos < data.length
61
+ # Subframe name length (1 byte) + name
62
+ name_length = data.getbyte(pos)
63
+ pos += 1
64
+
65
+ if pos + name_length > data.length
66
+ add_error("FRAM subframe name extends beyond chunk")
67
+ return false
68
+ end
69
+
70
+ if name_length.positive?
71
+ subframe_name = data[pos, name_length]
72
+ pos += name_length
73
+ context.store(:fram_subframe_name, subframe_name)
74
+ end
75
+ end
76
+
77
+ # Remaining parameters are change flags and their values
78
+ # We don't need to parse all of them in detail for basic validation
79
+ # Just ensure the chunk length is reasonable
80
+ if pos < data.length
81
+ # Store that FRAM has parameters
82
+ context.store(:fram_has_parameters, true)
83
+ end
84
+
85
+ context.store(:fram_present, true)
86
+ true
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG LOOP (Loop control) chunks
7
+ #
8
+ # The LOOP chunk marks the beginning of a loop in an MNG animation.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Length must be 5 or 6 bytes
13
+ # - Nesting level must be >= 0
14
+ # - Iteration count must be >= 0 (0 = infinite)
15
+ # - Must have corresponding ENDL
16
+ # - Must appear before MEND
17
+ class LoopValidator < BaseValidator
18
+ VALID_LENGTHS = [5, 6].freeze
19
+
20
+ def validate
21
+ return false unless check_crc
22
+
23
+ unless context.retrieve(:mhdr_present)
24
+ add_error("LOOP must appear after MHDR")
25
+ return false
26
+ end
27
+
28
+ data = chunk.chunk_data
29
+ unless VALID_LENGTHS.include?(data.length)
30
+ add_error(
31
+ "LOOP chunk must be 5 or 6 bytes, got #{data.length}",
32
+ )
33
+ return false
34
+ end
35
+
36
+ if context.seen?("MEND")
37
+ add_error("LOOP must appear before MEND")
38
+ return false
39
+ end
40
+
41
+ # Nesting level (1 byte)
42
+ nesting_level = data.getbyte(0)
43
+
44
+ # Iteration count (4 bytes)
45
+ iteration_count = data[1, 4].unpack1("N")
46
+
47
+ context.store(:loop_nesting_level, nesting_level)
48
+ context.store(:loop_iteration_count, iteration_count)
49
+
50
+ if data.length == 6
51
+ # Termination condition (1 byte)
52
+ termination = data.getbyte(5)
53
+
54
+ unless (0..3).cover?(termination)
55
+ add_error(
56
+ "LOOP termination condition must be 0-3, got #{termination}",
57
+ )
58
+ return false
59
+ end
60
+
61
+ context.store(:loop_termination, termination)
62
+ end
63
+
64
+ # Track loop nesting
65
+ loop_stack = context.retrieve(:loop_stack) || []
66
+ loop_stack.push(nesting_level)
67
+ context.store(:loop_stack, loop_stack)
68
+
69
+ context.store(:loop_present, true)
70
+ true
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validator for MEND (MNG End) chunk
7
+ #
8
+ # The MEND chunk marks the end of a MNG datastream.
9
+ # It must be the last chunk in the file and contains no data.
10
+ #
11
+ class MendValidator < BaseValidator
12
+ CHUNK_TYPE = "MEND"
13
+ EXPECTED_LENGTH = 0
14
+
15
+ def validate
16
+ return false unless check_crc
17
+ return false unless check_length(EXPECTED_LENGTH)
18
+
19
+ # MEND must have MHDR before it
20
+ unless context.seen?("MHDR")
21
+ add_error("MEND requires MHDR chunk")
22
+ return false
23
+ end
24
+
25
+ # MEND should be last chunk (this will be validated by orchestration)
26
+ true
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validator for MHDR (MNG Header) chunk
7
+ #
8
+ # The MHDR chunk is the first chunk in a MNG datastream and contains
9
+ # basic information about the MNG file.
10
+ #
11
+ # Structure (28 bytes):
12
+ # - frame_width (4 bytes): Frame width in pixels
13
+ # - frame_height (4 bytes): Frame height in pixels
14
+ # - ticks_per_second (4 bytes): Nominal tick rate
15
+ # - nominal_layer_count (4 bytes): Nominal layer count
16
+ # - nominal_frame_count (4 bytes): Nominal frame count
17
+ # - nominal_play_time (4 bytes): Nominal play time
18
+ # - simplicity_profile (4 bytes): Simplicity profile flags
19
+ #
20
+ class MhdrValidator < BaseValidator
21
+ CHUNK_TYPE = "MHDR"
22
+ EXPECTED_LENGTH = 28
23
+
24
+ def validate
25
+ return false unless check_crc
26
+ return false unless check_length(EXPECTED_LENGTH)
27
+
28
+ # MHDR must be first chunk
29
+ unless context.chunks_seen.empty?
30
+ add_error("MHDR must be the first chunk in MNG file")
31
+ return false
32
+ end
33
+
34
+ data = chunk.chunk_data
35
+
36
+ # Extract fields
37
+ frame_width = data[0..3].unpack1("N")
38
+ frame_height = data[4..7].unpack1("N")
39
+ ticks_per_second = data[8..11].unpack1("N")
40
+ nominal_layer_count = data[12..15].unpack1("N")
41
+ nominal_frame_count = data[16..19].unpack1("N")
42
+ nominal_play_time = data[20..23].unpack1("N")
43
+ simplicity_profile = data[24..27].unpack1("N")
44
+
45
+ # Validate dimensions
46
+ valid = true
47
+ if frame_width.zero? || frame_height.zero?
48
+ add_error("MHDR frame dimensions must be > 0")
49
+ valid = false
50
+ end
51
+
52
+ if valid
53
+ # Store in context
54
+ context.store(:mhdr_frame_width, frame_width)
55
+ context.store(:mhdr_frame_height, frame_height)
56
+ context.store(:mhdr_ticks_per_second, ticks_per_second)
57
+ context.store(:mhdr_nominal_layer_count, nominal_layer_count)
58
+ context.store(:mhdr_nominal_frame_count, nominal_frame_count)
59
+ context.store(:mhdr_nominal_play_time, nominal_play_time)
60
+ context.store(:mhdr_simplicity_profile, simplicity_profile)
61
+ context.store(:mhdr_present, true)
62
+ end
63
+
64
+ valid
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG MOVE (Move object) chunks
7
+ #
8
+ # The MOVE chunk moves an object to a new location.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Must appear before MEND
13
+ # - Length must be 13 bytes
14
+ # - Contains first object ID, last object ID, move type, and X/Y offsets
15
+ class MoveValidator < BaseValidator
16
+ EXPECTED_LENGTH = 13
17
+
18
+ def validate
19
+ return false unless check_crc
20
+
21
+ unless context.retrieve(:mhdr_present)
22
+ add_error("MOVE must appear after MHDR")
23
+ return false
24
+ end
25
+
26
+ if context.seen?("MEND")
27
+ add_error("MOVE 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
+ "MOVE chunk must be #{EXPECTED_LENGTH} bytes, " \
36
+ "got #{data.length}",
37
+ )
38
+ return false
39
+ end
40
+
41
+ # Parse MOVE data
42
+ # First object ID (2 bytes)
43
+ # Last object ID (2 bytes)
44
+ # Move type (1 byte)
45
+ # X offset (4 bytes, signed)
46
+ # Y offset (4 bytes, signed)
47
+ first_id, last_id, move_type, x_offset, y_offset =
48
+ data.unpack("nnCl>l>")
49
+
50
+ context.store(:move_first_id, first_id)
51
+ context.store(:move_last_id, last_id)
52
+ context.store(:move_type, move_type)
53
+ context.store(:move_x_offset, x_offset)
54
+ context.store(:move_y_offset, y_offset)
55
+ context.store(:move_present, true)
56
+ true
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG SAVE (Save state) chunks
7
+ #
8
+ # The SAVE chunk saves the current MNG state for later restoration with SEEK.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Length must be 0 bytes (empty)
13
+ # - Must appear before MEND
14
+ class SaveValidator < BaseValidator
15
+ EXPECTED_LENGTH = 0
16
+
17
+ def validate
18
+ return false unless check_crc
19
+ return false unless check_length(EXPECTED_LENGTH)
20
+
21
+ # Must appear after MHDR
22
+ unless context.retrieve(:mhdr_present)
23
+ add_error("SAVE must appear after MHDR")
24
+ return false
25
+ end
26
+
27
+ # Must appear before MEND
28
+ if context.seen?("MEND")
29
+ add_error("SAVE must appear before MEND")
30
+ return false
31
+ end
32
+
33
+ context.store(:save_present, true)
34
+ true
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG SEEK (Seek to saved state) chunks
7
+ #
8
+ # The SEEK chunk restores the MNG state previously saved with SAVE.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Length must be 0 bytes (empty)
13
+ # - Should have corresponding SAVE before it
14
+ # - Must appear before MEND
15
+ class SeekValidator < BaseValidator
16
+ EXPECTED_LENGTH = 0
17
+
18
+ def validate
19
+ return false unless check_crc
20
+ return false unless check_length(EXPECTED_LENGTH)
21
+
22
+ unless context.retrieve(:mhdr_present)
23
+ add_error("SEEK must appear after MHDR")
24
+ return false
25
+ end
26
+
27
+ if context.seen?("MEND")
28
+ add_error("SEEK must appear before MEND")
29
+ return false
30
+ end
31
+
32
+ unless context.retrieve(:save_present)
33
+ add_warning("SEEK without corresponding SAVE")
34
+ end
35
+
36
+ context.store(:seek_present, true)
37
+ true
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG SHOW (Show object) chunks
7
+ #
8
+ # The SHOW chunk makes a previously defined object visible.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Must appear before MEND
13
+ # - Length must be 0 or 2 bytes
14
+ # - If 2 bytes, contains object ID to show
15
+ class ShowValidator < BaseValidator
16
+ VALID_LENGTHS = [0, 2].freeze
17
+
18
+ def validate
19
+ return false unless check_crc
20
+
21
+ unless context.retrieve(:mhdr_present)
22
+ add_error("SHOW must appear after MHDR")
23
+ return false
24
+ end
25
+
26
+ if context.seen?("MEND")
27
+ add_error("SHOW must appear before MEND")
28
+ return false
29
+ end
30
+
31
+ data = chunk.chunk_data
32
+
33
+ unless VALID_LENGTHS.include?(data.length)
34
+ add_error(
35
+ "SHOW chunk must be 0 or 2 bytes, got #{data.length}",
36
+ )
37
+ return false
38
+ end
39
+
40
+ if data.length == 2
41
+ # Object ID (2 bytes)
42
+ object_id = data.unpack1("n")
43
+ context.store(:show_object_id, object_id)
44
+ end
45
+
46
+ context.store(:show_present, true)
47
+ true
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ module Validators
5
+ module Mng
6
+ # Validates MNG TERM (Termination action) chunks
7
+ #
8
+ # The TERM chunk specifies the action to take when the animation terminates.
9
+ #
10
+ # Validation rules:
11
+ # - Must appear after MHDR
12
+ # - Length must be 1 or 10 bytes
13
+ # - Termination action: 0-3
14
+ # - Action after iterations: 0-2
15
+ # - Must appear before MEND
16
+ class TermValidator < BaseValidator
17
+ VALID_LENGTHS = [1, 10].freeze
18
+ VALID_TERMINATION_ACTIONS = (0..3).to_a.freeze
19
+ VALID_AFTER_ITERATIONS = (0..2).to_a.freeze
20
+
21
+ def validate
22
+ return false unless check_crc
23
+
24
+ unless context.retrieve(:mhdr_present)
25
+ add_error("TERM must appear after MHDR")
26
+ return false
27
+ end
28
+
29
+ data = chunk.chunk_data
30
+ unless VALID_LENGTHS.include?(data.length)
31
+ add_error(
32
+ "TERM chunk must be 1 or 10 bytes, got #{data.length}",
33
+ )
34
+ return false
35
+ end
36
+
37
+ if context.seen?("MEND")
38
+ add_error("TERM must appear before MEND")
39
+ return false
40
+ end
41
+
42
+ # Termination action (1 byte)
43
+ termination_action = data.getbyte(0)
44
+
45
+ unless VALID_TERMINATION_ACTIONS.include?(termination_action)
46
+ add_error(
47
+ "Invalid TERM termination action: #{termination_action} " \
48
+ "(must be 0-3)",
49
+ )
50
+ return false
51
+ end
52
+
53
+ context.store(:term_termination_action, termination_action)
54
+
55
+ if data.length == 10
56
+ # Action after iterations (1 byte)
57
+ after_iterations = data.getbyte(1)
58
+
59
+ unless VALID_AFTER_ITERATIONS.include?(after_iterations)
60
+ add_error(
61
+ "Invalid TERM action after iterations: #{after_iterations} " \
62
+ "(must be 0-2)",
63
+ )
64
+ return false
65
+ end
66
+
67
+ # Delay (4 bytes)
68
+ delay = data[2, 4].unpack1("N")
69
+
70
+ # Maximum iterations (4 bytes)
71
+ max_iterations = data[6, 4].unpack1("N")
72
+
73
+ context.store(:term_after_iterations, after_iterations)
74
+ context.store(:term_delay, delay)
75
+ context.store(:term_max_iterations, max_iterations)
76
+ end
77
+
78
+ context.store(:term_present, true)
79
+ true
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PngConform
4
+ VERSION = "0.1.0"
5
+ end