diff --git a/wasm-thumbnail-rb/.gitignore b/wasm-thumbnail-rb/.gitignore new file mode 100644 index 0000000..9106b2a --- /dev/null +++ b/wasm-thumbnail-rb/.gitignore @@ -0,0 +1,8 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/wasm-thumbnail-rb/.rubocop.yml b/wasm-thumbnail-rb/.rubocop.yml new file mode 100644 index 0000000..8d1fdd3 --- /dev/null +++ b/wasm-thumbnail-rb/.rubocop.yml @@ -0,0 +1,13 @@ +AllCops: + TargetRubyVersion: 2.7 + +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + Enabled: true + EnforcedStyle: double_quotes + +Layout/LineLength: + Max: 120 diff --git a/wasm-thumbnail-rb/Gemfile b/wasm-thumbnail-rb/Gemfile new file mode 100644 index 0000000..64b7c83 --- /dev/null +++ b/wasm-thumbnail-rb/Gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in wasm-thumbnail-rb.gemspec +gemspec + +gem "rake", "~> 13.0" + +gem "minitest", "~> 5.0" + +gem "rubocop", "~> 1.7" diff --git a/wasm-thumbnail-rb/README.md b/wasm-thumbnail-rb/README.md new file mode 100644 index 0000000..7d11ef5 --- /dev/null +++ b/wasm-thumbnail-rb/README.md @@ -0,0 +1,29 @@ +# Wasm::Thumbnail::Rb + +## Installation + +Note this gem has not been released to rubygems. It may in the future but for now it is intended to be referenced via git. + +Add this line to your application's Gemfile: + +```ruby +gem 'wasm-thumbnail-rb', git: 'gitlocationofgem' +``` + +And then execute: + + $ bundle install + +## Usage + +See `rb_test.rb` for an example of usage. + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/brave-intl/wasm-thumbnail-rb. diff --git a/wasm-thumbnail-rb/Rakefile b/wasm-thumbnail-rb/Rakefile new file mode 100644 index 0000000..8bcc866 --- /dev/null +++ b/wasm-thumbnail-rb/Rakefile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[test rubocop] diff --git a/wasm-thumbnail-rb/bin/console b/wasm-thumbnail-rb/bin/console new file mode 100755 index 0000000..4a1fbc1 --- /dev/null +++ b/wasm-thumbnail-rb/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "wasm/thumbnail/rb" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/wasm-thumbnail-rb/bin/setup b/wasm-thumbnail-rb/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/wasm-thumbnail-rb/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/wasm-thumbnail-rb/lib/wasm/thumbnail/rb.rb b/wasm-thumbnail-rb/lib/wasm/thumbnail/rb.rb new file mode 100644 index 0000000..3c88676 --- /dev/null +++ b/wasm-thumbnail-rb/lib/wasm/thumbnail/rb.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require_relative "rb/version" +require "wasmer" +module Wasm + module Thumbnail + # As opposed to the PY implementation + module Rb + class Error < StandardError; end + + # Now the module is compiled, we can instantiate it. Doing so outside the method where used results in errors. + def self.register_panic(_msg_ptr = nil, _msg_len = nil, _file_ptr = nil, _file_len = nil, _line = nil, _column = nil) + puts("WASM panicked") + end + + WASMStore = Wasmer::Store.new + WASMImportObject = Wasmer::ImportObject.new + WASMImportObject.register( + "env", + register_panic: Wasmer::Function.new( + WASMStore, + :register_panic, + Wasmer::FunctionType.new([Wasmer::Type::I32, + Wasmer::Type::I32, + Wasmer::Type::I32, + Wasmer::Type::I32, + Wasmer::Type::I32, + Wasmer::Type::I32], []) + ) + ) + + def self.resize_and_pad_with_header(file_bytes:, width:, height:, size:) + # Let's compile the module to be able to execute it! + wasm_instance = Wasm::Thumbnail::Rb::GetWasmInstance.call + + # This tells us how much space we'll need to put our image in the WASM env + image_length = file_bytes.length + input_pointer = wasm_instance.exports.allocate.call(image_length) + # Get a pointer on the allocated memory so we can write to it + memory = wasm_instance.exports.memory.uint8_view input_pointer + + # Put the image to resize in the allocated space + (0..image_length - 1).each do |nth| + memory[nth] = file_bytes[nth] + end + + # Do the actual resize and pad + # Note that this writes to a portion of memory the new JPEG file, but right pads the rest of the space + # we gave it with 0. + begin + output_pointer = wasm_instance.exports.resize_and_pad.call(input_pointer, + image_length, + width, + height, + size) + rescue RuntimeError + raise "Error processing the image." + end + # Get a pointer to the result + memory = wasm_instance.exports.memory.uint8_view output_pointer + + # Only take the buffer that we told the rust function we needed. The resize function + # makes a smaller image than the buffer we said, and then pads out the rest. + bytes = memory.to_a.take(size) + + # Deallocate + wasm_instance.exports.deallocate.call(input_pointer, image_length) + wasm_instance.exports.deallocate.call(output_pointer, bytes.length) + + bytes + end + + def self.resize_and_pad(file_bytes:, width:, height:, size:) + bytes = resize_and_pad_with_header(file_bytes: file_bytes, width: width, height: height, size: size + 4) + + # The first 4 bytes are a header until the image. The actual image probably ends well before + # the whole buffer, but we keep the junk data on the end to make all the images the same size + # for privacy concerns. + bytes[4..].pack("C*") + end + + # Return an instance so you don't have to constantly compile + class GetWasmInstance + def self.call + # Let's compile the module to be able to execute it! + Wasmer::Instance.new( + Wasmer::Module.new(WASMStore, IO.read("#{__dir__}/rb/data/wasm_thumbnail.wasm", mode: "rb")), + WASMImportObject + ) + end + end + end + end +end diff --git a/wasm-thumbnail-rb/lib/wasm/thumbnail/rb/data/wasm_thumbnail.wasm b/wasm-thumbnail-rb/lib/wasm/thumbnail/rb/data/wasm_thumbnail.wasm new file mode 100755 index 0000000..d27716c Binary files /dev/null and b/wasm-thumbnail-rb/lib/wasm/thumbnail/rb/data/wasm_thumbnail.wasm differ diff --git a/wasm-thumbnail-rb/lib/wasm/thumbnail/rb/version.rb b/wasm-thumbnail-rb/lib/wasm/thumbnail/rb/version.rb new file mode 100644 index 0000000..3068eff --- /dev/null +++ b/wasm-thumbnail-rb/lib/wasm/thumbnail/rb/version.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Wasm + module Thumbnail + module Rb + VERSION = "0.1.0" + end + end +end diff --git a/wasm-thumbnail-rb/test/test_helper.rb b/wasm-thumbnail-rb/test/test_helper.rb new file mode 100644 index 0000000..58df5b2 --- /dev/null +++ b/wasm-thumbnail-rb/test/test_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "wasm/thumbnail/rb" + +require "minitest/autorun" diff --git a/wasm-thumbnail-rb/test/wasm/thumbnail/brave.png b/wasm-thumbnail-rb/test/wasm/thumbnail/brave.png new file mode 100644 index 0000000..8f7f7fa Binary files /dev/null and b/wasm-thumbnail-rb/test/wasm/thumbnail/brave.png differ diff --git a/wasm-thumbnail-rb/test/wasm/thumbnail/rb_test.rb b/wasm-thumbnail-rb/test/wasm/thumbnail/rb_test.rb new file mode 100644 index 0000000..0bd34cb --- /dev/null +++ b/wasm-thumbnail-rb/test/wasm/thumbnail/rb_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "test_helper" + +class Wasm::Thumbnail::RbTest < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::Wasm::Thumbnail::Rb::VERSION + end + + def test_it_does_something_useful + file_bytes = File.binread("#{__dir__}/brave.png").unpack("C*") + image = Wasm::Thumbnail::Rb.resize_and_pad(file_bytes: file_bytes, + width: 100, + height: 200, + size: 250_000) + puts "Image resized and padded to size #{image.length}" + end +end diff --git a/wasm-thumbnail-rb/wasm-thumbnail-rb.gemspec b/wasm-thumbnail-rb/wasm-thumbnail-rb.gemspec new file mode 100644 index 0000000..922075f --- /dev/null +++ b/wasm-thumbnail-rb/wasm-thumbnail-rb.gemspec @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative "lib/wasm/thumbnail/rb/version" + +Gem::Specification.new do |spec| + spec.name = "wasm-thumbnail-rb" + spec.version = Wasm::Thumbnail::Rb::VERSION + spec.authors = ["Tyler Smart"] + spec.email = ["tsmart@brave.com"] + spec.licenses = ["MPL-2.0"] + spec.summary = "WASM based thumbnail library" + spec.homepage = "https://github.com/brave-intl/wasm-thumbnail" + spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/brave-intl/wasm-thumbnail" + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(File.expand_path(__dir__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "wasmer", "~> 1.0" +end