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.
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