Golang으로 이미지에 워터마크 삽입하기

Linsoo

이전부터 블로그에 이미지 올릴때 PhotoScape라는 툴을 써서 워터마크를 찍곤 했는데 이 툴의 단점이 이미지 크기에 따라 자동으로 워터마크를 찍을수 없다는것이다.

그래서 이미지 리사이징 하고 워터마크를 찍었는데 은근히 불편함. 그래서 이전에 파이썬으로 만들어본적이 있는데 ( https://linsoo.co.kr/archives/24037 ) 이런저런 사유로 인해 파이썬 공부를 안하게 되서 다른 언어로 만들어볼까 하다가 Golang으로 만들어봄.

파이썬버전이랑 좀 다른점은 이번에 만들면서 알게된 것인데 세로로 긴 이미지 찍을때 하단에 큰 글짜로 워터마크 찍혀서 보기 안좋길래 세로형 이미지는 90도 회전해서 우상단에 찍도록 했음.
(아래 이미지는 샘플)

갑천길꿀벌사진

일단 돌아만 가는 수준까지만 만들고… 끝 (추가기능은 직접 넣으세요 ㅋㅋㅋ)
기능이라면 현재 폴더에 있는 이미지 파일 읽어서 이미지 크기에 비례해서 텍스트 워터마크를 우하단에 찍어줌.

뭔가 궁금한거 있으면 댓글로…

package main

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"
	_ "image/gif"
	"image/jpeg"
	"image/png"
	"io/ioutil"
	"math"
	"strings"

	"os"
	"regexp"

	//_ "golang.org/x/image/webp"
	"github.com/golang/freetype"
	"github.com/golang/freetype/truetype"
	"golang.org/x/image/font"
)

//전역변수
var (
	g_text_Watermark = "Linsoo.co.kr `" //워터마크 텍스트
	g_fontFileName   = "Goyang.ttf"     //폰트파일이름
	g_dpi            = float64(72)      //DPI
	g_font           = new(truetype.Font)

	//워터마크 텍스트 색상
	g_colorText = image.NewUniform(color.RGBA{255, 255, 255, 255})
	//아웃라인 색상
	g_colorOutline = image.NewUniform(color.RGBA{128, 128, 128, 255})
	//그림자 색상
	g_colorShadow = image.NewUniform(color.RGBA{0, 0, 0, 90})

	//워터마크 폰트크기 비율 (이미지 크기의 몇%를 차지 하는가)
	g_watermarkFontHeightRatio = 0.071
)

//---------------------------------------------------------------------------------------------------
//정규식
var (
	//확장자 찾는 정규식
	re_findEXT, _ = regexp.Compile(".+\\.(\\w+)?$")

	//파일 이름만 찾는 정규식 (.+[\\\/](\w+)?\..+$)
	//파일 이름+확장자 까지 구하는 정규식
	re_findFileName, _ = regexp.Compile(`(?m).+[\\\/]([^\\\/:*?"<>|]+\.[^\\\/:*?"<>|]+)?$`)

	//이미지 파일만 선택
	re_findImageFiles, _ = regexp.Compile(`(?m)^.+\.(jpg|png|bmp)$`)
)

//---------------------------------------------------------------------------------------------------
func firstInit() {
	fontBytes, err := ioutil.ReadFile(g_fontFileName)
	if err != nil {
		fmt.Println(err)
		return
	}

	g_font, err = freetype.ParseFont(fontBytes)
	if err != nil {
		fmt.Println(err)
		return
	}
}

//---------------------------------------------------------------------------------------------------
//원본이미지 크기에 맞게 텍스트 이미지를 생성한다.
func makeTextImage(text string, originalImageWidth int, originalImageHeight int) (landscape bool, textImg *image.RGBA) {
	var fontSize float64 = 0

	if originalImageWidth > originalImageHeight {
		landscape = true
		//워터마크 폰트 크기 (이미지의 긴축을 기준으로 지정한다)
		fontSize = math.Round(float64(originalImageHeight) * g_watermarkFontHeightRatio)

	} else {
		landscape = false
		//워터마크 폰트 크기 (이미지의 긴축을 기준으로 지정한다)
		fontSize = math.Round(float64(originalImageWidth) * g_watermarkFontHeightRatio)
	}

	fontFace := truetype.NewFace(g_font, &truetype.Options{Size: fontSize, DPI: g_dpi})
	drawer := &font.Drawer{
		Face: fontFace,
	}

	//아웃라인 두께
	outlineAmount := int(math.Round(fontSize * 0.013))
	//외곽선 굵기가 0 나오면 1 줌
	if outlineAmount < 1 {
		outlineAmount = 1
	}

	//그림자 위치
	shadowX := 2 * outlineAmount
	shadowY := 2 * outlineAmount
	_, _ = shadowX, shadowY

	//이미지화된 가로세로 넓이
	watermarkImageWidth := drawer.MeasureString(text).Ceil()
	watermarkImageHeight := drawer.Face.Metrics().Height.Ceil()
	_, _ = watermarkImageWidth, watermarkImageHeight

	//이미지 텍스트를 그릴 캔버스 준비
	tmpTextImage := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{watermarkImageWidth, watermarkImageHeight}})
	drawer.Dst = tmpTextImage

	//워터마크 x위치
	posWatermarkStartX := 0
	posWatermarkStartY := watermarkImageHeight - drawer.Face.Metrics().Descent.Ceil()
	_, _ = posWatermarkStartX, posWatermarkStartY

	//-------------------------------------------------------------------------------
	//그림자
	drawer.Src = g_colorShadow
	drawer.Dot = freetype.Pt(posWatermarkStartX-outlineAmount+shadowX, posWatermarkStartY+shadowY)
	drawer.DrawString(text)
	drawer.Dot = freetype.Pt(posWatermarkStartX+outlineAmount+shadowX, posWatermarkStartY+shadowY)
	drawer.DrawString(text)
	drawer.Dot = freetype.Pt(posWatermarkStartX+shadowX, posWatermarkStartY-outlineAmount+shadowY)
	drawer.DrawString(text)
	drawer.Dot = freetype.Pt(posWatermarkStartX+shadowX, posWatermarkStartY+outlineAmount+shadowY)
	drawer.DrawString(text)

	drawer.Dot = freetype.Pt(posWatermarkStartX-outlineAmount+shadowX, posWatermarkStartY-outlineAmount+shadowY)
	drawer.DrawString(text)
	drawer.Dot = freetype.Pt(posWatermarkStartX+outlineAmount+shadowX, posWatermarkStartY-outlineAmount+shadowY)
	drawer.DrawString(text)
	drawer.Dot = freetype.Pt(posWatermarkStartX-outlineAmount+shadowX, posWatermarkStartY+outlineAmount+shadowY)
	drawer.DrawString(text)
	drawer.Dot = freetype.Pt(posWatermarkStartX+outlineAmount+shadowX, posWatermarkStartY+outlineAmount+shadowY)
	drawer.DrawString(text)
	//-------------------------------------------------------------------------------
	//그림자용 메인 텍스트
	drawer.Dot = freetype.Pt(posWatermarkStartX+shadowX, posWatermarkStartY+shadowY)
	drawer.DrawString(text)
	//-------------------------------------------------------------------------------
	//외곽선
	drawer.Src = g_colorOutline
	//left
	drawer.Dot = freetype.Pt(posWatermarkStartX-outlineAmount, posWatermarkStartY)
	drawer.DrawString(text)
	//right
	drawer.Dot = freetype.Pt(posWatermarkStartX+outlineAmount, posWatermarkStartY)
	drawer.DrawString(text)
	//up
	drawer.Dot = freetype.Pt(posWatermarkStartX, posWatermarkStartY-outlineAmount)
	drawer.DrawString(text)
	//down
	drawer.Dot = freetype.Pt(posWatermarkStartX, posWatermarkStartY+outlineAmount)
	drawer.DrawString(text)

	// left up
	drawer.Dot = freetype.Pt(posWatermarkStartX-outlineAmount, posWatermarkStartY-outlineAmount)
	drawer.DrawString(text)
	// right up
	drawer.Dot = freetype.Pt(posWatermarkStartX+outlineAmount, posWatermarkStartY-outlineAmount)
	drawer.DrawString(text)
	// left down
	drawer.Dot = freetype.Pt(posWatermarkStartX-outlineAmount, posWatermarkStartY+outlineAmount)
	drawer.DrawString(text)
	// right down
	drawer.Dot = freetype.Pt(posWatermarkStartX+outlineAmount, posWatermarkStartY+outlineAmount)
	drawer.DrawString(text)
	//-------------------------------------------------------------------------------
	//메인 텍스트
	drawer.Src = g_colorText
	drawer.Dot = freetype.Pt(posWatermarkStartX, posWatermarkStartY)
	drawer.DrawString(text)
	return landscape, tmpTextImage
}

//---------------------------------------------------------------------------------------------------
func convertImage(srcFilePath string, outputPath string) {

	extName := "" //확장자
	_ = extName
	srcFileName := "" //원본 파일이름
	dstFileName := "" //저장할 파일이름
	dstFilePath := "" //저장할 경로(파일명 포함)

	os.MkdirAll(outputPath, os.ModePerm)

	matched := re_findFileName.FindStringSubmatch(srcFilePath)
	if len(matched) > 0 {
		srcFileName = matched[1] //파일명 찾았음
	}

	matched = re_findEXT.FindStringSubmatch(srcFilePath)
	if len(matched) > 0 {
		extName = matched[1] //확장자 찾았음
	}

	dstFileName = "linsoo_" + srcFileName
	dstFilePath = outputPath + "/" + dstFileName

	//오리지날 이미지 읽고
	fsOriginalImage, err := os.Open(srcFilePath)
	defer fsOriginalImage.Close()
	if err != nil {
		fmt.Println(err)
		return
	}

	oriImage, imgType, err := image.Decode(fsOriginalImage)
	if err != nil {
		fmt.Printf("failed to decode: %s", err)
	}

	//배경이 될 새로운 이미지 캔버스를 만든다.
	tmpImage := image.NewRGBA(oriImage.Bounds())

	//작업이 이뤄지는 캔버스 크기(원본 이미지 크기)
	canvasWidth := oriImage.Bounds().Dx()
	canvasHeight := oriImage.Bounds().Dy()
	_, _ = canvasWidth, canvasHeight

	//오리지날 이미지를 배경으로 깐다
	draw.Draw(tmpImage, oriImage.Bounds(), oriImage, image.ZP, draw.Src)

	//텍스트 이미지 만들고
	landscape, textImg := makeTextImage(g_text_Watermark, canvasWidth, canvasHeight)
	textImageWidth := textImg.Bounds().Dx()
	textImageHeight := textImg.Bounds().Dy()

	//워터마크 이미지와 원본이미지와 떨어짐 간격
	offsetX := int(float64(canvasWidth) * 0.01)
	offsetY := int(float64(canvasHeight) * 0.01)
	_, _ = offsetX, offsetY

	//텍스트를 이미지에 그린다.
	if landscape == true {
		newPos := image.Rectangle{image.Point{canvasWidth - offsetX - textImageWidth, canvasHeight - offsetY - textImageHeight}, image.Point{canvasWidth - offsetX, canvasHeight - offsetY}}
		draw.Draw(tmpImage, newPos, textImg, image.ZP, draw.Over)
	} else {
		//90도 회전시킨 이미지를 만든다
		rotatedTextImage := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{textImageHeight, textImageWidth}})
		rotatedTextImgWidth := rotatedTextImage.Bounds().Dx()
		rotatedTextImgHeight := rotatedTextImage.Bounds().Dy()
		for y := 0; y < rotatedTextImgHeight; y++ {
			for x := 0; x < rotatedTextImgWidth; x++ {
				rotatedTextImage.Set(x, y, textImg.At(rotatedTextImgHeight-y, x))
			}
		}
		newPos := image.Rectangle{image.Point{canvasWidth - offsetX - rotatedTextImgWidth, offsetY}, image.Point{canvasWidth - offsetX, offsetY + rotatedTextImgHeight}}
		draw.Draw(tmpImage, newPos, rotatedTextImage, image.ZP, draw.Over)

	}

	//---------------------------------------------------------------------------------------------------
	//파일로 저장한다.
	fsOutput, err := os.Create(dstFilePath)
	defer fsOutput.Close()
	if err != nil {
		fmt.Println("failed to create:", err)
	}

	switch imgType {
	case "jpeg":
		jpeg.Encode(fsOutput, tmpImage, &jpeg.Options{Quality: 95})
	case "png":
		png.Encode(fsOutput, tmpImage)
	}

	fmt.Println("Name:", srcFileName, "width:", canvasWidth, "height:", canvasHeight, "type:", imgType)

}

func main() {
	firstInit()
	//이미지가 있는 폴더 (실행위치 폴더)
	pathImages := "./"

	files, err := ioutil.ReadDir(pathImages)
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, f := range files {
		if f.Mode().IsDir() == false {
			if re_findImageFiles.MatchString(strings.ToLower(f.Name())) == true {
				convertImage(pathImages+"/"+f.Name(), "./output")
			}
		}

	}

	fmt.Println("아무키나 눌러 종료합니다.")
	fmt.Scanln() // wait for Enter Key

}
크리에이티브 커먼즈 라이선스Linsoo의 저작물인 이 저작물은(는)크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.

댓글 남기기

이메일은 공개되지 않습니다.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

This site uses Akismet to reduce spam. Learn how your comment data is processed.