distorted-floor 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.
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