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