distorted 0.5.7 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/distorted/checking_you_out.rb +116 -0
- data/lib/distorted/error_code.rb +43 -0
- data/lib/distorted/injection_of_love.rb +247 -0
- data/lib/distorted/modular_technology/pango.rb +20 -5
- data/lib/distorted/modular_technology/triple_counter.rb +45 -0
- data/lib/distorted/modular_technology/ttfunk.rb +48 -0
- data/lib/distorted/modular_technology/vips.rb +17 -0
- data/lib/distorted/modular_technology/vips_load.rb +77 -0
- data/lib/distorted/modular_technology/vips_save.rb +172 -0
- data/lib/distorted/molecule/C18H27NO3.rb +10 -0
- data/lib/distorted/{font.rb → molecule/font.rb} +33 -27
- data/lib/distorted/molecule/image.rb +36 -0
- data/lib/distorted/{pdf.rb → molecule/pdf.rb} +23 -14
- data/lib/distorted/molecule/svg.rb +60 -0
- data/lib/distorted/{text.rb → molecule/text.rb} +70 -86
- data/lib/distorted/{video.rb → molecule/video.rb} +9 -7
- data/lib/distorted/types/README +4 -0
- data/lib/distorted/types/application.yaml +8 -0
- data/lib/distorted/types/font.yaml +29 -0
- data/lib/distorted/version.rb +3 -1
- metadata +33 -8
- data/lib/distorted/image.rb +0 -121
- data/lib/distorted/svg.rb +0 -21
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
require 'distorted/checking_you_out'
|
5
|
+
require 'distorted/modular_technology/vips'
|
6
|
+
require 'distorted/injection_of_love'
|
7
|
+
|
8
|
+
|
9
|
+
module Cooltrainer
|
10
|
+
module DistorteD
|
11
|
+
module Image
|
12
|
+
|
13
|
+
|
14
|
+
# Attributes for our <picture>/<img>.
|
15
|
+
# Automatically enabled as attrs for DD Liquid Tag.
|
16
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture#Attributes
|
17
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes
|
18
|
+
# https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading
|
19
|
+
ATTRIBUTES = Set[:alt, :caption, :href, :loading]
|
20
|
+
|
21
|
+
# Defaults for HTML Element attributes.
|
22
|
+
# Not every attr has to be listed here.
|
23
|
+
# Many need no default and just won't render.
|
24
|
+
ATTRIBUTES_DEFAULT = {
|
25
|
+
:loading => :eager,
|
26
|
+
}
|
27
|
+
ATTRIBUTES_VALUES = {
|
28
|
+
:loading => Set[:eager, :lazy],
|
29
|
+
}
|
30
|
+
|
31
|
+
include Cooltrainer::DistorteD::Technology::Vips
|
32
|
+
include Cooltrainer::DistorteD::InjectionOfLove
|
33
|
+
|
34
|
+
end # Image
|
35
|
+
end # DistorteD
|
36
|
+
end # Cooltrainer
|
@@ -1,17 +1,20 @@
|
|
1
1
|
require 'set'
|
2
2
|
|
3
3
|
require 'hexapdf'
|
4
|
-
|
4
|
+
|
5
|
+
require 'distorted/checking_you_out'
|
6
|
+
require 'distorted/injection_of_love'
|
7
|
+
require 'distorted/molecule/C18H27NO3'
|
5
8
|
|
6
9
|
|
7
10
|
module Cooltrainer
|
8
11
|
module DistorteD
|
9
|
-
|
12
|
+
module PDF
|
13
|
+
|
10
14
|
|
11
|
-
|
12
|
-
SUB_TYPE = 'pdf'.freeze
|
15
|
+
include Cooltrainer::DistorteD::Molecule::C18H27NO3
|
13
16
|
|
14
|
-
|
17
|
+
LOWER_WORLD = CHECKING::YOU::IN("application/pdf")
|
15
18
|
|
16
19
|
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object#Attributes
|
17
20
|
# https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdf_open_parameters.pdf
|
@@ -37,7 +40,7 @@ module Cooltrainer
|
|
37
40
|
:highlight,
|
38
41
|
:fdf,
|
39
42
|
]
|
40
|
-
|
43
|
+
ATTRIBUTES = Set[
|
41
44
|
:alt,
|
42
45
|
:caption,
|
43
46
|
:height, #<object> viewer container height.
|
@@ -50,9 +53,8 @@ module Cooltrainer
|
|
50
53
|
|
51
54
|
FLOAT_INT_FRAGMENT = '[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)'.freeze
|
52
55
|
ZERO_TO_ONE_HUNDRED = /^(([1-9]\d?|1\d{1})([.,]\d{0,1})?|100([.,]0{1})?)$/
|
53
|
-
BOOLEAN_SET = Set[0, 1, false, true, '0'.freeze, '1'.freeze, 'false'.freeze, 'true'.freeze]
|
54
56
|
|
55
|
-
|
57
|
+
ATTRIBUTES_DEFAULT = {
|
56
58
|
:height => '100%'.freeze,
|
57
59
|
:width => '100%'.freeze,
|
58
60
|
# BEGIN PDF Open Parameters
|
@@ -73,7 +75,7 @@ module Cooltrainer
|
|
73
75
|
# …but then goes on to show some examples (like `comment`)
|
74
76
|
# that are clearly longer than 32 characters.
|
75
77
|
# Dunno. I'll err on the side of giving you a footgun.
|
76
|
-
|
78
|
+
ATTRIBUTES_VALUES = {
|
77
79
|
:nameddest => /^#{RESERVED_CHARACTERS_FRAGMENT}$/,
|
78
80
|
:page => /\d/,
|
79
81
|
:comment => /^#{RESERVED_CHARACTERS_FRAGMENT}$/,
|
@@ -82,15 +84,18 @@ module Cooltrainer
|
|
82
84
|
:view => /^Fit(H|V|B|BH|BV(,#{FLOAT_INT_FRAGMENT})?)?$/,
|
83
85
|
:viewrect => /^#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT}$/,
|
84
86
|
:pagemode => Set[:none, :thumbs, :bookmarks],
|
85
|
-
:scrollbar =>
|
87
|
+
:scrollbar => BOOLEAN_ATTR_VALUES,
|
86
88
|
:search => /^#{RESERVED_CHARACTERS_FRAGMENT}(,\s#{RESERVED_CHARACTERS_FRAGMENT})*$/,
|
87
|
-
:toolbar =>
|
88
|
-
:statusbar =>
|
89
|
-
:messages =>
|
90
|
-
:navpanes =>
|
89
|
+
:toolbar => BOOLEAN_ATTR_VALUES,
|
90
|
+
:statusbar => BOOLEAN_ATTR_VALUES,
|
91
|
+
:messages => BOOLEAN_ATTR_VALUES,
|
92
|
+
:navpanes => BOOLEAN_ATTR_VALUES,
|
91
93
|
:fdf => /^#{RESERVED_CHARACTERS_FRAGMENT}$/,
|
92
94
|
}
|
93
95
|
|
96
|
+
include Cooltrainer::DistorteD::InjectionOfLove
|
97
|
+
|
98
|
+
# TODO: Use MuPDF instead of libvips magick-based PDF loader.
|
94
99
|
|
95
100
|
def self.optimize(src, dest)
|
96
101
|
HexaPDF::Document.open(src) do |doc|
|
@@ -105,6 +110,10 @@ module Cooltrainer
|
|
105
110
|
end
|
106
111
|
end
|
107
112
|
|
113
|
+
def to_application_pdf(*a, **k, &b)
|
114
|
+
copy_file(*a, **k, &b)
|
115
|
+
end
|
116
|
+
|
108
117
|
end # PDF
|
109
118
|
end # DistorteD
|
110
119
|
end # Cooltrainer
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'svg_optimizer'
|
4
|
+
|
5
|
+
require 'distorted/checking_you_out'
|
6
|
+
require 'distorted/injection_of_love'
|
7
|
+
require 'distorted/molecule/C18H27NO3'
|
8
|
+
|
9
|
+
|
10
|
+
module Cooltrainer
|
11
|
+
module DistorteD
|
12
|
+
module SVG
|
13
|
+
|
14
|
+
SUB_TYPE = 'svg'.freeze
|
15
|
+
include Cooltrainer::DistorteD::Molecule::C18H27NO3
|
16
|
+
|
17
|
+
#WISHLIST: Support VML for old IE compatibility.
|
18
|
+
# Example: RaphaëlJS — https://en.wikipedia.org/wiki/Rapha%C3%ABl_(JavaScript_library)
|
19
|
+
LOWER_WORLD = CHECKING::YOU::IN(/^image\/svg/)
|
20
|
+
|
21
|
+
ATTRIBUTES = Set[
|
22
|
+
:alt,
|
23
|
+
:caption,
|
24
|
+
:href,
|
25
|
+
:loading,
|
26
|
+
:optimize,
|
27
|
+
]
|
28
|
+
ATTRIBUTES_VALUES = {
|
29
|
+
:optimize => BOOLEAN_ATTR_VALUES,
|
30
|
+
}
|
31
|
+
ATTRIBUTES_DEFAULT = {
|
32
|
+
:optimize => false,
|
33
|
+
}
|
34
|
+
|
35
|
+
include Cooltrainer::DistorteD::Technology::VipsSave
|
36
|
+
include Cooltrainer::DistorteD::InjectionOfLove
|
37
|
+
|
38
|
+
def to_vips_image
|
39
|
+
# TODO: Load-time options for various formats, like SVG's `unlimited`:
|
40
|
+
# "SVGs larger than 10MB are normally blocked for security. Set unlimited to allow SVGs of any size."
|
41
|
+
# https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-svgload
|
42
|
+
@vips_image ||= Vips::Image.new_from_file(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_image_svg_xml(dest, *a, **k, &b)
|
46
|
+
if abstract(:optimize)
|
47
|
+
SvgOptimizer.optimize_file(path, dest, SvgOptimizer::DEFAULT_PLUGINS)
|
48
|
+
else
|
49
|
+
copy_file(dest, *a, **k, &b)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.optimize(src, dest)
|
54
|
+
# TODO: Make optimizations/plugins configurable
|
55
|
+
SvgOptimizer.optimize_file(src, dest, SvgOptimizer::DEFAULT_PLUGINS)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,45 +1,24 @@
|
|
1
1
|
require 'set'
|
2
2
|
|
3
|
-
require 'ttfunk' # Font metadata extraction
|
4
3
|
require 'charlock_holmes' # Text file charset detection
|
5
4
|
|
6
5
|
require 'distorted/monkey_business/string' # String#map
|
7
6
|
require 'distorted/modular_technology/pango'
|
7
|
+
require 'distorted/modular_technology/ttfunk'
|
8
|
+
require 'distorted/modular_technology/vips_save'
|
8
9
|
|
9
|
-
require 'distorted/
|
10
|
-
|
11
|
-
require '
|
12
|
-
|
13
|
-
# No need to do all the fancy library versioning in a subclass.
|
14
|
-
require 'vips'
|
10
|
+
require 'distorted/checking_you_out'
|
11
|
+
require 'distorted/injection_of_love'
|
12
|
+
require 'distorted/molecule/image'
|
15
13
|
|
16
14
|
|
17
15
|
module Cooltrainer
|
18
16
|
module DistorteD
|
19
|
-
|
20
|
-
|
21
|
-
include Cooltrainer::DistorteD::Tech::Pango;
|
22
|
-
|
17
|
+
module Text
|
23
18
|
|
24
|
-
MEDIA_TYPE = 'text'.freeze
|
25
19
|
|
26
|
-
|
27
|
-
|
28
|
-
ATTRS = Set[
|
29
|
-
:alt,
|
30
|
-
:crop,
|
31
|
-
:font,
|
32
|
-
:encoding,
|
33
|
-
:spacing,
|
34
|
-
:dpi,
|
35
|
-
]
|
36
|
-
ATTRS_VALUES = {
|
37
|
-
:spacing => Set[:monospace, :proportional],
|
38
|
-
}
|
39
|
-
ATTRS_DEFAULT = {
|
40
|
-
:crop => :none,
|
41
|
-
:dpi => 144,
|
42
|
-
}
|
20
|
+
LOWER_WORLD = CHECKING::YOU::IN(/^text\/(plain|x-nfo)/)
|
21
|
+
OUTER_LIMITS = CHECKING::YOU::IN(/^text\/(plain|x-nfo)/)
|
43
22
|
|
44
23
|
# Track supported fonts by codepage.
|
45
24
|
# Avoid renaming these from the original archives / websites.
|
@@ -91,17 +70,45 @@ module Cooltrainer
|
|
91
70
|
],
|
92
71
|
}
|
93
72
|
# …as well as the inverse, the numeric codepage for each font:
|
94
|
-
FONT_CODEPAGE = CODEPAGE_FONT.reduce(Hash.new([])) { |memo, (key, values)|
|
73
|
+
FONT_CODEPAGE = self::CODEPAGE_FONT.reduce(Hash.new([])) { |memo, (key, values)|
|
95
74
|
values.each { |value| memo[value] = key }
|
96
75
|
memo
|
97
76
|
}
|
98
77
|
|
78
|
+
self::OUTER_LIMITS.each { |t|
|
79
|
+
define_method(t.distorted_method) { |*a, **k, &b|
|
80
|
+
copy_file(*a, **k, &b)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
ATTRIBUTES = Set[
|
85
|
+
:alt,
|
86
|
+
:crop,
|
87
|
+
:font,
|
88
|
+
:encoding,
|
89
|
+
:spacing,
|
90
|
+
:dpi,
|
91
|
+
]
|
92
|
+
ATTRIBUTES_VALUES = {
|
93
|
+
:spacing => Set[:monospace, :proportional],
|
94
|
+
:font => self::FONT_FILENAME.keys.to_set,
|
95
|
+
}
|
96
|
+
ATTRIBUTES_DEFAULT = {
|
97
|
+
:crop => :none,
|
98
|
+
:dpi => 144,
|
99
|
+
:encoding => 'UTF-8'.freeze
|
100
|
+
}
|
101
|
+
|
102
|
+
include Cooltrainer::DistorteD::Technology::TTFunk
|
103
|
+
include Cooltrainer::DistorteD::Technology::Pango
|
104
|
+
include Cooltrainer::DistorteD::Technology::VipsSave
|
105
|
+
include Cooltrainer::DistorteD::InjectionOfLove
|
99
106
|
|
100
107
|
# Using a numeric key for things for simplicity.
|
101
108
|
# TODO: Replace this with Ruby's built-in Encoding class after I have
|
102
109
|
# a better idea what I want to do.
|
103
110
|
def codepage
|
104
|
-
case
|
111
|
+
case text_file_encoding
|
105
112
|
when 'UTF-8'.freeze then 65001
|
106
113
|
when 'Shift_JIS'.freeze then 932
|
107
114
|
when 'IBM437'.freeze then 437
|
@@ -112,35 +119,48 @@ module Cooltrainer
|
|
112
119
|
# Return a Pango Markup escaped version of the document.
|
113
120
|
def to_pango
|
114
121
|
# https://developer.gnome.org/glib/stable/glib-Simple-XML-Subset-Parser.html#g-markup-escape-text
|
115
|
-
escaped =
|
122
|
+
escaped = text_file_utf8_content.map{ |c|
|
116
123
|
g_markup_escape_char(c)
|
117
124
|
}
|
118
|
-
if
|
125
|
+
if font_spacing == :monospace
|
119
126
|
"<tt>" << escaped << "</tt>"
|
120
127
|
else
|
121
128
|
escaped
|
122
129
|
end
|
123
130
|
end
|
124
131
|
|
125
|
-
|
126
|
-
@src = src
|
127
|
-
@liquid_spacing = spacing
|
132
|
+
protected
|
128
133
|
|
134
|
+
def text_file_content
|
129
135
|
# VIPS makes us provide the text content as a single variable,
|
130
136
|
# so we may as well just one-shot File.read() it into memory.
|
131
137
|
# https://kunststube.net/encoding/
|
132
|
-
|
138
|
+
@text_file_content ||= File.read(path)
|
139
|
+
end
|
140
|
+
|
141
|
+
def text_file_utf8_content
|
142
|
+
CharlockHolmes::Converter.convert(text_file_content, text_file_encoding, 'UTF-8'.freeze)
|
143
|
+
end
|
133
144
|
|
145
|
+
def text_file_encoding
|
134
146
|
# It's not easy or even possible in some cases to tell the "true" codepage
|
135
147
|
# we should use for any given text document, but using character detection
|
136
148
|
# is worth a shot if the user gave us nothing.
|
137
|
-
|
138
|
-
|
139
|
-
@
|
149
|
+
#
|
150
|
+
# TODO: Figure out if/how we can get IBM437 files to not be detected as ISO-8859-1
|
151
|
+
@text_file_encoding ||= (
|
152
|
+
abstract(:encoding).to_s ||
|
153
|
+
CharlockHolmes::EncodingDetector.detect(text_file_content)[:encoding] ||
|
154
|
+
'UTF-8'.freeze
|
155
|
+
).to_s
|
156
|
+
end
|
140
157
|
|
141
|
-
|
142
|
-
|
158
|
+
def vips_font
|
159
|
+
# Set the shorthand Symbol key for our chosen font.
|
160
|
+
return abstract(:font)&.to_sym || CODEPAGE_FONT[codepage].first
|
161
|
+
end
|
143
162
|
|
163
|
+
def to_vips_image
|
144
164
|
# Load font metadata directly from the file so we don't have to
|
145
165
|
# duplicate it here to feed to Vips/Pango.
|
146
166
|
#
|
@@ -154,10 +174,9 @@ module Cooltrainer
|
|
154
174
|
# => "PerfectDOSVGA437"
|
155
175
|
# irb(main)> font_meta.line_gap
|
156
176
|
# => 0
|
157
|
-
@font_meta = TTFunk::File.open(font_path)
|
158
177
|
|
159
178
|
# https://libvips.github.io/libvips/API/current/libvips-create.html#vips-text
|
160
|
-
|
179
|
+
Vips::Image.text(
|
161
180
|
# This string must be well-escaped Pango Markup:
|
162
181
|
# https://developer.gnome.org/pango/stable/pango-Markup.html
|
163
182
|
# However the official function for escaping text is
|
@@ -170,23 +189,22 @@ module Cooltrainer
|
|
170
189
|
# we must also specify a font family, subfamily, and size.
|
171
190
|
:font => "#{font_name} 16",
|
172
191
|
# Space between lines (in Points).
|
173
|
-
:spacing =>
|
192
|
+
:spacing => to_ttfunk.line_gap,
|
174
193
|
:justify => true, # Requires libvips 8.8
|
175
|
-
:dpi => dpi
|
194
|
+
:dpi => abstract(:dpi)&.to_i,
|
176
195
|
},
|
177
196
|
)
|
178
197
|
end
|
179
198
|
|
180
|
-
protected
|
181
|
-
|
182
199
|
# Return the String absolute path to the TTF file
|
183
200
|
def font_path
|
184
201
|
File.join(
|
185
|
-
File.dirname(__FILE__), #
|
202
|
+
File.dirname(__FILE__), # molecule
|
203
|
+
'..'.freeze, # distorted
|
186
204
|
'..'.freeze, # lib
|
187
205
|
'..'.freeze, # DistorteD-Ruby
|
188
206
|
'font'.freeze,
|
189
|
-
font_codepage,
|
207
|
+
font_codepage.to_s,
|
190
208
|
font_filename,
|
191
209
|
)
|
192
210
|
end
|
@@ -194,46 +212,12 @@ module Cooltrainer
|
|
194
212
|
# Returns the numeric representation of the codepage
|
195
213
|
# covered by our font.
|
196
214
|
def font_codepage
|
197
|
-
|
215
|
+
FONT_CODEPAGE.dig(vips_font).to_s
|
198
216
|
end
|
199
217
|
|
200
218
|
# Returns the basename (with file extension) of our font.
|
201
219
|
def font_filename
|
202
|
-
|
203
|
-
end
|
204
|
-
|
205
|
-
# Returns a boolean for whether or not this font is monospaced.
|
206
|
-
# true == monospace
|
207
|
-
# false == proportional
|
208
|
-
def spacing
|
209
|
-
# Monospace fonts will (read: should) have the same width
|
210
|
-
# for every glyph, so we can tell a monospace font by
|
211
|
-
# checking if a deduplicated widths table has size == 1:
|
212
|
-
# irb(main)> font.horizontal_metrics.widths.count
|
213
|
-
# => 256
|
214
|
-
# irb(main)> font.horizontal_metrics.widths.uniq.compact.length
|
215
|
-
# => 1
|
216
|
-
@font_meta.horizontal_metrics.widths.uniq.compact.length == 1 ? :monospace : :proportional
|
217
|
-
end
|
218
|
-
|
219
|
-
# Returns the Family and Subfamily as one string suitable for libvips
|
220
|
-
def font_name
|
221
|
-
"#{@font_meta.name.font_family.first.encode('UTF-8')} #{@font_meta.name.font_subfamily.first.encode('UTF-8')}"
|
222
|
-
end
|
223
|
-
|
224
|
-
# Returns the Pango-Markup-encoded UTF-8 String version + revision of the font
|
225
|
-
def font_version
|
226
|
-
g_markup_escape_text(@font_meta.name&.version&.first&.encode('UTF-8').to_s)
|
227
|
-
end
|
228
|
-
|
229
|
-
# Returns the Pango-Markup-encoded UTF-8 String font file description
|
230
|
-
def font_description
|
231
|
-
g_markup_escape_text(@font_meta.name&.description&.first&.encode('UTF-8').to_s)
|
232
|
-
end
|
233
|
-
|
234
|
-
# Returns the Pango-Markup-encoded UTF-8 String copyright information of the font
|
235
|
-
def font_copyright
|
236
|
-
g_markup_escape_text(@font_meta.name&.copyright&.first&.encode('UTF-8').to_s)
|
220
|
+
FONT_FILENAME.dig(vips_font)
|
237
221
|
end
|
238
222
|
|
239
223
|
end # Text
|
@@ -17,25 +17,27 @@ end
|
|
17
17
|
|
18
18
|
require 'set'
|
19
19
|
|
20
|
-
require '
|
20
|
+
require 'distorted/checking_you_out'
|
21
|
+
require 'distorted/injection_of_love'
|
22
|
+
|
21
23
|
|
22
24
|
module Cooltrainer
|
23
25
|
module DistorteD
|
24
|
-
|
26
|
+
module Video
|
25
27
|
|
26
|
-
|
27
|
-
MIME_TYPES = MIME::Types[/^#{MEDIA_TYPE}/, :complete => true].to_set
|
28
|
+
LOWER_WORLD = CHECKING::YOU::IN(/^video\/mp4/)
|
28
29
|
|
29
30
|
# Attributes for our <video>.
|
30
31
|
# Automatically enabled as attrs for DD Liquid Tag.
|
31
32
|
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#Attributes
|
32
|
-
|
33
|
+
ATTRIBUTES = Set[:caption]
|
33
34
|
|
34
35
|
# Defaults for HTML Element attributes.
|
35
36
|
# Not every attr has to be listed here.
|
36
37
|
# Many need no default and just won't render.
|
37
|
-
|
38
|
-
|
38
|
+
ATTRIBUTES_DEFAULT = {}
|
39
|
+
ATTRIBUTES_VALUES = {}
|
40
|
+
include Cooltrainer::DistorteD::InjectionOfLove
|
39
41
|
|
40
42
|
attr_accessor :dest
|
41
43
|
|