distorted 0.6.0 → 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 +4 -4
- data/README.md +2 -2
- data/bin/console +14 -0
- data/bin/distorted +6 -0
- data/bin/setup +8 -0
- data/lib/distorted.rb +2 -0
- data/lib/distorted/checking_you_out.rb +116 -13
- data/lib/distorted/{types → checking_you_out}/README +0 -0
- data/lib/distorted/checking_you_out/application.yaml +33 -0
- data/lib/distorted/{types → checking_you_out}/font.yaml +0 -0
- data/lib/distorted/checking_you_out/image.yaml +108 -0
- data/lib/distorted/click_again.rb +333 -0
- data/lib/distorted/element_of_media.rb +2 -0
- data/lib/distorted/element_of_media/change.rb +119 -0
- data/lib/distorted/element_of_media/compound.rb +120 -0
- data/lib/distorted/floor.rb +17 -0
- data/lib/distorted/invoker.rb +97 -0
- data/lib/distorted/media_molecule.rb +58 -0
- data/lib/distorted/media_molecule/font.rb +195 -0
- data/lib/distorted/media_molecule/image.rb +33 -0
- data/lib/distorted/media_molecule/pdf.rb +44 -0
- data/lib/distorted/media_molecule/svg.rb +45 -0
- data/lib/distorted/media_molecule/text.rb +203 -0
- data/lib/distorted/media_molecule/video.rb +18 -0
- data/lib/distorted/modular_technology/gstreamer.rb +174 -0
- data/lib/distorted/modular_technology/vips.rb +4 -4
- data/lib/distorted/modular_technology/vips/foreign.rb +489 -0
- data/lib/distorted/modular_technology/vips/load.rb +133 -0
- data/lib/distorted/modular_technology/{vips_save.rb → vips/save.rb} +23 -34
- data/lib/distorted/monkey_business/encoding.rb +317 -0
- data/lib/distorted/monkey_business/hash.rb +0 -15
- data/lib/distorted/{modular_technology/triple_counter.rb → triple_counter.rb} +8 -1
- data/lib/distorted/version.rb +16 -16
- metadata +59 -46
- data/lib/distorted/injection_of_love.rb +0 -247
- data/lib/distorted/modular_technology/vips_load.rb +0 -77
- data/lib/distorted/molecule/C18H27NO3.rb +0 -10
- data/lib/distorted/molecule/font.rb +0 -198
- data/lib/distorted/molecule/image.rb +0 -36
- data/lib/distorted/molecule/pdf.rb +0 -119
- data/lib/distorted/molecule/svg.rb +0 -60
- data/lib/distorted/molecule/text.rb +0 -225
- data/lib/distorted/molecule/video.rb +0 -195
- data/lib/distorted/monkey_business/mnemoniq.rb +0 -8
- data/lib/distorted/types/application.yaml +0 -8
@@ -0,0 +1,119 @@
|
|
1
|
+
module Cooltrainer
|
2
|
+
|
3
|
+
# Fun Ruby Fact™: `false` is always object_id 0
|
4
|
+
# https://skorks.com/2009/09/true-false-and-nil-objects-in-ruby/
|
5
|
+
# irb(main):650:0> true.object_id
|
6
|
+
# => 20
|
7
|
+
# irb(main):651:0> false.object_id
|
8
|
+
# => 0
|
9
|
+
BOOLEAN_VALUES = Set[false, true]
|
10
|
+
|
11
|
+
|
12
|
+
# Struct to encapsulate all the data needed to perform one (1) MIME::Type transformation
|
13
|
+
# of a source media file into any supported MIME::Type, possibly even the same type as input.
|
14
|
+
Change = Struct.new(:type, :src, :basename, :molecule, :tag, :breaks, :atoms, keyword_init: true) do
|
15
|
+
|
16
|
+
# Customize the destination filename and other values before doing the normal Struct setup.
|
17
|
+
def initialize(type, src: nil, molecule: nil, tag: nil, breaks: Array.new, **atoms)
|
18
|
+
# `name` might have a leading slash if referenced as an absolute path as the Tag.
|
19
|
+
basename = File.basename(src, '.*'.freeze).reverse.chomp('/'.freeze).reverse
|
20
|
+
|
21
|
+
# Set the &default_proc on the kwarg-glob Hash instead of making a new Hash,
|
22
|
+
atoms.default_proc = lambda { |h,k| h[k] = Cooltrainer::Atom.new }
|
23
|
+
atoms.transform_values {
|
24
|
+
# We might get Atoms already instantiated, but do it for any that aren't.
|
25
|
+
# We won't have a default value for them in that case.
|
26
|
+
|v| v.is_a?(Cooltrainer::Atom) ? atom : Cooltrainer::Atom.new(v, nil)
|
27
|
+
}.each_key { |k|
|
28
|
+
# Define accessors for context-specific :atoms keys/values that aren't normal Struct members.
|
29
|
+
self.singleton_class.define_method(k) { self[:atoms]&.fetch(k, nil)&.get }
|
30
|
+
self.singleton_class.define_method("#{k}=".to_sym) { |v| self[:atoms][k] = v }
|
31
|
+
}
|
32
|
+
|
33
|
+
# And now back to your regularly-scheduled Struct
|
34
|
+
super(type: type, src: src, basename: basename, molecule: molecule, tag: tag, breaks: breaks, atoms: atoms)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the Change Type's :preferred_extension as a String with leading dot (.)
|
38
|
+
def extname
|
39
|
+
@extname ||= begin
|
40
|
+
dot = '.'.freeze unless type.preferred_extension.nil? || type.preferred_extension&.empty?
|
41
|
+
"#{dot}#{type.preferred_extension}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an Array[String] of filenames this Change should generate,
|
46
|
+
# one 'full'/'original' plus any limit-breaks,
|
47
|
+
# e.g. ["DistorteD.png", "DistorteD-333.png", "DistorteD-555.png", "DistorteD-888.png", "DistorteD-1111.png"]
|
48
|
+
def names
|
49
|
+
Array[''.freeze].concat(self[:breaks]).map { |b|
|
50
|
+
filetag = (b.nil? || b&.to_s.empty?) ? ''.freeze : '-'.concat(b.to_s)
|
51
|
+
"#{self[:basename]}#{"-#{self.tag}" unless self.tag.nil?}#{filetag}#{extname}"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a String describing the :names but rolled into one,
|
56
|
+
# e.g. "IIDX-turntable-(400|800|1500).png"
|
57
|
+
def name
|
58
|
+
tags = self[:breaks].length > 1 ? "-(#{self[:breaks].join('|'.freeze)})" : ''.freeze
|
59
|
+
"#{self.basename}#{tags}#{self.extname}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns an Array[String] of all absolute destination paths this Change should generate,
|
63
|
+
# given a root destination directory.
|
64
|
+
def paths(dest_root = ''.freeze) # Empty String will expand to current working directory
|
65
|
+
output_dir = self[:atoms]&.fetch(:dir, ''.freeze)
|
66
|
+
return self.names.map { |n| File.join(File.expand_path(dest_root), output_dir, n) }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a String absolute destination path for only one limit-break.
|
70
|
+
def path(dest_root, break_value)
|
71
|
+
output_dir = self[:atoms]&.fetch(:dir, ''.freeze)
|
72
|
+
return File.join(File.expand_path(dest_root), output_dir, "#{self.basename}#{"-#{self.tag}" unless self.tag.nil?}-#{break_value}#{self.extname}")
|
73
|
+
end
|
74
|
+
|
75
|
+
# A generic version of Struct#to_hash was rejected with good reason,
|
76
|
+
# but I'm going to use it here because I want the implicit Struct-to-Hash
|
77
|
+
# conversion to let me destructure these Structs with a double-splat:
|
78
|
+
# https://bugs.ruby-lang.org/issues/4862
|
79
|
+
#
|
80
|
+
# Defining this method causes Ruby 2.7 to emit a "Using the last argument as keyword parameters is deprecated" warning
|
81
|
+
# if this Struct is passed to a method as the final positional argument! Ruby 2.7 will actually do the
|
82
|
+
# conversion when calling the method in that scenario, causing incorrect behavior to methods expecting Struct.
|
83
|
+
# This is why DD-Floor's `:write` and DD-Jekyll's `:render_to_output_buffer` pass an empty kwargs Hash.
|
84
|
+
# https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
|
85
|
+
def to_hash # Implicit
|
86
|
+
Hash[self.members.reject{|m| m == :atoms}.zip(self.values.reject{|v| v.is_a?(Hash)})].merge(self[:atoms].transform_values(&:get))
|
87
|
+
end
|
88
|
+
# Struct#to_h does exist in stdlib, but redefine its behavior to match our `:to_hash`.
|
89
|
+
def to_h # Explicit
|
90
|
+
Hash[self.members.reject{|m| m == :atoms}.zip(self.values.reject{|v| v.is_a?(Hash)})].merge(self[:atoms].transform_values(&:get))
|
91
|
+
end
|
92
|
+
def dig(*keys); self.to_hash.dig(*keys); end
|
93
|
+
|
94
|
+
# Support setting Atoms that were not defined at instantiation.
|
95
|
+
def method_missing(meth, *a, **k, &b)
|
96
|
+
# Are we a setter?
|
97
|
+
if meth.to_s[-1] == '='.freeze
|
98
|
+
# Set the :value of an existing Atom Struct
|
99
|
+
self[:atoms][meth.to_s.chomp('='.freeze).to_sym].value = a.first
|
100
|
+
else
|
101
|
+
self[:atoms]&.fetch(meth, nil)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end # Struct Change
|
106
|
+
|
107
|
+
|
108
|
+
# Struct to wrap just the user and default values for a Compound or just for freeform usage.
|
109
|
+
Atom = Struct.new(:value, :default) do
|
110
|
+
# Return a value if set, otherwise a default. Both can be `nil`.
|
111
|
+
def get; self.value || self.default; end
|
112
|
+
# Override these default Struct methods with ones that reference our :get
|
113
|
+
def to_s; self.get.to_s; end # Explicit
|
114
|
+
def to_str; self.get.to_s; end # Implicit
|
115
|
+
# Send any unknown message through to a value/default.
|
116
|
+
def method_missing(meth, *a, **k, &b); self.get.send(meth, *a, **k, &b); end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Cooltrainer
|
5
|
+
|
6
|
+
# This is defined in writing in a comment in optparse.rb's RDoc,
|
7
|
+
# but I can't seem to find anywhere it's available directly in code,
|
8
|
+
# so I am going to build my own.
|
9
|
+
# Maybe I am missing something obvious, in which case I should use that
|
10
|
+
# and get rid of this :)
|
11
|
+
# Based on https://ruby-doc.org/stdlib/libdoc/optparse/rdoc/OptionParser.html#class-OptionParser-label-Type+Coercion
|
12
|
+
OPTIONPARSER_COERSIONS = [
|
13
|
+
Date,
|
14
|
+
DateTime,
|
15
|
+
Time,
|
16
|
+
URI,
|
17
|
+
#Shellwords, # Stock in optparse, but under autoload. I don't want it.
|
18
|
+
String,
|
19
|
+
Integer,
|
20
|
+
Float,
|
21
|
+
Numeric,
|
22
|
+
TrueClass,
|
23
|
+
FalseClass,
|
24
|
+
Array,
|
25
|
+
Regexp,
|
26
|
+
].concat(OptionParser::Acceptables::constants)
|
27
|
+
|
28
|
+
|
29
|
+
# Struct to wrap a MediaMolecule option/attribute datum.
|
30
|
+
Compound = Struct.new(:isotopes, :molecule, :valid, :default, :blurb, keyword_init: true) do
|
31
|
+
|
32
|
+
# Massage the data then call `super` to set the members/values and create the accessors.
|
33
|
+
def initialize(isotopes, molecule: nil, valid: nil, default: nil, blurb: nil)
|
34
|
+
super(
|
35
|
+
# The first argument defines the aliases for a single option and may be just a Symbol
|
36
|
+
# or may be an Enumerable[Symbol] in which case all items after the first are aliases for the first.
|
37
|
+
isotopes: isotopes.is_a?(Enumerable) ? isotopes.to_a : Array[isotopes],
|
38
|
+
# Hint the MediaMolecule that should execute this Change.
|
39
|
+
molecule: molecule,
|
40
|
+
# Valid values for this option may be expressed as a Class a value must be an instance of,
|
41
|
+
# a Regexp a String value must match, an Enumerable that a valid value must be in,
|
42
|
+
# a Range a Float/Integer must be within, a special Boolean Set, etc etc.
|
43
|
+
valid: case valid
|
44
|
+
when Set then valid.to_a
|
45
|
+
else valid
|
46
|
+
end,
|
47
|
+
# Optional default value to use when unset.
|
48
|
+
default: default,
|
49
|
+
# String description of this option's effect.
|
50
|
+
blurb: blurb,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# The first isotope is the """real""" option name. Any others are aliases for it.
|
55
|
+
def element; self.isotopes&.first; end
|
56
|
+
def to_s; self.element.to_s; end
|
57
|
+
|
58
|
+
# Returns a longform String representation of one option.
|
59
|
+
def inspect
|
60
|
+
# Intentionally not including the blurb here since they are pretty long and messy.
|
61
|
+
"#{self.isotopes.length > 1 ? self.isotopes : self.element}: #{"#{self.valid} " if self.valid}#{"(#{self.default})" if self.default}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns an Array of properly-formatted OptionParser::Switch strings for this Compound.
|
65
|
+
def to_options
|
66
|
+
# @isotopes is a Hash[Symbol] => Compound, allowing for Compound aliasing
|
67
|
+
# to multiple Hash keys, e.g. libvips' `:Q` and `:quality` are two Hash keys
|
68
|
+
# referencing the same Compound object.
|
69
|
+
self.isotopes.each_with_object(Array[]) { |aka, commands|
|
70
|
+
# Every Switch has at least one leading dash, and longer ones have two,
|
71
|
+
# e.g. `-Q` vs `--quality`.
|
72
|
+
command = "-"
|
73
|
+
if aka.length > 1
|
74
|
+
command << '-'.freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
# Compounds that take a boolean should format their Switch string
|
78
|
+
# as `--[no]-whatever` (including the brackets!) instead of taking
|
79
|
+
# any kind of boolean-ish argument like true/false/yes/no.
|
80
|
+
#
|
81
|
+
# TODO: There seems to be a bug with Ruby optparse and multiple of these
|
82
|
+
# "--[no]-whatever"-style Switches where only the final Switch will display,
|
83
|
+
# so disable this for now in favor of separate --whatever/--no-whatever.
|
84
|
+
# I have a very basic standalone repro case that fails, so it's not just DD.
|
85
|
+
#
|
86
|
+
#if @valid == BOOLEAN_VALUES or @valid == BOOLEAN_VALUES.to_a
|
87
|
+
# command << '[no]-'.freeze
|
88
|
+
#end
|
89
|
+
|
90
|
+
# Add the alias to form the command.
|
91
|
+
command << aka.to_s
|
92
|
+
|
93
|
+
# Format the valid values and/or default value and stick it on the end.
|
94
|
+
# https://ruby-doc.org/stdlib/libdoc/optparse/rdoc/OptionParser.html#class-OptionParser-label-Type+Coercion
|
95
|
+
if self.valid.is_a?(Range)
|
96
|
+
command << " [#{self.valid.to_s}]"
|
97
|
+
elsif self.valid == BOOLEAN_VALUES or self.valid == BOOLEAN_VALUES.to_a
|
98
|
+
# Intentional no-op
|
99
|
+
elsif self.valid.is_a?(Enumerable)
|
100
|
+
command << " [#{self.valid.join(', '.freeze)}]"
|
101
|
+
elsif not default.nil?
|
102
|
+
command << " [#{self.default}]"
|
103
|
+
else
|
104
|
+
command << " [#{self.element.upcase}]"
|
105
|
+
end
|
106
|
+
|
107
|
+
commands << command
|
108
|
+
|
109
|
+
# HACK around issue with multiple "--[no]-whatever"-style long arguments.
|
110
|
+
# See above note and the commented-out implementation I'd like to use
|
111
|
+
# instead of this. Remove this iff I can figure out what's wrong there.
|
112
|
+
if self.valid == BOOLEAN_VALUES or self.valid == BOOLEAN_VALUES.to_a
|
113
|
+
commands << "--no-#{aka}"
|
114
|
+
end
|
115
|
+
}
|
116
|
+
end # to_options
|
117
|
+
|
118
|
+
end # Compound
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
require 'distorted/monkey_business/set'
|
4
|
+
|
5
|
+
require 'distorted/invoker'
|
6
|
+
require 'distorted/click_again'
|
7
|
+
require 'distorted/checking_you_out'
|
8
|
+
|
9
|
+
|
10
|
+
module Cooltrainer; end
|
11
|
+
module Cooltrainer::DistorteD; end
|
12
|
+
|
13
|
+
class Cooltrainer::DistorteD::Floor
|
14
|
+
def initialize(src, dest, type: nil)
|
15
|
+
#TODO: Library-use entry-point
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
# Our custom Exceptions
|
3
|
+
require 'distorted/error_code'
|
4
|
+
|
5
|
+
# MIME::Typer
|
6
|
+
require 'distorted/checking_you_out'
|
7
|
+
require 'distorted/media_molecule'
|
8
|
+
|
9
|
+
# Set.to_hash
|
10
|
+
require 'distorted/monkey_business/set'
|
11
|
+
require 'set'
|
12
|
+
|
13
|
+
|
14
|
+
module Cooltrainer::DistorteD::Invoker
|
15
|
+
# Returns a Hash[MIME::Type] => Hash[MediaMolecule] => Hash[param_alias] => Compound
|
16
|
+
def lower_world
|
17
|
+
Cooltrainer::DistorteD::IMPLANTATION(:LOWER_WORLD).each_with_object(
|
18
|
+
Hash.new { |pile, type| pile[type] = Hash[] }
|
19
|
+
) { |(key, types), pile|
|
20
|
+
types.each { |type, elements| pile.update(type => {key.molecule => elements}) { |k,o,n| o.merge(n) }}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a Hash[MediaMolecule] => Hash[MIME::Type] => Hash[param_alias] => Compound
|
25
|
+
def outer_limits(all: false)
|
26
|
+
Cooltrainer::DistorteD::IMPLANTATION(
|
27
|
+
:OUTER_LIMITS,
|
28
|
+
(all || type_mars.empty?) ? Cooltrainer::DistorteD::media_molecules : type_mars.each_with_object(Set[]) { |type, molecules|
|
29
|
+
molecules.merge(lower_world[type].keys)
|
30
|
+
},
|
31
|
+
).each_with_object(Hash.new { |pile, type| pile[type] = Hash[] }) { |(key, types), pile|
|
32
|
+
types.each { |type, elements| pile.update(key.molecule => {type => elements}) { |k,o,n| o.merge(n) }}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Filename without the dot-and-extension.
|
37
|
+
def basename
|
38
|
+
File.basename(@name, '.*')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a Set of MIME::Types common to the source file and our supported MediaMolecules.
|
42
|
+
# Each of these Molecules will be plugged to the current instance.
|
43
|
+
def type_mars
|
44
|
+
@type_mars ||= CHECKING::YOU::OUT(path, so_deep: true) & lower_world.keys.to_set
|
45
|
+
raise MediaTypeNotImplementedError.new(@name) if @type_mars.empty?
|
46
|
+
@type_mars
|
47
|
+
end
|
48
|
+
|
49
|
+
# MediaMolecule file-type plugger.
|
50
|
+
# Any call to a MIME::Type's distorted_method will end up here unless
|
51
|
+
# the Molecule that defines it has been `prepend`ed to our instance.
|
52
|
+
def method_missing(meth, *args, **kwargs, &block)
|
53
|
+
# Only consider method names with our prefixes.
|
54
|
+
if MIME::Type::DISTORTED_METHOD_PREFIXES.values.map(&:to_s).include?(meth.to_s.split(MIME::Type::SUB_TYPE_SEPARATORS)[0])
|
55
|
+
# TODO: Might need to handle cases here where the Set[Molecule]
|
56
|
+
# exists but none of them defined our method.
|
57
|
+
unless self.singleton_class.instance_variable_get(:@media_molecules)
|
58
|
+
unless outer_limits.empty?
|
59
|
+
self.singleton_class.instance_variable_set(
|
60
|
+
:@media_molecules,
|
61
|
+
outer_limits.keys.each_with_object(Set[]) { |molecule, molecules|
|
62
|
+
self.singleton_class.prepend(molecule)
|
63
|
+
molecules.add(molecule)
|
64
|
+
}
|
65
|
+
)
|
66
|
+
# `return` to ensure we don't fall through to #method_missing:super
|
67
|
+
# if we are going to do any work, otherwise a NoMethodError will
|
68
|
+
# still be raised despite the distorted_method :sends suceeding.
|
69
|
+
#
|
70
|
+
# Use :__send__ in case a Molecule defines a `:send` method.
|
71
|
+
# https://ruby-doc.org/core/Object.html#method-i-send
|
72
|
+
return self.send(meth, *args, **kwargs, &block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# …and I still haven't found it! — What I'm looking for, that is.
|
77
|
+
# https://www.youtube.com/watch?v=xqse3vYcnaU
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
# Make sure :respond_to? works for yet-unplugged distorted_methods.
|
82
|
+
# http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/
|
83
|
+
def respond_to_missing?(meth, *a)
|
84
|
+
# We can tell if a method looks like one of ours if it has at least 3 (maybe more!)
|
85
|
+
# underscore-separated components with a valid prefix as the first component
|
86
|
+
# and the media-type and sub-type as the rest, e.g.
|
87
|
+
#
|
88
|
+
# irb(main)> 'to_application_pdf'.split('_')
|
89
|
+
# => ["to", "application", "pdf"]
|
90
|
+
#
|
91
|
+
# irb(main)> CHECKING::YOU::OUT('.docx').first.distorted_file_method.to_s.split('_')
|
92
|
+
# => ["write", "application", "vnd", "openxmlformats", "officedocument", "wordprocessingml", "document"]
|
93
|
+
parts = meth.to_s.split(MIME::Type::SUB_TYPE_SEPARATORS)
|
94
|
+
MIME::Type::DISTORTED_METHOD_PREFIXES.values.map(&:to_s).include?(parts[0]) && parts.length > 2 || super(meth, *a)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -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,195 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
# Font metadata extraction
|
4
|
+
require 'ttfunk'
|
5
|
+
|
6
|
+
require 'distorted/modular_technology/pango'
|
7
|
+
require 'distorted/modular_technology/ttfunk'
|
8
|
+
require 'distorted/modular_technology/vips/save'
|
9
|
+
require 'distorted/checking_you_out'
|
10
|
+
|
11
|
+
|
12
|
+
module Cooltrainer; end
|
13
|
+
module Cooltrainer::DistorteD; end
|
14
|
+
module Cooltrainer::DistorteD::Molecule; end
|
15
|
+
module Cooltrainer::DistorteD::Molecule::Font
|
16
|
+
|
17
|
+
|
18
|
+
# TODO: Test OTF, OTB, and others.
|
19
|
+
# NOTE: Traditional bitmap fonts won't be supported due to Pango 1.44
|
20
|
+
# and later switching to Harfbuzz from Freetype:
|
21
|
+
# https://gitlab.gnome.org/GNOME/pango/-/issues/386
|
22
|
+
# https://blogs.gnome.org/mclasen/2019/05/25/pango-future-directions/
|
23
|
+
LOWER_WORLD = CHECKING::YOU::IN(/^font\/ttf/).to_hash
|
24
|
+
OUTER_LIMITS = CHECKING::YOU::IN(/^font\/ttf/).to_hash
|
25
|
+
|
26
|
+
ATTRIBUTES = Set[
|
27
|
+
:alt,
|
28
|
+
]
|
29
|
+
ATTRIBUTES_VALUES = {
|
30
|
+
}
|
31
|
+
ATTRIBUTES_DEFAULT = {
|
32
|
+
}
|
33
|
+
|
34
|
+
|
35
|
+
# Maybe T0DO: Process output with TTFunk instead of only using it
|
36
|
+
# to generate images and metadata.
|
37
|
+
self::LOWER_WORLD.keys.each { |t|
|
38
|
+
define_method(t.distorted_file_method) { |dest_root, change|
|
39
|
+
copy_file(change.paths(dest_root).first)
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
include Cooltrainer::DistorteD::Technology::TTFunk
|
44
|
+
include Cooltrainer::DistorteD::Technology::Pango
|
45
|
+
include Cooltrainer::DistorteD::Technology::Vips::Save
|
46
|
+
|
47
|
+
|
48
|
+
# irb(main):089:0> chars.take(5)
|
49
|
+
# => [[1, 255], [2, 1], [3, 2], [4, 3], [5, 4]]
|
50
|
+
# irb(main):090:0> chars.values.take(5)
|
51
|
+
# => [255, 1, 2, 3, 4]
|
52
|
+
# irb(main):091:0> chars.values.map(&:chr).take(5)
|
53
|
+
# => ["\xFF", "\x01", "\x02", "\x03", "\x04"]
|
54
|
+
def to_pango
|
55
|
+
output = '' << cr << '<span>' << cr
|
56
|
+
|
57
|
+
output << "<span size='35387'> #{font_name}</span>" << cr << cr
|
58
|
+
|
59
|
+
output << "<span size='24576'> #{font_description}</span>" << cr
|
60
|
+
output << "<span size='24576'> #{font_copyright}</span>" << cr
|
61
|
+
output << "<span size='24576'> #{font_version}</span>" << cr << cr
|
62
|
+
|
63
|
+
# Print a preview String in using the loaded font. Or don't.
|
64
|
+
if abstract(:title)
|
65
|
+
output << cr << cr << "<span size='24576' foreground='grey'> #{g_markup_escape_text(abstract(:title))}</span>" << cr << cr << cr
|
66
|
+
end
|
67
|
+
|
68
|
+
# /!\ MANDATORY READING /!\
|
69
|
+
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html
|
70
|
+
#
|
71
|
+
# "The 'cmap' table maps character codes to glyph indices.
|
72
|
+
# The choice of encoding for a particular font is dependent upon the conventions
|
73
|
+
# used by the intended platform. A font intended to run on multiple platforms
|
74
|
+
# with different encoding conventions will require multiple encoding tables.
|
75
|
+
# As a result, the 'cmap' table may contain multiple subtables,
|
76
|
+
# one for each supported encoding scheme."
|
77
|
+
#
|
78
|
+
# Cmap#unicode is a convenient shortcut to sorting the subtables
|
79
|
+
# and removing any unusable ones:
|
80
|
+
# https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap.rb
|
81
|
+
#
|
82
|
+
# irb(main):174:0> font_meta.cmap.tables.count
|
83
|
+
# => 3
|
84
|
+
# irb(main):175:0> font_meta.cmap.unicode.count
|
85
|
+
# => 2
|
86
|
+
to_ttfunk.cmap.tables.each do |table|
|
87
|
+
next if !table.unicode?
|
88
|
+
# Each subtable's `code_map` is a Hash map of character codes (the Hash keys)
|
89
|
+
# to the glyph IDs from the original font (the Hash's values).
|
90
|
+
#
|
91
|
+
# Subtable::encode takes:
|
92
|
+
# - a Hash mapping character codes to original font glyph IDs.
|
93
|
+
# - the desired output encoding — Set[:mac_roman, :unicode, :unicode_ucs4]
|
94
|
+
# https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap/subtable.rb
|
95
|
+
# …and returns a Hash with keys:
|
96
|
+
# - :charmap — Hash mapping the characters in the input charmap
|
97
|
+
# to a another hash containing both the `:old`
|
98
|
+
# and `:new` glyph ids for each character code.
|
99
|
+
# - :subtable — String encoded subtable for the given encoding.
|
100
|
+
encoded = TTFunk::Table::Cmap::Subtable::encode(table&.code_map, :unicode).dig(:charmap)
|
101
|
+
|
102
|
+
output << "<span size='49152'>"
|
103
|
+
|
104
|
+
i = 0
|
105
|
+
encoded.each_pair { |c, (old, new)|
|
106
|
+
|
107
|
+
begin
|
108
|
+
if glyph = to_ttfunk.glyph_outlines.for(c)
|
109
|
+
# Add a space on either side of the character so they aren't
|
110
|
+
# all smooshed up against each other and unreadable.
|
111
|
+
output << ' ' << g_markup_escape_char(c) << ' '
|
112
|
+
if i >= 15
|
113
|
+
output << cr
|
114
|
+
i = 0
|
115
|
+
else
|
116
|
+
i = i + 1
|
117
|
+
end
|
118
|
+
else
|
119
|
+
end
|
120
|
+
rescue NoMethodError => nme
|
121
|
+
# TTFunk's `glyph_outlines.for()` will raise this if we call it
|
122
|
+
# for a codepoint that does not exist in the font, which we will
|
123
|
+
# not do because we are enumerating the codepoints in the font,
|
124
|
+
# but we should still handle the possibility.
|
125
|
+
# irb(main):060:0> font.glyph_outlines.for(555555)
|
126
|
+
#
|
127
|
+
# Traceback (most recent call last):
|
128
|
+
# 6: from /usr/bin/irb:23:in `<main>'
|
129
|
+
# 5: from /usr/bin/irb:23:in `load'
|
130
|
+
# 4: from /home/okeeblow/.gems/gems/irb-1.2.4/exe/irb:11:in `<top (required)>'
|
131
|
+
# 3: from (irb):60
|
132
|
+
# 2: from /home/okeeblow/.gems/gems/ttfunk-1.6.2.1/lib/ttfunk/table/glyf.rb:35:in `for'
|
133
|
+
# 1: from /home/okeeblow/.gems/gems/ttfunk-1.6.2.1/lib/ttfunk/table/loca.rb:35:in `size_of'
|
134
|
+
# NoMethodError (undefined method `-' for nil:NilClass)
|
135
|
+
end
|
136
|
+
}
|
137
|
+
|
138
|
+
output << '</span>' << cr
|
139
|
+
end
|
140
|
+
|
141
|
+
output << '</span>'
|
142
|
+
output
|
143
|
+
end
|
144
|
+
|
145
|
+
# Return the `src` as the font_path since we aren't using
|
146
|
+
# any of the built-in fonts.
|
147
|
+
def font_path
|
148
|
+
path
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_vips_image
|
152
|
+
# https://libvips.github.io/libvips/API/current/libvips-create.html#vips-text
|
153
|
+
Vips::Image.text(
|
154
|
+
# This string must be well-escaped Pango Markup:
|
155
|
+
# https://developer.gnome.org/pango/stable/pango-Markup.html
|
156
|
+
# However the official function for escaping text is
|
157
|
+
# not implemented in Ruby GLib, so we have to do it ourselves.
|
158
|
+
to_pango,
|
159
|
+
**{
|
160
|
+
# String absolute path to TTF
|
161
|
+
:fontfile => font_path,
|
162
|
+
# It's not enough to just specify the TTF path;
|
163
|
+
# we must also specify a font family, subfamily, and size.
|
164
|
+
:font => "#{font_name}",
|
165
|
+
# Space between lines (in Points).
|
166
|
+
:spacing => to_ttfunk.line_gap,
|
167
|
+
# Requires libvips 8.8
|
168
|
+
:justify => false,
|
169
|
+
:dpi => 144,
|
170
|
+
},
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
end # Font
|
175
|
+
|
176
|
+
|
177
|
+
# Notes on file-format specifics and software-library-specifics
|
178
|
+
#
|
179
|
+
# # TTF (via TTFunk)
|
180
|
+
#
|
181
|
+
# ## Cmap
|
182
|
+
#
|
183
|
+
# Each TTFunk::Table::Cmap::Format<whatever> class responds to `:supported?`
|
184
|
+
# with its own internal boolean telling us if that Format is usable in TTFunk.
|
185
|
+
# This has nothing to do with any font file itself, just the library code.
|
186
|
+
# irb(main)> font.cmap.tables.map{|t| t.supported?}
|
187
|
+
# => [true, true, true]
|
188
|
+
#
|
189
|
+
# Any subclass of TTFunk::Table::Cmap::Subtable responds to `:unicode?`
|
190
|
+
# with a boolean calculated from the instance `@platform_id` and `@encoding_id`,
|
191
|
+
# and those numeric IDs are assigned to the symbolic (e.g. `:macroman`) names in:
|
192
|
+
# https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap/subtable.rb
|
193
|
+
# irb(main)> font.cmap.tables.map{|t| t.unicode?}
|
194
|
+
# => [true, false, true]
|
195
|
+
#
|