ascii_pngfy 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1d0d17138de3a7998ba69e9cdb8ddb45bc4b0aa259f570b5b76e3f32824512b0
4
+ data.tar.gz: '08a2046403c7c76e8d0d150224d3ce1df4776539be57b8a0a56cdc16930ceab3'
5
+ SHA512:
6
+ metadata.gz: f4d7bb12da4c527081fa049e940e3af6e98333ff2850fe796371a6465579f8586409d27b157547abdf9daedcd48ba087d2afba3fe022c159ac42b5a2a4185d4b
7
+ data.tar.gz: 2345644e3d1f5bf09dbf0f366743dacd555943e6b8e5cbd72e3db9fddb1a5f96fdc491e01e1d7cdd9b4b3990d9ce5568b30b6bfddfce1d60f26bc207e285394e
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reponsibilities
4
+ # - Top level namespace that contains all AsciiPngfy
5
+ # sub-namespaces and general constants
6
+ # - Requires all files needed to use the AsciiPngfy Gem
7
+ module AsciiPngfy
8
+ MAX_RESULT_PNG_IMAGE_WIDTH = 3840
9
+ MAX_RESULT_PNG_IMAGE_HEIGHT = 2160
10
+ GLYPH_DESIGN_WIDTH = 5
11
+ GLYPH_DESIGN_HEIGHT = 9
12
+ SUPPORTED_ASCII_CODES_WITHOUT_NEWLINE_RANGE = (32..126).freeze
13
+ SUPPORTED_ASCII_CODES_WITHOUT_NEWLINE = SUPPORTED_ASCII_CODES_WITHOUT_NEWLINE_RANGE.to_a.freeze
14
+ SUPPORTED_ASCII_CODES = ([10] + SUPPORTED_ASCII_CODES_WITHOUT_NEWLINE).freeze
15
+ SUPPORTED_ASCII_CHARACTERS = SUPPORTED_ASCII_CODES.map(&:chr).freeze
16
+ end
17
+
18
+ require 'ascii_pngfy/pngfyer'
19
+
20
+ require 'ascii_pngfy/exceptions'
21
+
22
+ require 'ascii_pngfy/settings'
23
+ require 'ascii_pngfy/settings/settings_snapshot'
24
+ require 'ascii_pngfy/settings/setable_getable_settings'
25
+
26
+ require 'ascii_pngfy/settings/setable_getable'
27
+ require 'ascii_pngfy/settings/color_setting'
28
+ require 'ascii_pngfy/settings/font_height_setting'
29
+ require 'ascii_pngfy/settings/horizontal_spacing_setting'
30
+ require 'ascii_pngfy/settings/vertical_spacing_setting'
31
+ require 'ascii_pngfy/settings/text_setting'
32
+
33
+ require 'ascii_pngfy/vec2i'
34
+ require 'ascii_pngfy/aabb'
35
+ require 'ascii_pngfy/rendering_rules'
36
+ require 'ascii_pngfy/settings_renderer'
37
+
38
+ require 'ascii_pngfy/glyphs'
39
+ require 'ascii_pngfy/color_rgba'
40
+ require 'ascii_pngfy/result'
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiPngfy
4
+ # Reponsibilities
5
+ # - Represents an axis aligned bounding box through a minimum and
6
+ # maximum coordinate pair
7
+ # - Public getters for the min and max coordinate pair
8
+ # - Provides a way to iterate all the pixel coordinates in
9
+ # the respective bounding box with and without the pixel index
10
+ #
11
+ # This pixel index follows the conventions used for the glyph
12
+ # design string where the index increases based on the iterated
13
+ # pixel from:
14
+ # - topmost row to bottommost row
15
+ # - leftmost pixel to rightmost pixel for each of these rows
16
+ class AABB
17
+ attr_reader(:min, :max)
18
+
19
+ def initialize(min_x, min_y, max_x, max_y)
20
+ self.min = Vec2i.new(min_x, min_y)
21
+ self.max = Vec2i.new(max_x, max_y)
22
+ end
23
+
24
+ def each_pixel(&yielder)
25
+ min.y.upto(max.y) do |pixel_y|
26
+ min.x.upto(max.x) do |pixel_x|
27
+ yielder.call(pixel_x, pixel_y)
28
+ end
29
+ end
30
+ end
31
+
32
+ def each_pixel_with_index(&yielder)
33
+ pixel_index = 0
34
+ each_pixel do |pixel_x, pixel_y|
35
+ yielder.call(pixel_x, pixel_y, pixel_index)
36
+
37
+ pixel_index += 1
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ "min#{min} max#{max}"
43
+ end
44
+
45
+ private
46
+
47
+ attr_writer(:min, :max)
48
+ end
49
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiPngfy
4
+ # Reponsibilities
5
+ # - Provides RGBA color handling and validation
6
+ class ColorRGBA
7
+ VALID_RGBA_COLOR_RANGE = (0..255).freeze
8
+ attr_reader(:red, :green, :blue, :alpha)
9
+
10
+ def initialize(red, green, blue, alpha)
11
+ self.red = red
12
+ self.green = green
13
+ self.blue = blue
14
+ self.alpha = alpha
15
+ end
16
+
17
+ def red=(new_red)
18
+ @red = validate_color_value(new_red, :red)
19
+ end
20
+
21
+ def green=(new_green)
22
+ @green = validate_color_value(new_green, :green)
23
+ end
24
+
25
+ def blue=(new_blue)
26
+ @blue = validate_color_value(new_blue, :blue)
27
+ end
28
+
29
+ def alpha=(new_alpha)
30
+ @alpha = validate_color_value(new_alpha, :alpha)
31
+ end
32
+
33
+ def ==(other)
34
+ other.red == red &&
35
+ other.green == green &&
36
+ other.blue == blue &&
37
+ other.alpha == alpha
38
+ end
39
+
40
+ private
41
+
42
+ def validate_color_value(color_value, color_component)
43
+ return color_value if valid_color_value?(color_value)
44
+
45
+ error_message = String.new
46
+ error_message << "#{color_value.inspect} is not a valid #{color_component} color component value. "
47
+ error_message << "Must be an Integer in the range (#{VALID_RGBA_COLOR_RANGE})."
48
+
49
+ raise Exceptions::InvalidRGBAColorValueError, error_message
50
+ end
51
+
52
+ def valid_color_value?(color_value)
53
+ color_value.is_a?(Integer) && VALID_RGBA_COLOR_RANGE.cover?(color_value)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiPngfy
4
+ # Provides a custom AsciiPngfy Exceptions hierarchy
5
+ module Exceptions
6
+ # Base class to classify AsciiPngfy errors under StandardError
7
+ class AsciiPngfyError < StandardError; end
8
+
9
+ class InvalidRGBAColorValueError < AsciiPngfyError; end
10
+
11
+ class InvalidFontHeightError < AsciiPngfyError; end
12
+
13
+ class InvalidSpacingError < AsciiPngfyError; end
14
+
15
+ class InvalidHorizontalSpacingError < InvalidSpacingError; end
16
+
17
+ class InvalidVerticalSpacingError < InvalidSpacingError; end
18
+
19
+ class InvalidReplacementTextError < AsciiPngfyError; end
20
+
21
+ class InvalidCharacterError < AsciiPngfyError; end
22
+
23
+ class EmptyTextError < AsciiPngfyError; end
24
+
25
+ class TextLineTooLongError < AsciiPngfyError; end
26
+
27
+ class TooManyTextLinesError < AsciiPngfyError; end
28
+ end
29
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiPngfy
4
+ # Reponsibilities
5
+ # - Provides pixel plotting designs for all supported non-control
6
+ # ASCII characters
7
+ # - Decides which design character, '.' or '#', represents the
8
+ # font layer or the background layer
9
+ # rubocop: disable Metrics/ModuleLength
10
+ module Glyphs
11
+ DESIGNS = {
12
+ ' ' => '.............................................',
13
+ '!' => '..#....#....#....#....#.........#............',
14
+ '"' => '.#.#..#.#..#.#...............................',
15
+ '#' => '......#.#.#####.#.#..#.#.#####.#.#...........',
16
+ '$' => '..#...#####.#...###...#.#####...#............',
17
+ '%' => '#...##...#...#...#...#...#...##...#..........',
18
+ '&' => '.##..#..#.#..#..#####..#.#..#..##.#..........',
19
+ "'" => '..#....#....#................................',
20
+ '(' => '...#...#....#....#....#....#.....#...........',
21
+ ')' => '..#.....#....#....#....#....#...#............',
22
+ '*' => '.......#..#.#.#.###.#.#.#..#.................',
23
+ '+' => '.......#....#..#####..#....#.................',
24
+ ',' => '...........................#....#...#........',
25
+ '-' => '...............#####.........................',
26
+ '.' => '...........................#....#............',
27
+ '/' => '....#....#...#...#...#...#....#..............',
28
+ '0' => '.###.#...##..###.#.###..##...#.###...........',
29
+ '1' => '..#...##....#....#....#....#..#####..........',
30
+ '2' => '.###.#...#....#...#...#...#...#####..........',
31
+ '3' => '.###.#...#....#.###.....##...#.###...........',
32
+ '4' => '.#..#.#..##...######....#....#....#..........',
33
+ '5' => '######....#....####.....##...#.###...........',
34
+ '6' => '.###.#....#....####.#...##...#.###...........',
35
+ '7' => '#####....#....#...#...#....#....#............',
36
+ '8' => '.###.#...##...#.###.#...##...#.###...........',
37
+ '9' => '.###.#...##...#.####....##...#.###...........',
38
+ ':' => '.......#....#..............#....#............',
39
+ ';' => '.......#....#..............#....#...#........',
40
+ '<' => '........##.##..#.....##.....##...............',
41
+ '=' => '..........#####.....#####....................',
42
+ '>' => '.....##.....##.....#..##.##..................',
43
+ '?' => '.###.#...#....#...#...#.........#............',
44
+ '@' => '.###.#..###.#.##.#.##..###.....###...........',
45
+ 'A' => '.###.#...##...##...#######...##...#..........',
46
+ 'B' => '####.#...##...#####.#...##...#####...........',
47
+ 'C' => '.###.#...##....#....#....#...#.###...........',
48
+ 'D' => '####.#...##...##...##...##...#####...........',
49
+ 'E' => '######....#....####.#....#....#####..........',
50
+ 'F' => '######....#....####.#....#....#..............',
51
+ 'G' => '.###.#...##....#.####...##...#.###...........',
52
+ 'H' => '#...##...##...#######...##...##...#..........',
53
+ 'I' => '#####..#....#....#....#....#..#####..........',
54
+ 'J' => '....#....#....#....##...##...#.###...........',
55
+ 'K' => '#...##..#.#.#..##...#.#..#..#.#...#..........',
56
+ 'L' => '#....#....#....#....#....#....#####..........',
57
+ 'M' => '#...###.###.#.##...##...##...##...#..........',
58
+ 'N' => '#...##...###..##.#.##..###...##...#..........',
59
+ 'O' => '.###.#...##...##...##...##...#.###...........',
60
+ 'P' => '####.#...##...#####.#....#....#..............',
61
+ 'Q' => '.###.#...##...##...##...##...#.###....##.....',
62
+ 'R' => '####.#...##...#####.#...##...##...#..........',
63
+ 'S' => '.###.#...##.....###.....##...#.###...........',
64
+ 'T' => '#####..#....#....#....#....#....#............',
65
+ 'U' => '#...##...##...##...##...##...#.###...........',
66
+ 'V' => '#...##...##...##...#.#.#..#.#...#............',
67
+ 'W' => '#...##...##...##...##.#.###.###...#..........',
68
+ 'X' => '#...##...#.#.#...#...#.#.#...##...#..........',
69
+ 'Y' => '#...##...#.#.#...#....#....#....#............',
70
+ 'Z' => '#####....#...#...#...#...#....#####..........',
71
+ '[' => '..###..#....#....#....#....#....###..........',
72
+ '\\' => '#....#.....#.....#.....#.....#....#..........',
73
+ ']' => '###....#....#....#....#....#..###............',
74
+ '^' => '..#...#.#.#...#..............................',
75
+ '_' => '..............................#####..........',
76
+ '`' => '.#.....#.....................................',
77
+ 'a' => '...........#####...##...##...#.####..........',
78
+ 'b' => '#....#....####.#...##...##...#####...........',
79
+ 'c' => '...........###.#...##....#...#.###...........',
80
+ 'd' => '....#....#.#####...##...##...#.####..........',
81
+ 'e' => '...........###.#...#######.....###...........',
82
+ 'f' => '..##..#..#.#...####..#....#....#.............',
83
+ 'g' => '...........#####...##...##...#.####....#.###.',
84
+ 'h' => '#....#....####.#...##...##...##...#..........',
85
+ 'i' => '..#........##....#....#....#..#####..........',
86
+ 'j' => '....#........##....#....#....#....##...#.###.',
87
+ 'k' => '#....#....#...##..#.###..#..#.#...#..........',
88
+ 'l' => '##....#....#....#....#....#.....###..........',
89
+ 'm' => '..........####.#.#.##.#.##.#.##.#.#..........',
90
+ 'n' => '..........####.#...##...##...##...#..........',
91
+ 'o' => '...........###.#...##...##...#.###...........',
92
+ 'p' => '..........####.#...##...##...#####.#....#....',
93
+ 'q' => '...........#####...##...##...#.####....#....#',
94
+ 'r' => '..........#.##.##..###..##....#..............',
95
+ 's' => '...........#####.....###.....#####...........',
96
+ 't' => '.#....#...####..#....#....#.....###..........',
97
+ 'u' => '..........#...##...##...##...#.####..........',
98
+ 'v' => '..........#...##...##...#.#.#...#............',
99
+ 'w' => '..........#...##...##.#.##.#.#.#.#...........',
100
+ 'x' => '..........#...#.#.#...#...#.#.#...#..........',
101
+ 'y' => '..........#...##...##...##...#.####....#.###.',
102
+ 'z' => '..........#####...#...#...#...#####..........',
103
+ '{' => '...#...#....#...#.....#....#.....#...........',
104
+ '|' => '..#....#....#....#....#....#....#............',
105
+ '}' => '.#.....#....#.....#...#....#...#.............',
106
+ '~' => '................#..##.##.....................'
107
+ }.freeze
108
+
109
+ def self.font_layer_design_character?(some_design_character)
110
+ some_design_character == '#'
111
+ end
112
+
113
+ def self.background_layer_design_character?(some_design_character)
114
+ some_design_character == '.'
115
+ end
116
+ end
117
+ # rubocop: enable Metrics/ModuleLength
118
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiPngfy
4
+ # Reponsibilities
5
+ # - Provide the complete interface of this gem dynamically
6
+ # in order to force the interface of the Settings onto the caller
7
+ # - Orchestrates the Settings and the SettingsRenderer
8
+ class Pngfyer
9
+ def initialize(use_glyph_designs: true)
10
+ self.settings_renderer = SettingsRenderer.new(use_glyph_designs: use_glyph_designs)
11
+ self.settings = Settings::SetableGetableSettings.new
12
+ end
13
+
14
+ def respond_to_missing?(method_name, _)
15
+ setter?(method_name) || super
16
+ end
17
+
18
+ def method_missing(method_name, *arguments)
19
+ # forward only set_* calls to the settings so that the respective setting
20
+ # can enforce it's interface and any unsupported setting setters results
21
+ # in an undefined method error
22
+ if setter?(method_name)
23
+ setting_call = method_name
24
+ settings.public_send(setting_call, *arguments)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def pngfy
31
+ settings_snapshot = settings.snapshot
32
+ settings_renderer.render_result(settings_snapshot)
33
+ end
34
+
35
+ private
36
+
37
+ attr_accessor(:settings, :settings_renderer)
38
+
39
+ def setter?(method_name)
40
+ method_name.start_with?('set_')
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciiPngfy
4
+ # Reponsibilities
5
+ # - The reason for this particulare interface with singleton methods
6
+ # at this point, is to avoid coupling between the settings themselves
7
+ # and because the architecture is about to change in terms of the
8
+ # current implementation of the Settings
9
+ #
10
+ # - Provide a single point of reference for all computations that
11
+ # contribute to the renderer result of the #pngfy process
12
+ #
13
+ # - Computations based on setting values such as color;
14
+ # font height; spacing and text
15
+ #
16
+ # - General enough so that any settings related context can
17
+ # use this functionality
18
+ # rubocop: disable Metrics/ModuleLength
19
+ module RenderingRules
20
+ def self.text_lines(text)
21
+ text.split("\n", -1)
22
+ end
23
+
24
+ def self.longest_text_line(text)
25
+ text_lines(text).max_by(&:length)
26
+ end
27
+
28
+ def self.png_width(settings, override_text = nil)
29
+ use_text = override_text || settings.text
30
+
31
+ longest_text_line_length = longest_text_line(use_text).length
32
+ horizontal_spacing_count = longest_text_line_length - 1
33
+
34
+ (longest_text_line_length * GLYPH_DESIGN_WIDTH) + (horizontal_spacing_count * settings.horizontal_spacing)
35
+ end
36
+
37
+ def self.png_height(settings, override_text = nil)
38
+ use_text = override_text || settings.text
39
+
40
+ text_line_count = text_lines(use_text).size
41
+ vertical_spacing_count = text_line_count - 1
42
+
43
+ (text_line_count * GLYPH_DESIGN_HEIGHT) + (vertical_spacing_count * settings.vertical_spacing)
44
+ end
45
+
46
+ def self.font_multiplier(settings)
47
+ settings.font_height / GLYPH_DESIGN_HEIGHT
48
+ end
49
+
50
+ def self.render_width(settings)
51
+ png_width(settings) * font_multiplier(settings)
52
+ end
53
+
54
+ def self.render_height(settings)
55
+ png_height(settings) * font_multiplier(settings)
56
+ end
57
+
58
+ def self.text_lines_characters(settings)
59
+ text_lines(settings.text).map(&:chars)
60
+ end
61
+
62
+ def self.font_region(settings, character_column_index, character_row_index)
63
+ font_region_top_left_x = character_column_index * (settings.horizontal_spacing + GLYPH_DESIGN_WIDTH)
64
+ font_region_top_left_y = character_row_index * (settings.vertical_spacing + GLYPH_DESIGN_HEIGHT)
65
+ font_region_bottom_right_x = font_region_top_left_x + (GLYPH_DESIGN_WIDTH - 1)
66
+ font_region_bottom_right_y = font_region_top_left_y + (GLYPH_DESIGN_HEIGHT - 1)
67
+
68
+ AABB.new(
69
+ font_region_top_left_x,
70
+ font_region_top_left_y,
71
+ font_region_bottom_right_x,
72
+ font_region_bottom_right_y
73
+ )
74
+ end
75
+
76
+ def self.each_font_region_with_associated_character(settings, &yielder)
77
+ text_lines_characters(settings).each_with_index do |line_characters, row_index|
78
+ line_characters.each_with_index do |character, column_index|
79
+ font_region = font_region(settings, column_index, row_index)
80
+
81
+ yielder.call(font_region, character)
82
+ end
83
+ end
84
+ end
85
+
86
+ def self.each_font_region(settings, &yielder)
87
+ each_font_region_with_associated_character(settings) do |font_region, _font_region_character|
88
+ yielder.call(font_region)
89
+ end
90
+ end
91
+
92
+ def self.color_rgba_to_chunky_png_integer(color_rgba)
93
+ ChunkyPNG::Color.rgba(
94
+ color_rgba.red,
95
+ color_rgba.green,
96
+ color_rgba.blue,
97
+ color_rgba.alpha
98
+ )
99
+ end
100
+
101
+ def self.straight_alpha_composite_color_value(over_component, over_alpha, under_component, under_alpha)
102
+ # over refers to the top layer, i.e. the font layer
103
+ ca = over_component
104
+ aa = over_alpha.fdiv(255)
105
+
106
+ # under refers to the bottom layer, i.e. the background layer
107
+ cb = under_component
108
+ ab = under_alpha.fdiv(255)
109
+
110
+ # return alpha composited color component as integer in range 0..255
111
+ # avoid divisions by zero
112
+ numerator = (ca * aa + cb * ab * (1 - aa))
113
+ denumerator = (aa + ab * (1 - aa))
114
+
115
+ return 0 if denumerator.zero? || numerator.zero?
116
+
117
+ (numerator / denumerator).to_i
118
+ end
119
+
120
+ def self.straight_alpha_composite_alpha_value(over_alpha, under_alpha)
121
+ aa = over_alpha.fdiv(255)
122
+ ab = under_alpha.fdiv(255)
123
+
124
+ # return alpha composited alpha component as integer in range 0..255
125
+ ((aa + ab * (1 - aa)) * 255).to_i
126
+ end
127
+
128
+ def self.straight_alpha_composite_color(over_color, under_color)
129
+ over_color_alpha = over_color.alpha
130
+ under_color_alpha = under_color.alpha
131
+
132
+ AsciiPngfy::ColorRGBA.new(
133
+ straight_alpha_composite_color_value(over_color.red, over_color_alpha, under_color.red, under_color_alpha),
134
+ straight_alpha_composite_color_value(over_color.green, over_color_alpha, under_color.green, under_color_alpha),
135
+ straight_alpha_composite_color_value(over_color.blue, over_color_alpha, under_color.blue, under_color_alpha),
136
+ straight_alpha_composite_alpha_value(over_color_alpha, under_color_alpha)
137
+ )
138
+ end
139
+
140
+ def self.possibly_blended_font_and_background_color(settings)
141
+ case settings.font_color.alpha
142
+ when 255
143
+ settings.font_color
144
+ else
145
+ straight_alpha_composite_color(settings.font_color, settings.background_color)
146
+ end
147
+ end
148
+
149
+ def self.design_character_pixel_color(settings, design_character)
150
+ if AsciiPngfy::Glyphs.font_layer_design_character?(design_character)
151
+ possibly_blended_font_and_background_color(settings)
152
+ elsif AsciiPngfy::Glyphs.background_layer_design_character?(design_character)
153
+ settings.background_color
154
+ end
155
+ end
156
+
157
+ def self.plot_font_regions_with_design(settings, png)
158
+ each_font_region_with_associated_character(settings) do |font_region, character|
159
+ # the only ASCII character that has an empty glyph desgn is the space
160
+ # so avoid unnecessary work for spaces
161
+ next if character == ' '
162
+
163
+ # mirror the font design for each font region into the png
164
+ font_region_character_design = AsciiPngfy::Glyphs::DESIGNS[character]
165
+
166
+ font_region.each_pixel_with_index do |font_pixel_x, font_pixel_y, font_pixel_index|
167
+ png_pixel_design_character = font_region_character_design[font_pixel_index]
168
+
169
+ png_pixel_plot_color = design_character_pixel_color(settings, png_pixel_design_character)
170
+ png_pixel_plot_color_as_integer = color_rgba_to_chunky_png_integer(png_pixel_plot_color)
171
+
172
+ png[font_pixel_x, font_pixel_y] = png_pixel_plot_color_as_integer
173
+ end
174
+ end
175
+ end
176
+
177
+ def self.plot_font_regions_without_design(settings, png)
178
+ final_font_color = possibly_blended_font_and_background_color(settings)
179
+ final_font_color_as_integer = color_rgba_to_chunky_png_integer(final_font_color)
180
+
181
+ each_font_region(settings) do |font_region|
182
+ # fill every font region entirely with, the potentially mixed, font and backround color
183
+ font_region.each_pixel do |font_region_x, font_region_y|
184
+ png[font_region_x, font_region_y] = final_font_color_as_integer
185
+ end
186
+ end
187
+ end
188
+
189
+ def self.plot_settings(settings, use_glyph_designs, png)
190
+ if use_glyph_designs
191
+ plot_font_regions_with_design(settings, png)
192
+ else
193
+ plot_font_regions_without_design(settings, png)
194
+ end
195
+ end
196
+ end
197
+ end
198
+ # rubocop: enable Metrics/ModuleLength