虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Golangで画像に透かしを挿入するCLIツールの作成

こんにちは。虎の穴ラボのCTO野田です。

この記事は「虎の穴ラボ夏のアドベントカレンダー」18日目の記事です。
17日目は辻村さんによる「本番環境に寄り添った開発用Docker環境の構築手法」が投稿されました。

今回紹介するもの

画像の任意の位置にロゴ透かし(ウォーターマーク)を挿入し合成できるCLIツールをGo言語で作ってみました。

今回作った動機

  • Golangの勉強にはいい教材だから。
  • 画像を扱う業務では使えるかもしれない。

メイン実装

package main

import (
    "flag"
    "fmt"
    "image"
    "log"
    "os"
    "path/filepath"
    "strings"

    "image/draw"
    "image/jpeg"
    "image/png"
)

func main() {

    var oImageFilePath string
    var logoImagePath string
    var outFolderPath string
    var logoPosition string
    var useOriginalFilename bool
    flag.StringVar(&oImageFilePath, "f", "", "originalImageFilePath")
    flag.StringVar(&logoImagePath, "l", "", "logoImageFilePath")
    flag.StringVar(&outFolderPath, "o", "", "outFolderPath")
    flag.StringVar(&logoPosition, "p", "", "TopLeft | TopRight | BottomLeft | BottomRight | Center")
    flag.BoolVar(&useOriginalFilename, "u", false, "出力ファイル名にオリジナルファイル名を使う")
    flag.Parse()

    originFile, err := os.Open(oImageFilePath)
    if err != nil {
        fmt.Println(err)
    }
    defer originFile.Close()

    logoFile, err := os.Open(logoImagePath)
    if err != nil {
        fmt.Println(err)
    }
    defer logoFile.Close()

    originImg, format, err := image.Decode(originFile)
    if err != nil {
        log.Fatalf("failed to decode image: %s", err.Error())
    }
    logoImg, _, err := image.Decode(logoFile)
    if err != nil {
        log.Fatalf("failed to decode image: %s", err.Error())
    }

    oPoint := originImg.Bounds().Size()
    lPoint := logoImg.Bounds().Size()

    x, y := positionInt(logoPosition, oPoint.X, oPoint.Y, lPoint.X, lPoint.Y)
    fmt.Printf("%d, %d, %d ,%d\n", oPoint.X, oPoint.Y, lPoint.X, lPoint.Y)
    fmt.Printf("%d, %d\n", x, y)

    startPointLogo := image.Point{x, y}

    logoRectangle := image.Rectangle{startPointLogo, startPointLogo.Add(logoImg.Bounds().Size())}
    originRectangle := image.Rectangle{image.Point{0, 0}, originImg.Bounds().Size()}

    rgba := image.NewRGBA(originRectangle)
    draw.Draw(rgba, originRectangle, originImg, image.Point{0, 0}, draw.Src)
    draw.Draw(rgba, logoRectangle, logoImg, image.Point{0, 0}, draw.Over)

    if format == "jpeg" {
        out, err := os.Create(outFilePath(oImageFilePath, "jpg", outFolderPath, useOriginalFilename))
        if err != nil {
            fmt.Println(err)
        }

        var opt jpeg.Options
        opt.Quality = 80

        err = jpeg.Encode(out, rgba, &opt)
        if err != nil {
            log.Fatalf("failed to encode image: %s", err.Error())
        }

    } else {
        out, err := os.Create(outFilePath(oImageFilePath, "png", outFolderPath, useOriginalFilename))
        if err != nil {
            fmt.Println(err)
        }
        err = png.Encode(out, rgba)
        if err != nil {
            log.Fatalf("failed to encode image: %s", err.Error())
        }
    }
}

func outFilePath(oFilePath string, ext string, outFolder string, useOfn bool) string {
    const defaultFileName = "logo-watermark"
    const addFileName = "-lw"
    var filaneme string

    if useOfn {
        ext := filepath.Ext(oFilePath)
        pf := strings.TrimSuffix(filepath.Base(oFilePath), ext)

        filaneme = pf + addFileName
    } else {
        filaneme = defaultFileName
    }

    if outFolder == "" {
        return filaneme + "." + ext
    } else {
        return filepath.Join(outFolder, filaneme+"."+ext)
    }

}

ロゴの位置を決める部分の実装

func positionInt(position string, w int, h int, lw int, lh int) (int, int) {

    const xLeft = 0
    const yTop = 0
    xRight := w - lw
    ybottom := h - lh

    switch position {
    case "TopLeft":
        return xLeft, yTop
    case "TopRight":
        return xRight, yTop
    case "BottomLeft":
        return xLeft, ybottom
    case "BottomRight":
        return xRight, ybottom
    case "Center":
        cx, cy := w/2, h/2
        clx, cly := lw/2, lh/2
        return cx - clx, cy - cly
    default:
        // TopLeftをデフォルトとする
        return xLeft, yTop
    }
}

書いて気づいたTips

image.Decode関数は画像ファイルをフォーマット(jpegやpng)によらずいい感じに開いてくれますが、import _"image/jpeg" などで開きたフォーマットライブラリのブランクインポートが必要になります。それに気づかず一回画像ファイルを開いてフォーマットを調べてから jpeg.Decodeやpng.Decodeを個別に呼び出すコードを最初書いてました。

使い方

ビルド

go build -o ./bin/logo-watermark logo-watermark/logo-watermark.go

ファイル名をlogo-watermark.goとした場合

HELP

./bin/logo-watermark -h
Usage of ./bin/logo-watermark:
  -f string
        originalImageFilePath
  -l string
        logoImageFilePath
  -o string
        outFolderPath
  -p string
        TopLeft | TopRight | BottomLeft | BottomRight | Center
  -u    出力ファイル名にオリジナルファイル名を使う

右下にロゴを入れる

./bin/logo-watermark -f ./private/gop-500x500.png -l ./image/logo-sample.png -p BottomRight

真ん中にロゴを入れる

./bin/logo-watermark -f ./private/gop-500x500.png -l ./image/logo-sample.png -p Center

サンプルで使ったGopher君画像

とらラボのサイトで配布しています。 画像プログラムの検証などで是非どんどん使ってください。

yumenosora.co.jp

実用性を考えてプログラムを発展するには

  • 画像でなく文字列をフォントで埋め込み。
  • 対象画像が巨大でロゴサイズが小さいと透かし画像が小さくダサく見えるのでロゴをいい感じにプログラムで拡大縮小したい。
  • むしろロゴ画像をプログラムで自動生成したい。
  • ロゴ画像をベクタ形式の画像で用意すれば解決?
  • golangでWEBサーバーてててWEBツール化、もしくはAPI Gateway+AWS lambda化すると応用が効きそう。