2

I'm writing text into an image and I want this text to be scaled up (like doubled or tripled).

import (
        "golang.org/x/image/font"
        "golang.org/x/image/font/basicfont"
        "golang.org/x/image/math/fixed"
        "image"
        "image/color"
)

func DrawTextSimplified(x, y int, img *image.RGBA) {
        col := color.RGBA{0xff, 0x55, 0, 0xff}
        point := fixed.Point26_6{
                X: fixed.I(x),
                Y: fixed.I(y),
        }

        d := &font.Drawer{
                Dst:  img,
                Src:  image.NewUniform(col),
                Face: basicfont.Face7x13,
                Dot:  point,
        }
        d.DrawString("hello")
}

Upscaling the image byte by byte works, but it's overcomplicated and I feel like there should be a default option for this. I've tried modifying the face struct manually, but I think you're not supposed to do that (it's a mess).

2
  • For reference, drawing on images can be a bit low-level in some cases by design: "Package image/draw defines only one operation: drawing a source image onto a destination image, through an optional mask image. [...] Composition is performed pixel by pixel". See the section on Drawing Font Glyphs for some visual examples on the complexity of adding text Commented Sep 21 at 17:57
  • 1
    That's what I was looking for! The blog post made it a lot easier to understand, thanks a lot Commented Sep 21 at 18:13

2 Answers 2

3

I don't believe you can easily resize basicfont because it's a static fixed-size font, as mentioned in its overview. If you'd like a dynamic font face, use something like the opentype package to parse a TTF/OTF font, such as goregular. Then, you can set its size. It'll look something like this:

font, err := opentype.Parse(goregular.TTF)
if err != nil {
    return err
}

face, err := opentype.NewFace(font, &opentype.FaceOptions{
    Size: 12, // Size of font in pt
    DPI:  72,
})
if err != nil {
    return err
}
Sign up to request clarification or add additional context in comments.

2 Comments

I did not know about fixed and dynamic fonts, that makes things lot easier!
I wrote up a full, runnable example which includes your code as a helper function and uses it along with the question's code in my answer
2

Image Composition

[...] it's overcomplicated and I feel like there should be a default option for this. I've tried modifying the face struct manually but I think you're not supposed to do that (it's a mess).

Elaborating on my comment, the reason why it feels a bit complicated and unintuitive to scale up some text is because the image/draw package has an intentionally simple, low-level interface. Per the Go blog post on it:

Package image/draw defines only one operation: drawing a source image onto a destination image, through an optional mask image.

[...]

Composition is performed pixel by pixel [...]

As it just overlays pixels on top of each other, there is no concept of "text" or "fonts" in its interface. Indeed, note how your Drawer is instead implemented in the golang.org/x/image/font package. In order to write text on an image pixel by pixel, you essentially need to create an image of the text with your desired font at your desired size.
In the blog post, the section on Drawing Font Glyphs has a helpful visual example that demonstrates some of this complexity:

image showing compositing sections of a destination, source, and mask images in order to put text onto a result image

Font Scaling

That example also skips over the part of creating a font glyph of desired size, which is the practice of the field of typography, involving creating a typeface/font family and creating instances of it as fonts of different size, including adjusting various spacing like the kerning. A font face generally includes the specific selection of font family and style, weight, etc1.

As @Elara6331's answer briefly mentions, the golang.org/x/image/font/basicfont package's Face7x13 variable is a fixed-size bitmap/raster font -- note the "7x13" size in the name. In comparison, the TrueType Font format (TTF) can specify a collection of different size fonts, and the OpenType Font format (OTF) can further specify font variations, usually referred to as "Variable Fonts". These specifications allow for dynamically scalable vector/outline fonts2 and the files can be very complex and dense with lots of tables.

I'm not a typography expert, so I may have missed some things, but I hope that basic summary and all the additional links and citations help to further illustrate the complexity that goes into modern scaling fonts 😅.

Full Example

With all that background and terminology out of the way, we can get into a full, runnable code sample.

I adapted your DrawTextSimplified function and named it drawRedHello. I added a size parameter which is used to call a createRegularFace function, which is an adapted version of @Elara6331's answer3. The main function then creates an image, draws a black background, draws a centered red "hello", and saves the resulting image as a PNG file. Add some error handling and put it all together as the code below:

package main

import (
    "image"
    "image/color"
    "image/draw"
    "image/png"
    "os"

    "golang.org/x/image/font"
    "golang.org/x/image/font/gofont/goregular"
    "golang.org/x/image/font/opentype"
    "golang.org/x/image/math/fixed"
)

// createRegularFace creates a new font face of goregular with the given size
func createRegularFace(size int) (*font.Face, error) {
    font, err := opentype.Parse(goregular.TTF)
    if err != nil {
        return nil, err
    }

    face, err := opentype.NewFace(font, &opentype.FaceOptions{
        Size: float64(size), // Size of font in pt
        DPI:  72,
    })
    if err != nil {
        return nil, err
    }

    return &face, nil
}

// drawRedHello draws the string "hello" with the given size on img at the coordinates x, y
func drawRedHello(size, x, y int, img *image.RGBA) error {
    col := color.RGBA{0xff, 0x55, 0, 0xff}
    point := fixed.Point26_6{
        X: fixed.I(x),
        Y: fixed.I(y),
    }

    face, err := createRegularFace(size)
    if err != nil {
        return err
    }

    d := &font.Drawer{
        Dst:  img,
        Src:  image.NewUniform(col),
        Face: *face,
        Dot:  point,
    }
    d.DrawString("hello")

    return nil
}

// main saves a "hello.png" file which contains an image of a red "hello" centered on a black background
func main() {
    size := 16
    width := size * 4
    height := size * 3
    // can use font.MeasureString for more precise calculations, this is just approximate for example purposes
    midX := width/2 - size
    midY := height/2 + size/2 - 1

    // create blank black destination image
    img := image.NewRGBA(image.Rect(0, 0, width, height))
    draw.Draw(img, img.Bounds(), image.Black, image.Point{}, draw.Src)

    err := drawRedHello(size, midX, midY, img)
    if err != nil {
        panic(err)
    }

    // save image as png
    file, err := os.Create("hello.png")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    png.Encode(file, img)
}

You can run this in the Go Playground, but since it outputs a PNG as a file, you can't really see the results. As such, I wrote up some steps to reproduce it locally:

  1. Create a folder to hold the file:

    mkdir image-font-test/
    cd image-font-test/
    
  2. Copy and paste the file as main.go into the image-font-test folder.

  3. Initialize the module and download the dependencies:

    go mod init main
    go mod tidy
    
  4. Run the code:

    go run main.go
    
  5. Check the output by opening hello.png:

    open hello.png
    
  6. See an image like this:
    black background with the text "hello" in red, centered


  1. See also Difference between font face, typeface, font in the context of typography? from Graphic Design Stack Exchange and the Wikipedia page for Web typography.

  2. See also the Wikipedia page for Computer font.

  3. While I posted my answer later, I did start writing it before. I originally saw your question in Staging Ground and couldn't find any existing questions that it might duplicate nor other code samples online, so thought it was a good question to write an answer for. It ended up getting approved and answered before I could finish!
    As such, I independently created some similar code, but matched the code from the other answer so that, essentially, there would be a "short-form" and "long-form" version.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.