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.
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:
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");
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:
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");
Saved credited.jpgLimits
- 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.