text.go (2521B)
1 // Copyright 2021 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package images
15
16 import (
17 "image"
18 "image/draw"
19 "io"
20 "strings"
21
22 "github.com/disintegration/gift"
23 "github.com/gohugoio/hugo/common/hugio"
24
25 "golang.org/x/image/font"
26 "golang.org/x/image/font/gofont/goregular"
27 "golang.org/x/image/font/opentype"
28 "golang.org/x/image/math/fixed"
29 )
30
31 var _ gift.Filter = (*textFilter)(nil)
32
33 type textFilter struct {
34 text, color string
35 x, y int
36 size float64
37 linespacing int
38 fontSource hugio.ReadSeekCloserProvider
39 }
40
41 func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
42 color, err := hexStringToColor(f.color)
43 if err != nil {
44 panic(err)
45 }
46
47 // Load and parse font
48 ttf := goregular.TTF
49 if f.fontSource != nil {
50 rs, err := f.fontSource.ReadSeekCloser()
51 if err != nil {
52 panic(err)
53 }
54 defer rs.Close()
55 ttf, err = io.ReadAll(rs)
56 if err != nil {
57 panic(err)
58 }
59 }
60 otf, err := opentype.Parse(ttf)
61 if err != nil {
62 panic(err)
63 }
64
65 // Set font options
66 face, err := opentype.NewFace(otf, &opentype.FaceOptions{
67 Size: f.size,
68 DPI: 72,
69 Hinting: font.HintingNone,
70 })
71 if err != nil {
72 panic(err)
73 }
74
75 d := font.Drawer{
76 Dst: dst,
77 Src: image.NewUniform(color),
78 Face: face,
79 }
80
81 gift.New().Draw(dst, src)
82
83 // Draw text, consider and include linebreaks
84 maxWidth := dst.Bounds().Dx() - 20
85 fontHeight := face.Metrics().Ascent.Ceil()
86
87 // Correct y position based on font and size
88 f.y = f.y + fontHeight
89
90 // Start position
91 y := f.y
92 d.Dot = fixed.P(f.x, f.y)
93
94 // Draw text and break line at max width
95 parts := strings.Fields(f.text)
96 for _, str := range parts {
97 strWith := font.MeasureString(face, str)
98 if (d.Dot.X.Ceil() + strWith.Ceil()) >= maxWidth {
99 y = y + fontHeight + f.linespacing
100 d.Dot = fixed.P(f.x, y)
101 }
102 d.DrawString(str + " ")
103 }
104 }
105
106 func (f textFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
107 return image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
108 }