loading...

How to make a logo watermark tool in Golang on any Unix Distribution

ope__o profile image Onikute. ・8 min read

Introduction

A common need for creatives is to place our logo on pictures we produce. e.g. A photographer with 100s of dope pictures about to post them on social media.

In this tutorial, we will make a simple watermark tool in Golang. The program will place a smaller image (the logo) on the larger one, at the bottom right.

Go is described as an "open source programming language that makes it easy to build simple, reliable, and efficient software". It's pretty fun to learn and is worth a closer look if you're just hearing about it.

Prerequisites

You will need the following:

  • A computer with a working internet connection.
  • A local Golang installation. These tutorials by DigitalOcean can help you install and setup Go on MacOS and Ubuntu. You can also download Go for your distribution directly and follow the installation instructions after downloading.

Goals

We are setting out to make a watermark tool that can accomplish the following:

  • Accept names of a background image and a watermark.
  • Resize the watermark while preserving it’s aspect ratio.
  • Place the water mark on the bottom right of the background, with a default padding.
  • Save the new image with a different name.

Step 1 - Setting up the project folder

If you followed the instructions to install Go, you now have a working installation on your machine. We will be working from the $GOPATH/src directory, which is usually /go/src on most installations. You can run echo $GOPATH and copy the output.

Navigate to this directory using cd /go/src and create a folder named watermark by running the command mkdir watermark. Create the main.go file which we'll be using for the remainder of this tutorial by running the command touch main.go.

We also need two sample images that we'll use for our testing. You can use any images you deem fit. Just ensure they are named sample1.png and sample2.png. The output image will be stored in the output directory.

Our resulting directory structure should look like this:

/go/src    
│
└─── watermark
│   │   main.go
│   │   sample1.png
│   │   sample2.png
|   └── output

Step 2 - Installing the Image processing package

For basic image manipulation, the imaging package by disintegration is a good choice. It has features to resize, filter, transform images and more.

To install the package, run go get -u http://github.com/disintegration/imaging

Step 3 - Receiving input as command line arguments

Since this is a command line application, a good place to start is with receiving arguments. Go has a simple interface to do this using the native os package. We can retrieve arguments as strings separated by spaces in the Args property of os, which is a slice.

The first argument in os.Args is the path to the file being executed, we can remove the first element in the slice and use the rest. We can then validate the input to ensure the right arguments were entered. The code block below contains the implementation.

package main

import (
    "fmt"
    "os"
)

const invalidCommand = "Please enter a valid input."

func main() {

    // The first argument is the path to the program, so we will omit it.
    args := os.Args[1:]

    if len(args) < 2 {
        fmt.Println(invalidCommand)
        return
    }

    background := args[0]
    watermark := args[1]
}

Step 4 - Placing one image over the other

Considering the Single Responsibility Principle, we will separate the image placement logic from the watermark logic to make the program more flexible. We will also separate other common logic into functions so they are re-usable.

The PlaceImage function will receive 5 arguments which include the output image name (what we want the output to be called), plus the names and dimensions of the background image and smaller image.

The code block below provides the implementation of PlaceImage. For brevity, i'm only including the function and it's associated imports.

import (
    "fmt"
    "os"
    "github.com/disintegration/imaging"
)

func PlaceImage(outName, bgImg, markImg, markDimensions, locationDimensions string) {

    // Coordinate to super-impose on. e.g. 200x500
    locationX, locationY := ParseCoordinates(locationDimensions, "x")

    src := OpenImage(bgImg)

    // Resize the watermark to fit these dimensions, preserving aspect ratio. 
    markFit := ResizeImage(markImg, markDimensions)

    // Place the watermark over the background in the location
    dst := imaging.Paste(src, markFit, image.Pt(locationX, locationY))

    err := imaging.Save(dst, outName)

    if err != nil {
        log.Fatalf("failed to save image: %v", err)
    }

    fmt.Printf("Placed image '%s' on '%s'.\n", markImg, bgImg)
}

If you read through the code, you must have noticed there are a couple of functions that have not been defined yet. These are the helper functions I mentioned above. Let's have a look at their implementations.

ParseCoordinates - This function get x and y coordinates from text such as 200x200. It receives a string and returns two integers.

func ParseCoordinates(input, delimiter string) (int, int) {

    arr := strings.Split(input, delimiter)

    // convert a string to an int
    x, err := strconv.Atoi(arr[0])

    if err != nil {
        log.Fatalf("failed to parse x coordinate: %v", err)
    }

    y, err := strconv.Atoi(arr[1])

    if err != nil {
        log.Fatalf("failed to parse y coordinate: %v", err)
    }

    return x, y
}

OpenImage - This function reads an image from the specified path. If the image is not found, it throws a fatal error and the program is exited.

func OpenImage(name string) image.Image {
    src, err := imaging.Open(name)
    if err != nil {
        log.Fatalf("failed to open image: %v", err)
    }
    return src
}

ResizeImage - Resize an image to fit these dimensions, preserving aspect ratio.

func ResizeImage (image, dimensions string) image.Image {
    width, height := ParseCoordinates(dimensions, "x")
    src := OpenImage(image)
    return imaging.Fit(src, width, height, imaging.Lanczos)
}

Combined, these functions provide the image placement logic we need, and we can now proceed to write our watermark logic. We are getting there!

Step 5 - Calculating The Water Mark Position

Since we know the watermark is to be placed on the bottom right, we need to:

  1. Get the co-ordinates required to place the watermark on the bottom right

    We can achieve this by subtracting the watermark size from both extremes of the x and y coordinates of the background image. e.g. If the dimensions of the image are 1300x700, and the watermark size is 200x200, the watermark will be placed at 1200x500. To help in getting a mental picture of this, have a look at this image.

  2. Add a padding

    The watermark has to be spaced equidistant from the edges of the background to look good. So we need to add a padding.

    This can be done easily by subtracting the padding (e.g. 20px) from both the x and y coordinates of the watermark position.

    But that presents a small problem: images with an imperfect aspect ratio won't resize to 200x200 since the aspect ratio is preserved. Instead, they'd be skewed (e.g. 200x40 or 40x200), making the padding look uneven.

    To solve this problem, we specify a constant padding of 20 and multiply that by the aspect ratio of the background. This means that the larger side of the image will have less padding, and will remain equidistant from the borders.

The function itself is brief and is separated into different variable names for clarity.

  // Subtracts the dimensions of the watermark and padding based on the background's aspect ratio

    func CalcWaterMarkPosition(bgDimensions, markDimensions image.Point, aspectRatio float64) (int, int) {

        bgX := bgDimensions.X
        bgY := bgDimensions.Y
        markX := markDimensions.X
        markY := markDimensions.Y

        padding := 20 * int(aspectRatio)

        return bgX - markX - padding, bgY - markY - padding
    }

Step 6 - Adding the water mark

We're almost there! Now we can implement the function to add the watermark. This function does the following:

  • Generates a name for the output image.
  • Gets the dimensions of both the background and watermark, using the resize function.
  • Calculates the watermark position.
  • Places the image on the watermark position.

This is implemented in the following code-block.

    func addWaterMark(bgImg, watermark string) {

        outName := fmt.Sprintf("watermark-new-%s", watermark)

        src := OpenImage(bgImg)

        markFit := ResizeImage(watermark, "200x200")

        bgDimensions := src.Bounds().Max
        markDimensions := markFit.Bounds().Max

        bgAspectRatio := math.Round(float64(bgDimensions.X) / float64(bgDimensions.Y))

        xPos, yPos := CalcWaterMarkPosition(bgDimensions, markDimensions, bgAspectRatio)

        PlaceImage(outName, bgImg, watermark, watermarkSize, fmt.Sprintf("%dx%d", xPos, yPos))

        fmt.Printf("Added watermark '%s' to image '%s' with dimensions %s.\n", watermark, bgImg, watermarkSize)
    }

Step 7 - Bringing it all together

We can now complete our main function by bringing all the functions together and running a command. e.g. go run main.go sample1.png sample2.png. The following code-block contains all the code.

    package main

    import (
        "fmt"
        "image"
        "log"
        "math"
        "os"
        "strconv"
        "strings"

        "github.com/disintegration/imaging"
    )

    const invalidCommand = "Please enter a valid input."

    func main() {

        // The first argument is the path to the program, so we will omit it.
        args := os.Args[1:]

        if len(args) < 2 {
            fmt.Println(invalidCommand)
            return
        }

        background := args[0]
        watermark := args[1]

        addWaterMark(background, watermark)
    }

    func addWaterMark(bgImg, watermark string) {

        outName := fmt.Sprintf("watermark-new-%s", watermark)

        src := OpenImage(bgImg)

        markFit := ResizeImage(watermark, "200x200")

        bgDimensions := src.Bounds().Max
        markDimensions := markFit.Bounds().Max

        bgAspectRatio := math.Round(float64(bgDimensions.X) / float64(bgDimensions.Y))

        xPos, yPos := CalcWaterMarkPosition(bgDimensions, markDimensions, bgAspectRatio)

        PlaceImage(outName, bgImg, watermark, watermarkSize, fmt.Sprintf("%dx%d", xPos, yPos))

        fmt.Printf("Added watermark '%s' to image '%s' with dimensions %s.\n", watermark, bgImg, watermarkSize)
    }

    func PlaceImage(outName, bgImg, markImg, markDimensions, locationDimensions string) {

        // Coordinate to super-impose on. e.g. 200x500
        locationX, locationY := ParseCoordinates(locationDimensions, "x")

        src := OpenImage(bgImg)

        // Resize the watermark to fit these dimensions, preserving aspect ratio. 
        markFit := ResizeImage(markImg, markDimensions)

        // Place the watermark over the background in the location
        dst := imaging.Paste(src, markFit, image.Pt(locationX, locationY))

        err := imaging.Save(dst, outName)

        if err != nil {
            log.Fatalf("failed to save image: %v", err)
        }

        fmt.Printf("Placed image '%s' on '%s'.\n", markImg, bgImg)
    }

    func resizeImage (image, dimensions string) image.Image {

        width, height := ParseCoordinates(dimensions, "x")

        src := OpenImage(image)

        return imaging.Fit(src, width, height, imaging.Lanczos)
    }

    func OpenImage(name string) image.Image {
        src, err := imaging.Open(name)
        if err != nil {
            log.Fatalf("failed to open image: %v", err)
        }
        return src
    }

    func ParseCoordinates(input, delimiter string) (int, int) {

        arr := strings.Split(input, delimiter)

        // convert a string to an int
        x, err := strconv.Atoi(arr[0])

        if err != nil {
            log.Fatalf("failed to parse x coordinate: %v", err)
        }

        y, err := strconv.Atoi(arr[1])

        if err != nil {
            log.Fatalf("failed to parse y coordinate: %v", err)
        }

        return x, y
    }

Conclusion

In this tutorial, we have written a basic watermark tool in ~100 lines of Golang. Hopefully this was pretty straightforward and easy to replicate. We can extend this and make it better in a couple of ways.

  1. Add support for multiple background images.

  2. Refactor ParseCoordinates - There has to be a shorter way to do this lol. Maybe map and convert all elements to int.

  3. Add support for different positions.

It's important to keep thinking of new ways to improve the software we come across daily, as we can inadvertently push software engineering forward by doing so.

I hope you had as much fun as I did making this!

Posted on by:

ope__o profile

Onikute.

@ope__o

I am a software engineer with experience building full-stack web applications and a keen interest in distributed systems

Discussion

pic
Editor guide