distorted 0.5.7 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e9fec7b7fe03bc5511ba906007acfd52d6385e4d297d057332731013c91eb6a
4
- data.tar.gz: e67c7cf1c1abbddc5919d5438ffa62b4c5ff151da0c3bc72b8243ded53ace047
3
+ metadata.gz: a96a3b49df5a194c2d18bae2a71d208e02aaad05c6af3eb5399c0f5c87167bd0
4
+ data.tar.gz: 7260d18a57ab0a14008bb92712c9dec322c5be4469329a656d5ef287c09e52bf
5
5
  SHA512:
6
- metadata.gz: 146598bcf7117c4d52927a9d903142223fc0cefa5bb4fbde0ca00353b53a29e3ec8f586ac01e3dcf5871fce674f51b2b5375eef0acdcefbe77f42a53a029e08e
7
- data.tar.gz: 447013da397b280e2fac32862d5d415b583ece04bb6552177428dad346fbbc6370f7ac4e21144042bc5842b510ef7d04105e44fc693e4609e5aedffae8abc625
6
+ metadata.gz: e290cb8d83991ad9391bf3cfd1c495142692af34b3ff424da78be54439e85f38507612df20814d5cb98e935db20ea970095a6890ca3291a6d3e0e6805948d928
7
+ data.tar.gz: a5388b7db8e91e16ad74080295882ad9bf106257c062ad0b54fdd60813c6300073ec25a1c031f8356c0f4b46dae3150e3bbf14a9c32466faaa848b2c8b10e445
@@ -0,0 +1,116 @@
1
+ require 'set'
2
+
3
+ require 'mime/types'
4
+ require 'ruby-filemagic'
5
+
6
+ module MIME
7
+ class Type
8
+ # Give MIME::Type objects an easy way to get the DistorteD saver method name.
9
+ def distorted_method
10
+ # Standardize MIME::Types' media_type+sub_type to DistorteD method mapping
11
+ # by replacing all the combining characters with underscores (snake case)
12
+ # to match Ruby conventions:
13
+ # https://rubystyle.guide/#snake-case-symbols-methods-vars
14
+ #
15
+ # For the worst possible example, an intended outout Type of
16
+ # "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
17
+ # (a.k.a. a MSWord `docx` file) would map to a DistorteD saver method
18
+ # :to_application_vnd_openxmlformats_officedocument_wordprocessingml_document
19
+ # which would most likely be defined by the :included method of a library-specific
20
+ # module for handling OpenXML MS Office documents.
21
+ "to_#{self.media_type}_#{self.sub_type.gsub(/[-+\.]/, '_'.freeze)}".to_sym
22
+ end
23
+ end
24
+ end
25
+
26
+ module CHECKING
27
+ class YOU
28
+
29
+ # Returns a Set of MIME::Type for a given file path, by default only
30
+ # based on the file extension. If the file extension is unavailable—
31
+ # or if `so_deep` is enabled—the `path` will be used as an actual
32
+ # path to look at the magic bytes with ruby-filemagic.
33
+ def self.OUT(path, so_deep: false)
34
+ unless so_deep || types.type_for(path).empty?
35
+ # NOTE: `type_for`'s return order is supposed to be deterministic:
36
+ # https://github.com/mime-types/ruby-mime-types/issues/148
37
+ # My use case so far has never required order but has required
38
+ # many Set comparisons, so I am going to return a Set here
39
+ # and possibly throw the order away.
40
+ # In my experience the order is usually preserved anyway:
41
+ # irb(main)> MIME::Types.type_for(File.expand_path('lol.ttf'))
42
+ # => [#<MIME::Type: font/ttf>, #<MIME::Type: application/font-sfnt>, #<MIME::Type: application/x-font-truetype>, #<MIME::Type: application/x-font-ttf>]
43
+ # irb(main)> MIME::Types.type_for('lol.ttf')).to_set
44
+ # => #<Set: {#<MIME::Type: font/ttf>, #<MIME::Type: application/font-sfnt>, #<MIME::Type: application/x-font-truetype>, #<MIME::Type: application/x-font-ttf>}>
45
+ return types.type_for(path).to_set
46
+ else
47
+ # Did we fail to guess any MIME::Types from the given filename?
48
+ # We're going to have to look at the actual file
49
+ # (or at least its first four bytes).
50
+ FileMagic.open(:mime) do |fm|
51
+ # The second argument makes fm.file return just the simple
52
+ # MIME::Type String, e.g.:
53
+ #
54
+ # irb(main)> fm.file('/home/okeeblow/IIDX-turntable.svg')
55
+ # => "image/svg+xml; charset=us-ascii"
56
+ # irb(main)> fm.file('/home/okeeblow/IIDX-turntable.svg', true)
57
+ # => "image/svg"
58
+ #
59
+ # However MIME::Types won't take short variants like 'image/svg',
60
+ # so explicitly have FM return long types and split it ourself
61
+ # on the semicolon:
62
+ #
63
+ # irb(main)> "image/svg+xml; charset=us-ascii".split(';').first
64
+ # => "image/svg+xml"
65
+ mime = types[fm.file(path, false).split(';'.freeze).first].to_set
66
+ end
67
+ end
68
+ end
69
+
70
+ # Returns a Set of MIME::Type objects matching a String search key of the
71
+ # format MEDIA_TYPE/SUB_TYPE.
72
+ # This can return multiple Types, e.g. 'font/collection' TTC/OTC variations:
73
+ # [#<MIME::Type: font/collection>, #<MIME::Type: font/collection>]
74
+ def self.IN(type)
75
+ types[type, :complete => type.is_a?(Regexp)].to_set
76
+ end
77
+
78
+ # Returns the MIME::Types container or loads one
79
+ def self.types
80
+ @@types ||= types_loader
81
+ end
82
+
83
+ # Returns a loaded MIME::Types container containing both the upstream
84
+ # mime-types-data and our own local data.
85
+ def self.types_loader
86
+ container = MIME::Types.new
87
+
88
+ # Load the upstream mime-types-data by providing a nil `path`:
89
+ # path || ENV['RUBY_MIME_TYPES_DATA'] || MIME::Types::Data::PATH
90
+ loader = MIME::Types::Loader.new(nil, container)
91
+ loader.load_columnar
92
+
93
+ # Change default JPEG file extension from .jpeg to .jpg
94
+ # because it pisses me off lol
95
+ container['image/jpeg'].last.preferred_extension = 'jpg'
96
+
97
+ # Override the loader's path with the path to our local data directory
98
+ # after we've loaded the upstream data.
99
+ # :@path is set up in Loader::initialize and only has an attr_reader
100
+ # but we can reach in and change it.
101
+ loader.instance_variable_set(:@path, File.join(__dir__, 'types'.freeze))
102
+
103
+ # Load our local types data. The YAML files are separated by type,
104
+ # and :load_yaml will load all of them in the :@path we just set.
105
+ # MAYBE: Integrate MIME::Types YAML conversion scripts and commit
106
+ # JSON/Columnar artifacts for SPEEEEEED, but YAML is probably fine
107
+ # since we will have so few custom types compared to upstream.
108
+ # Convert.from_yaml_to_json
109
+ # Convert::Columnar.from_yaml_to_columnar
110
+ loader.load_yaml
111
+
112
+ container
113
+ end
114
+
115
+ end
116
+ end
@@ -6,3 +6,46 @@
6
6
  # the library add new exception subclasses."
7
7
  class StandardDistorteDError < StandardError
8
8
  end
9
+
10
+ # The built-in NotImplementedError is for "when a feature is not implemented
11
+ # on the current platform", so make our own more appropriate ones.
12
+ class MediaTypeNotImplementedError < StandardDistorteDError
13
+ attr_reader :name
14
+ def initialize(name)
15
+ super
16
+ @name = name
17
+ end
18
+
19
+ def message
20
+ "No supported media type for #{name}"
21
+ end
22
+ end
23
+
24
+ class MediaTypeOutputNotImplementedError < MediaTypeNotImplementedError
25
+ attr_reader :type, :context
26
+ def initialize(name, type, context)
27
+ super(name)
28
+ @type = type
29
+ @context = context
30
+ end
31
+
32
+ def message
33
+ "Unable to save #{name} as #{type.to_s} from #{context}"
34
+ end
35
+ end
36
+
37
+ class MediaTypeNotFoundError < StandardDistorteDError
38
+ attr_reader :name
39
+ def initialize(name)
40
+ super
41
+ @name = name
42
+ end
43
+
44
+ def message
45
+ "Failed to detect media type for #{name}"
46
+ end
47
+ end
48
+
49
+
50
+ class OutOfDateLibraryError < LoadError
51
+ end
@@ -0,0 +1,247 @@
1
+ require 'set'
2
+ require 'distorted/monkey_business/set'
3
+
4
+
5
+ # This Module supports Module "Piles"* in DistorteD by merging
6
+ # an arbitrarily-deep nest of attribute-definition constants
7
+ # into a single combined datastructure per constant at include-/
8
+ # extend-/prepend-time.
9
+ # [*] 'Monad' doesn't feel quite right, but http://www.geekculture.com/joyoftech/joyimages/469.gif
10
+ #
11
+ # The combined structures can be accessed in one shot and trusted as
12
+ # a source of truth, versus inspecting Module::nesting or whatever
13
+ # to iterate over the masked same-Symbol constants we'd get when
14
+ # including/extending/prepending in Ruby normally:
15
+ # - https://ruby-doc.org/core/Module.html#method-c-nesting
16
+ # - http://valve.github.io/blog/2013/10/26/constant-resolution-in-ruby/
17
+
18
+ # There's some general redundancy here with Bundler's const_get_safely:
19
+ # https://ruby-doc.org/stdlib/libdoc/bundler/rdoc/Bundler/SharedHelpers.html#method-i-const_get_safely
20
+ #
21
+ # …but even though I use (and enjoy using) Bundler it feels Wrong™ to me to have
22
+ # that method in stdlib and especially in Core but not as part of Module since
23
+ # 'Bundler' still feels like a third-party namespace to me v(._. )v
24
+
25
+
26
+ module Cooltrainer; end
27
+ module Cooltrainer::DistorteD; end
28
+ module Cooltrainer::DistorteD::InjectionOfLove
29
+
30
+ # These hold (possibly-runtime-generated) Sets of MIME::Types (from our loader)*
31
+ # describing any supported input media-types (:LOWER_WORLD)
32
+ # and any supported output media-types (:OUTER_LIMITS).
33
+ TYPE_CONSTANTS = Set[
34
+ :LOWER_WORLD,
35
+ :OUTER_LIMITS,
36
+ ]
37
+ # These hold Hashes or Sets describing supported attributes,
38
+ # supported attribute-values (otherwise freeform),
39
+ # attribute defaults if any, and mappings for any of those things
40
+ # to any aliased equivalents for normalization and localization.
41
+ ATTRIBUTE_CONSTANTS = Set[
42
+ :ATTRIBUTES,
43
+ :ATTRIBUTES_DEFAULT,
44
+ :ATTRIBUTES_VALUES,
45
+ ]
46
+ # 🄒 All of the above.
47
+ DISTORTED_CONSTANTS = Set[].merge(TYPE_CONSTANTS).merge(ATTRIBUTE_CONSTANTS)
48
+
49
+ # Name of our fully-merged-Hash's class variable.
50
+ AFTERPARTY = :@@DistorteD
51
+
52
+
53
+ # Activate this module when it's included.
54
+ # We will merge DistorteD attributes to the singleton class from
55
+ # our including context and from out including context's included_modules,
56
+ # then we will define methods in the including context to perpetuate
57
+ # the merging process when that context is included/extended/prepended.
58
+ def self.included(otra)
59
+ self::Injection_Of_Love.call(otra)
60
+ super
61
+ end
62
+
63
+ # "Attribute" fragments are processed in one additional step to support
64
+ # aliased/equivalent attribute names and values.
65
+ # This is a quality-of-life feature to help normalize/localize attributes
66
+ # defined in a wide range of places by multiple unrelated upstream devs.
67
+ #
68
+ # For example, libvips savers expect a single-character upper-case
69
+ # `Q` argument for their 1–100 integer quality factor,
70
+ # and my VipsSave module's `:ATTRIBUTES` additionally aliases it
71
+ # to the more typical `quality` to provide consistent UX
72
+ # with other attributes from VIPS and other sources.
73
+ # https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-jpegsave
74
+ #
75
+ # VIPS also provides our example of the need for attribute-value equivalents,
76
+ # such as how it only accepts the spelling of "centre" and not "center"
77
+ # like myself and many millions of other people will reflexively enter :)
78
+ # https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsInteresting
79
+ def self.so_deep(fragment)
80
+ fragment.each_with_object(Array[]) { |(attribute, raw), to_merge|
81
+ # Each attribute's :raw may be an object (probably a Symbol),
82
+ # a Set (e.g. of aliases), or nil (for all values in a Set.to_hash)
83
+ case raw
84
+ when Set then raw.add(attribute)
85
+ when NilClass then [attribute]
86
+ else [attribute]
87
+ end.each{ |equivalent|
88
+ to_merge << [equivalent, attribute]
89
+ }
90
+ }.to_h
91
+ end
92
+
93
+ # Returns a block that will define methods in a given context
94
+ # such that when the given context is included/extended/prepended
95
+ # we will also merge our DD attributes into the new layer.
96
+ Injection_Of_Love = Proc.new { |otra|
97
+ # These are the methods that actively perform the include/extend/prepend process.
98
+ [:append_features, :prepend_features, :extend_object].each { |m|
99
+ otra.define_singleton_method(m) do |winter|
100
+ # Perform the normal include/extend/prepend that will mask our constants.
101
+ super(winter)
102
+
103
+ # Get new values to override masked constants.
104
+ pile = Cooltrainer::DistorteD::InjectionOfLove::trip_machine(winter)
105
+
106
+ # Record each constant individually as well as the entire pile.
107
+ # This doesn't currently get used, as this :class_variable_set call
108
+ # is broken in KRI:
109
+ # - https://bugs.ruby-lang.org/issues/7475
110
+ # - https://bugs.ruby-lang.org/issues/8297
111
+ # - https://bugs.ruby-lang.org/issues/11022
112
+ winter.singleton_class.class_variable_set(AFTERPARTY, pile)
113
+ pile.each_pair{ |k, v|
114
+ if winter.singleton_class.const_defined?(k, false)
115
+ # Since we are setting constants in the singleton_class
116
+ # we must remove any old ones first to avoid a warning.
117
+ winter.singleton_class.send(:remove_const, k)
118
+ end
119
+ winter.singleton_class.const_set(k, v)
120
+ }
121
+ end
122
+ }
123
+ # These are the callback methods called after the above methods fire.
124
+ # Use them to perpetuate our merge by calling the thing that calls us :)
125
+ [:included, :prepended, :extended].each { |m|
126
+ otra.define_singleton_method(m) do |winter|
127
+ Cooltrainer::DistorteD::InjectionOfLove::Injection_Of_Love.call(winter)
128
+ super(winter)
129
+ end
130
+ }
131
+ }
132
+
133
+ # Returns an instance-level copy of the complete attribute pile.
134
+ def trip_machine
135
+ @DistorteD ||= Cooltrainer::DistorteD::InjectionOfLove::trip_machine(self.singleton_class)
136
+ end
137
+
138
+ # Builds the attribute pile (e.g. suported atrs, values, defaults, etc) for any given scope.
139
+ def self.trip_machine(scope)
140
+ attribute_aliases = Hash[]
141
+ alias_attributes = Hash[]
142
+ values = Hash[]
143
+ defaults = Hash[]
144
+
145
+ scope&.ancestors.each { |otra|
146
+ DISTORTED_CONSTANTS.each { |invitation| # OUT OF CONTROL / MY WHEELS IN CONSTANT MOTION
147
+ if otra.const_defined?(invitation)
148
+ part = otra.const_get(invitation) rescue Hash[]
149
+
150
+ if invitation == :ATTRIBUTES
151
+ # Support both alias-to-attribute and attribute-to-aliases
152
+ attribute_aliases.merge!(part) { |invitation, old, new|
153
+ if old.nil?
154
+ new.nil? ? Set[invitation] : Set[new]
155
+ elsif new.nil?
156
+ old
157
+ elsif new.is_a?(Enumerable)
158
+ old.merge(new)
159
+ else
160
+ old << new
161
+ end
162
+ }
163
+ alias_attributes.merge!(Cooltrainer::DistorteD::InjectionOfLove::so_deep(part))
164
+ elsif invitation == :ATTRIBUTES_VALUES
165
+ # Regexes currently override Enumerables
166
+ to_merge = {}
167
+ part.each_pair { |attribute, values|
168
+ if values.is_a?(Regexp)
169
+ to_merge.update(attribute => values)
170
+ else
171
+ to_merge.update(attribute => Cooltrainer::DistorteD::InjectionOfLove::so_deep(values))
172
+ end
173
+ }
174
+ values.merge!(to_merge)
175
+ elsif invitation == :ATTRIBUTES_DEFAULT
176
+ defaults.merge!(part)
177
+ end
178
+
179
+ end
180
+ }
181
+ }
182
+
183
+ return {
184
+ :ATTRIBUTE_ALIASES => attribute_aliases,
185
+ :ALIAS_ATTRIBUTES => alias_attributes,
186
+ :ATTRIBUTES_VALUES => values,
187
+ :ATTRIBUTES_DEFAULT => defaults,
188
+ }
189
+ end
190
+
191
+ # Returns a value for any attribute.
192
+ # In order of priority, that means:
193
+ # - A user-given value (Liquid, CLI, etc) iff it passes a validity check,
194
+ # - the default value if the given value is not in the accepted Set,
195
+ # - nil for unset attributes with no default defined.
196
+ def abstract(argument)
197
+ # Reject any unknown arguments.
198
+ if trip_machine.dig(:ATTRIBUTE_ALIASES)&.keys.include?(argument)
199
+ alias_possibilities = trip_machine.dig(:ATTRIBUTE_ALIASES)&.dig(argument) || Set[]
200
+ possibilities = user_arguments&.keys.to_set & alias_possibilities
201
+
202
+ # How many matching user-defined attributes are there for our aliases?
203
+ case possibilities.length
204
+ when 0
205
+ # None; take the default.
206
+ trip_machine.dig(:ATTRIBUTES_DEFAULT)&.dig(argument)
207
+ when 1
208
+ # One; does it look valid?
209
+ is_valid = false
210
+ user_value = user_arguments&.dig(argument)
211
+
212
+ # Supported values may be declared as:
213
+ # - A Hash of values-and-their-aliases to values.
214
+ # - A Regex.
215
+ # - nil for freeform input.
216
+ valid_value = trip_machine.dig(:ATTRIBUTES_VALUES)&.dig(argument)
217
+ if valid_value.is_a?(Enumerable)
218
+ if valid_value.include?(user_value)
219
+ is_valid = true
220
+ end
221
+ elsif valid_value.is_a?(Regexp)
222
+ if valid_value.match(user_value.to_s)
223
+ is_valid = true
224
+ end
225
+ end
226
+
227
+ # Return a valid user value, a default, or nil if all else fails.
228
+ if is_valid
229
+ # TODO: boolean casting
230
+ user_value
231
+ else
232
+ trip_machine.dig(:ATTRIBUTES_DEFAULT)&.dig(argument)
233
+ end
234
+
235
+ else # case user_values.length
236
+ # Two or more; what do??
237
+ raise RuntimeError("Can't have multiple settings for #{argument} and its aliases.")
238
+ end
239
+ else
240
+ # The programmer asked for the value of an attribute that is
241
+ # not supported by its MediaMolecule. This is most likely a bug.
242
+ raise RuntimeError("#{argument} is not supported for #{@name}")
243
+ end
244
+ end
245
+
246
+
247
+ end
@@ -1,6 +1,6 @@
1
1
  module Cooltrainer
2
2
  module DistorteD
3
- module Tech
3
+ module Technology
4
4
  module Pango
5
5
 
6
6
  # Escape text as necessary for Pango Markup, which is what Vips::Image.text()
@@ -38,12 +38,21 @@ module Cooltrainer
38
38
  cr << lf
39
39
  end
40
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
+
41
48
  # The char-by-char actual function used by g_markup_escape_text
42
49
  def g_markup_escape_char(c)
43
50
  # I think a fully-working version of this function would
44
- # be as simple `sprintf('&#x%x;', c.ord)`, but I want to copy
45
- # the C implementation as closely as possible, which means using
46
- # the named escape sequences for common characters and separating
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
47
56
  # the Unicode control characters (> 0x7f) even though three's no
48
57
  # need to in Ruby.
49
58
  case c.ord
@@ -59,7 +68,13 @@ module Cooltrainer
59
68
  '&quot;'
60
69
  when 0x1..0x8, 0xb..0xc, 0xe..0x1f, 0x7f
61
70
  sprintf('&#x%x;', c.ord)
62
- when 0x7f..0x84, 0x86..0x9f
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 :)
63
78
  sprintf('&#x%x;', c.ord)
64
79
  when 0x0 # what's this…?
65
80
  # Avoid a `ArgumentError: string contains null byte`