How to Use Jekyll on macOS Catalina with RVM

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:

$ bundle install

You may then need to update Jekyll for your Rakefile requirements:

$ bundle update jekyll

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.

Optimizing Virgo Using NArray Cover Image

Optimizing Virgo Using NArray

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:

  1. Can I adapt Virgo to use OilyPNG instead of ChunkyPNG?
  2. 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 OilyPNG1, 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:

Virgo Example Output

Note the exponential time complexity of the original implementation, and the linear time complexity.2 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!

Virgo Example Output

  1. In fact, the library didn’t seem to have implemented much, if any, of the ChunkyPNG functionality. 

  2. Well, O(n) at least comparative to the ChunkyPNG implementation and tests I conducted up to an image size of 5000x5000. 

Virgo: Ruby Wallpaper Generator

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.

The Thousand Ruby Galaxy

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 Example Output

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:

Virgo Example Output Virgo Example Output Virgo Example Output Virgo Example Output Virgo Example Output

You can find Virgo on GitHub here.