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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +197 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +323 -0
- data/Gemfile +13 -0
- data/LICENSE +43 -0
- data/README.adoc +859 -0
- data/Rakefile +10 -0
- data/SECURITY.md +147 -0
- data/docs/ARCHITECTURE.adoc +681 -0
- data/docs/CHUNK_TYPES.adoc +450 -0
- data/docs/CLI_OPTIONS.adoc +913 -0
- data/docs/COMPATIBILITY.adoc +616 -0
- data/examples/README.adoc +398 -0
- data/examples/advanced_usage.rb +304 -0
- data/examples/basic_usage.rb +210 -0
- data/exe/png_conform +6 -0
- data/lib/png_conform/analyzers/comparison_analyzer.rb +230 -0
- data/lib/png_conform/analyzers/metrics_analyzer.rb +176 -0
- data/lib/png_conform/analyzers/optimization_analyzer.rb +190 -0
- data/lib/png_conform/analyzers/resolution_analyzer.rb +274 -0
- data/lib/png_conform/bindata/chunk_structure.rb +153 -0
- data/lib/png_conform/bindata/jng_file.rb +79 -0
- data/lib/png_conform/bindata/mng_file.rb +97 -0
- data/lib/png_conform/bindata/png_file.rb +162 -0
- data/lib/png_conform/cli.rb +116 -0
- data/lib/png_conform/commands/check_command.rb +323 -0
- data/lib/png_conform/commands/list_command.rb +67 -0
- data/lib/png_conform/models/chunk.rb +84 -0
- data/lib/png_conform/models/chunk_info.rb +71 -0
- data/lib/png_conform/models/compression_info.rb +49 -0
- data/lib/png_conform/models/decoded_chunk_data.rb +143 -0
- data/lib/png_conform/models/file_analysis.rb +181 -0
- data/lib/png_conform/models/file_info.rb +91 -0
- data/lib/png_conform/models/image_info.rb +52 -0
- data/lib/png_conform/models/validation_error.rb +89 -0
- data/lib/png_conform/models/validation_result.rb +137 -0
- data/lib/png_conform/readers/full_load_reader.rb +113 -0
- data/lib/png_conform/readers/streaming_reader.rb +180 -0
- data/lib/png_conform/reporters/base_reporter.rb +53 -0
- data/lib/png_conform/reporters/color_reporter.rb +65 -0
- data/lib/png_conform/reporters/json_reporter.rb +18 -0
- data/lib/png_conform/reporters/palette_reporter.rb +48 -0
- data/lib/png_conform/reporters/quiet_reporter.rb +18 -0
- data/lib/png_conform/reporters/reporter_factory.rb +108 -0
- data/lib/png_conform/reporters/summary_reporter.rb +65 -0
- data/lib/png_conform/reporters/text_reporter.rb +66 -0
- data/lib/png_conform/reporters/verbose_reporter.rb +87 -0
- data/lib/png_conform/reporters/very_verbose_reporter.rb +33 -0
- data/lib/png_conform/reporters/visual_elements.rb +66 -0
- data/lib/png_conform/reporters/yaml_reporter.rb +18 -0
- data/lib/png_conform/services/profile_manager.rb +242 -0
- data/lib/png_conform/services/validation_service.rb +457 -0
- data/lib/png_conform/services/zlib_validator.rb +270 -0
- data/lib/png_conform/validators/ancillary/bkgd_validator.rb +140 -0
- data/lib/png_conform/validators/ancillary/chrm_validator.rb +178 -0
- data/lib/png_conform/validators/ancillary/cicp_validator.rb +202 -0
- data/lib/png_conform/validators/ancillary/gama_validator.rb +105 -0
- data/lib/png_conform/validators/ancillary/hist_validator.rb +147 -0
- data/lib/png_conform/validators/ancillary/iccp_validator.rb +243 -0
- data/lib/png_conform/validators/ancillary/itxt_validator.rb +280 -0
- data/lib/png_conform/validators/ancillary/mdcv_validator.rb +201 -0
- data/lib/png_conform/validators/ancillary/offs_validator.rb +132 -0
- data/lib/png_conform/validators/ancillary/pcal_validator.rb +289 -0
- data/lib/png_conform/validators/ancillary/phys_validator.rb +107 -0
- data/lib/png_conform/validators/ancillary/sbit_validator.rb +176 -0
- data/lib/png_conform/validators/ancillary/scal_validator.rb +180 -0
- data/lib/png_conform/validators/ancillary/splt_validator.rb +223 -0
- data/lib/png_conform/validators/ancillary/srgb_validator.rb +117 -0
- data/lib/png_conform/validators/ancillary/ster_validator.rb +111 -0
- data/lib/png_conform/validators/ancillary/text_validator.rb +129 -0
- data/lib/png_conform/validators/ancillary/time_validator.rb +132 -0
- data/lib/png_conform/validators/ancillary/trns_validator.rb +154 -0
- data/lib/png_conform/validators/ancillary/ztxt_validator.rb +173 -0
- data/lib/png_conform/validators/apng/actl_validator.rb +81 -0
- data/lib/png_conform/validators/apng/fctl_validator.rb +155 -0
- data/lib/png_conform/validators/apng/fdat_validator.rb +117 -0
- data/lib/png_conform/validators/base_validator.rb +241 -0
- data/lib/png_conform/validators/chunk_registry.rb +219 -0
- data/lib/png_conform/validators/critical/idat_validator.rb +77 -0
- data/lib/png_conform/validators/critical/iend_validator.rb +68 -0
- data/lib/png_conform/validators/critical/ihdr_validator.rb +160 -0
- data/lib/png_conform/validators/critical/plte_validator.rb +120 -0
- data/lib/png_conform/validators/jng/jdat_validator.rb +66 -0
- data/lib/png_conform/validators/jng/jhdr_validator.rb +116 -0
- data/lib/png_conform/validators/jng/jsep_validator.rb +66 -0
- data/lib/png_conform/validators/mng/back_validator.rb +87 -0
- data/lib/png_conform/validators/mng/clip_validator.rb +65 -0
- data/lib/png_conform/validators/mng/clon_validator.rb +45 -0
- data/lib/png_conform/validators/mng/defi_validator.rb +104 -0
- data/lib/png_conform/validators/mng/dhdr_validator.rb +104 -0
- data/lib/png_conform/validators/mng/disc_validator.rb +44 -0
- data/lib/png_conform/validators/mng/endl_validator.rb +65 -0
- data/lib/png_conform/validators/mng/fram_validator.rb +91 -0
- data/lib/png_conform/validators/mng/loop_validator.rb +75 -0
- data/lib/png_conform/validators/mng/mend_validator.rb +31 -0
- data/lib/png_conform/validators/mng/mhdr_validator.rb +69 -0
- data/lib/png_conform/validators/mng/move_validator.rb +61 -0
- data/lib/png_conform/validators/mng/save_validator.rb +39 -0
- data/lib/png_conform/validators/mng/seek_validator.rb +42 -0
- data/lib/png_conform/validators/mng/show_validator.rb +52 -0
- data/lib/png_conform/validators/mng/term_validator.rb +84 -0
- data/lib/png_conform/version.rb +5 -0
- data/lib/png_conform.rb +101 -0
- data/png_conform.gemspec +43 -0
- 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
|