distorted 0.5.2 → 0.5.7

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 (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 +77 -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