distorted 0.5.6 → 0.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +661 -0
  3. data/README.md +4 -139
  4. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Less_Perfect_DOS_VGA.png +0 -0
  5. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/More_Perfect_DOS_VGA.png +0 -0
  6. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Perfect_DOS_VGA.png +0 -0
  7. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/less_more_perfect_dos_vga_437.html +52 -0
  8. data/font/1252/LICENSE/PerfectDOSVGA437/font-comment.php@file=perfect_dos_vga_437.html +5 -0
  9. data/font/1252/LessPerfectDOSVGA.ttf +0 -0
  10. data/font/1252/MorePerfectDOSVGA.ttf +0 -0
  11. data/font/1252/Perfect DOS VGA 437 Win.ttf +0 -0
  12. data/font/437/Perfect DOS VGA 437.ttf +0 -0
  13. data/font/437/dos437.txt +72 -0
  14. data/font/65001/Anonymous Pro B.ttf +0 -0
  15. data/font/65001/Anonymous Pro BI.ttf +0 -0
  16. data/font/65001/Anonymous Pro I.ttf +0 -0
  17. data/font/65001/Anonymous Pro.ttf +0 -0
  18. data/font/65001/LICENSE/AnonymousPro/FONTLOG.txt +45 -0
  19. data/font/65001/LICENSE/AnonymousPro/OFL-FAQ.txt +235 -0
  20. data/font/65001/LICENSE/AnonymousPro/OFL.txt +94 -0
  21. data/font/65001/LICENSE/AnonymousPro/README.txt +55 -0
  22. data/font/850/ProFont-Bold-01/LICENSE +22 -0
  23. data/font/850/ProFont-Bold-01/readme.txt +28 -0
  24. data/font/850/ProFontWindows-Bold.ttf +0 -0
  25. data/font/850/ProFontWindows.ttf +0 -0
  26. data/font/850/Profont/LICENSE +22 -0
  27. data/font/850/Profont/readme.txt +31 -0
  28. data/font/932/LICENSE/README-ttf.txt +213 -0
  29. data/font/932/mona.ttf +0 -0
  30. data/lib/distorted/error_code.rb +8 -0
  31. data/lib/distorted/font.rb +192 -0
  32. data/lib/distorted/image.rb +121 -0
  33. data/lib/distorted/modular_technology/pango.rb +75 -0
  34. data/lib/distorted/monkey_business/hash.rb +33 -0
  35. data/lib/distorted/monkey_business/mnemoniq.rb +8 -0
  36. data/lib/distorted/monkey_business/set.rb +15 -0
  37. data/lib/distorted/monkey_business/string.rb +6 -0
  38. data/lib/distorted/pdf.rb +110 -0
  39. data/lib/distorted/svg.rb +21 -0
  40. data/lib/distorted/text.rb +241 -0
  41. data/lib/distorted/version.rb +20 -0
  42. data/lib/distorted/video.rb +193 -0
  43. data/test/distorted_test.rb +11 -0
  44. data/test/test_helper.rb +4 -0
  45. metadata +49 -5
Binary file
@@ -0,0 +1,8 @@
1
+ # https://ruby-doc.org/core/Exception.html sez:
2
+ # "It is recommended that a library should have one subclass of StandardError
3
+ # or RuntimeError and have specific exception types inherit from it.
4
+ # This allows the user to rescue a generic exception type to catch
5
+ # all exceptions the library may raise even if future versions of
6
+ # the library add new exception subclasses."
7
+ class StandardDistorteDError < StandardError
8
+ end
@@ -0,0 +1,192 @@
1
+ require 'set'
2
+
3
+ # Font metadata extraction
4
+ require 'ttfunk'
5
+
6
+ require 'mime/types'
7
+
8
+ # No need to do all the fancy library versioning in a subclass.
9
+ require 'vips'
10
+
11
+ require 'distorted/text'
12
+
13
+
14
+ module Cooltrainer
15
+ module DistorteD
16
+ class Font < Text
17
+
18
+ MEDIA_TYPE = 'font'.freeze
19
+
20
+ # TODO: Test OTF, OTB, and others.
21
+ # NOTE: Traditional bitmap fonts won't be supported due to Pango 1.44
22
+ # and later switching to Harfbuzz from Freetype:
23
+ # https://gitlab.gnome.org/GNOME/pango/-/issues/386
24
+ # https://blogs.gnome.org/mclasen/2019/05/25/pango-future-directions/
25
+ MIME_TYPES = MIME::Types[/^#{self::MEDIA_TYPE}\/ttf/].to_set
26
+
27
+ ATTRS = Set[
28
+ :alt,
29
+ ]
30
+ ATTRS_VALUES = {
31
+ }
32
+ ATTRS_DEFAULT = {
33
+ }
34
+
35
+
36
+ # irb(main):089:0> chars.take(5)
37
+ # => [[1, 255], [2, 1], [3, 2], [4, 3], [5, 4]]
38
+ # irb(main):090:0> chars.values.take(5)
39
+ # => [255, 1, 2, 3, 4]
40
+ # irb(main):091:0> chars.values.map(&:chr).take(5)
41
+ # => ["\xFF", "\x01", "\x02", "\x03", "\x04"]
42
+ def to_pango
43
+ output = '' << cr << '<span>' << cr
44
+
45
+ output << "<span size='35387'> #{font_name}</span>" << cr << cr
46
+
47
+ output << "<span size='24576'> #{font_description}</span>" << cr
48
+ output << "<span size='24576'> #{font_copyright}</span>" << cr
49
+ output << "<span size='24576'> #{font_version}</span>" << cr << cr
50
+
51
+ # Print a preview String in using the loaded font. Or don't.
52
+ if @demo
53
+ output << cr << cr << "<span size='24576' foreground='grey'> #{g_markup_escape_text(@demo)}</span>" << cr << cr << cr
54
+ end
55
+
56
+ # /!\ MANDATORY READING /!\
57
+ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html
58
+ #
59
+ # "The 'cmap' table maps character codes to glyph indices.
60
+ # The choice of encoding for a particular font is dependent upon the conventions
61
+ # used by the intended platform. A font intended to run on multiple platforms
62
+ # with different encoding conventions will require multiple encoding tables.
63
+ # As a result, the 'cmap' table may contain multiple subtables,
64
+ # one for each supported encoding scheme."
65
+ #
66
+ # Cmap#unicode is a convenient shortcut to sorting the subtables
67
+ # and removing any unusable ones:
68
+ # https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap.rb
69
+ #
70
+ # irb(main):174:0> font_meta.cmap.tables.count
71
+ # => 3
72
+ # irb(main):175:0> font_meta.cmap.unicode.count
73
+ # => 2
74
+ @font_meta.cmap.tables.each do |table|
75
+ next if !table.unicode?
76
+ # Each subtable's `code_map` is a Hash map of character codes (the Hash keys)
77
+ # to the glyph IDs from the original font (the Hash's values).
78
+ #
79
+ # Subtable::encode takes:
80
+ # - a Hash mapping character codes to original font glyph IDs.
81
+ # - the desired output encoding — Set[:mac_roman, :unicode, :unicode_ucs4]
82
+ # https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap/subtable.rb
83
+ # …and returns a Hash with keys:
84
+ # - :charmap — Hash mapping the characters in the input charmap
85
+ # to a another hash containing both the `:old`
86
+ # and `:new` glyph ids for each character code.
87
+ # - :subtable — String encoded subtable for the given encoding.
88
+ encoded = TTFunk::Table::Cmap::Subtable::encode(table&.code_map, :unicode).dig(:charmap)
89
+
90
+ output << "<span size='49152'>"
91
+
92
+ i = 0
93
+ encoded.each_pair { |c, (old, new)|
94
+
95
+ begin
96
+ if glyph = @font_meta.glyph_outlines.for(c)
97
+ # Add a space on either side of the character so they aren't
98
+ # all smooshed up against each other and unreadable.
99
+ output << ' ' << g_markup_escape_char(c) << ' '
100
+ if i >= 15
101
+ output << cr
102
+ i = 0
103
+ else
104
+ i = i + 1
105
+ end
106
+ else
107
+ end
108
+ rescue NoMethodError => nme
109
+ # TTFunk's `glyph_outlines.for()` will raise this if we call it
110
+ # for a codepoint that does not exist in the font, which we will
111
+ # not do because we are enumerating the codepoints in the font,
112
+ # but we should still handle the possibility.
113
+ # irb(main):060:0> font.glyph_outlines.for(555555)
114
+ #
115
+ # Traceback (most recent call last):
116
+ # 6: from /usr/bin/irb:23:in `<main>'
117
+ # 5: from /usr/bin/irb:23:in `load'
118
+ # 4: from /home/okeeblow/.gems/gems/irb-1.2.4/exe/irb:11:in `<top (required)>'
119
+ # 3: from (irb):60
120
+ # 2: from /home/okeeblow/.gems/gems/ttfunk-1.6.2.1/lib/ttfunk/table/glyf.rb:35:in `for'
121
+ # 1: from /home/okeeblow/.gems/gems/ttfunk-1.6.2.1/lib/ttfunk/table/loca.rb:35:in `size_of'
122
+ # NoMethodError (undefined method `-' for nil:NilClass)
123
+ end
124
+ }
125
+
126
+ output << '</span>' << cr
127
+ end
128
+
129
+ output << '</span>'
130
+ output
131
+ end
132
+
133
+ # Return the `src` as the font_path since we aren't using
134
+ # any of the built-in fonts.
135
+ def font_path
136
+ @src
137
+ end
138
+
139
+ def initialize(src, demo: nil)
140
+ @src = src
141
+ @demo = demo
142
+
143
+ # TODO: Check that src exists, because TTFunk won't and will just
144
+ # give us an unusable object instead.
145
+ @font_meta = TTFunk::File.open(src)
146
+
147
+ # https://libvips.github.io/libvips/API/current/libvips-create.html#vips-text
148
+ @image = Vips::Image.text(
149
+ # This string must be well-escaped Pango Markup:
150
+ # https://developer.gnome.org/pango/stable/pango-Markup.html
151
+ # However the official function for escaping text is
152
+ # not implemented in Ruby GLib, so we have to do it ourselves.
153
+ to_pango,
154
+ **{
155
+ # String absolute path to TTF
156
+ :fontfile => font_path,
157
+ # It's not enough to just specify the TTF path;
158
+ # we must also specify a font family, subfamily, and size.
159
+ :font => "#{font_name}",
160
+ # Space between lines (in Points).
161
+ :spacing => @font_meta.line_gap,
162
+ # Requires libvips 8.8
163
+ :justify => false,
164
+ :dpi => 144,
165
+ },
166
+ )
167
+ end
168
+
169
+ end # Font
170
+ end # DistorteD
171
+ end # Cooltrainer
172
+
173
+
174
+ # Notes on file-format specifics and software-library-specifics
175
+ #
176
+ # # TTF (via TTFunk)
177
+ #
178
+ # ## Cmap
179
+ #
180
+ # Each TTFunk::Table::Cmap::Format<whatever> class responds to `:supported?`
181
+ # with its own internal boolean telling us if that Format is usable in TTFunk.
182
+ # This has nothing to do with any font file itself, just the library code.
183
+ # irb(main)> font.cmap.tables.map{|t| t.supported?}
184
+ # => [true, true, true]
185
+ #
186
+ # Any subclass of TTFunk::Table::Cmap::Subtable responds to `:unicode?`
187
+ # with a boolean calculated from the instance `@platform_id` and `@encoding_id`,
188
+ # and those numeric IDs are assigned to the symbolic (e.g. `:macroman`) names in:
189
+ # https://github.com/prawnpdf/ttfunk/blob/master/lib/ttfunk/table/cmap/subtable.rb
190
+ # irb(main)> font.cmap.tables.map{|t| t.unicode?}
191
+ # => [true, false, true]
192
+ #
@@ -0,0 +1,121 @@
1
+ # Requiring libvips 8.8 for HEIC/HEIF (moo) support, `justify` support in the
2
+ # Vips::Image text operator, animated WebP support, and more:
3
+ # https://libvips.github.io/libvips/2019/04/22/What's-new-in-8.8.html
4
+ VIPS_MINIMUM_VER = [8, 8, 0]
5
+
6
+ # Tell the user to install the shared library if it's missing.
7
+ begin
8
+ require 'vips'
9
+
10
+ we_good = false
11
+ if Vips::version(0) >= VIPS_MINIMUM_VER[0]
12
+ if Vips::version(1) >= VIPS_MINIMUM_VER[1]
13
+ if Vips::version(2) >= VIPS_MINIMUM_VER[2]
14
+ we_good = true
15
+ end
16
+ end
17
+ end
18
+ unless we_good
19
+ raise LoadError.new("libvips is older than DistorteD's minimum requirement: needed #{VIPS_MINIMUM_VER.join('.'.freeze)} vs available '#{Vips::version_string}'")
20
+ end
21
+
22
+ rescue LoadError => le
23
+ # Only match libvips.so load failure
24
+ raise unless le.message =~ /libvips.so/
25
+
26
+ # Multiple OS help
27
+ help = <<~INSTALL
28
+
29
+ Please install the libvips image processing library.
30
+
31
+ FreeBSD:
32
+ pkg install graphics/vips
33
+
34
+ macOS:
35
+ brew install vips
36
+
37
+ Debian/Ubuntu/Mint:
38
+ apt install libvips libvips-dev
39
+ INSTALL
40
+
41
+ # Re-raise with install message
42
+ raise $!, "#{help}\n#{$!}", $!.backtrace
43
+ end
44
+
45
+ require 'set'
46
+
47
+ require 'mime/types'
48
+
49
+ module Cooltrainer
50
+ module DistorteD
51
+ class Image
52
+
53
+ MEDIA_TYPE = 'image'.freeze
54
+
55
+ # SVG support is a sub-class and not directly supported here:
56
+ # `write_to_file': No known saver for '/home/okeeblow/Works/cooltrainer/_site/IIDX-turntable.svg'. (Vips::Error)
57
+ MIME_TYPES = MIME::Types[/^#{MEDIA_TYPE}\/(?!svg)/, :complete => true].to_set
58
+
59
+ # Attributes for our <picture>/<img>.
60
+ # Automatically enabled as attrs for DD Liquid Tag.
61
+ # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture#Attributes
62
+ # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes
63
+ # https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading
64
+ # :crop is a Vips-only attr
65
+ ATTRS = Set[:alt, :caption, :href, :crop, :loading]
66
+
67
+ # Defaults for HTML Element attributes.
68
+ # Not every attr has to be listed here.
69
+ # Many need no default and just won't render.
70
+ ATTRS_DEFAULT = {
71
+ :crop => :attention,
72
+ :loading => :eager,
73
+ }
74
+ ATTRS_VALUES = {
75
+ # https://www.rubydoc.info/gems/ruby-vips/Vips/Interesting
76
+ :crop => Set[:none, :centre, :entropy, :attention],
77
+ :loading => Set[:eager, :lazy],
78
+ }
79
+
80
+
81
+ def initialize(src)
82
+ @image = Vips::Image.new_from_file(src)
83
+ @src = src
84
+ end
85
+
86
+ def rotate(angle: nil)
87
+ if angle == :auto
88
+ @image = @image&.autorot
89
+ end
90
+ end
91
+
92
+ def clean
93
+ # Nuke the entire site from orbit. It's the only way to be sure.
94
+ @image.get_fields.grep(/exif-ifd/).each {|field| @image.remove field}
95
+ end
96
+
97
+ def save(dest, width: nil, crop: nil)
98
+ begin
99
+ if width.nil? or width == :full
100
+ return @image.write_to_file(dest)
101
+ elsif width.respond_to?(:to_i)
102
+ ver = @image.thumbnail_image(
103
+ width.to_i,
104
+ # Use `self` namespace for constants so subclasses can redefine
105
+ **{:crop => crop || self.singleton_class.const_get(:ATTRS_DEFAULT)[:crop]},
106
+ )
107
+ return ver.write_to_file(dest)
108
+ end
109
+ rescue Vips::Error => v
110
+ if v.message.include?('No known saver')
111
+ # TODO: Handle missing output formats. Replacements? Skip it? Die?
112
+ return nil
113
+ else
114
+ raise
115
+ end
116
+ end
117
+ end # save
118
+
119
+ end # Image
120
+ end # DistorteD
121
+ end # Cooltrainer
@@ -0,0 +1,75 @@
1
+ module Cooltrainer
2
+ module DistorteD
3
+ module Tech
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
+ # The char-by-char actual function used by g_markup_escape_text
42
+ def g_markup_escape_char(c)
43
+ # 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
47
+ # the Unicode control characters (> 0x7f) even though three's no
48
+ # need to in Ruby.
49
+ case c.ord
50
+ when '&'.ord
51
+ '&amp;'
52
+ when '<'.ord
53
+ '&lt;'
54
+ when '>'.ord
55
+ '&gt;'
56
+ when '\''.ord
57
+ '&apos;'
58
+ when '"'.ord
59
+ '&quot;'
60
+ when 0x1..0x8, 0xb..0xc, 0xe..0x1f, 0x7f
61
+ sprintf('&#x%x;', c.ord)
62
+ when 0x7f..0x84, 0x86..0x9f
63
+ sprintf('&#x%x;', c.ord)
64
+ when 0x0 # what's this…?
65
+ # Avoid a `ArgumentError: string contains null byte`
66
+ # by not printing one :)
67
+ else
68
+ c
69
+ end
70
+ end
71
+
72
+ end # Pango
73
+ end # Tech
74
+ end # DistorteD
75
+ end # Cooltrainer
@@ -0,0 +1,33 @@
1
+ require 'set'
2
+
3
+ class Hash
4
+
5
+ # Complement Ruby::YAML behavior, where usage of Set syntax
6
+ # returns a Hash with all-nil values.
7
+ # Calling :to_set on a Hash with all-nil values should return
8
+ # a Set of the Hash's keys.
9
+ this_old_set = instance_method(:to_set)
10
+ define_method(:to_set) do
11
+ if self.values.all?{ |v| v.nil? }
12
+ self.keys.to_set
13
+ else
14
+ this_old_set.bind(self).()
15
+ end
16
+ end
17
+
18
+ # https://github.com/dam13n/ruby-bury/blob/master/hash.rb
19
+ # This is not packaged as a Gem or I'd be using it instead of including my own.
20
+ def bury(*args)
21
+ if args.count < 2
22
+ raise ArgumentError.new('2 or more arguments required')
23
+ elsif args.count == 2
24
+ self[args[0]] = args[1]
25
+ else
26
+ arg = args.shift
27
+ self[arg] = {} unless self[arg]
28
+ self[arg].bury(*args) unless args.empty?
29
+ end
30
+ self
31
+ end
32
+
33
+ end
@@ -0,0 +1,8 @@
1
+ require 'mime/types'
2
+
3
+ # MIME::Types#preferred_extension returns @extensions.first unless
4
+ # otherwise set. I don't like some of the defaults, so this file
5
+ # changes them.
6
+ # Normally I don't like to monkey patch just on import without calling
7
+ # some method, but this is one time I explicitly want to do that.
8
+ MIME::Types['image/jpeg'].last.preferred_extension = 'jpg'
@@ -0,0 +1,15 @@
1
+ require 'set'
2
+
3
+ # Override Set.to_hash to complement Ruby::YAML's Set implementation,
4
+ # where the YAML Set syntax returns a Hash with all-nil values,
5
+ # at least without some decorator sugar in the YAML itself:
6
+ # https://rhnh.net/2011/01/31/yaml-tutorial/
7
+ #
8
+ # Since Set is implemented using a Hash internally I think it makes
9
+ # more sense for Set.to_hash to return a Hash with all-nil values
10
+ # with keys matching the contents of the original Set.
11
+ class Set
12
+ def to_hash
13
+ Hash[self.map { |s| [s, nil] }]
14
+ end
15
+ end