distorted 0.5.6 → 0.5.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +661 -0
- data/README.md +4 -139
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Less_Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/More_Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/less_more_perfect_dos_vga_437.html +52 -0
- data/font/1252/LICENSE/PerfectDOSVGA437/font-comment.php@file=perfect_dos_vga_437.html +5 -0
- data/font/1252/LessPerfectDOSVGA.ttf +0 -0
- data/font/1252/MorePerfectDOSVGA.ttf +0 -0
- data/font/1252/Perfect DOS VGA 437 Win.ttf +0 -0
- data/font/437/Perfect DOS VGA 437.ttf +0 -0
- data/font/437/dos437.txt +72 -0
- data/font/65001/Anonymous Pro B.ttf +0 -0
- data/font/65001/Anonymous Pro BI.ttf +0 -0
- data/font/65001/Anonymous Pro I.ttf +0 -0
- data/font/65001/Anonymous Pro.ttf +0 -0
- data/font/65001/LICENSE/AnonymousPro/FONTLOG.txt +45 -0
- data/font/65001/LICENSE/AnonymousPro/OFL-FAQ.txt +235 -0
- data/font/65001/LICENSE/AnonymousPro/OFL.txt +94 -0
- data/font/65001/LICENSE/AnonymousPro/README.txt +55 -0
- data/font/850/ProFont-Bold-01/LICENSE +22 -0
- data/font/850/ProFont-Bold-01/readme.txt +28 -0
- data/font/850/ProFontWindows-Bold.ttf +0 -0
- data/font/850/ProFontWindows.ttf +0 -0
- data/font/850/Profont/LICENSE +22 -0
- data/font/850/Profont/readme.txt +31 -0
- data/font/932/LICENSE/README-ttf.txt +213 -0
- data/font/932/mona.ttf +0 -0
- data/lib/distorted/error_code.rb +8 -0
- data/lib/distorted/font.rb +192 -0
- data/lib/distorted/image.rb +121 -0
- data/lib/distorted/modular_technology/pango.rb +75 -0
- data/lib/distorted/monkey_business/hash.rb +33 -0
- data/lib/distorted/monkey_business/mnemoniq.rb +8 -0
- data/lib/distorted/monkey_business/set.rb +15 -0
- data/lib/distorted/monkey_business/string.rb +6 -0
- data/lib/distorted/pdf.rb +110 -0
- data/lib/distorted/svg.rb +21 -0
- data/lib/distorted/text.rb +241 -0
- data/lib/distorted/version.rb +20 -0
- data/lib/distorted/video.rb +193 -0
- data/test/distorted_test.rb +11 -0
- data/test/test_helper.rb +4 -0
- metadata +49 -5
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'hexapdf'
|
4
|
+
require 'mime/types'
|
5
|
+
|
6
|
+
|
7
|
+
module Cooltrainer
|
8
|
+
module DistorteD
|
9
|
+
class PDF
|
10
|
+
|
11
|
+
MEDIA_TYPE = 'application'.freeze
|
12
|
+
SUB_TYPE = 'pdf'.freeze
|
13
|
+
|
14
|
+
MIME_TYPES = MIME::Types["#{MEDIA_TYPE}/#{SUB_TYPE}"].to_set
|
15
|
+
|
16
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object#Attributes
|
17
|
+
# https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdf_open_parameters.pdf
|
18
|
+
PDF_OPEN_PARAMS = Array[
|
19
|
+
# Keep the PDF Open Params in the order they are defined
|
20
|
+
# in the Adobe documentation, since it says they should
|
21
|
+
# be specified in the URL in that same order.
|
22
|
+
# Ruby's Set doesn't guarantee order, so use a plain Array here.
|
23
|
+
:nameddest,
|
24
|
+
:page,
|
25
|
+
:comment,
|
26
|
+
:collab,
|
27
|
+
:zoom,
|
28
|
+
:view,
|
29
|
+
:viewrect,
|
30
|
+
:pagemode,
|
31
|
+
:scrollbar,
|
32
|
+
:search,
|
33
|
+
:toolbar,
|
34
|
+
:statusbar,
|
35
|
+
:messages,
|
36
|
+
:navpanes,
|
37
|
+
:highlight,
|
38
|
+
:fdf,
|
39
|
+
]
|
40
|
+
ATTRS = Set[
|
41
|
+
:alt,
|
42
|
+
:caption,
|
43
|
+
:height, #<object> viewer container height.
|
44
|
+
:width, # <object> viewer container width.
|
45
|
+
].merge(PDF_OPEN_PARAMS)
|
46
|
+
|
47
|
+
# "You cannot use the reserved characters =, #, and &.
|
48
|
+
# There is no way to escape these special characters."
|
49
|
+
RESERVED_CHARACTERS_FRAGMENT = '[^=#&]+'.freeze
|
50
|
+
|
51
|
+
FLOAT_INT_FRAGMENT = '[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)'.freeze
|
52
|
+
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
|
+
|
55
|
+
ATTRS_DEFAULT = {
|
56
|
+
:height => '100%'.freeze,
|
57
|
+
:width => '100%'.freeze,
|
58
|
+
# BEGIN PDF Open Parameters
|
59
|
+
:page => 1,
|
60
|
+
:view => :Fit,
|
61
|
+
:pagemode => :none,
|
62
|
+
:scrollbar => 1,
|
63
|
+
:toolbar => 1,
|
64
|
+
:statusbar => 1,
|
65
|
+
:messages => 0,
|
66
|
+
:navpanes => 1,
|
67
|
+
# END PDF Open Parameters
|
68
|
+
}
|
69
|
+
|
70
|
+
# Adobe's PDF Open Parameters documentation sez:
|
71
|
+
# "Individual parameters, together with their values (separated by & or #),
|
72
|
+
# can be no greater then 32 characters in length."
|
73
|
+
# …but then goes on to show some examples (like `comment`)
|
74
|
+
# that are clearly longer than 32 characters.
|
75
|
+
# Dunno. I'll err on the side of giving you a footgun.
|
76
|
+
ATTRS_VALUES = {
|
77
|
+
:nameddest => /^#{RESERVED_CHARACTERS_FRAGMENT}$/,
|
78
|
+
:page => /\d/,
|
79
|
+
:comment => /^#{RESERVED_CHARACTERS_FRAGMENT}$/,
|
80
|
+
:collab => /^(DAVFDF|FSFDF|DB)@#{RESERVED_CHARACTERS_FRAGMENT}$/,
|
81
|
+
:zoom => /^#{FLOAT_INT_FRAGMENT}(,#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT})?$/,
|
82
|
+
:view => /^Fit(H|V|B|BH|BV(,#{FLOAT_INT_FRAGMENT})?)?$/,
|
83
|
+
:viewrect => /^#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT}$/,
|
84
|
+
:pagemode => Set[:none, :thumbs, :bookmarks],
|
85
|
+
:scrollbar => BOOLEAN_SET,
|
86
|
+
:search => /^#{RESERVED_CHARACTERS_FRAGMENT}(,\s#{RESERVED_CHARACTERS_FRAGMENT})*$/,
|
87
|
+
:toolbar => BOOLEAN_SET,
|
88
|
+
:statusbar => BOOLEAN_SET,
|
89
|
+
:messages => BOOLEAN_SET,
|
90
|
+
:navpanes => BOOLEAN_SET,
|
91
|
+
:fdf => /^#{RESERVED_CHARACTERS_FRAGMENT}$/,
|
92
|
+
}
|
93
|
+
|
94
|
+
|
95
|
+
def self.optimize(src, dest)
|
96
|
+
HexaPDF::Document.open(src) do |doc|
|
97
|
+
doc.task(
|
98
|
+
:optimize,
|
99
|
+
compact: true,
|
100
|
+
object_streams: :generate,
|
101
|
+
xref_streams: :generate,
|
102
|
+
compress_pages: false,
|
103
|
+
)
|
104
|
+
doc.write(dest)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end # PDF
|
109
|
+
end # DistorteD
|
110
|
+
end # Cooltrainer
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'mime/types'
|
4
|
+
require 'svg_optimizer'
|
5
|
+
|
6
|
+
module Cooltrainer
|
7
|
+
module DistorteD
|
8
|
+
class SVG < Image
|
9
|
+
|
10
|
+
SUB_TYPE = 'svg'.freeze
|
11
|
+
|
12
|
+
MIME_TYPES = MIME::Types[/^#{self::MEDIA_TYPE}\/#{self::SUB_TYPE}/, :complete => true].to_set
|
13
|
+
|
14
|
+
def self.optimize(src, dest)
|
15
|
+
# TODO: Make optimizations/plugins configurable
|
16
|
+
SvgOptimizer.optimize_file(src, dest, SvgOptimizer::DEFAULT_PLUGINS)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'ttfunk' # Font metadata extraction
|
4
|
+
require 'charlock_holmes' # Text file charset detection
|
5
|
+
|
6
|
+
require 'distorted/monkey_business/string' # String#map
|
7
|
+
require 'distorted/modular_technology/pango'
|
8
|
+
|
9
|
+
require 'distorted/image'
|
10
|
+
|
11
|
+
require 'mime/types'
|
12
|
+
|
13
|
+
# No need to do all the fancy library versioning in a subclass.
|
14
|
+
require 'vips'
|
15
|
+
|
16
|
+
|
17
|
+
module Cooltrainer
|
18
|
+
module DistorteD
|
19
|
+
class Text < Image
|
20
|
+
|
21
|
+
include Cooltrainer::DistorteD::Tech::Pango;
|
22
|
+
|
23
|
+
|
24
|
+
MEDIA_TYPE = 'text'.freeze
|
25
|
+
|
26
|
+
MIME_TYPES = MIME::Types[/^#{self::MEDIA_TYPE}\/(plain|x-nfo)/, :complete => true].to_set
|
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
|
+
}
|
43
|
+
|
44
|
+
# Track supported fonts by codepage.
|
45
|
+
# Avoid renaming these from the original archives / websites.
|
46
|
+
# Try not to go nuts here bloating the size of our Gem for a
|
47
|
+
# very niche feature, but I want to ensure good coverage too.
|
48
|
+
#
|
49
|
+
# Treat codepage 8859 documents as codepage 1252 to avoid breaking smart-
|
50
|
+
# quotes and other printable chars in 1252 that are control chars in 8859.
|
51
|
+
# https://encoding.spec.whatwg.org/#names-and-labels
|
52
|
+
#
|
53
|
+
# Numeric key for UTF-8 is codepage 65001 like Win32:
|
54
|
+
# https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
|
55
|
+
FONT_FILENAME = {
|
56
|
+
:anonpro => 'Anonymous Pro.ttf'.freeze,
|
57
|
+
:anonpro_b => 'Anonymous Pro B.ttf'.freeze,
|
58
|
+
:anonpro_bi => 'Anonymous Pro BI.ttf'.freeze,
|
59
|
+
:anonpro_i => 'Anonymous Pro I.ttf'.freeze,
|
60
|
+
:lessperfectdosvga => 'LessPerfectDOSVGA.ttf'.freeze,
|
61
|
+
:moreperfectdisvga => 'MorePerfectDOSVGA.ttf'.freeze,
|
62
|
+
:perfectdosvgawin => 'Perfect DOS VGA 437 Win.ttf'.freeze,
|
63
|
+
:mona => 'mona.ttf'.freeze,
|
64
|
+
:perfectdosvga => 'Perfect DOS VGA 437.ttf'.freeze,
|
65
|
+
:profont => 'ProFontWindows.ttf'.freeze,
|
66
|
+
:profont_b => 'ProFontWindows-Bold.ttf'.freeze,
|
67
|
+
}
|
68
|
+
# Certain fonts are more suitable for certain codepages,
|
69
|
+
# so track each codepage's available fonts…
|
70
|
+
CODEPAGE_FONT = {
|
71
|
+
65001 => [
|
72
|
+
:anonpro,
|
73
|
+
:anonpro_b,
|
74
|
+
:anonpro_bi,
|
75
|
+
:anonpro_i,
|
76
|
+
],
|
77
|
+
1252 => [
|
78
|
+
:lessperfectdosvga,
|
79
|
+
:moreperfectdosvga,
|
80
|
+
:perfectdosvgawin,
|
81
|
+
],
|
82
|
+
932 => [
|
83
|
+
:mona,
|
84
|
+
],
|
85
|
+
850 => [
|
86
|
+
:profont,
|
87
|
+
:profont_b,
|
88
|
+
],
|
89
|
+
437 => [
|
90
|
+
:perfectdosvga,
|
91
|
+
],
|
92
|
+
}
|
93
|
+
# …as well as the inverse, the numeric codepage for each font:
|
94
|
+
FONT_CODEPAGE = CODEPAGE_FONT.reduce(Hash.new([])) { |memo, (key, values)|
|
95
|
+
values.each { |value| memo[value] = key }
|
96
|
+
memo
|
97
|
+
}
|
98
|
+
|
99
|
+
|
100
|
+
# Using a numeric key for things for simplicity.
|
101
|
+
# TODO: Replace this with Ruby's built-in Encoding class after I have
|
102
|
+
# a better idea what I want to do.
|
103
|
+
def codepage
|
104
|
+
case @encoding
|
105
|
+
when 'UTF-8'.freeze then 65001
|
106
|
+
when 'Shift_JIS'.freeze then 932
|
107
|
+
when 'IBM437'.freeze then 437
|
108
|
+
else 1252
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return a Pango Markup escaped version of the document.
|
113
|
+
def to_pango
|
114
|
+
# https://developer.gnome.org/glib/stable/glib-Simple-XML-Subset-Parser.html#g-markup-escape-text
|
115
|
+
escaped = @contents.map{ |c|
|
116
|
+
g_markup_escape_char(c)
|
117
|
+
}
|
118
|
+
if spacing == :monospace
|
119
|
+
"<tt>" << escaped << "</tt>"
|
120
|
+
else
|
121
|
+
escaped
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def initialize(src, encoding: nil, font: nil, spacing: nil, dpi: ATTRS_DEFAULT[:dpi])
|
126
|
+
@src = src
|
127
|
+
@liquid_spacing = spacing
|
128
|
+
|
129
|
+
# VIPS makes us provide the text content as a single variable,
|
130
|
+
# so we may as well just one-shot File.read() it into memory.
|
131
|
+
# https://kunststube.net/encoding/
|
132
|
+
contents = File.read(@src)
|
133
|
+
|
134
|
+
# It's not easy or even possible in some cases to tell the "true" codepage
|
135
|
+
# we should use for any given text document, but using character detection
|
136
|
+
# is worth a shot if the user gave us nothing.
|
137
|
+
detected = CharlockHolmes::EncodingDetector.detect(contents)
|
138
|
+
@encoding = (encoding || detected[:encoding] || 'UTF-8'.freeze).to_s
|
139
|
+
@contents = CharlockHolmes::Converter.convert(contents, @encoding, 'UTF-8'.freeze)
|
140
|
+
|
141
|
+
# Set the shorthand symbol key for our chosen font.
|
142
|
+
@font = font&.to_sym || self.singleton_class.const_get(:CODEPAGE_FONT)[codepage].first
|
143
|
+
|
144
|
+
# Load font metadata directly from the file so we don't have to
|
145
|
+
# duplicate it here to feed to Vips/Pango.
|
146
|
+
#
|
147
|
+
# irb(main)> font_meta.name.font_name
|
148
|
+
# => ["Perfect DOS VGA 437", "\x00P\x00e\x00r\x00f\x00e\x00c\x00t\x00 \x00D\x00O\x00S\x00 \x00V\x00G\x00A\x00 \x004\x003\x007"]
|
149
|
+
# irb(main)> font_meta.name.font_family
|
150
|
+
# => ["Perfect DOS VGA 437", "\x00P\x00e\x00r\x00f\x00e\x00c\x00t\x00 \x00D\x00O\x00S\x00 \x00V\x00G\x00A\x00 \x004\x003\x007"]
|
151
|
+
# irb(main)> font_meta.name.font_subfamily
|
152
|
+
# => ["Regular", "\x00R\x00e\x00g\x00u\x00l\x00a\x00r"]
|
153
|
+
# irb(main)> font_meta.name.postscript_name
|
154
|
+
# => "PerfectDOSVGA437"
|
155
|
+
# irb(main)> font_meta.line_gap
|
156
|
+
# => 0
|
157
|
+
@font_meta = TTFunk::File.open(font_path)
|
158
|
+
|
159
|
+
# https://libvips.github.io/libvips/API/current/libvips-create.html#vips-text
|
160
|
+
@image = Vips::Image.text(
|
161
|
+
# This string must be well-escaped Pango Markup:
|
162
|
+
# https://developer.gnome.org/pango/stable/pango-Markup.html
|
163
|
+
# However the official function for escaping text is
|
164
|
+
# not implemented in Ruby GLib, so we have to do it ourselves.
|
165
|
+
to_pango,
|
166
|
+
**{
|
167
|
+
# String absolute path to TTF
|
168
|
+
:fontfile => font_path,
|
169
|
+
# It's not enough to just specify the TTF path;
|
170
|
+
# we must also specify a font family, subfamily, and size.
|
171
|
+
:font => "#{font_name} 16",
|
172
|
+
# Space between lines (in Points).
|
173
|
+
:spacing => @font_meta.line_gap,
|
174
|
+
:justify => true, # Requires libvips 8.8
|
175
|
+
:dpi => dpi.to_i,
|
176
|
+
},
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
protected
|
181
|
+
|
182
|
+
# Return the String absolute path to the TTF file
|
183
|
+
def font_path
|
184
|
+
File.join(
|
185
|
+
File.dirname(__FILE__), # distorted
|
186
|
+
'..'.freeze, # lib
|
187
|
+
'..'.freeze, # DistorteD-Ruby
|
188
|
+
'font'.freeze,
|
189
|
+
font_codepage,
|
190
|
+
font_filename,
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Returns the numeric representation of the codepage
|
195
|
+
# covered by our font.
|
196
|
+
def font_codepage
|
197
|
+
self.singleton_class.const_get(:FONT_CODEPAGE)&.dig(@font).to_s
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns the basename (with file extension) of our font.
|
201
|
+
def font_filename
|
202
|
+
self.singleton_class.const_get(:FONT_FILENAME)&.dig(@font)
|
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)
|
237
|
+
end
|
238
|
+
|
239
|
+
end # Text
|
240
|
+
end # DistorteD
|
241
|
+
end # Cooltrainer
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#
|
2
|
+
# `.........-` `:/:::://.`
|
3
|
+
# `+/``+ssss:``-/:` `:o+y. `::::-----``
|
4
|
+
# -+- :hmNNmdhs- `o++` `-:--/ -/... ./shhyoy. +mmmmds+:--...`
|
5
|
+
# .//``odmNmmd+++` :+/h:-------------:+. ./...-..........---/--:.------// -.--------//o//o+/+- :hhdmmmmmh/`-/-`
|
6
|
+
# `:+- :ymNmmyy:...-:` /: ./////////////- ::/:/- `++++++++` -y /:.`.////+- /////- `/+++++++. :- .::+sydmNm+ `/+.
|
7
|
+
# -+:` /syyyo:--:+oso` /: ---:::::::syys. -yoy+s` :mmmmmmmd- .h` ``:osshhyyy` /oshhs` -:::::::. :: `-..-/dmo` `/yo:
|
8
|
+
# `/o:--------:+syyy++``+s+++++++++/``:h+os``oshdos``odhhhddyy-`.h.`.ssyhmmdmoh/``o+mmho``:++++++++++y+``----::``-oyss.
|
9
|
+
# +sssssssyyyyyhdmmsyo-+/----------.`.yooy-`-hodss/``---------.`.h-`.sohdhsymhsy.`:+yNmy/`.-----------:++ooooooo+sss+`
|
10
|
+
# `+dmmmdddddmmNNddhsyosooooooooooooooysoysssyymssssssssssssssssshyssyoh:` .mhsyssssodddyooooooooooooooysoyyyyyyyyy/
|
11
|
+
# /mNmdddhmdmmdy+-+mdhhhhhhhhhhhhhhhmhhyhhhmdmmdhhhhhhhhhhhhhhhdhhhsm+ ydddhhhhd-hdmdddddddddddddddyhdhdmmNms.
|
12
|
+
# .---.......` -yhNmmhddhhhdhmhmhddmNNmyyy+dmNdydhyyyhddhmmmmNmhh. -hyhNNNh: .mNmdmdddhhhdddmmyssssssso:
|
13
|
+
# `/oooooooooooooo:`-+o+/. `+oo+o++++ooo+oooooo- `-+oo/` :oooooooooooooo/
|
14
|
+
#
|
15
|
+
|
16
|
+
module Cooltrainer
|
17
|
+
module DistorteD
|
18
|
+
VERSION = '0.5.7'.freeze
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Tell the user to install the shared library if it's missing.
|
3
|
+
begin
|
4
|
+
require 'gst'
|
5
|
+
rescue LoadError => le
|
6
|
+
raise unless le.message =~ /libgst/
|
7
|
+
|
8
|
+
# Multiple OS help
|
9
|
+
help = <<~INSTALL
|
10
|
+
|
11
|
+
Please install the GStreamer library for your system.
|
12
|
+
INSTALL
|
13
|
+
|
14
|
+
# Re-raise with install message
|
15
|
+
raise $!, "#{help}\n#{$!}", $!.backtrace
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'set'
|
19
|
+
|
20
|
+
require 'mime/types'
|
21
|
+
|
22
|
+
module Cooltrainer
|
23
|
+
module DistorteD
|
24
|
+
class Video
|
25
|
+
|
26
|
+
MEDIA_TYPE = 'video'.freeze
|
27
|
+
MIME_TYPES = MIME::Types[/^#{MEDIA_TYPE}/, :complete => true].to_set
|
28
|
+
|
29
|
+
# Attributes for our <video>.
|
30
|
+
# Automatically enabled as attrs for DD Liquid Tag.
|
31
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#Attributes
|
32
|
+
ATTRS = Set[:caption]
|
33
|
+
|
34
|
+
# Defaults for HTML Element attributes.
|
35
|
+
# Not every attr has to be listed here.
|
36
|
+
# Many need no default and just won't render.
|
37
|
+
ATTRS_DEFAULT = {}
|
38
|
+
ATTRS_VALUES = {}
|
39
|
+
|
40
|
+
attr_accessor :dest
|
41
|
+
|
42
|
+
|
43
|
+
def initialize(src, dest, basename)
|
44
|
+
@src = src
|
45
|
+
@dest = dest
|
46
|
+
@basename = basename
|
47
|
+
end
|
48
|
+
|
49
|
+
def rotate(angle=nil)
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def clean
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
def generate
|
58
|
+
self.generate_hls
|
59
|
+
begin
|
60
|
+
self.generate_dash
|
61
|
+
rescue Gst::ParseError::NoSuchElement
|
62
|
+
# This is going away once the new dashsink2 lands in Gst so :effort:
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def generate_dash
|
67
|
+
orig_dest = @dest
|
68
|
+
orig_path = @src
|
69
|
+
|
70
|
+
FileUtils.mkdir_p(File.dirname(orig_dest))
|
71
|
+
|
72
|
+
hls_dest = File.join(File.dirname(orig_dest), @basename + '.dash')
|
73
|
+
FileUtils.mkdir_p(hls_dest)
|
74
|
+
Jekyll.logger.debug(@tag_name, "Re-muxing #{orig_path} to #{hls_dest}.")
|
75
|
+
|
76
|
+
#FileUtils.rm(orig_dest) if File.exist?(orig_dest)
|
77
|
+
if not File.file?(orig_dest)
|
78
|
+
FileUtils.cp(orig_path, orig_dest)
|
79
|
+
end
|
80
|
+
|
81
|
+
# https://gstreamer.freedesktop.org/documentation/tools/gst-launch.html?gi-language=c#pipeline-description
|
82
|
+
# TODO: Convert this from parse_launch() pipeline notation to Element objects
|
83
|
+
# TODO: Get source video duration/resolution/etc and use it to compute a
|
84
|
+
# value for `target-duration`.
|
85
|
+
# TODO: Also support urldecodebin for remote media.
|
86
|
+
pipeline, error = Gst.parse_launch("filesrc name=src ! decodebin name=demux ! videoconvert ! vaapih264enc ! queue2 ! h264parse ! queue2 ! mux.video dashsink name=mux max-files=0 playlist-length=0 target-duration=2 demux. ! audioconvert ! voaacenc ! queue2 ! mux.audio")
|
87
|
+
|
88
|
+
if pipeline.nil?
|
89
|
+
Jekyll.logger.error(@tag_name, "Parse error: #{error.message}")
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
|
93
|
+
filesrc = pipeline.get_by_name('src')
|
94
|
+
filesrc.location = orig_path
|
95
|
+
|
96
|
+
hls_playlist = "#{hls_dest}/#{@basename}.m3u8"
|
97
|
+
hls = pipeline.get_by_name('mux')
|
98
|
+
hls.location = "#{hls_dest}/#{@basename}%05d.ts"
|
99
|
+
hls.playlist_location = hls_playlist
|
100
|
+
|
101
|
+
# TODO: config option for absolute vs relative segment URIs in the playlist.
|
102
|
+
#hls.playlist_root = @url
|
103
|
+
|
104
|
+
# TODO: dashsink support once there is a stable GStreamer release including it:
|
105
|
+
# https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/merge_requests/704
|
106
|
+
|
107
|
+
pipeline.play
|
108
|
+
|
109
|
+
# Play until End Of Stream
|
110
|
+
event_loop(pipeline)
|
111
|
+
|
112
|
+
pipeline.stop
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
def generate_hls
|
117
|
+
orig_dest = @dest
|
118
|
+
orig_path = @src
|
119
|
+
|
120
|
+
FileUtils.mkdir_p(File.dirname(orig_dest))
|
121
|
+
|
122
|
+
hls_dest = File.join(File.dirname(orig_dest), @basename + '.hls')
|
123
|
+
FileUtils.mkdir_p(hls_dest)
|
124
|
+
Jekyll.logger.debug(@tag_name, "Re-muxing #{orig_path} to #{hls_dest}.")
|
125
|
+
|
126
|
+
#FileUtils.rm(orig_dest) if File.exist?(orig_dest)
|
127
|
+
if not File.file?(orig_dest)
|
128
|
+
FileUtils.cp(orig_path, orig_dest)
|
129
|
+
end
|
130
|
+
|
131
|
+
# https://gstreamer.freedesktop.org/documentation/tools/gst-launch.html?gi-language=c#pipeline-description
|
132
|
+
# TODO: Convert this from parse_launch() pipeline notation to Element objects
|
133
|
+
# TODO: Get source video duration/resolution/etc and use it to compute a
|
134
|
+
# value for `target-duration`.
|
135
|
+
# TODO: Also support urldecodebin for remote media.
|
136
|
+
pipeline, error = Gst.parse_launch("filesrc name=src ! decodebin name=demux ! videoconvert ! vaapih264enc ! queue2 ! h264parse ! queue2 ! mux.video hlssink2 name=mux max-files=0 playlist-length=0 target-duration=2 demux. ! audioconvert ! voaacenc ! queue2 ! mux.audio")
|
137
|
+
|
138
|
+
if pipeline.nil?
|
139
|
+
Jekyll.logger.error(@tag_name, "Parse error: #{error.message}")
|
140
|
+
return false
|
141
|
+
end
|
142
|
+
|
143
|
+
filesrc = pipeline.get_by_name('src')
|
144
|
+
filesrc.location = orig_path
|
145
|
+
|
146
|
+
hls_playlist = "#{hls_dest}/#{@basename}.m3u8"
|
147
|
+
hls = pipeline.get_by_name('mux')
|
148
|
+
hls.location = "#{hls_dest}/#{@basename}%05d.ts"
|
149
|
+
hls.playlist_location = hls_playlist
|
150
|
+
|
151
|
+
# TODO: config option for absolute vs relative segment URIs in the playlist.
|
152
|
+
#hls.playlist_root = @url
|
153
|
+
|
154
|
+
# TODO: dashsink support once there is a stable GStreamer release including it:
|
155
|
+
# https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/merge_requests/704
|
156
|
+
|
157
|
+
pipeline.play
|
158
|
+
|
159
|
+
# Play until End Of Stream
|
160
|
+
event_loop(pipeline)
|
161
|
+
|
162
|
+
pipeline.stop
|
163
|
+
|
164
|
+
# HACK HACK HACK: Replace X-ALLOW-CACHE line in playlist with YES.
|
165
|
+
# This property does not seem to be exposed to the outside of hlssink:
|
166
|
+
# https://cgit.freedesktop.org/gstreamer/gst-plugins-bad/tree/ext/hls/gsthlssink.c
|
167
|
+
text = File.read(hls_playlist)
|
168
|
+
File.write(hls_playlist, text.gsub(/^#EXT-X-ALLOW-CACHE:NO$/, '#EXT-X-ALLOW-CACHE:YES'))
|
169
|
+
end
|
170
|
+
|
171
|
+
def event_loop(pipeline)
|
172
|
+
running = true
|
173
|
+
bus = pipeline.bus
|
174
|
+
|
175
|
+
while running
|
176
|
+
message = bus.poll(Gst::MessageType::ANY, -1)
|
177
|
+
|
178
|
+
case message.type
|
179
|
+
when Gst::MessageType::EOS
|
180
|
+
running = false
|
181
|
+
when Gst::MessageType::WARNING
|
182
|
+
warning, _debug = message.parse_warning
|
183
|
+
Jekyll.logger.warning(@tag_name, warning)
|
184
|
+
when Gst::MessageType::ERROR
|
185
|
+
error, _debug = message.parse_error
|
186
|
+
Jekyll.logger.error(@tag_name, error)
|
187
|
+
running = false
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end # Image
|
192
|
+
end # DistorteD
|
193
|
+
end # Cooltrainer
|