diff --git a/docs/content/en/functions/images/Text.md b/docs/content/en/functions/images/Text.md index 8c6670d422d..6a18468b746 100644 --- a/docs/content/en/functions/images/Text.md +++ b/docs/content/en/functions/images/Text.md @@ -37,6 +37,10 @@ x y : (`int`) The vertical offset, in pixels, relative to the top of the image. Default is `10`. +alignx + {{< new-in 0.141.0 >}} +: (`string`) The horizontal alignment of the text relative to the `x` position. One of `left`, `center`, or `right`. Default is `left`. + [global resource]: /getting-started/glossary/#global-resource [page resource]: /getting-started/glossary/#page-resource [remote resource]: /getting-started/glossary/#remote-resource diff --git a/resources/images/filters.go b/resources/images/filters.go index 0a620716d99..99fecdf20d7 100644 --- a/resources/images/filters.go +++ b/resources/images/filters.go @@ -69,6 +69,7 @@ func (*Filters) Text(text string, options ...any) gift.Filter { size: 20, x: 10, y: 10, + alignx: "left", linespacing: 2, } @@ -87,6 +88,12 @@ func (*Filters) Text(text string, options ...any) gift.Filter { tf.x = cast.ToInt(v) case "y": tf.y = cast.ToInt(v) + case "alignx": + tf.alignx = cast.ToString(v) + if tf.alignx != "left" && tf.alignx != "center" && tf.alignx != "right" { + panic("alignx must be one of left, center, right") + } + case "linespacing": tf.linespacing = cast.ToInt(v) case "font": diff --git a/resources/images/text.go b/resources/images/text.go index c1abc60bd43..4d77034f16a 100644 --- a/resources/images/text.go +++ b/resources/images/text.go @@ -35,6 +35,7 @@ type textFilter struct { text string color color.Color x, y int + alignx string size float64 linespacing int fontSource hugio.ReadSeekCloserProvider @@ -77,30 +78,62 @@ func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) gift.New().Draw(dst, src) - // Draw text, consider and include linebreaks maxWidth := dst.Bounds().Dx() - 20 + + var availableWidth int + switch f.alignx { + case "right": + availableWidth = f.x + case "center": + availableWidth = min((maxWidth-f.x), f.x) * 2 + case "left": + availableWidth = maxWidth - f.x + } + fontHeight := face.Metrics().Ascent.Ceil() + // Calculate lines, consider and include linebreaks + finalLines := []string{} + f.text = strings.ReplaceAll(f.text, "\r", "") + for _, line := range strings.Split(f.text, "\n") { + currentLine := "" + // Break each line at the maximum width. + for _, str := range strings.Fields(line) { + fieldStrWidth := font.MeasureString(face, str) + currentLineStrWidth := font.MeasureString(face, currentLine) + + if (currentLineStrWidth.Ceil() + fieldStrWidth.Ceil()) >= availableWidth { + finalLines = append(finalLines, currentLine) + currentLine = "" + } + currentLine += str + " " + } + finalLines = append(finalLines, currentLine) + } + // Correct y position based on font and size f.y = f.y + fontHeight // Start position y := f.y - d.Dot = fixed.P(f.x, f.y) - // Draw text line by line, breaking each line at the maximum width. - f.text = strings.ReplaceAll(f.text, "\r", "") - for _, line := range strings.Split(f.text, "\n") { - for _, str := range strings.Fields(line) { - strWidth := font.MeasureString(face, str) - if (d.Dot.X.Ceil() + strWidth.Ceil()) >= maxWidth { - y = y + fontHeight + f.linespacing - d.Dot = fixed.P(f.x, y) - } - d.DrawString(str + " ") + // Draw text line by line + for _, line := range finalLines { + line = strings.TrimSpace(line) + strWidth := font.MeasureString(face, line) + var x int + switch f.alignx { + case "right": + x = f.x - strWidth.Ceil() + case "center": + x = f.x - (strWidth.Ceil() / 2) + + case "left": + x = f.x } + d.Dot = fixed.P(x, y) + d.DrawString(line) y = y + fontHeight + f.linespacing - d.Dot = fixed.P(f.x, y) } }