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