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,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../services/profile_manager"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Commands
|
|
7
|
+
# Command to list available validation profiles.
|
|
8
|
+
#
|
|
9
|
+
# Displays information about all available profiles including
|
|
10
|
+
# their requirements and restrictions.
|
|
11
|
+
class ListCommand
|
|
12
|
+
attr_reader :options
|
|
13
|
+
|
|
14
|
+
# @param options [Hash] Command-line options
|
|
15
|
+
def initialize(options = {})
|
|
16
|
+
@options = options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Execute the list command.
|
|
20
|
+
#
|
|
21
|
+
# @return [Integer] Exit code (always 0)
|
|
22
|
+
def run
|
|
23
|
+
profiles = Services::ProfileManager.available_profiles
|
|
24
|
+
|
|
25
|
+
puts "Available Validation Profiles:"
|
|
26
|
+
puts
|
|
27
|
+
|
|
28
|
+
profiles.each do |profile_name|
|
|
29
|
+
display_profile(profile_name)
|
|
30
|
+
puts
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Display information about a single profile.
|
|
39
|
+
#
|
|
40
|
+
# @param profile_name [String] Name of the profile to display
|
|
41
|
+
def display_profile(profile_name)
|
|
42
|
+
profile = Services::ProfileManager.get_profile(profile_name)
|
|
43
|
+
|
|
44
|
+
puts " #{profile_name.upcase}"
|
|
45
|
+
puts " #{'-' * (profile_name.length + 2)}"
|
|
46
|
+
|
|
47
|
+
puts " Description: #{profile[:description]}" if profile[:description]
|
|
48
|
+
|
|
49
|
+
if profile[:required_chunks] && !profile[:required_chunks].empty?
|
|
50
|
+
puts " Required chunks: #{profile[:required_chunks].join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if profile[:optional_chunks] && !profile[:optional_chunks].empty?
|
|
54
|
+
puts " Optional chunks: #{profile[:optional_chunks].join(', ')}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if profile[:prohibited_chunks] && !profile[:prohibited_chunks].empty?
|
|
58
|
+
puts " Prohibited chunks: #{profile[:prohibited_chunks].join(', ')}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return unless profile[:strict_mode]
|
|
62
|
+
|
|
63
|
+
puts " Strict mode: enabled"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Models
|
|
5
|
+
# Domain model representing a PNG/MNG/JNG chunk
|
|
6
|
+
# This is the business logic representation, separate from binary parsing
|
|
7
|
+
class Chunk < Lutaml::Model::Serializable
|
|
8
|
+
attribute :type, :string
|
|
9
|
+
attribute :length, :integer
|
|
10
|
+
attribute :data, :string
|
|
11
|
+
attribute :crc, :integer
|
|
12
|
+
attribute :offset, :integer
|
|
13
|
+
attribute :valid_crc, :boolean, default: -> { false }
|
|
14
|
+
attribute :crc_expected, :string
|
|
15
|
+
attribute :crc_actual, :string
|
|
16
|
+
|
|
17
|
+
# Create from BinData chunk structure
|
|
18
|
+
def self.from_bindata(bindata_chunk, offset = 0)
|
|
19
|
+
new(
|
|
20
|
+
type: bindata_chunk.type,
|
|
21
|
+
length: bindata_chunk.length,
|
|
22
|
+
data: bindata_chunk.data,
|
|
23
|
+
crc: bindata_chunk.crc,
|
|
24
|
+
offset: offset,
|
|
25
|
+
# valid_crc will be set by ValidationService
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if chunk is critical (uppercase first letter)
|
|
30
|
+
def critical?
|
|
31
|
+
type[0] == type[0].upcase
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if chunk is ancillary (lowercase first letter)
|
|
35
|
+
def ancillary?
|
|
36
|
+
!critical?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if chunk is public (uppercase second letter)
|
|
40
|
+
def public?
|
|
41
|
+
type[1] == type[1].upcase
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if chunk is private (lowercase second letter)
|
|
45
|
+
def private?
|
|
46
|
+
!public?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if chunk is reserved (uppercase third letter)
|
|
50
|
+
def reserved?
|
|
51
|
+
type[2] == type[2].upcase
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if chunk is safe to copy (lowercase fourth letter)
|
|
55
|
+
def safe_to_copy?
|
|
56
|
+
type[3] == type[3].downcase
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get chunk type as symbol
|
|
60
|
+
def type_symbol
|
|
61
|
+
type.to_sym
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get total size including all fields
|
|
65
|
+
def total_size
|
|
66
|
+
4 + 4 + length + 4
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Format offset as hex
|
|
70
|
+
def offset_hex
|
|
71
|
+
format("0x%05x", offset)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Alias methods for CRC validation (compatibility with validators and specs)
|
|
75
|
+
def crc_valid?
|
|
76
|
+
valid_crc
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def valid_crc?
|
|
80
|
+
valid_crc
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Models
|
|
7
|
+
# Domain model representing PNG chunk metadata
|
|
8
|
+
class ChunkInfo < Lutaml::Model::Serializable
|
|
9
|
+
attribute :type, :string
|
|
10
|
+
attribute :offset, :integer
|
|
11
|
+
attribute :length, :integer
|
|
12
|
+
attribute :crc_valid, :boolean
|
|
13
|
+
attribute :critical, :boolean
|
|
14
|
+
attribute :private, :boolean
|
|
15
|
+
attribute :safe_to_copy, :boolean
|
|
16
|
+
attribute :decoded_data, DecodedChunkData
|
|
17
|
+
|
|
18
|
+
# Binary data - use attr_accessor to avoid Lutaml serialization corruption
|
|
19
|
+
attr_accessor :data, :crc
|
|
20
|
+
|
|
21
|
+
# Custom initializer to handle binary data fields
|
|
22
|
+
def initialize(attributes = {})
|
|
23
|
+
@data = attributes.delete(:data)
|
|
24
|
+
@crc = attributes.delete(:crc)
|
|
25
|
+
super(attributes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Format offset as hex for display
|
|
29
|
+
def offset_hex
|
|
30
|
+
format("0x%05x", offset)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Chunk property summary
|
|
34
|
+
def properties
|
|
35
|
+
props = []
|
|
36
|
+
props << "critical" if critical
|
|
37
|
+
props << "ancillary" unless critical
|
|
38
|
+
props << "private" if private
|
|
39
|
+
props << "safe-to-copy" if safe_to_copy
|
|
40
|
+
props
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Format chunk for verbose output
|
|
44
|
+
# Example: "chunk IHDR at offset 0x0000c, length 13"
|
|
45
|
+
def summary
|
|
46
|
+
"chunk #{type} at offset #{offset_hex}, length #{length}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Format with decoded data
|
|
50
|
+
def detailed_summary
|
|
51
|
+
parts = [summary]
|
|
52
|
+
parts << decoded_data.summary if decoded_data
|
|
53
|
+
parts.join("\n ")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Aliases for compatibility with spec expectations
|
|
57
|
+
alias chunk_type type
|
|
58
|
+
alias abs_offset offset
|
|
59
|
+
|
|
60
|
+
# Force binary encoding for chunk data
|
|
61
|
+
def chunk_data
|
|
62
|
+
data&.b
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# CRC validation method alias
|
|
66
|
+
def crc_valid?
|
|
67
|
+
crc_valid
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Models
|
|
7
|
+
# Domain model representing PNG compression metadata
|
|
8
|
+
class CompressionInfo < Lutaml::Model::Serializable
|
|
9
|
+
attribute :uncompressed_size, :integer
|
|
10
|
+
attribute :compressed_size, :integer
|
|
11
|
+
attribute :compression_ratio, :float
|
|
12
|
+
attribute :window_bits, :integer
|
|
13
|
+
attribute :compression_level, :string
|
|
14
|
+
|
|
15
|
+
# Compression level constants
|
|
16
|
+
COMPRESSION_LEVELS = {
|
|
17
|
+
1 => "fastest",
|
|
18
|
+
6 => "default",
|
|
19
|
+
9 => "maximum",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Calculate compression ratio from sizes
|
|
23
|
+
def self.calculate_ratio(uncompressed, compressed)
|
|
24
|
+
return 0.0 if uncompressed.zero?
|
|
25
|
+
|
|
26
|
+
((compressed.to_f / uncompressed) - 1.0) * 100.0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get compression level name
|
|
30
|
+
def level_name
|
|
31
|
+
COMPRESSION_LEVELS[compression_level] || "custom"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format compression info for display
|
|
35
|
+
def summary
|
|
36
|
+
format("%.1f%%", compression_ratio)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Format full details (for verbose mode)
|
|
40
|
+
def details
|
|
41
|
+
parts = []
|
|
42
|
+
parts << "deflated"
|
|
43
|
+
parts << "#{window_bits}K window" if window_bits
|
|
44
|
+
parts << "#{level_name} compression"
|
|
45
|
+
parts.join(", ")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Models
|
|
7
|
+
# Base class for decoded chunk data
|
|
8
|
+
class DecodedChunkData < Lutaml::Model::Serializable
|
|
9
|
+
# Override in subclasses to provide formatted summary
|
|
10
|
+
def summary
|
|
11
|
+
""
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# IHDR chunk decoded data
|
|
16
|
+
class IhdrData < DecodedChunkData
|
|
17
|
+
attribute :width, :integer
|
|
18
|
+
attribute :height, :integer
|
|
19
|
+
attribute :bit_depth, :integer
|
|
20
|
+
attribute :color_type, :integer
|
|
21
|
+
attribute :compression_method, :integer
|
|
22
|
+
attribute :filter_method, :integer
|
|
23
|
+
attribute :interlace_method, :integer
|
|
24
|
+
|
|
25
|
+
COLOR_TYPE_NAMES = {
|
|
26
|
+
0 => "grayscale",
|
|
27
|
+
2 => "truecolor",
|
|
28
|
+
3 => "palette",
|
|
29
|
+
4 => "grayscale+alpha",
|
|
30
|
+
6 => "truecolor+alpha",
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def color_type_name
|
|
34
|
+
COLOR_TYPE_NAMES[color_type] || "unknown"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def interlaced?
|
|
38
|
+
interlace_method == 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def summary
|
|
42
|
+
parts = []
|
|
43
|
+
parts << "#{width} x #{height} image"
|
|
44
|
+
parts << "#{bit_depth}-bit #{color_type_name}"
|
|
45
|
+
parts << (interlaced? ? "interlaced" : "non-interlaced")
|
|
46
|
+
parts.join(", ")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# gAMA chunk decoded data
|
|
51
|
+
class GamaData < DecodedChunkData
|
|
52
|
+
attribute :gamma, :float
|
|
53
|
+
|
|
54
|
+
def summary
|
|
55
|
+
format("%.4f", gamma)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# IDAT chunk decoded data
|
|
60
|
+
class IdatData < DecodedChunkData
|
|
61
|
+
attribute :compression_method, :integer
|
|
62
|
+
attribute :window_bits, :integer
|
|
63
|
+
attribute :compression_level, :string
|
|
64
|
+
attribute :row_filters, :integer, collection: true
|
|
65
|
+
|
|
66
|
+
def summary
|
|
67
|
+
parts = []
|
|
68
|
+
parts << "zlib: deflated"
|
|
69
|
+
parts << "#{window_bits}K window" if window_bits
|
|
70
|
+
parts << "#{compression_level} compression" if compression_level
|
|
71
|
+
parts.join(", ")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Format row filters for very verbose mode
|
|
75
|
+
def filter_summary
|
|
76
|
+
return nil if row_filters.nil? || row_filters.empty?
|
|
77
|
+
|
|
78
|
+
filter_names = {
|
|
79
|
+
0 => "none",
|
|
80
|
+
1 => "sub",
|
|
81
|
+
2 => "up",
|
|
82
|
+
3 => "avg",
|
|
83
|
+
4 => "paeth",
|
|
84
|
+
}
|
|
85
|
+
"row filters (#{filter_names.map do |k, v|
|
|
86
|
+
"#{k} #{v}"
|
|
87
|
+
end.join(', ')}):\n #{row_filters.join(' ')}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Palette entry
|
|
92
|
+
class PaletteEntry < Lutaml::Model::Serializable
|
|
93
|
+
attribute :red, :integer
|
|
94
|
+
attribute :green, :integer
|
|
95
|
+
attribute :blue, :integer
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# PLTE chunk decoded data
|
|
99
|
+
class PlteData < DecodedChunkData
|
|
100
|
+
attribute :entries, PaletteEntry, collection: true
|
|
101
|
+
|
|
102
|
+
def summary
|
|
103
|
+
"#{entries&.size || 0} palette entries"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Format for palette printing mode
|
|
107
|
+
def detailed_entries
|
|
108
|
+
return [] unless entries
|
|
109
|
+
|
|
110
|
+
entries.each_with_index.map do |entry, index|
|
|
111
|
+
format("%3d: (%3d,%3d,%3d) = (0x%02x,0x%02x,0x%02x)",
|
|
112
|
+
index, entry.red, entry.green, entry.blue,
|
|
113
|
+
entry.red, entry.green, entry.blue)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# tEXt chunk decoded data
|
|
119
|
+
class TextData < DecodedChunkData
|
|
120
|
+
attribute :keyword, :string
|
|
121
|
+
attribute :text, :string
|
|
122
|
+
|
|
123
|
+
def summary
|
|
124
|
+
"#{keyword}: #{text}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# tIME chunk decoded data
|
|
129
|
+
class TimeData < DecodedChunkData
|
|
130
|
+
attribute :year, :integer
|
|
131
|
+
attribute :month, :integer
|
|
132
|
+
attribute :day, :integer
|
|
133
|
+
attribute :hour, :integer
|
|
134
|
+
attribute :minute, :integer
|
|
135
|
+
attribute :second, :integer
|
|
136
|
+
|
|
137
|
+
def summary
|
|
138
|
+
format("%04d-%02d-%02d %02d:%02d:%02d",
|
|
139
|
+
year, month, day, hour, minute, second)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Models
|
|
7
|
+
# Top-level domain model for PNG file analysis
|
|
8
|
+
class FileAnalysis < Lutaml::Model::Serializable
|
|
9
|
+
attribute :file_path, :string
|
|
10
|
+
attribute :file_size, :integer
|
|
11
|
+
attribute :file_type, :string # 'PNG', 'MNG', 'JNG'
|
|
12
|
+
attribute :validation_result, ValidationResult
|
|
13
|
+
attribute :image_info, ImageInfo
|
|
14
|
+
attribute :compression_info, CompressionInfo
|
|
15
|
+
|
|
16
|
+
# Analyzer results (proper Model → Formatter pattern)
|
|
17
|
+
attribute :resolution_analysis, :hash
|
|
18
|
+
attribute :optimization_analysis, :hash
|
|
19
|
+
attribute :metrics, :hash
|
|
20
|
+
|
|
21
|
+
# Total chunk count
|
|
22
|
+
def chunk_count
|
|
23
|
+
chunks.size
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get chunks (either from direct attribute or validation_result)
|
|
27
|
+
def chunks
|
|
28
|
+
validation_result&.chunks || []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if file is valid
|
|
32
|
+
def valid?
|
|
33
|
+
validation_result&.valid? || false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get validation status text
|
|
37
|
+
def status
|
|
38
|
+
valid? ? "OK" : "ERROR"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Format file header line (for verbose mode)
|
|
42
|
+
# Example: "File: file.png (164 bytes)"
|
|
43
|
+
def file_header
|
|
44
|
+
"File: #{file_path} (#{file_size} bytes)"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Format summary line (default output mode)
|
|
48
|
+
# Example: "OK: file.png (32x32, 1-bit grayscale, non-interlaced, -28.1%)."
|
|
49
|
+
def summary_line
|
|
50
|
+
parts = []
|
|
51
|
+
parts << "#{status}:"
|
|
52
|
+
parts << file_path
|
|
53
|
+
if image_info
|
|
54
|
+
info_parts = []
|
|
55
|
+
info_parts << image_info.summary
|
|
56
|
+
info_parts << compression_info.summary if compression_info
|
|
57
|
+
parts << "(#{info_parts.join(', ')})"
|
|
58
|
+
end
|
|
59
|
+
"#{parts.join(' ')}."
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Format validation summary (for verbose mode)
|
|
63
|
+
# Example: "No errors detected in file.png (4 chunks, -28.1% compression)."
|
|
64
|
+
def validation_summary
|
|
65
|
+
return validation_result.error_summary if validation_result && !valid?
|
|
66
|
+
|
|
67
|
+
parts = []
|
|
68
|
+
parts << "No errors detected in #{file_path}"
|
|
69
|
+
detail_parts = []
|
|
70
|
+
detail_parts << "#{chunk_count} chunks" if chunk_count.positive?
|
|
71
|
+
if compression_info
|
|
72
|
+
detail_parts << "#{compression_info.summary} compression"
|
|
73
|
+
end
|
|
74
|
+
parts << "(#{detail_parts.join(', ')})" unless detail_parts.empty?
|
|
75
|
+
"#{parts.join(' ')}."
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Alias for compatibility with reporters
|
|
79
|
+
alias filename file_path
|
|
80
|
+
|
|
81
|
+
# Delegate to validation_result for API compatibility
|
|
82
|
+
def errors
|
|
83
|
+
validation_result&.errors || []
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def error_messages
|
|
87
|
+
validation_result&.error_messages || []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def error_count
|
|
91
|
+
validation_result&.error_count || 0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def warning_count
|
|
95
|
+
validation_result&.warning_count || 0
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def info_count
|
|
99
|
+
validation_result&.info_count || 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Delegate to compression_info
|
|
103
|
+
def compression_ratio
|
|
104
|
+
compression_info&.compression_ratio
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Convert to complete hash for serialization
|
|
108
|
+
# This provides a single source of truth for all output formats
|
|
109
|
+
def to_h
|
|
110
|
+
hash = {
|
|
111
|
+
"filename" => file_path,
|
|
112
|
+
"file_type" => file_type,
|
|
113
|
+
"file_size" => file_size,
|
|
114
|
+
"compression_ratio" => compression_ratio,
|
|
115
|
+
"crc_errors_count" => validation_result&.crc_errors_count || 0,
|
|
116
|
+
"valid" => valid?,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Add image info if available
|
|
120
|
+
hash["image"] = image_info.to_h if image_info
|
|
121
|
+
|
|
122
|
+
# Add chunks info
|
|
123
|
+
if chunks.any?
|
|
124
|
+
hash["chunks"] = {
|
|
125
|
+
"total" => chunk_count,
|
|
126
|
+
"types" => chunks.map(&:type).uniq.sort,
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Add errors if any
|
|
131
|
+
if validation_result&.errors&.any?
|
|
132
|
+
hash["errors"] = validation_result.errors.map do |error|
|
|
133
|
+
error_hash = {
|
|
134
|
+
"severity" => error.severity,
|
|
135
|
+
"message" => error.message,
|
|
136
|
+
}
|
|
137
|
+
error_hash["chunk_type"] = error.chunk_type if error.chunk_type
|
|
138
|
+
error_hash["expected"] = error.expected if error.expected
|
|
139
|
+
error_hash["actual"] = error.actual if error.actual
|
|
140
|
+
error_hash
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Add resolution analysis if available
|
|
145
|
+
if resolution_analysis && !resolution_analysis.empty?
|
|
146
|
+
hash["resolution"] =
|
|
147
|
+
resolution_analysis
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Add optimization if available
|
|
151
|
+
if optimization_analysis && optimization_analysis[:suggestions]&.any?
|
|
152
|
+
hash["optimization"] = {
|
|
153
|
+
"suggestions" => optimization_analysis[:suggestions],
|
|
154
|
+
"total_savings_bytes" => optimization_analysis[:potential_savings_bytes],
|
|
155
|
+
"total_savings_percent" => optimization_analysis[:potential_savings_percent],
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Add recommendations
|
|
160
|
+
recs = extract_recommendations
|
|
161
|
+
hash["recommendations"] = recs if recs&.any?
|
|
162
|
+
|
|
163
|
+
hash
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Extract recommendations from analyzers
|
|
167
|
+
def extract_recommendations
|
|
168
|
+
recs = []
|
|
169
|
+
|
|
170
|
+
# From resolution analysis
|
|
171
|
+
if resolution_analysis && resolution_analysis[:recommendations]
|
|
172
|
+
recs.concat(resolution_analysis[:recommendations].map do |r|
|
|
173
|
+
r[:message]
|
|
174
|
+
end)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
recs.empty? ? nil : recs
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Models
|
|
5
|
+
# Domain model representing file metadata
|
|
6
|
+
class FileInfo < Lutaml::Model::Serializable
|
|
7
|
+
attribute :filename, :string
|
|
8
|
+
attribute :file_size, :integer
|
|
9
|
+
attribute :file_type, :string
|
|
10
|
+
attribute :signature, :string
|
|
11
|
+
attribute :width, :integer
|
|
12
|
+
attribute :height, :integer
|
|
13
|
+
attribute :bit_depth, :integer
|
|
14
|
+
attribute :color_type, :integer
|
|
15
|
+
attribute :compression_method, :integer
|
|
16
|
+
attribute :filter_method, :integer
|
|
17
|
+
attribute :interlace_method, :integer
|
|
18
|
+
|
|
19
|
+
# File types
|
|
20
|
+
FILE_TYPE_PNG = "PNG"
|
|
21
|
+
FILE_TYPE_MNG = "MNG"
|
|
22
|
+
FILE_TYPE_JNG = "JNG"
|
|
23
|
+
FILE_TYPE_UNKNOWN = "UNKNOWN"
|
|
24
|
+
|
|
25
|
+
# Color type constants (PNG)
|
|
26
|
+
COLOR_TYPE_GRAYSCALE = 0
|
|
27
|
+
COLOR_TYPE_RGB = 2
|
|
28
|
+
COLOR_TYPE_INDEXED = 3
|
|
29
|
+
COLOR_TYPE_GRAYSCALE_ALPHA = 4
|
|
30
|
+
COLOR_TYPE_RGB_ALPHA = 6
|
|
31
|
+
|
|
32
|
+
# Color type names
|
|
33
|
+
COLOR_TYPE_NAMES = {
|
|
34
|
+
COLOR_TYPE_GRAYSCALE => "grayscale",
|
|
35
|
+
COLOR_TYPE_RGB => "RGB",
|
|
36
|
+
COLOR_TYPE_INDEXED => "indexed",
|
|
37
|
+
COLOR_TYPE_GRAYSCALE_ALPHA => "grayscale+alpha",
|
|
38
|
+
COLOR_TYPE_RGB_ALPHA => "RGB+alpha",
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Get color type name
|
|
42
|
+
def color_type_name
|
|
43
|
+
COLOR_TYPE_NAMES[color_type] || "unknown"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get interlace method name
|
|
47
|
+
def interlace_method_name
|
|
48
|
+
case interlace_method
|
|
49
|
+
when 0 then "non-interlaced"
|
|
50
|
+
when 1 then "Adam7 interlaced"
|
|
51
|
+
else "unknown"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if image is interlaced
|
|
56
|
+
def interlaced?
|
|
57
|
+
interlace_method == 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get image dimensions as string
|
|
61
|
+
def dimensions
|
|
62
|
+
"#{width}x#{height}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get bit depth description
|
|
66
|
+
def bit_depth_description
|
|
67
|
+
"#{bit_depth}-bit #{color_type_name}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Format signature as hex
|
|
71
|
+
def signature_hex
|
|
72
|
+
signature&.unpack1("H*")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if file is PNG
|
|
76
|
+
def png?
|
|
77
|
+
file_type == FILE_TYPE_PNG
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check if file is MNG
|
|
81
|
+
def mng?
|
|
82
|
+
file_type == FILE_TYPE_MNG
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if file is JNG
|
|
86
|
+
def jng?
|
|
87
|
+
file_type == FILE_TYPE_JNG
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|