distorted-floor 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +32 -0
- data/bin/distorted-floor +16 -0
- data/bin/repl +14 -0
- data/bin/setup +8 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Less_Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/More_Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/less_more_perfect_dos_vga_437.html +52 -0
- data/font/1252/LICENSE/PerfectDOSVGA437/font-comment.php@file=perfect_dos_vga_437.html +5 -0
- data/font/1252/LessPerfectDOSVGA.ttf +0 -0
- data/font/1252/MorePerfectDOSVGA.ttf +0 -0
- data/font/1252/Perfect DOS VGA 437 Win.ttf +0 -0
- data/font/437/Perfect DOS VGA 437.ttf +0 -0
- data/font/437/dos437.txt +72 -0
- data/font/65001/Anonymous Pro B.ttf +0 -0
- data/font/65001/Anonymous Pro BI.ttf +0 -0
- data/font/65001/Anonymous Pro I.ttf +0 -0
- data/font/65001/Anonymous Pro.ttf +0 -0
- data/font/65001/LICENSE/AnonymousPro/FONTLOG.txt +45 -0
- data/font/65001/LICENSE/AnonymousPro/OFL-FAQ.txt +235 -0
- data/font/65001/LICENSE/AnonymousPro/OFL.txt +94 -0
- data/font/65001/LICENSE/AnonymousPro/README.txt +55 -0
- data/font/850/ProFont-Bold-01/LICENSE +22 -0
- data/font/850/ProFont-Bold-01/readme.txt +28 -0
- data/font/850/ProFontWindows-Bold.ttf +0 -0
- data/font/850/ProFontWindows.ttf +0 -0
- data/font/850/Profont/LICENSE +22 -0
- data/font/850/Profont/readme.txt +31 -0
- data/font/932/LICENSE/README-ttf.txt +213 -0
- data/font/932/mona.ttf +0 -0
- data/lib/distorted-floor/checking_you_out.rb +78 -0
- data/lib/distorted-floor/click_again.rb +406 -0
- data/lib/distorted-floor/element_of_media/change.rb +114 -0
- data/lib/distorted-floor/element_of_media/compound.rb +120 -0
- data/lib/distorted-floor/element_of_media.rb +2 -0
- data/lib/distorted-floor/error_code.rb +55 -0
- data/lib/distorted-floor/floor.rb +17 -0
- data/lib/distorted-floor/invoker.rb +100 -0
- data/lib/distorted-floor/media_molecule/font.rb +200 -0
- data/lib/distorted-floor/media_molecule/image.rb +33 -0
- data/lib/distorted-floor/media_molecule/pdf.rb +45 -0
- data/lib/distorted-floor/media_molecule/svg.rb +46 -0
- data/lib/distorted-floor/media_molecule/text.rb +247 -0
- data/lib/distorted-floor/media_molecule/video.rb +21 -0
- data/lib/distorted-floor/media_molecule.rb +58 -0
- data/lib/distorted-floor/modular_technology/gstreamer.rb +175 -0
- data/lib/distorted-floor/modular_technology/pango.rb +90 -0
- data/lib/distorted-floor/modular_technology/ttfunk.rb +48 -0
- data/lib/distorted-floor/modular_technology/vips/ffi.rb +66 -0
- data/lib/distorted-floor/modular_technology/vips/load.rb +174 -0
- data/lib/distorted-floor/modular_technology/vips/operatio$.rb +268 -0
- data/lib/distorted-floor/modular_technology/vips/save.rb +135 -0
- data/lib/distorted-floor/modular_technology/vips.rb +17 -0
- data/lib/distorted-floor/monkey_business/encoding.rb +374 -0
- data/lib/distorted-floor/monkey_business/hash.rb +18 -0
- data/lib/distorted-floor/monkey_business/set.rb +15 -0
- data/lib/distorted-floor/monkey_business/string.rb +6 -0
- data/lib/distorted-floor.rb +2 -0
- 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
|