png_conform 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +19 -0
  4. data/.rubocop_todo.yml +197 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/CONTRIBUTING.md +323 -0
  7. data/Gemfile +13 -0
  8. data/LICENSE +43 -0
  9. data/README.adoc +859 -0
  10. data/Rakefile +10 -0
  11. data/SECURITY.md +147 -0
  12. data/docs/ARCHITECTURE.adoc +681 -0
  13. data/docs/CHUNK_TYPES.adoc +450 -0
  14. data/docs/CLI_OPTIONS.adoc +913 -0
  15. data/docs/COMPATIBILITY.adoc +616 -0
  16. data/examples/README.adoc +398 -0
  17. data/examples/advanced_usage.rb +304 -0
  18. data/examples/basic_usage.rb +210 -0
  19. data/exe/png_conform +6 -0
  20. data/lib/png_conform/analyzers/comparison_analyzer.rb +230 -0
  21. data/lib/png_conform/analyzers/metrics_analyzer.rb +176 -0
  22. data/lib/png_conform/analyzers/optimization_analyzer.rb +190 -0
  23. data/lib/png_conform/analyzers/resolution_analyzer.rb +274 -0
  24. data/lib/png_conform/bindata/chunk_structure.rb +153 -0
  25. data/lib/png_conform/bindata/jng_file.rb +79 -0
  26. data/lib/png_conform/bindata/mng_file.rb +97 -0
  27. data/lib/png_conform/bindata/png_file.rb +162 -0
  28. data/lib/png_conform/cli.rb +116 -0
  29. data/lib/png_conform/commands/check_command.rb +323 -0
  30. data/lib/png_conform/commands/list_command.rb +67 -0
  31. data/lib/png_conform/models/chunk.rb +84 -0
  32. data/lib/png_conform/models/chunk_info.rb +71 -0
  33. data/lib/png_conform/models/compression_info.rb +49 -0
  34. data/lib/png_conform/models/decoded_chunk_data.rb +143 -0
  35. data/lib/png_conform/models/file_analysis.rb +181 -0
  36. data/lib/png_conform/models/file_info.rb +91 -0
  37. data/lib/png_conform/models/image_info.rb +52 -0
  38. data/lib/png_conform/models/validation_error.rb +89 -0
  39. data/lib/png_conform/models/validation_result.rb +137 -0
  40. data/lib/png_conform/readers/full_load_reader.rb +113 -0
  41. data/lib/png_conform/readers/streaming_reader.rb +180 -0
  42. data/lib/png_conform/reporters/base_reporter.rb +53 -0
  43. data/lib/png_conform/reporters/color_reporter.rb +65 -0
  44. data/lib/png_conform/reporters/json_reporter.rb +18 -0
  45. data/lib/png_conform/reporters/palette_reporter.rb +48 -0
  46. data/lib/png_conform/reporters/quiet_reporter.rb +18 -0
  47. data/lib/png_conform/reporters/reporter_factory.rb +108 -0
  48. data/lib/png_conform/reporters/summary_reporter.rb +65 -0
  49. data/lib/png_conform/reporters/text_reporter.rb +66 -0
  50. data/lib/png_conform/reporters/verbose_reporter.rb +87 -0
  51. data/lib/png_conform/reporters/very_verbose_reporter.rb +33 -0
  52. data/lib/png_conform/reporters/visual_elements.rb +66 -0
  53. data/lib/png_conform/reporters/yaml_reporter.rb +18 -0
  54. data/lib/png_conform/services/profile_manager.rb +242 -0
  55. data/lib/png_conform/services/validation_service.rb +457 -0
  56. data/lib/png_conform/services/zlib_validator.rb +270 -0
  57. data/lib/png_conform/validators/ancillary/bkgd_validator.rb +140 -0
  58. data/lib/png_conform/validators/ancillary/chrm_validator.rb +178 -0
  59. data/lib/png_conform/validators/ancillary/cicp_validator.rb +202 -0
  60. data/lib/png_conform/validators/ancillary/gama_validator.rb +105 -0
  61. data/lib/png_conform/validators/ancillary/hist_validator.rb +147 -0
  62. data/lib/png_conform/validators/ancillary/iccp_validator.rb +243 -0
  63. data/lib/png_conform/validators/ancillary/itxt_validator.rb +280 -0
  64. data/lib/png_conform/validators/ancillary/mdcv_validator.rb +201 -0
  65. data/lib/png_conform/validators/ancillary/offs_validator.rb +132 -0
  66. data/lib/png_conform/validators/ancillary/pcal_validator.rb +289 -0
  67. data/lib/png_conform/validators/ancillary/phys_validator.rb +107 -0
  68. data/lib/png_conform/validators/ancillary/sbit_validator.rb +176 -0
  69. data/lib/png_conform/validators/ancillary/scal_validator.rb +180 -0
  70. data/lib/png_conform/validators/ancillary/splt_validator.rb +223 -0
  71. data/lib/png_conform/validators/ancillary/srgb_validator.rb +117 -0
  72. data/lib/png_conform/validators/ancillary/ster_validator.rb +111 -0
  73. data/lib/png_conform/validators/ancillary/text_validator.rb +129 -0
  74. data/lib/png_conform/validators/ancillary/time_validator.rb +132 -0
  75. data/lib/png_conform/validators/ancillary/trns_validator.rb +154 -0
  76. data/lib/png_conform/validators/ancillary/ztxt_validator.rb +173 -0
  77. data/lib/png_conform/validators/apng/actl_validator.rb +81 -0
  78. data/lib/png_conform/validators/apng/fctl_validator.rb +155 -0
  79. data/lib/png_conform/validators/apng/fdat_validator.rb +117 -0
  80. data/lib/png_conform/validators/base_validator.rb +241 -0
  81. data/lib/png_conform/validators/chunk_registry.rb +219 -0
  82. data/lib/png_conform/validators/critical/idat_validator.rb +77 -0
  83. data/lib/png_conform/validators/critical/iend_validator.rb +68 -0
  84. data/lib/png_conform/validators/critical/ihdr_validator.rb +160 -0
  85. data/lib/png_conform/validators/critical/plte_validator.rb +120 -0
  86. data/lib/png_conform/validators/jng/jdat_validator.rb +66 -0
  87. data/lib/png_conform/validators/jng/jhdr_validator.rb +116 -0
  88. data/lib/png_conform/validators/jng/jsep_validator.rb +66 -0
  89. data/lib/png_conform/validators/mng/back_validator.rb +87 -0
  90. data/lib/png_conform/validators/mng/clip_validator.rb +65 -0
  91. data/lib/png_conform/validators/mng/clon_validator.rb +45 -0
  92. data/lib/png_conform/validators/mng/defi_validator.rb +104 -0
  93. data/lib/png_conform/validators/mng/dhdr_validator.rb +104 -0
  94. data/lib/png_conform/validators/mng/disc_validator.rb +44 -0
  95. data/lib/png_conform/validators/mng/endl_validator.rb +65 -0
  96. data/lib/png_conform/validators/mng/fram_validator.rb +91 -0
  97. data/lib/png_conform/validators/mng/loop_validator.rb +75 -0
  98. data/lib/png_conform/validators/mng/mend_validator.rb +31 -0
  99. data/lib/png_conform/validators/mng/mhdr_validator.rb +69 -0
  100. data/lib/png_conform/validators/mng/move_validator.rb +61 -0
  101. data/lib/png_conform/validators/mng/save_validator.rb +39 -0
  102. data/lib/png_conform/validators/mng/seek_validator.rb +42 -0
  103. data/lib/png_conform/validators/mng/show_validator.rb +52 -0
  104. data/lib/png_conform/validators/mng/term_validator.rb +84 -0
  105. data/lib/png_conform/version.rb +5 -0
  106. data/lib/png_conform.rb +101 -0
  107. data/png_conform.gemspec +43 -0
  108. metadata +201 -0
@@ -0,0 +1,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