rubyemojicairo

Is there a way to use emoji with Cairo's "toy" text API?


Is there any font or technique I can use with Cairo (via Ruby bindings) to draw text containing emoji? Everything I've read says "use Pango for real text handling" but Cairo does everything else I need so I'm hoping there's a way to avoid adding that as a dependency.

Here's a simplified version of the dynamic Open Graph images I'm trying to make:

require 'cairo'

# Create a new image surface based on recommended OpenGraph dimensions
width = 1_200
height = 630
surface = Cairo::ImageSurface.new(width, height)

# Create a Cairo Context for the surface
cr = Cairo::Context.new(surface)

# Apply a background colour to the whole thing (almost white)
cr.set_source_rgb(0.95, 0.95, 0.95)
cr.paint

# Add a heading
heading = "Hi! 👋"
cr.set_source_rgb(0.31, 0.21, 0.27)
cr.select_font_face("Sans", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD)
cr.set_font_size(100.0)
extents = cr.text_extents(heading)
centred_text_left_edge = width / 2 - extents.width / 2
cr.move_to(centred_text_left_edge, 130)
cr.show_text(heading)

# Write the image surface to a PNG file
surface.write_to_png("emoji-test-#{Time.now.strftime('%Y-%m-%dT%H:%M:%S.%L%z')}.png")

Unfortunately the emoji gets rendered as a rectangle (running on macOS, might be different on other platforms):

Generated image contains a square instead of the waving hand emoji

Can I use a custom font or is there some other technique that will get the emoji to display?

It seems the "correct" option is to use Pango, but is that advice still current and applicable to Ruby? I couldn't get the Pango gem to install on my M1 Mac and even if I could, after much searching the only reference I can find to using Pango with Cairo from Ruby is a very old sample script that's no longer included in the Pango distibution.

Failing that I guess I'll have to save the emoji as images and paint them onto the Surface/Context. This is feasible for me because I only want to use 3 specific emoji but it's not scalable for including arbitrary emoji.


Solution

  • I will happily accept another answer if someone else has a better solution but I ended up falling back to painting on a PNG of an emoji. Read to the end for the downsides to this approach.

    First I saved a 96x96 pixel PNG of the emojis I wanted to use (e.g. wave.png). Then I painted them on to the surface. Here's a POC:

    require 'cairo'
    
    # Create a new image surface based on recommended OpenGraph dimensions
    width = 1_200
    height = 630
    surface = Cairo::ImageSurface.new(width, height)
    
    # Create a Cairo Context for the surface
    cr = Cairo::Context.new(surface)
    
    # Apply a background colour to the whole thing (almost white)
    cr.set_source_rgb(0.95, 0.95, 0.95)
    cr.paint
    
    # Set up the heading
    string = "Hi!"
    cr.set_source_rgb(0.31, 0.21, 0.27)
    cr.select_font_face("Sans", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD)
    cr.set_font_size(100.0)
    text_box = cr.text_extents(string)
    
    # Set up the emoji image
    image = Cairo::ImageSurface.from_png("wave.png")
    
    # Position the text in the centre (taking into account the image width)
    horizontal_gap = 20 # trial and error
    top_of_text = 130 # trial and error
    line_width = text_box.width + image.width + horizontal_gap
    text_left_edge = (width - line_width) / 2 # centred on page
    cr.move_to(text_left_edge, top_of_text)
    cr.show_text(string)
    
    # Position the image next to the text
    top_of_image = top_of_text - 85 # trial and error
    image_left_edge = text_left_edge + text_box.width + horizontal_gap
    cr.set_source(image, image_left_edge, top_of_image)
    cr.paint
    
    # Write the image surface to a PNG file
    surface.write_to_png("emoji-test.png")
    

    This generates my OpenGraph image with text and an emoji: Sample Open Graph image with the word Hi and a waving hand emoji

    The downsides to this approach are: