distorted-floor 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +32 -0
- data/bin/distorted-floor +16 -0
- data/bin/repl +14 -0
- data/bin/setup +8 -0
- 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-floor/checking_you_out.rb +78 -0
- data/lib/distorted-floor/click_again.rb +406 -0
- data/lib/distorted-floor/element_of_media/change.rb +114 -0
- data/lib/distorted-floor/element_of_media/compound.rb +120 -0
- data/lib/distorted-floor/element_of_media.rb +2 -0
- data/lib/distorted-floor/error_code.rb +55 -0
- data/lib/distorted-floor/floor.rb +17 -0
- data/lib/distorted-floor/invoker.rb +100 -0
- data/lib/distorted-floor/media_molecule/font.rb +200 -0
- data/lib/distorted-floor/media_molecule/image.rb +33 -0
- data/lib/distorted-floor/media_molecule/pdf.rb +45 -0
- data/lib/distorted-floor/media_molecule/svg.rb +46 -0
- data/lib/distorted-floor/media_molecule/text.rb +247 -0
- data/lib/distorted-floor/media_molecule/video.rb +21 -0
- data/lib/distorted-floor/media_molecule.rb +58 -0
- data/lib/distorted-floor/modular_technology/gstreamer.rb +175 -0
- data/lib/distorted-floor/modular_technology/pango.rb +90 -0
- data/lib/distorted-floor/modular_technology/ttfunk.rb +48 -0
- data/lib/distorted-floor/modular_technology/vips/ffi.rb +66 -0
- data/lib/distorted-floor/modular_technology/vips/load.rb +174 -0
- data/lib/distorted-floor/modular_technology/vips/operatio$.rb +268 -0
- data/lib/distorted-floor/modular_technology/vips/save.rb +135 -0
- data/lib/distorted-floor/modular_technology/vips.rb +17 -0
- data/lib/distorted-floor/monkey_business/encoding.rb +374 -0
- data/lib/distorted-floor/monkey_business/hash.rb +18 -0
- data/lib/distorted-floor/monkey_business/set.rb +15 -0
- data/lib/distorted-floor/monkey_business/string.rb +6 -0
- data/lib/distorted-floor.rb +2 -0
- metadata +215 -0
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'ffi-icu' # Text file charset detection
|
4
|
+
|
5
|
+
require 'distorted-floor/monkey_business/encoding'
|
6
|
+
require 'distorted-floor/monkey_business/string' # String#map
|
7
|
+
require 'distorted-floor/modular_technology/pango'
|
8
|
+
require 'distorted-floor/modular_technology/ttfunk'
|
9
|
+
require 'distorted-floor/modular_technology/vips/save'
|
10
|
+
|
11
|
+
require 'distorted-floor/checking_you_out'
|
12
|
+
using ::DistorteD::CHECKING::YOU::OUT
|
13
|
+
|
14
|
+
|
15
|
+
module Cooltrainer; end
|
16
|
+
module Cooltrainer::DistorteD; end
|
17
|
+
module Cooltrainer::DistorteD::Molecule; end
|
18
|
+
module Cooltrainer::DistorteD::Molecule::Text
|
19
|
+
|
20
|
+
#TODO: Generate separate images per-size to stop text being blurry from resizing.
|
21
|
+
|
22
|
+
include Cooltrainer::DistorteD::Technology::TTFunk
|
23
|
+
include Cooltrainer::DistorteD::Technology::Pango
|
24
|
+
include Cooltrainer::DistorteD::Technology::Vips::Save
|
25
|
+
|
26
|
+
# Track supported fonts by codepage.
|
27
|
+
# Avoid renaming these from the original archives / websites.
|
28
|
+
# Try not to go nuts here bloating the size of our Gem for a
|
29
|
+
# very niche feature, but I want to ensure good coverage too.
|
30
|
+
#
|
31
|
+
# Treat codepage 8859 documents as codepage 1252 to avoid breaking smart-
|
32
|
+
# quotes and other printable chars in 1252 that are control chars in 8859.
|
33
|
+
# https://encoding.spec.whatwg.org/#names-and-labels
|
34
|
+
#
|
35
|
+
# Numeric key for UTF-8 is codepage 65001 like Win32:
|
36
|
+
# https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
|
37
|
+
FONT_FILENAME = {
|
38
|
+
:anonpro => 'Anonymous Pro.ttf'.freeze,
|
39
|
+
:anonpro_b => 'Anonymous Pro B.ttf'.freeze,
|
40
|
+
:anonpro_bi => 'Anonymous Pro BI.ttf'.freeze,
|
41
|
+
:anonpro_i => 'Anonymous Pro I.ttf'.freeze,
|
42
|
+
:lessperfectdosvga => 'LessPerfectDOSVGA.ttf'.freeze,
|
43
|
+
:moreperfectdisvga => 'MorePerfectDOSVGA.ttf'.freeze,
|
44
|
+
:perfectdosvgawin => 'Perfect DOS VGA 437 Win.ttf'.freeze,
|
45
|
+
:mona => 'mona.ttf'.freeze,
|
46
|
+
:perfectdosvga => 'Perfect DOS VGA 437.ttf'.freeze,
|
47
|
+
:profont => 'ProFontWindows.ttf'.freeze,
|
48
|
+
:profont_b => 'ProFontWindows-Bold.ttf'.freeze,
|
49
|
+
}
|
50
|
+
# Certain fonts are more suitable for certain codepages,
|
51
|
+
# so track each codepage's available fonts…
|
52
|
+
CODEPAGE_FONT = {
|
53
|
+
65001 => [
|
54
|
+
:anonpro,
|
55
|
+
:anonpro_b,
|
56
|
+
:anonpro_bi,
|
57
|
+
:anonpro_i,
|
58
|
+
],
|
59
|
+
1252 => [
|
60
|
+
:lessperfectdosvga,
|
61
|
+
:moreperfectdosvga,
|
62
|
+
:perfectdosvgawin,
|
63
|
+
],
|
64
|
+
932 => [
|
65
|
+
:mona,
|
66
|
+
],
|
67
|
+
850 => [
|
68
|
+
:profont,
|
69
|
+
:profont_b,
|
70
|
+
],
|
71
|
+
437 => [
|
72
|
+
:perfectdosvga,
|
73
|
+
],
|
74
|
+
}
|
75
|
+
# TODO: Figure out what to do here. ProFont isn't suitable for many (most?) Encodings,
|
76
|
+
# but the gem would be way way too big if I tried to include coverage for everything.
|
77
|
+
# Using system fonts is probably the solution, but I need to be able to get a path to them for VIPS.
|
78
|
+
CODEPAGE_FONT.default = Array[:profont, :profont_b]
|
79
|
+
# …as well as the inverse, the numeric codepage for each font:
|
80
|
+
FONT_CODEPAGE = self::CODEPAGE_FONT.each_with_object(Hash.new([])) { |(key, values), memo|
|
81
|
+
values.each { |value| memo[value] = key }
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
LOWER_WORLD = {
|
86
|
+
::CHECKING::YOU::OUT::from_ietf_media_type('text/plain') => nil,
|
87
|
+
::CHECKING::YOU::OUT::from_ietf_media_type('text/x-nfo') => nil,
|
88
|
+
}.transform_values { |v| Hash[
|
89
|
+
:encoding => Cooltrainer::Compound.new(:encoding, valid: Encoding, blurb: 'Character encoding used in this document. (default: automatically detect)', default: nil),
|
90
|
+
]}
|
91
|
+
OUTER_LIMITS = {
|
92
|
+
::CHECKING::YOU::OUT::from_ietf_media_type('text/plain') => nil,
|
93
|
+
::CHECKING::YOU::OUT::from_ietf_media_type('text/x-nfo') => nil,
|
94
|
+
}.merge(
|
95
|
+
Cooltrainer::DistorteD::Technology::Vips::Save::OUTER_LIMITS.dup.transform_values{ |v| Hash[
|
96
|
+
:spacing => Cooltrainer::Compound.new(:spacing, blurb: 'Document-wide character spacing style.', valid: Set[:monospace, :proportional]),
|
97
|
+
:dpi => Cooltrainer::Compound.new(:dpi, blurb: 'Dots per inch for text rendering.', valid: Integer, default: 144),
|
98
|
+
:font => Cooltrainer::Compound.new(:font, blurb: 'Font to use for text rendering.', valid: self::FONT_FILENAME.keys.to_set),
|
99
|
+
]}
|
100
|
+
)
|
101
|
+
|
102
|
+
self::LOWER_WORLD.keys.each { |t|
|
103
|
+
define_method(t.distorted_file_method) { |dest_root, change|
|
104
|
+
p change.paths(dest_root)
|
105
|
+
copy_file(change.paths(dest_root).first)
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
|
110
|
+
# Return a Pango Markup escaped version of the document.
|
111
|
+
def to_pango
|
112
|
+
# https://developer.gnome.org/glib/stable/glib-Simple-XML-Subset-Parser.html#g-markup-escape-text
|
113
|
+
escaped = text_file_utf8_content.map{ |c|
|
114
|
+
g_markup_escape_char(c)
|
115
|
+
}
|
116
|
+
if font_spacing == :monospace
|
117
|
+
"<tt>" << escaped << "</tt>"
|
118
|
+
else
|
119
|
+
escaped
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
protected
|
124
|
+
|
125
|
+
# Returns a boolean guess of whether our document uses box-drawing characters of a given Encoding.
|
126
|
+
def oobe?(encoding)
|
127
|
+
# Re-interpret our raw source file's bytes as the given Encoding,
|
128
|
+
# then take the codepoints seven at a time and see if any of those
|
129
|
+
# septagrams consist of all box-drawing characters of our given Encoding.
|
130
|
+
text_file_content.force_encoding(encoding).each_codepoint.each_cons(7).map{ |septagram|
|
131
|
+
septagram.uniq.length == 1 and Encoding::OOBE.fetch(encoding, nil)&.include?(septagram.first)
|
132
|
+
}.select(&TrueClass.method(:===)).length >= 1
|
133
|
+
end
|
134
|
+
|
135
|
+
def text_file_content
|
136
|
+
# VIPS makes us provide the text content as a single variable,
|
137
|
+
# so we may as well just one-shot File.read() it into memory.
|
138
|
+
# https://kunststube.net/encoding/
|
139
|
+
@text_file_content ||= File.read(path)
|
140
|
+
end
|
141
|
+
|
142
|
+
def text_file_utf8_content
|
143
|
+
# https://ruby-doc.org/core/Encoding/Converter.html#method-c-new
|
144
|
+
@text_file_utf8_content ||= text_file_encoding == Encoding::UTF_8 ?
|
145
|
+
text_file_content :
|
146
|
+
Encoding::Converter.new(
|
147
|
+
text_file_encoding,
|
148
|
+
Encoding::UTF_8,
|
149
|
+
undef: :replace,
|
150
|
+
invalid: :replace,
|
151
|
+
).convert(text_file_content)
|
152
|
+
end
|
153
|
+
|
154
|
+
def text_file_encoding
|
155
|
+
# It's not easy or even possible in some cases to tell the "true" codepage
|
156
|
+
# we should use for any given text document, but using character detection
|
157
|
+
# is worth a shot if the user gave us nothing.
|
158
|
+
#
|
159
|
+
# FFI-ICU::CharDet returns a Struct, e.g.:
|
160
|
+
# #<struct ICU::CharDet::Detector::Match name="ISO-8859-1", confidence=19, language="en">
|
161
|
+
@text_file_encoding ||= begin
|
162
|
+
Encoding::find(ICU::CharDet.detect(text_file_content).name).yield_self { |detected|
|
163
|
+
# Fix files with ASCII/ANSI art (like NFOs) from being detected as ISO-8859-1
|
164
|
+
# when they should be IBM437 to display properly.
|
165
|
+
[
|
166
|
+
type_mars.include?(::CHECKING::YOU::OUT::from_ietf_media_type('text/x-nfo')), # Only certain souce file types.
|
167
|
+
detected == Encoding::ISO_8859_1, # Only if ICU detects ISO-8859-1.
|
168
|
+
oobe?(Encoding::IBM437), # Does this look like IBM437 based on box-drawing characters?
|
169
|
+
].all? ? Encoding::IBM437 : detected
|
170
|
+
}
|
171
|
+
rescue ArgumentError
|
172
|
+
# Raised by Encoding::find if we give it an unknown Encoding name.
|
173
|
+
Encoding::UTF_8
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def vips_font
|
178
|
+
# Set the shorthand Symbol key for our chosen font.
|
179
|
+
CODEPAGE_FONT[text_file_encoding&.code_page].first
|
180
|
+
end
|
181
|
+
|
182
|
+
def to_vips_image(change)
|
183
|
+
# Load font metadata directly from the file so we don't have to
|
184
|
+
# duplicate it here to feed to Vips/Pango.
|
185
|
+
#
|
186
|
+
# irb(main)> font_meta.name.font_name
|
187
|
+
# => ["Perfect DOS VGA 437", "\x00P\x00e\x00r\x00f\x00e\x00c\x00t\x00 \x00D\x00O\x00S\x00 \x00V\x00G\x00A\x00 \x004\x003\x007"]
|
188
|
+
# irb(main)> font_meta.name.font_family
|
189
|
+
# => ["Perfect DOS VGA 437", "\x00P\x00e\x00r\x00f\x00e\x00c\x00t\x00 \x00D\x00O\x00S\x00 \x00V\x00G\x00A\x00 \x004\x003\x007"]
|
190
|
+
# irb(main)> font_meta.name.font_subfamily
|
191
|
+
# => ["Regular", "\x00R\x00e\x00g\x00u\x00l\x00a\x00r"]
|
192
|
+
# irb(main)> font_meta.name.postscript_name
|
193
|
+
# => "PerfectDOSVGA437"
|
194
|
+
# irb(main)> font_meta.line_gap
|
195
|
+
# => 0
|
196
|
+
|
197
|
+
# It would be gross to pass this through so many methods in this mostly-untouched-since-0.5 code,
|
198
|
+
# so just stick these directly into the instance variables used for memoization.
|
199
|
+
unless change.encoding.nil?
|
200
|
+
# TODO: Turning the String arguments into an Encoding should be a centralized thing
|
201
|
+
# of some sort, probably in Cooltrainer::Compound.
|
202
|
+
@text_file_encoding = change.encoding.is_a?(Encoding) ? change.encoding : Encoding::find(change.encoding)
|
203
|
+
end
|
204
|
+
|
205
|
+
# https://libvips.github.io/libvips/API/current/libvips-create.html#vips-text
|
206
|
+
Vips::Image.text(
|
207
|
+
# This string must be well-escaped Pango Markup:
|
208
|
+
# https://developer.gnome.org/pango/stable/pango-Markup.html
|
209
|
+
# However the official function for escaping text is
|
210
|
+
# not implemented in Ruby GLib, so we have to do it ourselves.
|
211
|
+
to_pango,
|
212
|
+
**{
|
213
|
+
# String absolute path to TTF
|
214
|
+
:fontfile => font_path,
|
215
|
+
# It's not enough to just specify the TTF path;
|
216
|
+
# we must also specify a font family, subfamily, and size.
|
217
|
+
:font => "#{font_name} 16",
|
218
|
+
# Space between lines (in Points).
|
219
|
+
:spacing => to_ttfunk.line_gap,
|
220
|
+
:justify => true, # Requires libvips 8.8
|
221
|
+
:dpi => change.dpi&.to_i,
|
222
|
+
},
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Return the String absolute path to the TTF file
|
227
|
+
def font_path
|
228
|
+
File.join(
|
229
|
+
Cooltrainer::DistorteD::GEM_ROOT, # DistorteD-Floor
|
230
|
+
'font'.freeze,
|
231
|
+
font_codepage.to_s,
|
232
|
+
font_filename,
|
233
|
+
)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns the numeric representation of the codepage
|
237
|
+
# covered by our font.
|
238
|
+
def font_codepage
|
239
|
+
FONT_CODEPAGE.dig(vips_font).to_s
|
240
|
+
end
|
241
|
+
|
242
|
+
# Returns the basename (with file extension) of our font.
|
243
|
+
def font_filename
|
244
|
+
FONT_FILENAME.dig(vips_font)
|
245
|
+
end
|
246
|
+
|
247
|
+
end # Text
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'distorted-floor/monkey_business/set'
|
3
|
+
|
4
|
+
require 'distorted-floor/checking_you_out'
|
5
|
+
using ::DistorteD::CHECKING::YOU::OUT
|
6
|
+
|
7
|
+
require 'distorted-floor/modular_technology/gstreamer'
|
8
|
+
|
9
|
+
|
10
|
+
module Cooltrainer; end
|
11
|
+
module Cooltrainer::DistorteD; end
|
12
|
+
module Cooltrainer::DistorteD::Molecule; end
|
13
|
+
module Cooltrainer::DistorteD::Molecule::Video
|
14
|
+
|
15
|
+
LOWER_WORLD = {
|
16
|
+
::CHECKING::YOU::OUT::from_ietf_media_type('video/mp4') => nil,
|
17
|
+
}
|
18
|
+
|
19
|
+
include Cooltrainer::DistorteD::Technology::GStreamer
|
20
|
+
|
21
|
+
end # Video
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Cooltrainer; end
|
4
|
+
module Cooltrainer::DistorteD
|
5
|
+
|
6
|
+
# Discover DistorteD MediaMolecules bundled with this Gem
|
7
|
+
# TODO: and any installed as separate Gems.
|
8
|
+
@@loaded_molecules rescue begin
|
9
|
+
Dir[File.join(__dir__, 'media_molecule', '*.rb')].each { |molecule| require molecule }
|
10
|
+
@@loaded_molecules = true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns a Set[Module] of our discovered MediaMolecules.
|
14
|
+
def self.media_molecules
|
15
|
+
Cooltrainer::DistorteD::Molecule.constants.map { |molecule|
|
16
|
+
Cooltrainer::DistorteD::Molecule::const_get(molecule)
|
17
|
+
}.to_set
|
18
|
+
end
|
19
|
+
|
20
|
+
# Reusable IMPLANTATION Hash key, since instances of the same Struct subclass are equal:
|
21
|
+
# irb> Pair = Struct.new(:uno, :dos)
|
22
|
+
# irb> lol = Pair.new(:a, 1)
|
23
|
+
# irb> rofl = Pair.new(:a, 1)
|
24
|
+
# irb> lol === rofl
|
25
|
+
# => true
|
26
|
+
KEY = Struct.new(:molecule, :constant, :inherit) do
|
27
|
+
# Descend into ancestor Modules by default.
|
28
|
+
def initialize(molecule, constant, inherit = true); super(molecule, constant, inherit); end
|
29
|
+
def inspect; "#{molecule}#{'∫'.freeze if inherit}::#{constant}"; end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check and create attribute-memoizing Hash whose default_proc will fetch
|
33
|
+
# and collate the data for a given KEY.
|
34
|
+
@@implantation rescue begin
|
35
|
+
@@implantation = Hash.new { |piles, key|
|
36
|
+
# Optionally limit search to top-level Module like `:const_defined?` with `inherit`
|
37
|
+
piles[key] = Set[key.molecule].merge(key.inherit ? key.molecule.ancestors : []).each_with_object(Hash.new) { |mod, pile|
|
38
|
+
mod.const_get(key.constant).each { |target, elements|
|
39
|
+
pile.update(target => elements) { |_key, existing, new| existing.merge(new) }
|
40
|
+
} rescue nil
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Generic entry-point for attribute-collation of a given constant
|
46
|
+
# over a given Molecule or Enumerable of Molecules.
|
47
|
+
def self.IMPLANTATION(constant, corpus = self.media_molecules)
|
48
|
+
(corpus.is_a?(Enumerable) ? corpus : Array[corpus]).map { |molecule|
|
49
|
+
KEY.new(molecule, constant)
|
50
|
+
}.each_with_object(Hash[]) { |key, piles|
|
51
|
+
# Hash#slice doesn't trigger the default_proc, so access each directly.
|
52
|
+
piles.store(key, @@implantation[key])
|
53
|
+
}.yield_self { |piles|
|
54
|
+
# Return just the data when we were given a single Molecule to search.
|
55
|
+
corpus.is_a?(Enumerable) ? piles : piles.shift[1]
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'distorted-floor/checking_you_out'
|
4
|
+
using ::DistorteD::CHECKING::YOU::OUT
|
5
|
+
|
6
|
+
require('xross-the-xoul/version') unless defined?(::XROSS::THE::Version::TripleCounter)
|
7
|
+
GST_MINIMUM_VER = ::XROSS::THE::Version::TripleCounter.new(1, 18, 0)
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'gst'
|
11
|
+
GST_AVAILABLE_VER = ::XROSS::THE::Version::TripleCounter.new(*(Gst.version))
|
12
|
+
unless GST_AVAILABLE_VER >= GST_MINIMUM_VER
|
13
|
+
raise LoadError.new(
|
14
|
+
"DistorteD needs GStreamer #{GST_MINIMUM_VER}, but the available version is '#{Gst.version_string}'"
|
15
|
+
)
|
16
|
+
end
|
17
|
+
rescue LoadError => le
|
18
|
+
raise unless le.message =~ /libgst/
|
19
|
+
|
20
|
+
# Multiple OS help
|
21
|
+
help = <<~INSTALL
|
22
|
+
|
23
|
+
Please install the GStreamer library for your system, version #{GST_MINIMUM_VER} or later.
|
24
|
+
INSTALL
|
25
|
+
|
26
|
+
# Re-raise with install message
|
27
|
+
raise $!, "#{help}\n#{$!}", $!.backtrace
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
module Cooltrainer; end
|
32
|
+
module Cooltrainer::DistorteD; end
|
33
|
+
module Cooltrainer::DistorteD::Technology; end
|
34
|
+
module Cooltrainer::DistorteD::Technology::GStreamer
|
35
|
+
|
36
|
+
OUTER_LIMITS = Set[
|
37
|
+
'application/dash+xml',
|
38
|
+
'application/vnd.apple.mpegurl',
|
39
|
+
'video/mp4',
|
40
|
+
].map(&::CHECKING::YOU::OUT::method(:from_ietf_media_type))
|
41
|
+
|
42
|
+
|
43
|
+
def write_video_mp4(dest_root, change)
|
44
|
+
copy_file(change.paths(dest_root).first)
|
45
|
+
end
|
46
|
+
|
47
|
+
def write_application_dash_xml(dest, *a, **k)
|
48
|
+
begin
|
49
|
+
segment_dest = File.join(File.dirname(dest), "#{basename}.dash", '/')
|
50
|
+
#segment_dest = segment_dest.sub("#{@base}/", '')
|
51
|
+
FileUtils.mkdir_p(segment_dest)
|
52
|
+
Jekyll.logger.debug(@tag_name, "Re-muxing #{path} to #{segment_dest}")
|
53
|
+
|
54
|
+
# https://gstreamer.freedesktop.org/documentation/tools/gst-launch.html?gi-language=c#pipeline-description
|
55
|
+
# TODO: Convert this from parse_launch() pipeline notation to Element objects
|
56
|
+
# TODO: Get source video duration/resolution/etc and use it to compute a
|
57
|
+
# value for `target-duration`.
|
58
|
+
# TODO: Also support urldecodebin for remote media.
|
59
|
+
pipeline, error = Gst.parse_launch("dashsink name=mux filesrc name=src ! decodebin name=demux ! audioconvert ! avenc_aac ! mux.audio_0 demux. ! videoconvert ! x264enc ! mux.video_0")
|
60
|
+
|
61
|
+
if pipeline.nil?
|
62
|
+
Jekyll.logger.error(@tag_name, "Parse error: #{error.message}")
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
|
66
|
+
filesrc = pipeline.get_by_name('src')
|
67
|
+
filesrc.location = path
|
68
|
+
|
69
|
+
mux = pipeline.get_by_name('mux')
|
70
|
+
mux.mpd_filename = File.basename(dest)
|
71
|
+
mux.target_duration = 3
|
72
|
+
#mux.segment_tpl_path = "#{segment_dest}/#{basename}%05d.mp4"
|
73
|
+
mux.mpd_root_path = segment_dest
|
74
|
+
Jekyll.logger.warn('MPD ROOT PATH', mux.get_property('mpd-root-path'))
|
75
|
+
|
76
|
+
# typedef enum
|
77
|
+
# {
|
78
|
+
# GST_DASH_SINK_MUXER_TS = 0,
|
79
|
+
# GST_DASH_SINK_MUXER_MP4 = 1,
|
80
|
+
# } GstDashSinkMuxerType;
|
81
|
+
mux.muxer = 1
|
82
|
+
|
83
|
+
pipeline.play
|
84
|
+
|
85
|
+
# Play until End Of Stream
|
86
|
+
event_loop(pipeline)
|
87
|
+
|
88
|
+
pipeline.stop
|
89
|
+
|
90
|
+
rescue Gst::ParseError::NoSuchElement
|
91
|
+
raise
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def write_application_vnd_apple_mpegurl(dest, *a, **k)
|
96
|
+
begin
|
97
|
+
orig_dest = dest
|
98
|
+
orig_path = path
|
99
|
+
|
100
|
+
FileUtils.mkdir_p(File.dirname(orig_dest))
|
101
|
+
|
102
|
+
hls_dest = File.join(File.dirname(orig_dest), basename + '.hls')
|
103
|
+
FileUtils.mkdir_p(hls_dest)
|
104
|
+
Jekyll.logger.debug(@tag_name, "Re-muxing #{orig_path} to #{hls_dest}.")
|
105
|
+
|
106
|
+
#FileUtils.rm(orig_dest) if File.exist?(orig_dest)
|
107
|
+
if not File.file?(orig_dest)
|
108
|
+
FileUtils.cp(orig_path, orig_dest)
|
109
|
+
end
|
110
|
+
|
111
|
+
# https://gstreamer.freedesktop.org/documentation/tools/gst-launch.html?gi-language=c#pipeline-description
|
112
|
+
# TODO: Convert this from parse_launch() pipeline notation to Element objects
|
113
|
+
# TODO: Get source video duration/resolution/etc and use it to compute a
|
114
|
+
# value for `target-duration`.
|
115
|
+
# TODO: Also support urldecodebin for remote media.
|
116
|
+
pipeline, error = Gst.parse_launch("filesrc name=src ! decodebin name=demux ! videoconvert ! x264enc ! queue2 ! h264parse ! queue2 ! mux.video hlssink2 name=mux max-files=0 playlist-length=0 target-duration=2 demux. ! audioconvert ! faac ! queue2 ! mux.audio")
|
117
|
+
|
118
|
+
if pipeline.nil?
|
119
|
+
Jekyll.logger.error(@tag_name, "Parse error: #{error.message}")
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
|
123
|
+
filesrc = pipeline.get_by_name('src')
|
124
|
+
filesrc.location = orig_path
|
125
|
+
|
126
|
+
hls_playlist = "#{hls_dest}/#{basename}.m3u8"
|
127
|
+
hls = pipeline.get_by_name('mux')
|
128
|
+
hls.location = "#{hls_dest}/#{basename}%05d.ts"
|
129
|
+
hls.playlist_location = hls_playlist
|
130
|
+
|
131
|
+
# TODO: config option for absolute vs relative segment URIs in the playlist.
|
132
|
+
#hls.playlist_root = @url
|
133
|
+
|
134
|
+
# TODO: dashsink support once there is a stable GStreamer release including it:
|
135
|
+
# https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/merge_requests/704
|
136
|
+
|
137
|
+
pipeline.play
|
138
|
+
|
139
|
+
# Play until End Of Stream
|
140
|
+
event_loop(pipeline)
|
141
|
+
|
142
|
+
pipeline.stop
|
143
|
+
|
144
|
+
# HACK HACK HACK: Replace X-ALLOW-CACHE line in playlist with YES.
|
145
|
+
# This property does not seem to be exposed to the outside of hlssink:
|
146
|
+
# https://cgit.freedesktop.org/gstreamer/gst-plugins-bad/tree/ext/hls/gsthlssink.c
|
147
|
+
text = File.read(hls_playlist)
|
148
|
+
File.write(hls_playlist, text.gsub(/^#EXT-X-ALLOW-CACHE:NO$/, '#EXT-X-ALLOW-CACHE:YES'))
|
149
|
+
rescue Gst::ParseError::NoSuchElement
|
150
|
+
raise
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def event_loop(pipeline)
|
155
|
+
running = true
|
156
|
+
bus = pipeline.bus
|
157
|
+
|
158
|
+
while running
|
159
|
+
message = bus.poll(Gst::MessageType::ANY, -1)
|
160
|
+
|
161
|
+
case message.type
|
162
|
+
when Gst::MessageType::EOS
|
163
|
+
running = false
|
164
|
+
when Gst::MessageType::WARNING
|
165
|
+
warning, _debug = message.parse_warning
|
166
|
+
Jekyll.logger.warning(@tag_name, warning)
|
167
|
+
when Gst::MessageType::ERROR
|
168
|
+
error, _debug = message.parse_error
|
169
|
+
Jekyll.logger.error(@tag_name, error)
|
170
|
+
running = false
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Cooltrainer
|
2
|
+
module DistorteD
|
3
|
+
module Technology
|
4
|
+
module Pango
|
5
|
+
|
6
|
+
# Escape text as necessary for Pango Markup, which is what Vips::Image.text()
|
7
|
+
# expects for its argv. This code should be in GLib but is unimplemented in Ruby's:
|
8
|
+
#
|
9
|
+
# https://ruby-gnome2.osdn.jp/hiki.cgi?Gtk%3A%3ALabel#Markup+%28styled+text%29
|
10
|
+
# "The markup passed to Gtk::Label#set_markup() must be valid; for example,
|
11
|
+
# literal </>/& characters must be escaped as <, >, and &.
|
12
|
+
# If you pass text obtained from the user, file, or a network to
|
13
|
+
# Gtk::Label#set_markup(), you'll want to escape it
|
14
|
+
# with GLib::Markup.escape_text?(not implemented yet)."
|
15
|
+
#
|
16
|
+
# Base my own implementation on the original C version found in gmarkup:
|
17
|
+
# https://gitlab.gnome.org/GNOME/glib/-/blob/master/glib/gmarkup.c
|
18
|
+
def g_markup_escape_text(text)
|
19
|
+
text.map{ |c| g_markup_escape_char(c) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns a Pango-escaped Carriage Return.
|
23
|
+
# Use this for linebreaking Pango Markup output.
|
24
|
+
def cr
|
25
|
+
g_markup_escape_char(0x0D)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns a Pango-escapped Line Feed.
|
29
|
+
# This isn't used/needed for anything with Pango
|
30
|
+
# but it felt weird to include CR and not LF lmao
|
31
|
+
def lf
|
32
|
+
g_markup_escape_char(0x0A)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a Pango'escaped CRLF pair.
|
36
|
+
# Also not needed for anything.
|
37
|
+
def crlf
|
38
|
+
cr << lf
|
39
|
+
end
|
40
|
+
|
41
|
+
# "Modified UTF-8" uses a normally-illegal byte sequence
|
42
|
+
# to encode the NULL character so 0x00 can exclusively
|
43
|
+
# be a string terminator.
|
44
|
+
def overlong_null
|
45
|
+
[0xC0, 0x80].pack('C*').force_encoding('UTF-8')
|
46
|
+
end
|
47
|
+
|
48
|
+
# The char-by-char actual function used by g_markup_escape_text
|
49
|
+
def g_markup_escape_char(c)
|
50
|
+
# I think a fully-working version of this function would
|
51
|
+
# be as simple as `sprintf('&#x%x;', c.ord)` ALL THE THINGS,
|
52
|
+
# but I want to copy the structure of the C implementation
|
53
|
+
# as closely as possible, which means using the named escape
|
54
|
+
# sequences for common characters and separating the
|
55
|
+
# Latin-1 Supplement range from the other
|
56
|
+
# the Unicode control characters (> 0x7f) even though three's no
|
57
|
+
# need to in Ruby.
|
58
|
+
case c.ord
|
59
|
+
when '&'.ord
|
60
|
+
'&'
|
61
|
+
when '<'.ord
|
62
|
+
'<'
|
63
|
+
when '>'.ord
|
64
|
+
'>'
|
65
|
+
when '\''.ord
|
66
|
+
'''
|
67
|
+
when '"'.ord
|
68
|
+
'"'
|
69
|
+
when 0x1..0x8, 0xb..0xc, 0xe..0x1f, 0x7f
|
70
|
+
sprintf('&#x%x;', c.ord)
|
71
|
+
when 0x80..0x84, 0x86..0x9f
|
72
|
+
# The original C implementation separates this range
|
73
|
+
# from the above range due to its need to handle the
|
74
|
+
# UTF control character bytes with gunichar:
|
75
|
+
# https://wiki.tcl-lang.org/page/UTF%2D8+bit+by+bit
|
76
|
+
# https://www.fileformat.info/info/unicode/utf8.htm
|
77
|
+
# Ruby has already done this for us here :)
|
78
|
+
sprintf('&#x%x;', c.ord)
|
79
|
+
when 0x0 # what's this…?
|
80
|
+
# Avoid a `ArgumentError: string contains null byte`
|
81
|
+
# by not printing one :)
|
82
|
+
else
|
83
|
+
c
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end # Pango
|
88
|
+
end # Tech
|
89
|
+
end # DistorteD
|
90
|
+
end # Cooltrainer
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'ttfunk'
|
2
|
+
|
3
|
+
module Cooltrainer; end
|
4
|
+
module Cooltrainer::DistorteD; end
|
5
|
+
module Cooltrainer::DistorteD::Technology; end
|
6
|
+
module Cooltrainer::DistorteD::Technology::TTFunk
|
7
|
+
|
8
|
+
def to_ttfunk
|
9
|
+
# TODO: Check that src exists, because TTFunk won't and will just
|
10
|
+
# give us an unusable object instead.
|
11
|
+
@ttfunk_file ||= TTFunk::File.open(font_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a boolean for whether or not this font is monospaced.
|
15
|
+
# true == monospace
|
16
|
+
# false == proportional
|
17
|
+
def font_spacing
|
18
|
+
# Monospace fonts will (read: should) have the same width
|
19
|
+
# for every glyph, so we can tell a monospace font by
|
20
|
+
# checking if a deduplicated widths table has size == 1:
|
21
|
+
# irb(main)> font.horizontal_metrics.widths.count
|
22
|
+
# => 256
|
23
|
+
# irb(main)> font.horizontal_metrics.widths.uniq.compact.length
|
24
|
+
# => 1
|
25
|
+
to_ttfunk.horizontal_metrics.widths.uniq.compact.length == 1 ? :monospace : :proportional
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the Family and Subfamily as one string suitable for libvips
|
29
|
+
def font_name
|
30
|
+
"#{to_ttfunk.name.font_family.first.encode('UTF-8')} #{to_ttfunk.name.font_subfamily.first.encode('UTF-8')}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the Pango-Markup-encoded UTF-8 String version + revision of the font
|
34
|
+
def font_version
|
35
|
+
g_markup_escape_text(to_ttfunk.name&.version&.first&.encode('UTF-8').to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the Pango-Markup-encoded UTF-8 String font file description
|
39
|
+
def font_description
|
40
|
+
g_markup_escape_text(to_ttfunk.name&.description&.first&.encode('UTF-8').to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the Pango-Markup-encoded UTF-8 String copyright information of the font
|
44
|
+
def font_copyright
|
45
|
+
g_markup_escape_text(to_ttfunk.name&.copyright&.first&.encode('UTF-8').to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|