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.