-
Notifications
You must be signed in to change notification settings - Fork 17
/
watermark.go
282 lines (243 loc) · 6.41 KB
/
watermark.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
// SPDX-FileCopyrightText: 2018-2024 caixw
//
// SPDX-License-Identifier: MIT
// Package watermark 提供一个简单的水印功能
package watermark
import (
"errors"
"image"
"image/draw"
"image/gif"
"image/jpeg"
"image/png"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
)
// 水印的位置
const (
TopLeft Pos = iota
TopRight
BottomLeft
BottomRight
Center
)
var (
// ErrUnsupportedWatermarkType 不支持的水印类型
ErrUnsupportedWatermarkType = errors.New("不支持的水印类型")
// ErrWatermarkTooLarge 当水印位置距离右下角的范围小于水印图片时,返回错误。
ErrWatermarkTooLarge = errors.New("水印太大")
)
// 允许做水印的图片类型
var allowExts = []string{
".gif", ".jpg", ".jpeg", ".png",
}
// Pos 表示水印的位置
type Pos int
// Watermark 用于给图片添加水印功能
//
// 目前支持 gif、jpeg 和 png 三种图片格式。
// 若是 gif 图片,则只取图片的第一帧;png 支持透明背景。
type Watermark struct {
image image.Image // 水印图片
gifImg *gif.GIF // 如果是 GIF 图片,image 保存第一帧的图片, gifImg 保存全部内容
padding int // 水印留的边白
pos Pos // 水印的位置
}
// NewFromFile 从文件声明一个 [Watermark] 对象
//
// path 为水印文件的路径;
// padding 为水印在目标图像上的留白大小;
// pos 水印的位置。
func NewFromFile(path string, padding int, pos Pos) (*Watermark, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return New(f, filepath.Ext(path), padding, pos)
}
// NewFromFS 从文件系统初始化 [Watermark] 对象
func NewFromFS(fsys fs.FS, path string, padding int, pos Pos) (*Watermark, error) {
f, err := fsys.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return New(f, filepath.Ext(path), padding, pos)
}
// New 声明 [Watermark] 对象
//
// r 为水印图片内容;
// ext 为水印图片的扩展名,会根据扩展名判断图片类型;
// padding 为水印在目标图像上的留白大小;
// pos 图片位置;
func New(r io.Reader, ext string, padding int, pos Pos) (w *Watermark, err error) {
if pos < TopLeft || pos > Center {
panic("无效的 pos 值")
}
var img image.Image
var gifImg *gif.GIF
switch strings.ToLower(ext) {
case ".jpg", ".jpeg":
img, err = jpeg.Decode(r)
case ".png":
img, err = png.Decode(r)
case ".gif":
gifImg, err = gif.DecodeAll(r)
img = gifImg.Image[0]
default:
return nil, ErrUnsupportedWatermarkType
}
if err != nil {
return nil, err
}
return &Watermark{
image: img,
gifImg: gifImg,
padding: padding,
pos: pos,
}, nil
}
// IsAllowExt 该扩展名的图片是否允许使用水印
//
// ext 必须带上 . 符号
func IsAllowExt(ext string) bool {
if ext == "" {
panic("参数 ext 不能为空")
}
if ext[0] != '.' {
panic("参数 ext 必须以 . 开头")
}
ext = strings.ToLower(ext)
for _, e := range allowExts {
if e == ext {
return true
}
}
return false
}
// MarkFile 给指定的文件打上水印
func (w *Watermark) MarkFile(path string) error {
file, err := os.OpenFile(path, os.O_RDWR, os.ModePerm)
if err != nil {
return err
}
defer file.Close()
return w.Mark(file, strings.ToLower(filepath.Ext(path)))
}
// Mark 将水印写入 src 中,由 ext 确定当前图片的类型。
func (w *Watermark) Mark(src io.ReadWriteSeeker, ext string) (err error) {
var srcImg image.Image
ext = strings.ToLower(ext)
switch ext {
case ".gif":
return w.markGIF(src) // GIF 另外单独处理
case ".jpg", ".jpeg":
srcImg, err = jpeg.Decode(src)
case ".png":
srcImg, err = png.Decode(src)
default:
return ErrUnsupportedWatermarkType
}
if err != nil {
return err
}
bound := srcImg.Bounds()
point := w.getPoint(bound.Dx(), bound.Dy())
if err = w.checkTooLarge(point, bound); err != nil {
return err
}
dstImg := image.NewNRGBA64(srcImg.Bounds())
draw.Draw(dstImg, dstImg.Bounds(), srcImg, image.Point{}, draw.Src)
draw.Draw(dstImg, dstImg.Bounds(), w.image, point, draw.Over)
if _, err = src.Seek(0, 0); err != nil {
return err
}
switch ext {
case ".jpg", ".jpeg":
return jpeg.Encode(src, dstImg, nil)
case ".png":
return png.Encode(src, dstImg)
default:
return ErrUnsupportedWatermarkType
}
}
func (w *Watermark) markGIF(src io.ReadWriteSeeker) error {
srcGIF, err := gif.DecodeAll(src)
if err != nil {
return err
}
bound := srcGIF.Image[0].Bounds()
point := w.getPoint(bound.Dx(), bound.Dy())
if err = w.checkTooLarge(point, bound); err != nil {
return err
}
if w.gifImg == nil {
for index, img := range srcGIF.Image {
dstImg := image.NewPaletted(img.Bounds(), img.Palette)
draw.Draw(dstImg, dstImg.Bounds(), img, image.Point{}, draw.Src)
draw.Draw(dstImg, dstImg.Bounds(), w.image, point, draw.Over)
srcGIF.Image[index] = dstImg
}
} else { // 水印也是 GIF
windex := 0
wmax := len(w.gifImg.Image)
for index, img := range srcGIF.Image {
dstImg := image.NewPaletted(img.Bounds(), img.Palette)
draw.Draw(dstImg, dstImg.Bounds(), img, image.Point{}, draw.Src)
// 获取对应帧数的水印图片
if windex >= wmax {
windex = 0
}
draw.Draw(dstImg, dstImg.Bounds(), w.gifImg.Image[windex], point, draw.Over)
windex++
srcGIF.Image[index] = dstImg
}
}
if _, err = src.Seek(0, 0); err != nil {
return err
}
return gif.EncodeAll(src, srcGIF)
}
func (w *Watermark) checkTooLarge(start image.Point, dst image.Rectangle) error {
// 允许的最大高宽
width := dst.Dx() - start.X - w.padding
height := dst.Dy() - start.Y - w.padding
if width < w.image.Bounds().Dx() || height < w.image.Bounds().Dy() {
return ErrWatermarkTooLarge
}
return nil
}
func (w *Watermark) getPoint(width, height int) image.Point {
var point image.Point
switch w.pos {
case TopLeft:
point = image.Point{X: -w.padding, Y: -w.padding}
case TopRight:
point = image.Point{
X: -(width - w.padding - w.image.Bounds().Dx()),
Y: -w.padding,
}
case BottomLeft:
point = image.Point{
X: -w.padding,
Y: -(height - w.padding - w.image.Bounds().Dy()),
}
case BottomRight:
point = image.Point{
X: -(width - w.padding - w.image.Bounds().Dx()),
Y: -(height - w.padding - w.image.Bounds().Dy()),
}
case Center:
point = image.Point{
X: -(width - w.padding - w.image.Bounds().Dx()) / 2,
Y: -(height - w.padding - w.image.Bounds().Dy()) / 2,
}
default:
panic("无效的 pos 值")
}
return point
}