distorted-floor 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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,66 @@
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
+ require('xross-the-xoul/version') unless defined?(::XROSS::THE::Version::TripleCounter)
5
+ VIPS_MINIMUM_VER = ::XROSS::THE::Version::TripleCounter.new(8, 8, 0)
6
+
7
+ # Tell the user to install the shared library if it's missing or too old.
8
+ begin
9
+ require 'vips'
10
+ VIPS_AVAILABLE_VER = ::XROSS::THE::Version::TripleCounter.new(Vips::version(0), Vips::version(1), Vips::version(2))
11
+
12
+ unless VIPS_AVAILABLE_VER >= VIPS_MINIMUM_VER
13
+ raise LoadError.new(
14
+ "DistorteD needs libvips #{VIPS_MINIMUM_VER}, but the available version is '#{Vips::version_string}'"
15
+ )
16
+ end
17
+
18
+ rescue LoadError => le
19
+ # Only match libvips.so load failure
20
+ raise unless le.message =~ /libvips.so/
21
+
22
+ # Multiple OS help
23
+ help = <<~INSTALL
24
+
25
+ Please install the VIPS (libvips) image processing library, version #{VIPS_MINIMUM_VER} or later.
26
+
27
+ FreeBSD:
28
+ pkg install graphics/vips
29
+
30
+ macOS:
31
+ brew install vips
32
+
33
+ Debian/Ubuntu/Mint:
34
+ apt install libvips libvips-dev
35
+ INSTALL
36
+
37
+ # Re-raise with install message
38
+ raise $!, "#{help}\n#{$!}", $!.backtrace
39
+ end
40
+
41
+ # FFI Struct layout to read a VipsObject class summary.
42
+ # Based on https://github.com/libvips/ruby-vips/issues/186#issuecomment-433691412
43
+ module Vips
44
+ attach_function :vips_class_find, [:string, :string], :pointer
45
+ attach_function :vips_object_summary_class, [:pointer, :pointer], :void
46
+
47
+ class BufStruct < ::FFI::Struct
48
+ layout :base, :pointer,
49
+ :mx, :int,
50
+ :i, :int,
51
+ :full, :bool,
52
+ :lasti, :int,
53
+ :dynamic, :bool
54
+ end
55
+
56
+ end
57
+
58
+ module GObject
59
+ # Fundamental types not already defined in ruby-vips' `lib/vips.rb`
60
+ GBOXED_TYPE = g_type_from_name('GBoxed')
61
+
62
+ # Attach to functions that aren't already attached in `lib/vips/object.rb`,
63
+ # `lib/vips/operation.rb`, `lib/vips/gvalue.rb`, `lib/vips/gobject.rb`,
64
+ # or some other related file.
65
+ attach_function :g_param_spec_get_default_value, [:pointer], GValue
66
+ end
@@ -0,0 +1,174 @@
1
+
2
+ require 'set'
3
+
4
+ require 'distorted-floor/checking_you_out'
5
+ using ::DistorteD::CHECKING::YOU::OUT
6
+ require 'distorted-floor/modular_technology/vips/operation'
7
+ require 'distorted-floor/modular_technology/vips/save'
8
+
9
+
10
+ module Cooltrainer; end
11
+ module Cooltrainer::DistorteD; end
12
+ module Cooltrainer::DistorteD::Technology; end
13
+ module Cooltrainer::DistorteD::Technology::Vips::Load
14
+
15
+ # Returns a `::Set` of `::CHECKING::YOU::OUT` based on libvips `VipsForeignLoad` capabilities.
16
+ # NOTE: libvips only declares support (via :get_suffixes) for the "saver" types,
17
+ # but libvips can use additional external libraries for wider media-types support, e.g.:
18
+ #
19
+ # - SVG with librsvg2★ / libcairo. [*]
20
+ # - PDF with PDFium if available, otherwise with libpoppler-glib / libcairo.
21
+ # - OpenEXR/libIlmImf — ILM high dynamic range image format.
22
+ # - maybe more: https://github.com/libvips/libvips/blob/master/configure.ac
23
+ #
24
+ # [FITS]: https://heasarc.gsfc.nasa.gov/docs/heasarc/fits.html
25
+ #
26
+ # [RSVG2]: This is the normal SVG library for the GNOME/GLib world and is
27
+ # probably fine for 95% of use-cases, but I'm pissed off at it because of:
28
+ #
29
+ # - https://gitlab.gnome.org/GNOME/librsvg/-/issues/56
30
+ # - https://gitlab.gnome.org/GNOME/librsvg/-/issues/100
31
+ # - https://gitlab.gnome.org/GNOME/librsvg/-/issues/183
32
+ # - https://gitlab.gnome.org/GNOME/librsvg/-/issues/494
33
+ # - https://bugzilla.gnome.org/show_bug.cgi?id=666477
34
+ # - https://phabricator.wikimedia.org/T35245
35
+ #
36
+ # TLDR: SVG <tspan> elements' [:x, :y, :dy, :dx] attributes can be
37
+ # a space-delimited list of position values for individual
38
+ # characters in the <tspan>, but librsvg2 only supported reading
39
+ # those attributes as a single one-shot numeric value.
40
+ # Documents using this totally-common and totally-in-spec feature
41
+ # rendered incorrectly with librsvg2. Effected <tspan> elements'
42
+ # subsequent children would hug one edge of the rendered output.
43
+ #
44
+ # And wouldn't you know it but the one (1) SVG on my website
45
+ # at the time I built this feature (IIDX-Turntable-parts.svg) used
46
+ # this feature for the double-digit parts diagram labels.
47
+ # I ended up having to edit my input document to just squash the
48
+ # offending <tspan>s down to a single child each.
49
+ # I guess that's semantically more correct in my document since they are
50
+ # numbers like Eleven and not two separate characters like '1 1'
51
+ # but still ugh lol
52
+ #
53
+ # This was finally fixed in 2019 as of librsvg2 version 2.45.91 :)
54
+ # https://gitlab.gnome.org/GNOME/librsvg/-/issues/494#note_579774
55
+ #
56
+ # [MAGICK]: The Magick-based '.bmp' loader is broken/missing in libvips <= 8.9.1,
57
+ # but our automatic Loader detection will handle that. Just FYI :)
58
+ #
59
+
60
+ # Vips::vips_foreign_find_save is based on filename suffix (extension),
61
+ # but :vips_foreign_find_load seems to be based on file magic.
62
+ # That is, we can't `vips_foreign_find_load` for a made-up filename
63
+ # or plain suffix like we can to to build 'vips/save'::OUTER_LIMITS.
64
+ # This caught me off guard but doesn't *entirely* not-make-sense,
65
+ # considering Vips::Image::new_from_filename calls :vips_foreign_find_load
66
+ # and obviously expects a file to be present.
67
+ #
68
+ ## Example — works with real file and fails with only suffix:
69
+ # irb> Vips::vips_foreign_find_load '/home/okeeblow/cover.jpg'
70
+ # => "VipsForeignLoadJpegFile"
71
+ # irb> Vips::vips_foreign_find_load 'cover.jpg'
72
+ # => nil
73
+ #
74
+ ## Syscalls of successful real-file :vips_foreign_find_load call
75
+ # showing how it works:
76
+ # [okeeblow@emi#okeeblow] strace ruby -e "require 'vips'; Vips::vips_foreign_find_load '/home/okeeblow/cover.jpg'" 2>&1|grep cover.jpg
77
+ # access("/home/okeeblow/cover.jpg", R_OK) = 0
78
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY) = 5
79
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY) = 5
80
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY) = 5
81
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY) = 5
82
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY|O_CLOEXEC) = 5
83
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY|O_CLOEXEC) = 5
84
+ # lstat("/home/okeeblow/cover.jpg", {st_mode=S_IFREG|0740, st_size=6242228, ...}) = 0
85
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY|O_CLOEXEC) = 5
86
+ # stat("/home/okeeblow/cover.jpg", {st_mode=S_IFREG|0740, st_size=6242228, ...}) = 0
87
+ # stat("/home/okeeblow/cover.jpg-journal", 0x7fffa70f4df0) = -1 ENOENT (No such file or directory)
88
+ # stat("/home/okeeblow/cover.jpg-wal", 0x7fffa70f4df0) = -1 ENOENT (No such file or directory)
89
+ # stat("/home/okeeblow/cover.jpg", {st_mode=S_IFREG|0740, st_size=6242228, ...}) = 0
90
+ # openat(AT_FDCWD, "/home/okeeblow/cover.jpg", O_RDONLY) = 5
91
+ #
92
+ ## …and of a fake suffix-only filename to show how it doesn't:
93
+ # [okeeblow@emi#okeeblow] strace ruby -e "require 'vips'; Vips::vips_foreign_find_load 'fartbutt.jpg'" 2>&1|grep '.jpg'
94
+ # read(5, ".write_to_target target, \".jpg[Q"..., 8192) = 8192
95
+ # access("fartbutt.jpg", R_OK) = -1 ENOENT (No such file or directory)
96
+ #
97
+ ## Versus the corresponding Vips::vips_foreign_find_save which is *only* based
98
+ # on filename suffix and does not try to look at a file at all,
99
+ # perhaps (read: obviously) because that file wouldn't exist yet to test until we save it :)
100
+ # [okeeblow@emi#okeeblow] strace ruby -e "require 'vips'; p Vips::vips_foreign_find_save 'fartbutt.jpg'" 2>&1|grep -E 'Save|.jpg'
101
+ # read(5, ".write_to_target target, \".jpg[Q"..., 8192) = 8192
102
+ # write(1, "\"VipsForeignSaveJpegFile\"\n", 26"VipsForeignSaveJpegFile"
103
+ #
104
+ # For this reason I'm going to write my own shim Loader-finder and use it instead.
105
+ LOWER_WORLD = Cooltrainer::DistorteD::Technology::Vips::VipsType::loader_types.keep_if { |type, operations|
106
+ # Skip text types for image data until I have a way for multiple
107
+ # type-supporting Molecules to vote on a src file.
108
+ # TODO: Support loading image CSV
109
+ # TODO: Make this more robust/automatic.
110
+ Array[
111
+ type.phylum != :application, # e.g. application/pdf
112
+ type.phylum != :text, # e.g. text/csv
113
+ ].all? && Array[
114
+ type.genus.to_s.include?(-'zip'),
115
+ # Skip declaring SVG here since I want to handle it in a Vector-only Molecule
116
+ # and will re-declare this there. Prolly need to think up a better way to do this.
117
+ type.genus.to_s.include?(-'svg'),
118
+ ].none?
119
+ }.transform_values { |v| v.map(&:options).reduce(&:merge) }
120
+
121
+
122
+ self::LOWER_WORLD.each_key { |t|
123
+ define_method(t.distorted_open_method) { |src_path = path, change|
124
+ # Find a VipsType Struct for the saver operation
125
+ vips_operation = Cooltrainer::DistorteD::Technology::Vips::VipsType::loader_for(t).first
126
+
127
+ # Prepare a Hash of options appropriate for this operation.
128
+ # We explicitly declare all supported VipsArguments, using the FFI-detected
129
+ # default values for keys with no user-given value.
130
+ options = change.to_hash.slice(
131
+ # Get an Array[Symbol] of non-aliased option keys.
132
+ # Loading any aliases' values happens when the Change/Atoms are constructed.
133
+ *vips_operation.options.keep_if { |aka, compound| aka == compound.element }.keys
134
+ ).reject { |k,v| v.nil? }.transform_keys { |k|
135
+ # The `ruby-vips` binding expects hyphenated argument names to be converted to underscores:
136
+ # https://github.com/libvips/ruby-vips/blob/4f696e30796adcc99cbc70ff7fd778439f0cbac7/lib/vips/operation.rb#L78-L80
137
+ k.to_s.gsub('-', '_').to_sym
138
+ }
139
+
140
+ # Do the thing.
141
+ Vips::Operation.call(
142
+ # `:vips_call` expects the operation_name to be a String:
143
+ # https://libvips.github.io/libvips/API/current/VipsOperation.html#vips-call
144
+ vips_operation.name.to_s,
145
+ # Loaders only take the source path as a required argument.
146
+ [src_path],
147
+ # Operation-appropriate options Hash
148
+ options,
149
+ # `:options_string`, unused since we have everything in our Hash.
150
+ ''.freeze,
151
+ )
152
+ }
153
+ }
154
+
155
+
156
+ # Returns a Vips::Image from a file source.
157
+ # TODO: Get rid of this method! This is an old entrypoint.
158
+ # Consume lower Types as a Change once we support Change chaining, then execute a chain.
159
+ def to_vips_image(change = nil)
160
+ @vips_image ||= begin
161
+ lower_config = the_setting_sun(:lower_world, *(type_mars.first&.settings_paths)) || Hash.new
162
+ atoms = Hash.new
163
+ lower_world[type_mars.first].values.reduce(&:concat).each_pair { |aka, compound|
164
+ next if aka != compound.element # Skip alias Compounds since they will all be handled at once.
165
+ atoms.store(compound.element, Cooltrainer::Atom.new(compound.isotopes.reduce(nil) { |value, isotope|
166
+ value || lower_config.fetch(isotope, nil) || context_arguments&.fetch(isotope, nil)
167
+ }, compound.default))
168
+ }
169
+ self.send(type_mars.first.distorted_open_method, **atoms.transform_values(&:get))
170
+ end
171
+ end
172
+
173
+
174
+ end
@@ -0,0 +1,268 @@
1
+ require 'set'
2
+ require 'distorted-floor/modular_technology/vips/ffi'
3
+
4
+ require 'distorted-floor/checking_you_out'
5
+ require 'distorted-floor/element_of_media'
6
+
7
+
8
+ module Cooltrainer; end
9
+ module Cooltrainer::DistorteD; end
10
+ module Cooltrainer::DistorteD::Technology; end
11
+ module Cooltrainer::DistorteD::Technology::Vips
12
+
13
+
14
+ # 🄵🄸🄽🄳 🅃🄷🄴 🄲🄾🄼🄿🅄🅃🄴🅁 🅁🄾🄾🄼
15
+ # 🄵🄸🄽🄳 🅃🄷🄴 🄲🄾🄼🄿🅄🅃🄴🅁 🅁🄾🄾🄼
16
+ # 🄵🄸🄽🄳 🅃🄷🄴 🄲🄾🄼🄿🅄🅃🄴🅁 🅁🄾🄾🄼
17
+ Vips::vips_vector_set_enabled(1)
18
+
19
+
20
+ # All of the actual Loader/Saver classes we need to interact with
21
+ # will be tree children of one of these top-level class categories:
22
+ TOP_LEVEL_FOREIGN = :VipsForeign
23
+ TOP_LEVEL_LOADER = :VipsForeignLoad
24
+ TOP_LEVEL_SAVER = :VipsForeignSave
25
+
26
+
27
+ # Aliases we want to support for consistency and accessibility.
28
+ VIPS_ALIASES = {
29
+ :Q => Set[:Q, :quality],
30
+ :colours => Set[:colours, :colors],
31
+ :centre => Set[:centre, :center], # America; FUCK YEAH!
32
+ }
33
+
34
+
35
+ # GEnum valid values are detectable, but I don't know how to do the same
36
+ # for the numeric parameters. Specify them here manually for now.
37
+ VIPS_VALID = {
38
+ :"page-height" => (0..Vips::MAX_COORD),
39
+ :"quant-table" => (0..8),
40
+ :Q => (0..100),
41
+ :colours => (2..256),
42
+ :dither => (0.0..1.0),
43
+ :compression => (0..9),
44
+ :"alpha-q" => (0..100),
45
+ :"reduction-effort" => (0..6),
46
+ :kmin => (0..0x7FFFFFFF), # https://en.wikipedia.org/wiki/2,147,483,647
47
+ :kmax => (0..0x7FFFFFFF),
48
+ :"tile-width" => (0..0x8000), # 32768
49
+ :"tile-height" => (0..0x8000),
50
+ :xres => (0.001..1e+06),
51
+ :yres => (0.001..1e+06),
52
+ }
53
+
54
+
55
+ # Encapsulate any VipsObject descendant based on GType ID or name
56
+ VipsType = Struct.new(:id) do
57
+ def initialize(id_or_name)
58
+ super(id_or_name.is_a?(Integer) ? id_or_name : GObject::g_type_from_name(id_or_name.to_s))
59
+ end
60
+ def name; GObject::g_type_name(self.id); end
61
+ def to_s; self.name.to_s; end
62
+ def to_sym; self.name.to_sym; end
63
+ def nickname; Vips::nickname_find(self.id); end
64
+ def inspect; "#<#{self.name}>"; end
65
+
66
+ # Returns an Array[String] of VipsForeign suffixes.
67
+ #
68
+ # Suffixes are defined in a NULL-terminated C Array in each VIPS class:
69
+ # https://github.com/libvips/libvips/search?p=3&q=suffs%5B%5D
70
+ # It's kinda silly but the best way to discover these is parse them
71
+ # out of the single-line String descriptions as seen in `vips -l`.
72
+ def suffixes
73
+ @suffixes ||= begin
74
+ # vips_class_find returns an FFI::Pointer, and we can use our own class name as the first param:
75
+ # irb> Vips::vips_class_find('VipsForeignSaveJpegFile', 'jpegsave')
76
+ # => #<FFI::Pointer address=0x00005592a24bac40>
77
+ vips_class_pointer = Vips::vips_class_find(TOP_LEVEL_FOREIGN.to_s, nickname)
78
+ return Array.new if vips_class_pointer.null?
79
+ # 2K buffer should always be big enough to read single-line Strings like these:
80
+ # VipsForeignLoadJpegFile (jpegload), load jpeg from file (.jpg, .jpeg, .jpe), priority=50, is_a, get_flags, header, load
81
+ buf_struct = Vips::BufStruct.new
82
+ buf_struct_string = ::FFI::MemoryPointer.new(:char, 2048)
83
+ buf_struct[:base] = buf_struct_string
84
+ buf_struct[:mx] = 2048
85
+ Vips::vips_object_summary_class(vips_class_pointer, buf_struct.pointer)
86
+ class_summary = buf_struct_string.read_string
87
+
88
+ # Parse an Array[String] of file extensions out of a class summary.
89
+ class_summary.scan(/\.\w+\.?\w+/)
90
+ end
91
+ end # suffixes
92
+
93
+ # Returns a Hash[aka] => Compound based on this VipsType's VipsArguments.
94
+ def options
95
+ @options ||= Hash.new.tap { |options|
96
+ # `:vips_argument_map` itself will return void/nil, so we need to give it a function that modifies an existing Hash.
97
+ Vips::vips_argument_map(
98
+ # `:vips_operation_new` takes a String argument of the nickname, not the fullname:
99
+ # https://libvips.github.io/libvips/API/current/VipsOperation.html#vips-operation-new
100
+ Vips::vips_operation_new(self.nickname),
101
+ # `:vips_argument_map`'s second argument is a VipsArgumentMapFn to call for each VipsArgument:
102
+ # https://libvips.github.io/libvips/API/current/VipsObject.html#VipsArgumentMapFn
103
+ Proc.new { |_vips_object, param_spec, argument_class, _argument_instance|
104
+ flags = argument_class[:flags]
105
+ if (flags & Vips::ARGUMENT_INPUT) != 0 # We only want "input" arguments
106
+ # …and we also only want optional non-deprecated arguments.
107
+ if (flags & Vips::ARGUMENT_REQUIRED) == 0 && (flags & Vips::ARGUMENT_DEPRECATED) == 0
108
+ # ParameterSpec name will be a String e.g. 'Q' or 'interlace' or 'page-height'
109
+ element = param_spec[:name].to_sym
110
+
111
+ # `magicksave` takes an argument `format` to choose one of its many supported types,
112
+ # but that selection in DistorteD-land is via `::CHECKING::YOU::OUT()`, so this option should be dropped.
113
+ # https://github.com/libvips/libvips/blob/4de9b56725862edf872ae503a3dfb4cf05da9e77/libvips/foreign/magicksave.c#L455~L460
114
+ next if element == :format
115
+
116
+ Cooltrainer::Compound.new(
117
+ # Support aliasing options like 'Q' into 'quality' for consistency
118
+ # and 'colours' into 'colors' for accessibility.
119
+ VIPS_ALIASES.dig(element) || Set[element],
120
+ # Some libvips drivers seem to have mixed-leading-case options,
121
+ # like ppmsave and webp save for example:
122
+ # https://github.com/libvips/libvips/blob/4de9b56725862edf872ae503a3dfb4cf05da9e77/libvips/foreign/ppmsave.c#L396~L415
123
+ # https://github.com/libvips/libvips/blob/4de9b56725862edf872ae503a3dfb4cf05da9e77/libvips/foreign/webpsave.c#L152
124
+ blurb: GObject::g_param_spec_get_blurb(param_spec).tap { |blurb| blurb[0] = blurb[0].capitalize },
125
+ default: self.class.get_argument_default(param_spec),
126
+ valid: self.class.get_argument_valid_values(param_spec),
127
+ ).tap { |compound|
128
+ # Add the Compound for every alias
129
+ compound.isotopes.each{ |isotope| options.store(isotope, compound) }
130
+ }
131
+ end
132
+ end
133
+
134
+ # This isn't really a 'Saver' Option — rather an argument to a separate
135
+ # :smartcrop or :thumbnail VIPS method we can call, but I want to offer
136
+ # this option on every Type and use it to control the method we call
137
+ # to write the image.
138
+ # TODO: Handle VipsThumbnailImage's (and other Operations') full `:options` and remove this one-off.
139
+ # This is here as a temporary shim for feature parity during refactoring.
140
+ # TODO: VipsType#parent method so we can check upward for :VipsForeign heritage.
141
+ if self.name.include?('Foreign'.freeze) and self.name.include?('Save'.freeze)
142
+ options.store(:crop, self.class.new(:VipsThumbnailImage).options.fetch(:crop, nil))
143
+ end
144
+ },
145
+ nil, # `:a` "Client data"
146
+ nil, # `:b` "Client data"
147
+ )
148
+ }
149
+ end
150
+
151
+ # Returns a Set[CHECKING::YOU::OUT] based on our suffixes.
152
+ def types
153
+ @types ||= begin
154
+ # We will likely get duplicate suffixes for a single `::CHECKING::YOU::OUT`, but we may also get suffixes for multiple Types:
155
+ # irb> Cooltrainer::DistorteD::Technology::Vips::FFI::VipsType.new('VipsForeignSaveJpegFile').suffixes
156
+ # => [".jpg", ".jpeg", ".jpe"]
157
+ # irb> Cooltrainer::DistorteD::Technology::Vips::FFI::VipsType.new('VipsForeignSaveMagickFile').suffixes
158
+ # => [".gif", ".bmp"]
159
+ # TODO: CYO shouldn't make us prepend '*'.
160
+ # TODO: Warn when we don't have a CYO match for a VIPS suffix (taken care of by `#compact` for now).
161
+ # TODO: Fix mis-detection of Radiance HDR and fix handling of `Set`s here (currently `#flatten`ed)
162
+ self.suffixes&.map { _1.prepend(-?*) }.map(&::CHECKING::YOU::OUT::method(:from_postfix)).compact.to_set.flatten
163
+ end
164
+ end # types
165
+
166
+ # Returns an Array[VipsType] of our direct children.
167
+ def children
168
+ # https://libvips.github.io/libvips/API/current/VipsObject.html#vips-type-map
169
+ # "Map over a type's children. Stop when fn returns non-nil and return that value."
170
+ child_ids = Array.new
171
+ # vips_type_map will return an FFI::Pointer, so we can't return it directly.
172
+ Vips::vips_type_map(self.id, child_ids.method(:append).to_proc, nil)
173
+ # Calling :append in :vips_type_map will append a g_type as well as a FFI::Pointer to 0x0,
174
+ # so filter the pointers out (Interger g_types only), then turn it each child
175
+ # into another Hash of it to its children.
176
+ child_ids.select { |c| c.is_a?(Integer) }.map(&self.class.method(:new))
177
+ end
178
+
179
+ # Returns a Hash[self] => Array[VipsType/Hash] of our children and all of their childrens' children.
180
+ # Array members will be another Hash[child] => Array[grandchildren] if our children
181
+ # have any children, or just the child VipsType if it doesn't.
182
+ def family_tree
183
+ # Return only ourselves as a value instead of another empty Hash if we have no children.
184
+ children.empty? ? self : Hash[self => children.map(&:family_tree)]
185
+ end
186
+
187
+ # Returns an Array[VipsType] of all of our children and all of their children and
188
+ def family_reunion
189
+ self.children.each_with_object(Array[self]) { |child, family|
190
+ family.push(*child.family_reunion)
191
+ }
192
+ end
193
+
194
+ # Returns an `Array[VipsType]` of loaders/savers given a filename, suffix, or `::CHECKING::YOU::OUT`.
195
+ def self.loader_for(given); self.foreign_for(TOP_LEVEL_LOADER, given); end
196
+ def self.saver_for(given); self.foreign_for(TOP_LEVEL_SAVER, given); end
197
+
198
+ # Returns a Hash[CHECKING::YOU::OUT] => Set[VipsType] of loaders/savers.
199
+ def self.loader_types; self.foreign_types(TOP_LEVEL_LOADER); end
200
+ def self.saver_types; self.foreign_types(TOP_LEVEL_SAVER); end
201
+
202
+ private
203
+
204
+ # Helper method that returns an `Array[VipsType]` given a top-level `VipsType` and a filename, suffix, or `::CHECKING::YOU::OUT`.
205
+ def self.foreign_for(top_level, given)
206
+ search = case given
207
+ when Array then given
208
+ when ::CHECKING::YOU::OUT then Array[given]
209
+ when String then given.include?('/'.freeze) ? Array[::CHECKING::YOU::OUT::from_ietf_media_type(given)] : ::CHECKING::YOU::OUT(given)
210
+ end
211
+ self.new(top_level).family_reunion.select { |vt| vt.types&.intersection(search)&.length&.method(:>)&.call(0) }
212
+ end
213
+
214
+ # Helper method that returns a Hash[CHECKING::YOU::OUT] => Set[VipsType] given a top-level VipsType.
215
+ def self.foreign_types(top_level)
216
+ self.new(top_level).family_reunion.each_with_object(
217
+ Hash.new { |h,k| h[k] = Set.new }
218
+ ) { |operation, types|
219
+ next if operation.types.nil?
220
+ operation.types.each { |type| types[type].add(operation) }
221
+ }
222
+ end
223
+
224
+ # Returns a default value (type variable) given a GParamSpec
225
+ def self.get_argument_default(param_spec)
226
+ begin
227
+ default_pointer = GObject::g_param_spec_get_default_value(param_spec)
228
+ default = GObject::GValue.new(default_pointer)
229
+ return nil if default.null?
230
+ return default.get
231
+ rescue ::FFI::NullPointerError => npe
232
+ # For some reason the :null? check doesn't catch NPEs from `get_array_of_float64`
233
+ # which I think is the GBoxed GType like VipsArrayDouble
234
+ nil
235
+ end
236
+ end
237
+
238
+ # Returns a Range, Enumerable, Class, or other value constraint.
239
+ def self.get_argument_valid_values(param_spec)
240
+ # HACK: Define Ranges manually until I figure out how to introspect them.
241
+ if VIPS_VALID.has_key?(param_spec[:name].to_sym)
242
+ return VIPS_VALID[param_spec[:name].to_sym]
243
+ end
244
+
245
+ return case GObject::g_type_fundamental(param_spec[:value_type])
246
+ when GObject::GENUM_TYPE
247
+ # I think the """proper""" way to do this is with gobject-introspection,
248
+ # but it's way simpler for now to just iterate until we find the terminating NULL.
249
+ Set.new.tap { |values|
250
+ loop.with_index { |_, i|
251
+ value = Vips::vips_enum_nick(param_spec[:value_type], i)
252
+ break if value == '(null)'.freeze or i > 33 # Safety factor in case the String ever differs
253
+ values.add(value)
254
+ }
255
+ }.map(&:to_sym) # TODO: Fix value aliasing
256
+ when GObject::GBOOL_TYPE then Set[false, true]
257
+ when GObject::GDOUBLE_TYPE then Float
258
+ when GObject::GINT_TYPE then Integer
259
+ when GObject::GUINT64_TYPE then Integer
260
+ when GObject::GBOXED_TYPE then nil # TODO: Something besides nil
261
+ else nil
262
+ end
263
+ end
264
+
265
+ end # VipsType Struct
266
+
267
+
268
+ end
@@ -0,0 +1,135 @@
1
+ require 'set'
2
+
3
+ require 'distorted-floor/checking_you_out'
4
+ using ::DistorteD::CHECKING::YOU::OUT
5
+
6
+ require 'distorted-floor/modular_technology/vips/operation'
7
+
8
+
9
+ module Cooltrainer; end
10
+ module Cooltrainer::DistorteD; end
11
+ module Cooltrainer::DistorteD::Technology; end
12
+ module Cooltrainer::DistorteD::Technology::Vips::Save
13
+
14
+
15
+ # There is one (only one) native libvips image format, with file extname `.vips`.
16
+ # As I write this—running libvips 8.8—the :get_suffixes function does not include
17
+ # its own '.vips' as a supported extension.
18
+ # There also (as of mid 2020) seems to be no official media-type assigned
19
+ # for VIPS format, so I am going to make one up in `::CHECKING::YOU::OUT`'s local-data.
20
+ # - Raw pixel data
21
+ #
22
+ # [RAW]: https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-rawload
23
+ # https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-csvload
24
+ #
25
+ # Most libvips installations, even very minimally-built ones,
26
+ # will almost certainly support a few very common formats via the usual libraries:
27
+ # - JPEG with libjpeg.
28
+ # - PNG with libpng.
29
+ # - GIF with giflib.
30
+ # - WebP with libwebp.
31
+ # - TIFF with libtiff.
32
+ #
33
+ # Normal libvips installations probably also support many less-mainstream formats:
34
+ # - HEIF/HEIC with libheif.
35
+ # - ICC profiles with liblcms2.
36
+ # - Matlab with matio/libhdf5.
37
+ # - FITS★ with cfitsio.
38
+ # - Styled text with Pango/ft2.
39
+ # - Saving GIF/BMP with Magick.
40
+ # NOTE that GIFs are *loaded* using giflib.
41
+ # - Various simple ASCII/binary-based formats with libgsf★
42
+ # · Comma-separated values
43
+ # · Netpbm★
44
+ # · VIPS (non-Matlab) matrices★
45
+ #
46
+ # [NETPBM]: https://en.wikipedia.org/wiki/Netpbm#File_formats
47
+ # [LIBGSF]: https://developer.gnome.org/gsf/
48
+ # [MATRIX]: https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-matrixload
49
+
50
+ # Vips allows us to query supported *SAVE* types based on String file suffixes defined in Saver C code.
51
+ # irb(main)> Vips.get_suffixes
52
+ # => [".csv", ".mat", ".v", ".vips", ".ppm", ".pgm", ".pbm", ".pfm",
53
+ # ".hdr", ".dz", ".png", ".jpg", ".jpeg", ".jpe", ".webp", ".tif",
54
+ # ".tiff", ".fits", ".fit", ".fts", ".gif", ".bmp"]
55
+ #
56
+ # Vips chooses Loader modules, on the other hand, by sniffing the first few bytes of the file,
57
+ # so a list of file extensions for supported loadable formats won't always be complete.
58
+ # For example, SVG and PDF are usually supported as loaders (via rsvg and PDFium/Poppler)
59
+ # but are nowhere to be found in the Saver-based `:get_suffixes`:
60
+ # https://github.com/libvips/ruby-vips/issues/186
61
+ OUTER_LIMITS = Cooltrainer::DistorteD::Technology::Vips::VipsType::saver_types.keep_if { |type, _operations|
62
+ # Skip textual formats like CVSV image data, and skip mistakenly-detected font Types.
63
+ #
64
+ # Suffix-based Loader detection with the `mime-types` library/database we use
65
+ # causes us to detect a Netpbm PortableFloatmap as an Adobe Printer Font Metrics file:
66
+ # https://en.wikipedia.org/wiki/Netpbm#32-bit_extensions
67
+ !type.to_s.include?(-'text') and !type.to_s.include?(-'font')
68
+ }.transform_values { |v| v.map(&:options).reduce(&:merge) }
69
+
70
+ # Define a to_<mediatype>_<subtype> method for each `::CHECKING::YOU::OUT` supported by libvips,
71
+ # e.g. a supported Type 'image/png' will define a method :to_image_png in any
72
+ # context where this module is included.
73
+ self::OUTER_LIMITS.each_key { |t|
74
+ next if t.nil?
75
+ define_method(t.distorted_file_method) { |dest_root, change|
76
+ # Find a VipsType Struct for the saver operation
77
+ vips_operation = Cooltrainer::DistorteD::Technology::Vips::VipsType::saver_for(change.type).first
78
+
79
+ # Prepare a Hash of options appropriate for this operation.
80
+ # We explicitly declare all supported VipsArguments, using the FFI-detected
81
+ # default values for keys with no user-given value.
82
+ options = change.to_hash.slice(
83
+ # Get an Array[Symbol] of non-aliased option keys.
84
+ # Loading any aliases' values happens when the Change/Atoms are constructed.
85
+ *vips_operation.options.keep_if { |aka, compound| aka == compound.element }.keys
86
+ ).reject { |k,v|
87
+ # Skip options we manually added (like :crop) or ones with nil values.
88
+ # TODO: Handle all VipsOperation arguments (like :crop) automatically.
89
+ [k == :crop, v.nil?].any?
90
+ }.transform_keys { |k|
91
+ # The `ruby-vips` binding expects hyphenated argument names to be converted to underscores:
92
+ # https://github.com/libvips/ruby-vips/blob/4f696e30796adcc99cbc70ff7fd778439f0cbac7/lib/vips/operation.rb#L78-L80
93
+ k.to_s.gsub('-', '_').to_sym
94
+ }
95
+
96
+ # HACK: MagickSave needs us to specify the 'delegate' (ImageMagick-speak)
97
+ # via the :format VipsAttribute that we skipped when generating Compounds.
98
+ # TODO: Use VipsType#parents once it exists instead of checking :include? on a String.
99
+ # TODO: Choose the delegate more directly/intelligently than by just downcasing the type,
100
+ # e.g. 'GIF -> 'gif'. It does work, but this seems fragile.
101
+ if vips_operation.to_s.include?('VipsForeignSaveMagick')
102
+ options.store(:format, change.type.genus.downcase)
103
+ end
104
+
105
+ loaded_image = to_vips_image(change)
106
+
107
+ # Assume the first destination_path has a :nil limit-break.
108
+ change.paths(dest_root).zip(Array[nil].concat(change.breaks)).each { |(dest_path, width)|
109
+ # Chain a call to VipsThumbnailImage into our input Vips::Image iff we were given a width.
110
+ # TODO: Exand this to aarbitrary other operations and consume their options Hash e.g.
111
+ # Cooltrainer::DistorteD::Technology::Vips::VipsType.new(:VipsThumbnailImage).options
112
+ input_image = (width or not [nil, :none].include?(change.to_hash.fetch(:crop, nil))) ?
113
+ loaded_image.thumbnail_image(loaded_image.width, crop: change.to_hash.fetch(:crop, :none)) :
114
+ loaded_image
115
+ # Do the thing.
116
+ Vips::Operation.call(
117
+ # `:vips_call` expects the operation_name to be a String:
118
+ # https://libvips.github.io/libvips/API/current/VipsOperation.html#vips-call
119
+ vips_operation.name.to_s,
120
+ # Write what Vips::Image, to where?
121
+ [input_image, dest_path],
122
+ # Operation-appropriate options Hash
123
+ options,
124
+ # `:options_string`, unused since we have everything in our Hash.
125
+ ''.freeze,
126
+ )
127
+ }
128
+
129
+ # Vips::Image#write_gc is a private method, but the built-in
130
+ # :write_to_file/:write_to_buffer methods call it, so we should call it too.
131
+ loaded_image.send(:write_gc)
132
+ }
133
+ }
134
+
135
+ end
@@ -0,0 +1,17 @@
1
+ require 'set'
2
+
3
+ require 'distorted-floor/checking_you_out'
4
+
5
+ require 'distorted-floor/modular_technology/vips/load'
6
+ require 'distorted-floor/modular_technology/vips/save'
7
+
8
+
9
+ module Cooltrainer; end
10
+ module Cooltrainer::DistorteD; end
11
+ module Cooltrainer::DistorteD::Technology; end
12
+ module Cooltrainer::DistorteD::Technology::Vips
13
+
14
+ include Cooltrainer::DistorteD::Technology::Vips::Save
15
+ include Cooltrainer::DistorteD::Technology::Vips::Load
16
+
17
+ end