Repository: gohugoio/hugo (language: go)

Issue:
Add support for aligning text when adding to images using images.Text
If we could left/center/right align text within a textbox (and maybe support word-wrap?) this would make the text rendering a lot more useful.  Often when we are rendering images dynamically, to match some designer-specified template, they would include left/centre/right alignment, which I don't think we can currently do.


Discussion / hints:
That is something we would need as well!

Fix patch:
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..32487883987 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)
 	}
 }
 


Test patch: