distorted-floor 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +32 -0
  4. data/bin/distorted-floor +16 -0
  5. data/bin/repl +14 -0
  6. data/bin/setup +8 -0
  7. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Less_Perfect_DOS_VGA.png +0 -0
  8. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/More_Perfect_DOS_VGA.png +0 -0
  9. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Perfect_DOS_VGA.png +0 -0
  10. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/less_more_perfect_dos_vga_437.html +52 -0
  11. data/font/1252/LICENSE/PerfectDOSVGA437/font-comment.php@file=perfect_dos_vga_437.html +5 -0
  12. data/font/1252/LessPerfectDOSVGA.ttf +0 -0
  13. data/font/1252/MorePerfectDOSVGA.ttf +0 -0
  14. data/font/1252/Perfect DOS VGA 437 Win.ttf +0 -0
  15. data/font/437/Perfect DOS VGA 437.ttf +0 -0
  16. data/font/437/dos437.txt +72 -0
  17. data/font/65001/Anonymous Pro B.ttf +0 -0
  18. data/font/65001/Anonymous Pro BI.ttf +0 -0
  19. data/font/65001/Anonymous Pro I.ttf +0 -0
  20. data/font/65001/Anonymous Pro.ttf +0 -0
  21. data/font/65001/LICENSE/AnonymousPro/FONTLOG.txt +45 -0
  22. data/font/65001/LICENSE/AnonymousPro/OFL-FAQ.txt +235 -0
  23. data/font/65001/LICENSE/AnonymousPro/OFL.txt +94 -0
  24. data/font/65001/LICENSE/AnonymousPro/README.txt +55 -0
  25. data/font/850/ProFont-Bold-01/LICENSE +22 -0
  26. data/font/850/ProFont-Bold-01/readme.txt +28 -0
  27. data/font/850/ProFontWindows-Bold.ttf +0 -0
  28. data/font/850/ProFontWindows.ttf +0 -0
  29. data/font/850/Profont/LICENSE +22 -0
  30. data/font/850/Profont/readme.txt +31 -0
  31. data/font/932/LICENSE/README-ttf.txt +213 -0
  32. data/font/932/mona.ttf +0 -0
  33. data/lib/distorted-floor/checking_you_out.rb +78 -0
  34. data/lib/distorted-floor/click_again.rb +406 -0
  35. data/lib/distorted-floor/element_of_media/change.rb +114 -0
  36. data/lib/distorted-floor/element_of_media/compound.rb +120 -0
  37. data/lib/distorted-floor/element_of_media.rb +2 -0
  38. data/lib/distorted-floor/error_code.rb +55 -0
  39. data/lib/distorted-floor/floor.rb +17 -0
  40. data/lib/distorted-floor/invoker.rb +100 -0
  41. data/lib/distorted-floor/media_molecule/font.rb +200 -0
  42. data/lib/distorted-floor/media_molecule/image.rb +33 -0
  43. data/lib/distorted-floor/media_molecule/pdf.rb +45 -0
  44. data/lib/distorted-floor/media_molecule/svg.rb +46 -0
  45. data/lib/distorted-floor/media_molecule/text.rb +247 -0
  46. data/lib/distorted-floor/media_molecule/video.rb +21 -0
  47. data/lib/distorted-floor/media_molecule.rb +58 -0
  48. data/lib/distorted-floor/modular_technology/gstreamer.rb +175 -0
  49. data/lib/distorted-floor/modular_technology/pango.rb +90 -0
  50. data/lib/distorted-floor/modular_technology/ttfunk.rb +48 -0
  51. data/lib/distorted-floor/modular_technology/vips/ffi.rb +66 -0
  52. data/lib/distorted-floor/modular_technology/vips/load.rb +174 -0
  53. data/lib/distorted-floor/modular_technology/vips/operatio$.rb +268 -0
  54. data/lib/distorted-floor/modular_technology/vips/save.rb +135 -0
  55. data/lib/distorted-floor/modular_technology/vips.rb +17 -0
  56. data/lib/distorted-floor/monkey_business/encoding.rb +374 -0
  57. data/lib/distorted-floor/monkey_business/hash.rb +18 -0
  58. data/lib/distorted-floor/monkey_business/set.rb +15 -0
  59. data/lib/distorted-floor/monkey_business/string.rb +6 -0
  60. data/lib/distorted-floor.rb +2 -0
  61. 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 &lt;, &gt;, and &amp;.
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
+ '&amp;'
61
+ when '<'.ord
62
+ '&lt;'
63
+ when '>'.ord
64
+ '&gt;'
65
+ when '\''.ord
66
+ '&apos;'
67
+ when '"'.ord
68
+ '&quot;'
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