rublurhash 0.0.1

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.
data/ext/Cargo.toml ADDED
@@ -0,0 +1,14 @@
1
+ [package]
2
+ name = "rublurhash"
3
+ version = "0.0.1"
4
+ edition = "2024"
5
+ authors = ["Kitait Makoto <KitaitiMakoto@gmail.com>"]
6
+ publish = false
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ magnus = ">= 0.8.2"
13
+ blurhash-core = { version = "0.1.0", features = ["simd", "gpu"] }
14
+ grey-knights = "=0.1.1"
data/ext/src/lib.rs ADDED
@@ -0,0 +1,243 @@
1
+ use grey_knights::memory_view::{
2
+ Builder, Entry, Flags, FlagsChainable, ValidatedMemoryView, register,
3
+ };
4
+ use magnus::{
5
+ Attr, Module, Object, RArray, RClass, RModule, RString, Ruby, TryConvert, Value,
6
+ encoding::EncodingCapable,
7
+ error::IntoError,
8
+ function,
9
+ scan_args::{get_kwargs, scan_args},
10
+ value::ReprValue,
11
+ };
12
+ use std::num::TryFromIntError;
13
+
14
+ enum Error {
15
+ Arg(String),
16
+ Runtime(String),
17
+ Magnus(magnus::Error),
18
+ Blurhash(blurhash_core::BlurhashError),
19
+ }
20
+
21
+ impl magnus::error::IntoError for Error {
22
+ fn into_error(self, ruby: &Ruby) -> magnus::Error {
23
+ use Error::*;
24
+
25
+ match self {
26
+ Arg(msg) => magnus::Error::new(ruby.exception_arg_error(), msg),
27
+ Runtime(msg) => magnus::Error::new(ruby.exception_runtime_error(), msg),
28
+ Magnus(err) => err,
29
+ Blurhash(err) => magnus::Error::new(ruby.exception_runtime_error(), format!("{}", err)),
30
+ }
31
+ }
32
+ }
33
+
34
+ impl From<magnus::Error> for Error {
35
+ fn from(value: magnus::Error) -> Self {
36
+ Self::Magnus(value)
37
+ }
38
+ }
39
+
40
+ impl From<blurhash_core::BlurhashError> for Error {
41
+ fn from(value: blurhash_core::BlurhashError) -> Self {
42
+ Self::Blurhash(value)
43
+ }
44
+ }
45
+
46
+ impl From<TryFromIntError> for Error {
47
+ fn from(value: TryFromIntError) -> Self {
48
+ Self::Arg(format!("{}", value))
49
+ }
50
+ }
51
+
52
+ type Result<T> = std::result::Result<T, Error>;
53
+
54
+ fn encode(ruby: &Ruby, arguments: &[Value]) -> Result<String> {
55
+ let args = scan_args::<(Value,), (), (), (), _, ()>(arguments)?;
56
+ let kws = get_kwargs::<_, (u32, u32), (Option<Option<u32>>, Option<Option<u32>>), ()>(
57
+ args.keywords,
58
+ &["components_x", "components_y"],
59
+ &["width", "height"],
60
+ )?;
61
+ let (components_x, components_y) = kws.required;
62
+ let (width, height) = kws.optional;
63
+ let width = width.flatten();
64
+ let height = height.flatten();
65
+ match (width, height) {
66
+ (None, None) => {
67
+ let pxs = args.required.0;
68
+ let memview = ValidatedMemoryView::<u8>::new(
69
+ pxs,
70
+ Flags::format().multi_dimensional().row_major(),
71
+ )?;
72
+ // TODO: Support ndim == 2 && format == "CCC"
73
+ if memview.ndim() != 3_isize {
74
+ return Err(Error::Arg("ndim must be 3".into()));
75
+ }
76
+ let shape = memview
77
+ .shape()
78
+ .ok_or_else(|| Error::Arg("shape must not be null".into()))?;
79
+ if shape.len() != 3 {
80
+ return Err(Error::Arg("shape length must be 3".into()));
81
+ }
82
+ if shape[2] != 3 {
83
+ return Err(Error::Arg("shape[2] must be 3".into()));
84
+ }
85
+ if !memview.is_row_major_contiguous() {
86
+ return Err(Error::Arg("must be row-major contiguous".into()));
87
+ }
88
+ if memview.format().is_some() {
89
+ let item_desc = memview.item_desc()?;
90
+ if item_desc.len() != 1 {
91
+ return Err(Error::Arg(format!(
92
+ "item_desc length must be 1, but was {}",
93
+ item_desc.len()
94
+ )));
95
+ }
96
+ let component = item_desc
97
+ .into_iter()
98
+ .next()
99
+ .expect("item_desc length is exactly 1");
100
+ if component.format != 'C' || component.repeat != 1 {
101
+ return Err(Error::Arg(format!(
102
+ "format must be NULL or \"C\", but was {}",
103
+ component.format
104
+ )));
105
+ }
106
+ }
107
+ blurhash_core::gpu::encode_gpu(
108
+ memview.data(),
109
+ u32::try_from(shape[1])?,
110
+ u32::try_from(shape[0])?,
111
+ components_x,
112
+ components_y,
113
+ )
114
+ .map_err(Error::Blurhash)
115
+ }
116
+ (Some(width), Some(height)) => {
117
+ let pixels = arguments[0];
118
+ if RArray::from_value(pixels).is_some() {
119
+ blurhash_core::gpu::encode_gpu(
120
+ Vec::<u8>::try_convert(pixels)?.as_slice(),
121
+ width,
122
+ height,
123
+ components_x,
124
+ components_y,
125
+ )
126
+ } else if let Some(pxs) = RString::from_value(pixels) {
127
+ if pxs.enc_get() != ruby.ascii8bit_encindex() {
128
+ return Err(Error::Arg("Not ASCII-8BIT".into()));
129
+ }
130
+ blurhash_core::gpu::encode_gpu(
131
+ unsafe { pxs.as_slice() },
132
+ width,
133
+ height,
134
+ components_x,
135
+ components_y,
136
+ )
137
+ } else {
138
+ return Err(Error::Arg(format!(
139
+ "First argument must be Array or String, but given: {}",
140
+ pixels.class()
141
+ )));
142
+ }
143
+ .map_err(Error::Blurhash)
144
+ }
145
+ _ => Err(Error::Arg(
146
+ "Both width and height are required if given".into(),
147
+ )),
148
+ }
149
+ }
150
+
151
+ struct ImageString();
152
+
153
+ impl ImageString {
154
+ fn init(ruby: &Ruby, rb_self: Value, width: u32, height: u32) -> Result<()> {
155
+ let s = RString::from_value(rb_self)
156
+ .ok_or_else(|| Error::Runtime("Failed to create new instance of ImageString".into()))?;
157
+ s.ivar_set("@width", ruby.integer_from_u64(width.into()))?;
158
+ s.ivar_set("@height", ruby.integer_from_u64(height.into()))?;
159
+ s.freeze();
160
+
161
+ Ok(())
162
+ }
163
+ }
164
+
165
+ impl Entry for ImageString {
166
+ type PrivateData = ();
167
+
168
+ fn get(
169
+ obj: Value,
170
+ view: &mut Builder<Self::PrivateData>,
171
+ flags: &Flags,
172
+ ) -> std::result::Result<(), magnus::Error> {
173
+ let ruby = Ruby::get_with(obj);
174
+ if flags.has_writable() {
175
+ return Err(magnus::Error::new(
176
+ ruby.exception_arg_error(),
177
+ "Cannot be writable",
178
+ ));
179
+ }
180
+ if flags.has_indirect() {
181
+ return Err(magnus::Error::new(
182
+ ruby.exception_arg_error(),
183
+ "Cannot be indirect",
184
+ ));
185
+ }
186
+ let Some(rstring) = RString::from_value(obj) else {
187
+ return Err(magnus::Error::new(
188
+ ruby.exception_type_error(),
189
+ "Invalid type",
190
+ ));
191
+ };
192
+ let width: isize = rstring.ivar_get("@width")?;
193
+ let height: isize = rstring.ivar_get("@height")?;
194
+ unsafe { view.data(rstring.as_slice()) }
195
+ .format("C")
196
+ .map_err(|e| e.into_error(&ruby))?
197
+ .shape([height, width, 3]);
198
+
199
+ Ok(())
200
+ }
201
+
202
+ fn is_available(_obj: Value) -> bool {
203
+ true
204
+ }
205
+ }
206
+
207
+ fn decode(ruby: &Ruby, arguments: &[Value]) -> Result<Value> {
208
+ let args = scan_args::<(String,), (), (), (), _, ()>(arguments)?;
209
+ let (width, height, punch) = get_kwargs::<_, (u32, u32, f64), (), ()>(
210
+ args.keywords,
211
+ &["width", "height", "punch"],
212
+ &[],
213
+ )?
214
+ .required;
215
+ let blurhash = args.required.0;
216
+
217
+ let decoded = blurhash_core::gpu::decode_gpu(&blurhash, width, height, punch)?;
218
+
219
+ let rstring = ruby.str_from_slice(decoded.as_slice());
220
+ let module: RModule = ruby.class_object().const_get("RuBlurHash")?;
221
+ let img_class: RClass = module.const_get("ImageString")?;
222
+ let instance = img_class.funcall("__send__", ("new", rstring))?;
223
+ ImageString::init(ruby, instance, width, height)?;
224
+
225
+ Ok(instance)
226
+ }
227
+
228
+ #[magnus::init]
229
+ fn init(ruby: &Ruby) -> Result<()> {
230
+ let module = ruby.define_module("RuBlurHash")?;
231
+ module.define_module_function("encode", function!(encode, -1))?;
232
+ module.define_module_function("decode", function!(decode, -1))?;
233
+
234
+ let image_string = module.define_class("ImageString", ruby.class_string())?;
235
+ let _: Value = image_string
236
+ .singleton_class()?
237
+ .funcall("private", ("new",))?;
238
+ image_string.define_attr("width", Attr::Read)?;
239
+ image_string.define_attr("height", Attr::Read)?;
240
+ register::<ImageString>(image_string)?;
241
+
242
+ Ok(())
243
+ }
@@ -0,0 +1,28 @@
1
+ require "json"
2
+
3
+ rust_metadata = JSON.load(`cargo metadata --manifest-path=ext/Cargo.toml --no-deps --format-version=1`)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rublurhash"
7
+ s.version = rust_metadata["packages"][0]["version"]
8
+ s.authors = ["Kitaiti Makoto"]
9
+ s.licenses = "MIT"
10
+ s.summary = "Fast BlurHash implementation."
11
+ s.description = "Ruby binding for blurhash-core, a fast Rust implementation of BlurHash."
12
+ s.homepage = "https://gitlab.com/KitaitiMakoto/rublurhash"
13
+
14
+ s.files = Dir.chdir(__dir__) {`git ls-files -z`.split("\x0")}
15
+ s.extensions = ["ext/Cargo.toml"]
16
+ s.test_files = s.files.select {|file| file.start_with?("test/")}
17
+
18
+ s.add_development_dependency "rake"
19
+ s.add_development_dependency "kar"
20
+ s.add_development_dependency "test-unit"
21
+ s.add_development_dependency "test-unit-notify"
22
+ s.add_development_dependency "terminal-notifier" if RUBY_PLATFORM.match? /darwin/
23
+ s.add_development_dependency "rubygems-tasks"
24
+ s.add_development_dependency "steep"
25
+ s.add_development_dependency "ndav"
26
+ s.add_development_dependency "rdoc"
27
+ s.add_development_dependency "yard"
28
+ end
@@ -0,0 +1,14 @@
1
+ module RuBlurHash
2
+ def self?.encode: (Object memory_view_producer, components_x: Integer, components_y: Integer) -> String
3
+ | (Array[Integer] pixels, components_x: Integer, components_y: Integer, width: Integer, height: Integer) -> String
4
+ | (String binary_data, components_x: Integer, components_y: Integer, width: Integer, height: Integer) -> String
5
+ def self?.decode: (String blur_hash_string, width: Integer, height: Integer, punch: Float) -> ImageString
6
+
7
+ # Binary string of image data
8
+ #
9
+ # Also is a MemoryView producer
10
+ class ImageString < String
11
+ attr_reader width: Integer
12
+ attr_reader height: Integer
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module Fiddle
2
+ class MemoryView
3
+ def self.export: (untyped) { (instance) -> untyped } -> untyped
4
+ def format: -> String
5
+ def shape: -> Array[Integer]
6
+ def []: (*Integer) -> Integer
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ module Gem
2
+ class Specification
3
+ def self.load: (String) -> instance
4
+ def full_name: -> String
5
+ end
6
+
7
+ class Platform
8
+ def self.local: -> instance
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ module Test
2
+ module Unit
3
+ class TestCase
4
+ include Assertions
5
+
6
+ def setup: -> void
7
+ end
8
+
9
+ module Assertions
10
+ def assert: (bool?) -> void
11
+ | { () -> void } -> void
12
+ def assert_match: (Regexp, String) -> void
13
+ def assert_equal: (untyped, untyped, ?_ToS) -> void
14
+ def assert_raise: { () -> void } -> void
15
+ def assert_path_exist: (path) -> void
16
+ end
17
+ end
18
+ end
data/sig/test/test.rbs ADDED
@@ -0,0 +1,28 @@
1
+ class TestRuBlurHash < Test::Unit::TestCase
2
+ include RuBlurHash
3
+
4
+ RE: Regexp
5
+ ENCODED: String
6
+
7
+ @img: Array[Integer]
8
+ @components_x: Integer
9
+ @components_y: Integer
10
+
11
+ def test_encode: -> void
12
+ def test_encode_memory_view: -> void
13
+ def test_decode: -> void
14
+ def test_image_string: -> void
15
+ def test_module_functions: -> void
16
+
17
+ private
18
+
19
+ def encoded_length: -> Integer
20
+ end
21
+
22
+ class TestPackage < Test::Unit::TestCase
23
+ @gemspec: Gem::Specification
24
+ @pkg_file: String
25
+
26
+ def test_build: -> void
27
+ def test_install: -> void
28
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,7 @@
1
+ require "test/unit"
2
+ require "test/unit/notify" if RUBY_PLATFORM.match? /darwin/
3
+ require "fiddle"
4
+ require "ndav"
5
+ require "rublurhash"
6
+ require "fileutils"
7
+ require "tmpdir"
@@ -0,0 +1,27 @@
1
+ require_relative "helper"
2
+
3
+ class TestPackage < Test::Unit::TestCase
4
+ def setup
5
+ @gemspec = Gem::Specification.load("rublurhash.gemspec")
6
+ @pkg_file = File.join("pkg", @gemspec.full_name + ".gem")
7
+ FileUtils.rmtree @pkg_file
8
+ end
9
+
10
+ def test_build
11
+ assert system("rake", "build")
12
+ assert File.size(@pkg_file) > 0
13
+ end
14
+
15
+ def test_install
16
+ system "rake", "build", exception: true
17
+ gem_file = File.join("pkg", @gemspec.full_name + ".gem")
18
+ Dir.mktmpdir do |dir|
19
+ dir = File.realpath(dir)
20
+ install_dir = File.join(dir, "install")
21
+ FileUtils.mkdir install_dir
22
+ system "gem", "install", "--install-dir", install_dir, "--no-document", gem_file, exception: true
23
+
24
+ assert_path_exist File.join(install_dir, "extensions", Gem::Platform.local.to_s, RbConfig::CONFIG["ruby_version"], @gemspec.full_name, "rublurhash.#{RbConfig::CONFIG['DLEXT']}")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,96 @@
1
+ require_relative "helper"
2
+
3
+ class TestRuBlurHash < Test::Unit::TestCase
4
+ include RuBlurHash
5
+
6
+ RE = /\A[0-~]+\z/
7
+ ENCODED = "LEHV6nWB2yk8pyo0adR*.7kCMdnj"
8
+
9
+ def setup
10
+ @img = [
11
+ [255, 0, 0],
12
+ [255, 0, 0],
13
+ [255, 0, 0],
14
+ [255, 0, 0]
15
+ ].flatten
16
+ @components_x = 4
17
+ @components_y = 3
18
+ end
19
+
20
+ def test_encode
21
+ encoded = encode(@img, width: 2, height: 2, components_x: @components_x, components_y: @components_y)
22
+
23
+ assert do
24
+ encoded.length == encoded_length
25
+ end
26
+ assert_match RE, encoded
27
+ end
28
+
29
+ def test_encode_memory_view
30
+ memview = NDAV.new(Fiddle::Pointer[@img.pack("C*")], shape: [2, 2, 3])
31
+ encoded = encode(memview, components_x: @components_x, components_y: @components_y)
32
+
33
+ assert do
34
+ encoded.length == encoded_length
35
+ end
36
+ assert_match RE, encoded
37
+ assert do
38
+ encode(@img, width: 2, height: 2, components_x: @components_x, components_y: @components_y) == encoded
39
+ end
40
+ end
41
+
42
+ def test_decode
43
+ decoded = decode(ENCODED, width: 32, height: 32, punch: 1.0)
44
+
45
+ assert do
46
+ decoded.bytesize == 32 * 32 * 3
47
+ end
48
+
49
+ img = decoded.unpack("CCC*")
50
+ img.each do |pixel|
51
+ assert do
52
+ pixel > 0
53
+ end
54
+ end
55
+
56
+ assert_equal 32, decoded.width
57
+ assert_equal 32, decoded.height
58
+
59
+ Fiddle::MemoryView.export decoded do |view|
60
+ assert_equal "C", view.format
61
+ assert_equal [32, 32, 3], view.shape
62
+ view.shape[0].times do |i|
63
+ view.shape[1].times do |j|
64
+ view.shape[2].times do |k|
65
+ assert_equal img[i * 96 + j * 3 + k], view[i, j, k], {i:, j:, k:}
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ assert_raise do
72
+ decode("!EHVWB2yk8pyo0adR*.7kCMdnj", width: 32, height: 32, punch: 1.0)
73
+ end
74
+
75
+ assert_raise do
76
+ decode("LEHV6nWB", width: 8, height: 8, punch: 1.0)
77
+ end
78
+ end
79
+
80
+ def test_image_string
81
+ assert_raise do
82
+ ImageString.new("123")
83
+ end
84
+ end
85
+
86
+ def test_module_functions
87
+ assert_equal encode(@img, width: 2, height: 2, components_x: @components_x, components_y: @components_y), RuBlurHash.encode(@img, width: 2, height: 2, components_x: @components_x, components_y: @components_y)
88
+ assert_equal decode(ENCODED, width: 32, height: 32, punch: 1.0), ::RuBlurHash.decode(ENCODED, width: 32, height: 32, punch: 1.0)
89
+ end
90
+
91
+ private
92
+
93
+ def encoded_length
94
+ 4 + 2 * @components_x * @components_y
95
+ end
96
+ end