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):
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.
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:
The downsides to this approach are: