Recursive AI

Draw text on an image in C#

All oneimage recipes

Add the RecursiveAI.OneImage.Text package, load a font file into a FontCollection, and call DrawText with the text, a TextOptions saying where it goes, and a color. Text is shaped with OpenType, so kerning, ligatures, right-to-left and complex scripts come out right. The program below captions NASA’s “Blue Marble” photograph (public domain) in Noto Sans.

Terminal
dotnet new console -o DrawText && cd DrawText
dotnet add package RecursiveAI.OneImage.Text --version 1.10.4
curl -L -o photo.jpg \
  https://images-assets.nasa.gov/image/as17-148-22727/as17-148-22727~orig.jpg
fonts=https://github.com/notofonts/notofonts.github.io/raw/main/fonts
curl -L -o NotoSans-Regular.ttf $fonts/NotoSans/hinted/ttf/NotoSans-Regular.ttf

OneImage needs the .NET 10 SDK. Put this in Program.cs and run dotnet run:

C#
using OneImage;
using OneImage.Drawing;
using OneImage.Text;
using Path = OneImage.Drawing.Path;
 
Image photo = Image.Load("photo.jpg").Resize(width: 1200);
 
Font font = new FontCollection().Add("NotoSans-Regular.ttf").CreateFont(44);
var options = new TextOptions(font)
{
    // The origin is the bottom center of the text block, 40 px above the edge.
    Origin = new(photo.Width / 2, photo.Height - 40),
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Bottom,
    TextAlignment = TextAlignment.Center,
    WrappingLength = 1000,   // wrap lines wider than 1000 px
};
string caption = "The Blue Marble, photographed from Apollo 17 on its way to the Moon";
 
// Measure the text to put a translucent box behind it.
var box = TextMeasurer.MeasureBounds(caption, options);
Console.WriteLine($"{TextMeasurer.CountLines(caption, options)} lines, "
                + $"{box.Width:0}x{box.Height:0} px");
 
Path back = Path.RoundedRectangle(box.X - 24, box.Y - 16,
                                  box.Width + 48, box.Height + 32, radius: 12);
photo.Fill(new Rgba32(0, 0, 0, 160), back)
     .DrawText(caption, options, new Rgba32(255, 255, 255, 255))
     .Save("captioned.jpg");
Console.WriteLine("Saved captioned.jpg");
Text
2 lines, 972x88 px
Saved captioned.jpg

Origin is in pixels from the top-left corner, and the two alignments say which point of the text block it is, so you don’t have to measure text to center or right-align it. WrappingLength breaks lines at word boundaries.

Outlined text

To keep text readable over both light and dark areas without a box, draw it with a Brush for the fill and a Pen for the outline:

C#
using OneImage;
using OneImage.Drawing;
using OneImage.Text;
 
Image photo = Image.Load("photo.jpg").Resize(width: 1200);
Font font = new FontCollection().Add("NotoSans-Regular.ttf").CreateFont(36);
 
var options = new TextOptions(font)
{
    Origin = new(photo.Width - 30, photo.Height - 30),
    HorizontalAlignment = HorizontalAlignment.Right,
    VerticalAlignment = VerticalAlignment.Bottom,
};
photo.DrawText("Photo: NASA", options,
               Brush.Solid(new Rgba32(255, 255, 255, 220)),
               new Pen(Brush.Solid(new Rgba32(0, 0, 0, 160)), width: 2))
     .Save("credited.jpg");
Console.WriteLine("Saved credited.jpg");
Text
Saved credited.jpg

Limits

  • No fonts are bundled. Installed fonts are available through SystemFonts, but containers usually have none; a TrueType, OpenType, WOFF or WOFF2 file you ship gives the same output everywhere.
  • Sizes are in pixels. Scale the font size from the image width if your images vary in size.
  • Emoji and missing characters need a font that has them; add one with FallbackFontFamilies.

For a translucent mark across the whole image, see Add a watermark. Rich text, vertical layout and hinting are in the Text docs.