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,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_reporter"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Reporters
|
|
7
|
+
# Palette reporter - outputs palette details (-p flag)
|
|
8
|
+
# Matches pngcheck -p output format
|
|
9
|
+
# Prints PLTE, tRNS, hIST, sPLT and PPLT chunk contents
|
|
10
|
+
class PaletteReporter < BaseReporter
|
|
11
|
+
def report(file_analysis)
|
|
12
|
+
# File header (similar to verbose mode)
|
|
13
|
+
write_line("File: #{file_analysis.file_path} (#{file_analysis.file_size} bytes)")
|
|
14
|
+
|
|
15
|
+
# Find and output palette-related chunks
|
|
16
|
+
file_analysis.chunks&.each do |chunk|
|
|
17
|
+
case chunk.type
|
|
18
|
+
when "PLTE"
|
|
19
|
+
output_plte_chunk(chunk)
|
|
20
|
+
when "tRNS", "hIST", "sPLT", "PPLT"
|
|
21
|
+
output_palette_chunk(chunk)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Standard summary line
|
|
26
|
+
write_line(file_analysis.summary_line)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def output_plte_chunk(chunk)
|
|
32
|
+
return unless chunk.decoded_data.is_a?(PngConform::Models::PlteData)
|
|
33
|
+
|
|
34
|
+
write_line(" PLTE chunk: #{chunk.decoded_data.summary}")
|
|
35
|
+
chunk.decoded_data.detailed_entries.each do |entry_line|
|
|
36
|
+
write_line(" #{entry_line}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def output_palette_chunk(chunk)
|
|
41
|
+
write_line(" #{chunk.type} chunk at offset #{chunk.offset_hex}, length #{chunk.length}")
|
|
42
|
+
return unless chunk.decoded_data
|
|
43
|
+
|
|
44
|
+
write_line(" #{chunk.decoded_data.summary}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_reporter"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Reporters
|
|
7
|
+
# Quiet reporter - outputs only errors (-q flag)
|
|
8
|
+
# Matches pngcheck -q output format
|
|
9
|
+
class QuietReporter < BaseReporter
|
|
10
|
+
def report(file_analysis)
|
|
11
|
+
# Only output if there are errors
|
|
12
|
+
return if file_analysis.valid?
|
|
13
|
+
|
|
14
|
+
write_line(file_analysis.validation_result.error_summary)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "summary_reporter"
|
|
4
|
+
require_relative "verbose_reporter"
|
|
5
|
+
require_relative "very_verbose_reporter"
|
|
6
|
+
require_relative "quiet_reporter"
|
|
7
|
+
require_relative "palette_reporter"
|
|
8
|
+
require_relative "text_reporter"
|
|
9
|
+
require_relative "color_reporter"
|
|
10
|
+
require_relative "yaml_reporter"
|
|
11
|
+
require_relative "json_reporter"
|
|
12
|
+
|
|
13
|
+
module PngConform
|
|
14
|
+
module Reporters
|
|
15
|
+
# Factory for creating reporter instances based on options.
|
|
16
|
+
#
|
|
17
|
+
# Implements the Factory pattern to provide a clean interface for
|
|
18
|
+
# creating reporters with various combinations of options.
|
|
19
|
+
class ReporterFactory
|
|
20
|
+
# Create a reporter based on the specified options.
|
|
21
|
+
#
|
|
22
|
+
# @param format [String] Output format ("text", "yaml", "json")
|
|
23
|
+
# @param verbose [Boolean] Whether to use verbose output
|
|
24
|
+
# @param quiet [Boolean] Whether to use quiet output
|
|
25
|
+
# @param verbosity [Symbol] Verbosity level (:quiet, :summary, :verbose, :very_verbose)
|
|
26
|
+
# @param colorize [Boolean] Whether to colorize output (default: true)
|
|
27
|
+
# @param show_palette [Boolean] Whether to show palette details
|
|
28
|
+
# @param show_text [Boolean] Whether to show text chunk contents
|
|
29
|
+
# @param seven_bit [Boolean] Whether to escape characters >= 128
|
|
30
|
+
# @return [BaseReporter] Reporter instance
|
|
31
|
+
def self.create(format: "text", verbose: false, quiet: false,
|
|
32
|
+
verbosity: nil, colorize: true, show_palette: false,
|
|
33
|
+
show_text: false, seven_bit: false, escape_mode: :none)
|
|
34
|
+
# Format takes priority over verbosity
|
|
35
|
+
case format
|
|
36
|
+
when "yaml"
|
|
37
|
+
return YamlReporter.new
|
|
38
|
+
when "json"
|
|
39
|
+
return JsonReporter.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Text reporters with verbosity levels
|
|
43
|
+
reporter = if verbosity
|
|
44
|
+
case verbosity
|
|
45
|
+
when :quiet
|
|
46
|
+
QuietReporter.new($stdout, colorize: colorize)
|
|
47
|
+
when :verbose
|
|
48
|
+
VerboseReporter.new($stdout, colorize: colorize)
|
|
49
|
+
when :very_verbose
|
|
50
|
+
VeryVerboseReporter.new($stdout, colorize: colorize)
|
|
51
|
+
when :summary
|
|
52
|
+
SummaryReporter.new($stdout, colorize: colorize)
|
|
53
|
+
else
|
|
54
|
+
SummaryReporter.new($stdout, colorize: colorize)
|
|
55
|
+
end
|
|
56
|
+
elsif quiet
|
|
57
|
+
QuietReporter.new($stdout, colorize: colorize)
|
|
58
|
+
elsif verbose
|
|
59
|
+
VerboseReporter.new($stdout, colorize: colorize)
|
|
60
|
+
else
|
|
61
|
+
SummaryReporter.new($stdout, colorize: colorize)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Wrap with additional reporters based on options
|
|
65
|
+
reporter = wrap_with_palette(reporter) if show_palette
|
|
66
|
+
|
|
67
|
+
reporter = wrap_with_text(reporter, seven_bit, escape_mode) if show_text
|
|
68
|
+
|
|
69
|
+
reporter = wrap_with_color(reporter) if colorize
|
|
70
|
+
|
|
71
|
+
reporter
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Wrap a reporter with palette display functionality.
|
|
75
|
+
#
|
|
76
|
+
# @param reporter [BaseReporter] Reporter to wrap
|
|
77
|
+
# @return [PaletteReporter] Wrapped reporter
|
|
78
|
+
def self.wrap_with_palette(reporter)
|
|
79
|
+
PaletteReporter.new(reporter)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Wrap a reporter with text chunk display functionality.
|
|
83
|
+
#
|
|
84
|
+
# @param reporter [BaseReporter] Reporter to wrap
|
|
85
|
+
# @param seven_bit [Boolean] Whether to escape characters >= 128
|
|
86
|
+
# @param escape_mode [Symbol] Explicit escape mode setting
|
|
87
|
+
# @return [TextReporter] Wrapped reporter
|
|
88
|
+
def self.wrap_with_text(reporter, seven_bit, escape_mode)
|
|
89
|
+
mode = if escape_mode == :none
|
|
90
|
+
(seven_bit ? :seven_bit : :none)
|
|
91
|
+
else
|
|
92
|
+
escape_mode
|
|
93
|
+
end
|
|
94
|
+
TextReporter.new(reporter, escape_mode: mode)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Wrap a reporter with color functionality.
|
|
98
|
+
#
|
|
99
|
+
# @param reporter [BaseReporter] Reporter to wrap
|
|
100
|
+
# @return [ColorReporter] Wrapped reporter
|
|
101
|
+
def self.wrap_with_color(reporter)
|
|
102
|
+
ColorReporter.new(reporter)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private_class_method :wrap_with_palette, :wrap_with_text, :wrap_with_color
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_reporter"
|
|
4
|
+
require_relative "visual_elements"
|
|
5
|
+
|
|
6
|
+
module PngConform
|
|
7
|
+
module Reporters
|
|
8
|
+
# Summary reporter - outputs one-line summary per file with colors and emojis
|
|
9
|
+
# Default reporter matching pngcheck output format
|
|
10
|
+
# Example: "✅ OK: file.png (PNG, 164 bytes, 4 chunks 🗜️ -28.1%)"
|
|
11
|
+
class SummaryReporter < BaseReporter
|
|
12
|
+
include VisualElements
|
|
13
|
+
|
|
14
|
+
def initialize(output = $stdout, colorize: true)
|
|
15
|
+
super(output)
|
|
16
|
+
@colorize = colorize
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def report(validation_result)
|
|
20
|
+
if validation_result.valid?
|
|
21
|
+
write_line(format_success(validation_result))
|
|
22
|
+
else
|
|
23
|
+
write_line(format_error(validation_result))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def format_success(result)
|
|
30
|
+
emoji_check = emoji(:success)
|
|
31
|
+
status = colorize("OK", :green)
|
|
32
|
+
|
|
33
|
+
parts = ["#{emoji_check} #{status}: #{result.filename}"]
|
|
34
|
+
parts << "(#{result.file_type}"
|
|
35
|
+
parts << "#{result.file_size} bytes"
|
|
36
|
+
parts << "#{result.chunk_count} chunks"
|
|
37
|
+
|
|
38
|
+
if result.compression_ratio
|
|
39
|
+
compression = "#{emoji(:compression)} #{colorize(
|
|
40
|
+
format('%.1f%%', result.compression_ratio), :cyan
|
|
41
|
+
)}"
|
|
42
|
+
parts << compression
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
"#{parts.join(', ')})"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def format_error(result)
|
|
49
|
+
emoji_err = emoji(:error)
|
|
50
|
+
status = colorize("ERROR", :red)
|
|
51
|
+
|
|
52
|
+
lines = ["#{emoji_err} #{status}: #{result.filename}"]
|
|
53
|
+
|
|
54
|
+
result.errors.each do |error|
|
|
55
|
+
severity_emoji = emoji(error.severity.to_sym)
|
|
56
|
+
severity_text = colorize(error.severity.upcase,
|
|
57
|
+
color_for_severity(error.severity))
|
|
58
|
+
lines << " #{severity_emoji} #{severity_text}: #{error.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
lines.join("\n")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_reporter"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Reporters
|
|
7
|
+
# Text reporter - extracts and displays text chunk contents (-t flag)
|
|
8
|
+
# Matches pngcheck -t output format
|
|
9
|
+
# Prints tEXt, zTXt, iTXt chunk contents
|
|
10
|
+
class TextReporter < BaseReporter
|
|
11
|
+
attr_reader :escape_mode
|
|
12
|
+
|
|
13
|
+
# @param output [IO] Output stream
|
|
14
|
+
# @param escape_mode [Symbol] :seven_bit for -7 flag, :none for -t flag
|
|
15
|
+
def initialize(output = $stdout, escape_mode: :none)
|
|
16
|
+
super(output)
|
|
17
|
+
@escape_mode = escape_mode
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def report(file_analysis)
|
|
21
|
+
has_text_chunks = false
|
|
22
|
+
|
|
23
|
+
file_analysis.chunks&.each do |chunk|
|
|
24
|
+
next unless text_chunk?(chunk.type)
|
|
25
|
+
|
|
26
|
+
has_text_chunks = true
|
|
27
|
+
output_text_chunk(chunk)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# If no text chunks found, output standard summary
|
|
31
|
+
write_line(file_analysis.summary_line) unless has_text_chunks
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def text_chunk?(type)
|
|
37
|
+
%w[tEXt zTXt iTXt].include?(type)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def output_text_chunk(chunk)
|
|
41
|
+
write_line("#{chunk.type} chunk:")
|
|
42
|
+
if chunk.decoded_data.respond_to?(:keyword) && chunk.decoded_data.respond_to?(:text)
|
|
43
|
+
text = format_text(chunk.decoded_data.text)
|
|
44
|
+
write_line(" #{chunk.decoded_data.keyword}: #{text}")
|
|
45
|
+
else
|
|
46
|
+
write_line(" <unable to decode>")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def format_text(text)
|
|
51
|
+
case escape_mode
|
|
52
|
+
when :seven_bit
|
|
53
|
+
escape_non_ascii(text)
|
|
54
|
+
else
|
|
55
|
+
text
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def escape_non_ascii(text)
|
|
60
|
+
text.chars.map do |char|
|
|
61
|
+
char.ord >= 128 ? format("\\x%02x", char.ord) : char
|
|
62
|
+
end.join
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_reporter"
|
|
4
|
+
require_relative "visual_elements"
|
|
5
|
+
|
|
6
|
+
module PngConform
|
|
7
|
+
module Reporters
|
|
8
|
+
# Verbose reporter - outputs detailed chunk information with colors and emojis
|
|
9
|
+
# Matches pngcheck -v output format with visual enhancements
|
|
10
|
+
class VerboseReporter < BaseReporter
|
|
11
|
+
include VisualElements
|
|
12
|
+
|
|
13
|
+
def initialize(output = $stdout, colorize: true)
|
|
14
|
+
super(output)
|
|
15
|
+
@colorize = colorize
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def report(validation_result)
|
|
19
|
+
write_line(format_file_header(validation_result))
|
|
20
|
+
write_line("")
|
|
21
|
+
|
|
22
|
+
validation_result.chunks.each do |chunk|
|
|
23
|
+
write_line(format_chunk(chunk))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if validation_result.errors.any?
|
|
27
|
+
write_line("")
|
|
28
|
+
write_line(colorize("VALIDATION ERRORS:", :red))
|
|
29
|
+
validation_result.errors.each do |error|
|
|
30
|
+
write_line(format_error(error))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
write_line("")
|
|
35
|
+
write_line(format_summary(validation_result))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def format_file_header(result)
|
|
41
|
+
emoji_file = emoji(:file)
|
|
42
|
+
filename = colorize(result.filename, :bold)
|
|
43
|
+
"#{emoji_file} #{filename} (#{result.file_size} bytes)"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format_chunk(chunk)
|
|
47
|
+
emoji_chunk = emoji(:chunk)
|
|
48
|
+
|
|
49
|
+
status = if chunk.valid_crc
|
|
50
|
+
colorize(emoji(:valid_crc), :green)
|
|
51
|
+
else
|
|
52
|
+
colorize(emoji(:invalid_crc), :red)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
chunk_type = colorize(chunk.type, :cyan)
|
|
56
|
+
offset = colorize(chunk.offset_hex, :gray)
|
|
57
|
+
|
|
58
|
+
" #{status} #{emoji_chunk} #{chunk_type} at #{offset} (#{chunk.length} bytes)"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def format_error(error)
|
|
62
|
+
severity_emoji = emoji(error.severity.to_sym)
|
|
63
|
+
severity_text = colorize(error.severity.upcase,
|
|
64
|
+
color_for_severity(error.severity))
|
|
65
|
+
" #{severity_emoji} #{severity_text}: #{error.message}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def format_summary(result)
|
|
69
|
+
status = if result.valid?
|
|
70
|
+
colorize("#{emoji(:valid_crc)} No errors detected", :green)
|
|
71
|
+
else
|
|
72
|
+
colorize("#{emoji(:invalid_crc)} ERRORS DETECTED", :red)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
compression = if result.compression_ratio
|
|
76
|
+
" #{emoji(:compression)} #{colorize(
|
|
77
|
+
format('%.1f%%', result.compression_ratio), :cyan
|
|
78
|
+
)}"
|
|
79
|
+
else
|
|
80
|
+
""
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
"#{status} in #{result.filename} (#{result.chunk_count} chunks#{compression})"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "verbose_reporter"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Reporters
|
|
7
|
+
# Very verbose reporter - adds row filter information (-vv flag)
|
|
8
|
+
# Matches pngcheck -vv output format
|
|
9
|
+
class VeryVerboseReporter < VerboseReporter
|
|
10
|
+
def report(file_analysis)
|
|
11
|
+
# File header
|
|
12
|
+
write_line(file_analysis.file_header)
|
|
13
|
+
|
|
14
|
+
# Chunk details with filter information
|
|
15
|
+
file_analysis.chunks&.each do |chunk|
|
|
16
|
+
write_line(" #{chunk.summary}")
|
|
17
|
+
next unless chunk.decoded_data
|
|
18
|
+
|
|
19
|
+
write_line(" #{chunk.decoded_data.summary}")
|
|
20
|
+
|
|
21
|
+
# Add filter summary for IDAT chunks
|
|
22
|
+
if chunk.decoded_data.respond_to?(:filter_summary)
|
|
23
|
+
filter_info = chunk.decoded_data.filter_summary
|
|
24
|
+
write_line(" #{filter_info}") if filter_info
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Validation summary
|
|
29
|
+
write_line(file_analysis.validation_summary)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Reporters
|
|
5
|
+
# Module providing visual elements (emojis and colors) for CLI output
|
|
6
|
+
# Text reporters include this module to add visual appeal
|
|
7
|
+
# Structured reporters (YAML/JSON) do not use these elements
|
|
8
|
+
module VisualElements
|
|
9
|
+
# Emoji definitions for consistent visual representation
|
|
10
|
+
EMOJI = {
|
|
11
|
+
success: "✅",
|
|
12
|
+
error: "❌",
|
|
13
|
+
warning: "⚠️",
|
|
14
|
+
info: "ℹ️",
|
|
15
|
+
chunk: "📦",
|
|
16
|
+
file: "📄",
|
|
17
|
+
valid_crc: "✓",
|
|
18
|
+
invalid_crc: "✗",
|
|
19
|
+
compression: "🗜️",
|
|
20
|
+
image: "🖼️",
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# ANSI color codes for terminal output
|
|
24
|
+
COLORS = {
|
|
25
|
+
green: "\e[32m",
|
|
26
|
+
red: "\e[31m",
|
|
27
|
+
yellow: "\e[33m",
|
|
28
|
+
blue: "\e[34m",
|
|
29
|
+
cyan: "\e[36m",
|
|
30
|
+
gray: "\e[90m",
|
|
31
|
+
reset: "\e[0m",
|
|
32
|
+
bold: "\e[1m",
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Colorize text with ANSI color codes
|
|
36
|
+
# @param text [String] The text to colorize
|
|
37
|
+
# @param color [Symbol] The color name from COLORS
|
|
38
|
+
# @return [String] Colorized text or original if colorization disabled
|
|
39
|
+
def colorize(text, color)
|
|
40
|
+
return text unless @colorize
|
|
41
|
+
return text unless COLORS.key?(color)
|
|
42
|
+
|
|
43
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get emoji for a given name
|
|
47
|
+
# @param name [Symbol] The emoji name from EMOJI
|
|
48
|
+
# @return [String] The emoji character or empty string
|
|
49
|
+
def emoji(name)
|
|
50
|
+
EMOJI[name] || ""
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get color code for severity level
|
|
54
|
+
# @param severity [String] The severity level ("error", "warning", "info")
|
|
55
|
+
# @return [Symbol] The color symbol
|
|
56
|
+
def color_for_severity(severity)
|
|
57
|
+
case severity
|
|
58
|
+
when "error" then :red
|
|
59
|
+
when "warning" then :yellow
|
|
60
|
+
when "info" then :blue
|
|
61
|
+
else :gray
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_reporter"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module PngConform
|
|
7
|
+
module Reporters
|
|
8
|
+
# YAML reporter - outputs complete file analysis in YAML format
|
|
9
|
+
# Proper Model → Formatter pattern
|
|
10
|
+
# Receives FileAnalysis model and formats it (no analysis done here)
|
|
11
|
+
class YamlReporter < BaseReporter
|
|
12
|
+
def report(file_analysis)
|
|
13
|
+
# Simple formatting - model knows how to serialize itself
|
|
14
|
+
write_line(file_analysis.to_h.to_yaml)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|