Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
RMagick + DiscordRB

RMagick + DiscordRB = ❤️

This is a simple tutorial on how to use RMagick with your Discord bot (DiscordRB).

What you will need

Creating the bot

I'll use a scaffolding tool I wrote to create the bot files.

$ mkdir rmagick-bot && cd "$_"
$ ezdrb init
  
Set prefix: ++
Set token: blah blah blah

That will just create a bunch of files you don't need to worry about.

Rotate Command

Now let's create a command to rotate an image.

$ ezdrb command rotate

Open commands/Rotate.rb in your text editor and require 'rmagick' at the top of the file.

require 'rmagick'

class Rotate

  def activate(bot)
    bot.command :rotate do |event|
      # code goes here...
    end
  end

end

Rotate.new

Add minimum & maximum number of arguments, description and usage string. We need one argument: degrees, which will be a float.

bot.command(:rotate, min_args: 1, max_args: 1, description: 'Rotate an image N degrees', usage: 'rotate <degrees>') do |event, degrees|

  # Converting String to Float
  degrees = degrees.to_f
  
end

You might be wondering now... what about the image? Isn't the image an argument too? Well, yes and no. We will treat it as an argument but Discordrb doesn't know it is one, that is, it cannot be a block variable. Instead, we will upload the image and pass the ++rotate <degrees> command as an image comment. Then we can check if the message has one, and only one, attachment and if that attachment is an image.

bot.command(:rotate, min_args: 1, max_args: 1, description: 'Rotate an image N degrees', usage: 'rotate <degrees>') do |event, degrees|

  degrees = degrees.to_f
  
  # Fetching message attachments
  attachments = event.message.attachments
  
  # Checking if it has one, and only one, attachment and if that attachment is an image
  if attachments.length == 1 && attachments.first.image? then
    
  
  # If condition isn't met, ask for image.
  else
    event.channel.send('You need to upload an image!')
  end
  
end

Now let's fetch some attachment data! We want its name & URL.

bot.command(:rotate, min_args: 1, max_args: 1, description: 'Rotate an image N degrees', usage: 'rotate <degrees>') do |event, degrees|

  degrees = degrees.to_f
  attachments = event.message.attachments
  
  if attachments.length == 1 && attachments.first.image? then
    # Fetch attachment name and remove extension
    filename = attachments.first.filename.split('.')
    filename.pop
    filename = filename.join
    
    # Fetching URL
    fileurl = attachments.first.url
  else
    event.channel.send('You need to upload an image!')
  end
  
end

Now let's test it. Add event.channel.send("#{filename} in #{fileurl}") right after the fileurl variable.

Sample Image 1

The file name + extension is laplace.jpg

Returns

Sample Image 2

Neat!

Now we have a problem: RMagick doesn't like HTTPS and Discord has an HTTPS-based CDN. We will need to download the image with an HTTP library so we can work with it locally. Luckily, Discordrb uses RestClient, so that should be already installed. We will store the downloaded image in a temporary file (Tempfile) because we don't want to keep it forever. We want the opposite: to get rid of it as soon as possible.

bot.command(:rotate, min_args: 1, max_args: 1, description: 'Rotate an image N degrees', usage: 'rotate <degrees>') do |event, degrees|

  degrees = degrees.to_f
  attachments = event.message.attachments
  
  if attachments.length == 1 && attachments.first.image? then
    filename = attachments.first.filename.split('.')
    filename.pop
    filename = filename.join
    
    fileurl = attachments.first.url
    
    # Creating Tempfile
    image = Tempfile.new([filename, '.png'])
    # Creating a begin-ensure block to make sure we delete the Tempfile after using it
    begin
    ensure
    end
    
  else
    event.channel.send('You need to upload an image!')
  end
  
end

Why do we need the image to be .png? Because when we rotate it, if it doesn't have a transparent background, we will get an ugly image and we don't like ugly images. Continuing...

bot.command(:rotate, min_args: 1, max_args: 1, description: 'Rotate an image N degrees', usage: 'rotate <degrees>') do |event, degrees|

  degrees = degrees.to_f
  attachments = event.message.attachments
  
  if attachments.length == 1 && attachments.first.image? then
    filename = attachments.first.filename.split('.')
    filename.pop
    filename = filename.join
    
    fileurl = attachments.first.url
    
    image = Tempfile.new([filename, '.png'])
    
    begin
      # Downloading the image from `fileurl` and storing it in the Tempfile
      image.write(RestClient.get(fileurl))
      image.rewind
    ensure
      # Closing and unlinking Tempfile
      image.close! # this closes & unlinks the Tempfile (could've used `#close(true)`)
    end
    
  else
    event.channel.send('You need to upload an image!')
  end
  
end

Hm... but how do we know it worked? Tempfile has a really nice method #path that shows us where the file is stored.

bot.command(:rotate, min_args: 1, max_args: 1, description: 'Rotate an image N degrees', usage: 'rotate <degrees>') do |event, degrees|

  degrees = degrees.to_f
  attachments = event.message.attachments
  
  if attachments.length == 1 && attachments.first.image? then
    filename = attachments.first.filename.split('.')
    filename.pop
    filename = filename.join
    
    fileurl = attachments.first.url
    
    image = Tempfile.new([filename, '.png'])
    
    begin
      image.write(RestClient.get(fileurl))
      image.rewind
      
      # Checking before `image` get unlinked
      puts image.path
    ensure
      image.close!
    end
    
    # Checking after `image` get unlinked
    puts image.path
    
  else
    event.channel.send('You need to upload an image!')
  end
  
end

If you run this and send an image with the ++rotate <degrees> command, you should get something like this on your terminal:

/tmp/laplace20180805-22129-g22rag.png

Yup, the file path and a blank line. After you unlink a Tempfile, running Tempfile#path returns nil and puts nil prints a blank line. That means everything is working.

Now finally... let's rotate that image!

bot.command(:rotate, min_args: 1, max_args: 1, description: 'Rotate an image N degrees', usage: 'rotate <degrees>') do |event, degrees|

  degrees = degrees.to_f
  attachments = event.message.attachments
  
  if attachments.length == 1 && attachments.first.image? then
    filename = attachments.first.filename.split('.')
    filename.pop
    filename = filename.join
    
    fileurl = attachments.first.url
    
    image = Tempfile.new([filename, '.png'])
    
    begin
      image.write(RestClient.get(fileurl))
      image.rewind
      
      # Rotating image and saving it
      Magick::Image.read(image.path).first.rotate(degrees).matte_replace(0, 0).write(image.path)
      
      # Sending image to the channel
      event.channel.send_file(File.open(image.path, 'r'))
    ensure
      image.close!
    end
    
  else
    event.channel.send('You need to upload an image!')
  end
  
end

Wait, what? We don't need to create another Tempfile to store the new rotated image? Nope. We can replace the original image with the new one.

Our rotate command is ready to be used.

Sample Image 3

Returns

Sample Image 4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.