distorted-floor 0.7.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 +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
|