June 13, 2020
Apple bundles a system version of the Ruby programming language on macOS. Because system Ruby is used by the inner workings of the operating system, this version is not meant to be upgraded or modified by a user. With the Ruby Version Manager RVM, you can install an additional Ruby version for personal use.
Similar to pyenv, you can install multiple versions of Ruby with RVM and change the version you’re using on the fly. You can also install gems without sudo
.
Installing RVM and Ruby
Before downloading RVM, first install gpg and the mpapis public key:
$ brew install gnupg
$ gpg --keyserver hkp://ipv4.pool.sks-keyservers.net --recv-keys xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The keys (xxxx...
) change often, so you will need to copy the most recent ones from the RVM install page.
Next, download the most recent stable version of RVM:
$ \curl -sSL https://get.rvm.io | bash -s stable --ruby
After installation, RVM will tell you to either open a new terminal or source rvm
, so run the command it prints:
$ source ~/.rvm/scripts/rvm
You will also want to add rvm
to your ~/.zshrc
or ~/.bashrc
to load when you open a terminal:
# Add this to your ~/.zshrc or ~/.bashrc
[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"
Use rvm list
to find a Ruby version you want to install, then tell RVM which version to use:
$ rvm list
$ rvm use 2.7.0
You can then verify that you’re using an RVM-managed version of Ruby:
$ which ruby
~/.rvm/rubies/ruby-2.7.0/bin/ruby
Installing Jekyll
First, verify you’re using a Ruby version managed by RVM in the above step. Then, install the Jekyll gem:
$ gem install jekyll bundler
If you’re already in a Jekyll website repo (or any folder with a Rakefile
), you can use bundle
to install your remaining requirements:
You may then need to update Jekyll for your Rakefile
requirements:
Now you can up the Jekyll server:
$ bundle exec jekyll serve
Now check out your site at http://localhost:4000
! See the Jekyll Quickstart for more details on starting a Jekyll blog.
January 21, 2020
Earlier this week, I had released Virgo, a Ruby CLI to generate wallpapers (including the one above). My goal was to be able to create beautiful OLED wallpapers for my phone, but unfortunately, the first version of Virgo would take about 15 seconds to generate a wallpaper the size of an iPhone 11. The first version of Virgo used ChunkyPNG::Image
to place pixels on the background, and the author of ChunkyPNG alludes to this possible problem in his README:
Also, have a look at OilyPNG which is a mixin module that implements some of the ChunkyPNG algorithms in C, which provides a massive speed boost to encoding and decoding.
Improvement Goal
My goal was to reduce the time to generate an iPhone-sized wallpaper (about 1200 by 2200 pixels) in under one second. I chose this constraint so that I can eventually write a web frontend in Sinatra. Users won’t wait 15 seconds for an image to be generated, especially if the file is being provided by a server without any loading indication on the page.
Thinking of a Solution
When thinking of optimization ideas, two options stood out to me from Willem’s README:
- Can I adapt Virgo to use OilyPNG instead of ChunkyPNG?
- Is there another library that implements array manipulation in C?
After some research into OilyPNG, I found that the functions I used with ChunkyPNG weren’t implemented in OilyPNG, so I was left to find another library that enabled a faster manipulation of integer arrays. I knew that in the Python world, NumPy would be the immediate answer. After some research for a Ruby alternative to NumPy, I came across NArray, which appeared to be a solution others had relied on in the past. I alluded to this possible approach in my original post:
ChunkyPNG was used to manage and save the wallpaper images, and Commander enabled terminal argument parsing and help documentation. For optimization, I plan to use NArray for creating the pixel locations in the wallpaper and writing a custom PNG encoder.
Fast Pixel Placement
After some profiling, the section of code that could be improved the most was overlaying pixels on the background image:
def place_pixel
# Create a new canvas
pixel = Image.new(@pixel_diameter, @pixel_diameter, @theme.foreground)
# Replace the old image with the new canvas at the pixel coordinate
@image = @image.replace(pixel, pixel_coordinate.x, pixel_coordinate.y)
end
Rather than creating new ChunkyPNG::Images
and overlaying them on a master Image
, I decided to use a new data structure for the image. Instead, Virgo now uses an NArray
to represent the image, where each item is the Integer
representation of the pixel ChunkyPNG::Color
. For every pixel, a portion of the array is replaced with the integer color:
def create_map
# Start with each pixel in the image as the background color
map = NArray.int(@width, @height).fill!(@theme.background)
# Place each pixel in the map
count = number_pixels_to_place
(1..count).each do
# Determine pixel location
x = @x_distribution.random_point
x_max = x + @pixel_diameter
y = @y_distribution.random_point
y_max = y + @pixel_diameter
# Replace the pixel in the map with a new (random) color
map[x..x_max, y..y_max] = @theme.random_foreground
end
end
Then, to create save a Wallpaper
instance, a ChunkyPNG::Image
is created by inserting rows of the NArray
into the Image
:
def image
img = Image.new(@width, @height, Color::TRANSPARENT)
# Put each row of @map into the image
num_rows = @map.shape[1]
(0...num_rows).each do |row_idx|
# Replace the row in the image with the new colors
img.replace_row!(row_idx, @map[true, row_idx])
end
img
end
int
vs Integer
There was only one problem with this solution: the range of values for ChunkyPNG::Image
would often exceed the range of the int
type used by NArray
. Therefore, most of the colors in the predefined themes could not be placed into the color map as-is.
I decided to implement a color hash (enum) for a Theme
instance, where every key is a unique (low Integer
value identifier), and each value is the (large Integer
) ChunkyPNG::Image
value. The background color (@background
) will always have the identifier 0
, and the foreground colors (@foregrounds
) have identifiers from 1
to n
. The color hash is created in Theme.initialize
:
def initialize(background = BACKGROUNDS[:black],
foregrounds = FOREGROUNDS[:ruby])
@background = Color.from_hex(background)
@foregrounds = foregrounds.map { |x| Color.from_hex(x) }
# Because NArray can't handle the size of some ChunkyPNG::Color
# values, create a Hash of the background and foreground colors,
# where the key of the hash is an Integer, and the
# value is the value of the color
# Background has a key of 0, foregrounds have keys from 1..n
colors = [@background] + @foregrounds
colors_with_indices = colors.each_with_index.map do |color, idx|
[idx, color]
end
@color_hash = Hash[colors_with_indices]
end
Let’s look at an example:
2.6.3 :001 > # Construct a theme using predefined color names
2.6.3 :002 > t = Theme.from_syms(:black, :ruby)
=> #<Theme:0x00007f861f8c2128
@background=255,
@foregrounds=[
2367954943,
2720605439,
3073321215,
3425971711,
3561307903,
3646510079,
3731712255],
@color_hash={
0=>255,
1=>2367954943,
2=>2720605439,
3=>3073321215,
4=>3425971711,
5=>3561307903,
6=>3646510079,
7=>3731712255}>
Note that the @background
color 255
is in @color_hash
as 0=>255
, which means it has an identifier of 0
. The second color in @foregrounds
, 2720605439
, is in @color_hash
as 2=>2720605439
, meaning that color has an identifier of 2
.
Therefore, if the Wallpaper
map has the following values:
[ [0, 0, 0],
[0, 2, 0],
[0, 0, 0] ]
Then the middle pixel is red (a value of 2720605439
), and the border pixels are black (a value of 255
).
Next, we add a few helper functions to Theme
for retrieving a random foreground (pixel) color and color keys/values:
# Returns the key for the background color
def background_key
# The background always has a key of 0
0
end
# Returns a random @color_hash foreground key
def random_foreground_key
# (Slightly) speed up getting a foreground by returning the first
# item if only one exists
color = if @foregrounds.length == 1
@foregrounds[0]
else
@foregrounds.sample
end
key_from_color(color)
end
# Returns the ChunkyPNG::Color value for a color key
def color_from_key(key)
@color_hash[key]
end
# Returns the key (in @color_hash) for a color value
def key_from_color(color)
@color_hash.key(color)
end
And create_map
is updated to use the color keys rather than the large values:
def create_map
# Start with each pixel in the image as the background color
map = NArray.int(@width, @height).fill!(@theme.background_key)
# Place each pixel in the map
count = number_pixels_to_place
(1..count).each do
# Determine pixel location
x = @x_distribution.random_point
x_max = x + @pixel_diameter
y = @y_distribution.random_point
y_max = y + @pixel_diameter
# Replace the pixel in the map with a new (random) color
map[x..x_max, y..y_max] = @theme.random_foreground_key
end
map
end
Now we can test whether using NArray
vs ChunkyPNG::Image
reduced the time to generate large wallpapers.
Results
Using NArray
significantly improved Virgo’s speed:
Note the exponential time complexity of the original implementation, and the linear time complexity. With 10 trials, NArray
Virgo took 0.5 seconds on average to generate a 1000x1000 wallpaper, while the original implementation of Virgo takes 15.1 seconds on average. That’s a a 30x improvement! Even better, these updates will allow me to make a responsive Sinatra web frontend, without worrying about user retention or delays.
Future Improvements
I believe more improvements could be made to Virgo, especially since I plan on writing a web frontend. In particular, I’ve added a Distribution
class to enable both normal and uniform pixel distributions, but I have not added it as a feature to the CLI. Further, there is a new version of NArray
called Numo::NArray
that supports UInt64
values, so there will no longer be a need to map each color in a Theme
to unique identifiers.
Virgo is available on my GitHub profile, and it’s open to pull requests!
January 19, 2020
There are many galaxies with strange, funny, or mysterious names, such as the Sombrero Galaxy or the Cartwheel Galaxy. Inspired by these galaxies I wanted to be able to make space-like wallpapers for my devices.
As a result, I’ve written Virgo, a wallpaper generator CLI written in Ruby. Virgo is great for creating PNG OLED phone wallpapers with a true black background. Let’s create an iPhone 11 size wallpaper:
$ cd ~/virgo
$ ./virgo.rb --width 828 --height 1792
Virgo has many options for creating wallpapers, including the image dimension, colors, and pixel properties:
./virgo.rb save PATH [options]
OPTIONS:
--background BACKGROUND
Wallpaper background as a hexcode. Use list-backgrounds to list predefined background names
--foregrounds FOREGROUNDS
Wallpaper foregrounds as hexcodes separated by commans. Use list-foregrounds to list predefined foreground names
--width PIXELS
Width of the wallpaper
--height PIXELS
Height of the wallpaper
--density RATIO
Ratio of pixels to size of the image, as a percent integer
--diameter PIXELS
Diameter of each pixel drawn on the wallpaper
There are also many predefined backgrounds and foregrounds for you to explore! Use the following commands to list some options:
$ ./virgo.rb list_backgrounds
black: #000000
white: #ffffff
dark_blue: #355c7d
chalkboard: #2a363b
peach: #ff8c94
gray: #363636
teal: #2f9599
orange: ff4e50
brown: #594f4f
gray_green: #83af9b
$ ./virgo.rb list_foregrounds
white: ["#ffffff"]
ruby: ["#8d241f", "#a22924", "#b72f28", "#cc342d", "#d4453e", "#d95953", "#de6d68"]
sunset: ["#f8b195", "#f67280", "#c06c84", "#6c5b7b"]
primaries: ["#99b898", "#feceab", "#ff847c", "#e84a5f"]
primaries_light: ["#a8e6ce", "#bcedc2", "#ffd3b5", "#ffaaa6"]
gothic: ["#a8a7a7", "#cc527a", "#e8175d", "#474747"]
solar: ["#a7226e", "#ec2049", "#f26b38", "#9dedad"]
yellows: ["#e1f5c4", "#ede574", "#f9d423", "#fc913a"]
earth: ["#e5fcc2", "#9de0ad", "#45ada8", "#547980"]
faded: ["#fe4365", "#fc9d9a", "#f9cdad", "#c8c8a9"]
ChunkyPNG was used to manage and save the wallpaper images, and Commander enabled terminal argument parsing and help documentation. For optimization, I plan to use NArray for creating the pixel locations in the wallpaper and writing a custom PNG encoder. Here are a few example wallpapers created by Virgo:
You can find Virgo on GitHub here.