How to make a simple logo watermark tool in 100 lines of Golang

(October 2019)

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’ll be looking at how to make a simple watermark tool in Golang. The program will place a smaller image (the logo) on the larger one, at the bottom right.

Prerequisites

Overview

Our watermark tool will do the following:

Image Processing

For basic image manipulation, this imaging package by disintegration is sufficient. It has features to

To add the package, run go get -u [github.com/disintegration/imaging](http://github.com/disintegration/imaging)

Code

Let’s start with the main function. Since it’s a command line application, we’ll receive the file names as arguments.

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'll omit it.
	args := os.Args[1:]

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

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

Next, we need a function to place an image over the other.

Considering the Single Responsibility Principle, I chose to separate the image placement logic from the watermark logic to make the program more flexible.

In total, we’ll write three functions in this section. P.S. For brevity, i’m including only the dependencies needed for each function.

parseCoordinates - Get coordinates from text such as 200x200.

import (
	"log"
	"strconv"
	"strings"
)

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 - Read image from the specified path.

import (
	"log"
	"github.com/disintegration/imaging"
)

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.

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

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

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

	src := openImage(image)

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

placeImage - Put one image on another. This uses both parseCoordinates and openImage.

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)
}

This should be pretty easy to understand as we’ve separated all the different logic into functions.


We can now implement our watermark function, bringing all this together. In this getting this right, there are two things involved:

  1. Calculating the watermark position.
  2. Placing the watermark in that position.

Calculating The WaterMark Position

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

Adding the water mark

Finally, we can implement the function to add the watermark. This function does the following:

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.

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'll 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
}

That’s it. We’ve written a basic watermark tool in ~100 lines of Golang. Hopefully this was pretty straightforward and easy to replicate.

Ideas for Improvement

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.

P.S I never intend for these posts to get this long. But they eventually do 🙃

Hi! My name is Opeyemi. I like distributed systems, NodeJS, Golang and Puff Puff. You can learn more about me or message me on Twitter.

Share on