distorted 0.5.7 → 0.6.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 +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
|
|