Repository: gohugoio/hugo (language: go)

Issue:
images.Text: Add support for vertical alignment of text
<!-- Describe this new feature. Think about if it really belongs in the Hugo core module; you may want to discuss it on https://discourse.gohugo.io/ first.  -->

It would be useful to be able to choose a vertical alignment (relative to the ``y`` offset) for text added using ``images.Text``. This becomes useful in the following scenarios when the number of lines of text is not fixed.
- Centering text vertically.
- Creating an Open Graph image with a subtitle at a fixed distance below the title. This can be achieved by making the title "bottom" aligned and the subtitle "top" aligned and having the same ``y`` offset for both texts.

It is not currently possible to implement these using only the ``y`` option which sets a vertical offset for the top of the text relative to the top of the image.

Vertical alignment would complement the horizontal alignment feature (``alignx``) mentioned in #10849 and added in #13223.


Discussion / hints:

Fix patch:
diff --git a/docs/content/en/functions/images/Text.md b/docs/content/en/functions/images/Text.md
index 94cdb4e9de5..8f7e730baf9 100644
--- a/docs/content/en/functions/images/Text.md
+++ b/docs/content/en/functions/images/Text.md
@@ -18,6 +18,9 @@ alignx
 : {{< new-in 0.141.0 />}}
 : (`string`) The horizontal alignment of the text relative to the horizontal offset, one of `left`, `center`, or `right`. Default is `left`.
 
+aligny
+: (`string`) The vertical alignment of the text relative to the vertical offset, one of `top`, `center`, or `bottom`. Default is `top`.
+
 color
 : (`string`) The font color, either a 3-digit or 6-digit hexadecimal color code. Default is `#ffffff` (white).
 
diff --git a/resources/images/filters.go b/resources/images/filters.go
index 9c2b9b46f9e..1e44f11841f 100644
--- a/resources/images/filters.go
+++ b/resources/images/filters.go
@@ -79,6 +79,7 @@ func (*Filters) Text(text string, options ...any) gift.Filter {
 		x:           10,
 		y:           10,
 		alignx:      "left",
+		aligny:      "top",
 		linespacing: 2,
 	}
 
@@ -102,6 +103,11 @@ func (*Filters) Text(text string, options ...any) gift.Filter {
 				if tf.alignx != "left" && tf.alignx != "center" && tf.alignx != "right" {
 					panic("alignx must be one of left, center, right")
 				}
+			case "aligny":
+				tf.aligny = cast.ToString(v)
+				if tf.aligny != "top" && tf.aligny != "center" && tf.aligny != "bottom" {
+					panic("aligny must be one of top, center, bottom")
+				}
 
 			case "linespacing":
 				tf.linespacing = cast.ToInt(v)
diff --git a/resources/images/text.go b/resources/images/text.go
index 32487883987..f3943a475b1 100644
--- a/resources/images/text.go
+++ b/resources/images/text.go
@@ -36,6 +36,7 @@ type textFilter struct {
 	color       color.Color
 	x, y        int
 	alignx      string
+	aligny      string
 	size        float64
 	linespacing int
 	fontSource  hugio.ReadSeekCloserProvider
@@ -110,12 +111,19 @@ func (f textFilter) Draw(dst draw.Image, src image.Image, options *gift.Options)
 		}
 		finalLines = append(finalLines, currentLine)
 	}
+	// Total height of the text from the top of the first line to the baseline of the last line
+	totalHeight := len(finalLines)*fontHeight + (len(finalLines)-1)*f.linespacing
 
 	// Correct y position based on font and size
-	f.y = f.y + fontHeight
-
-	// Start position
-	y := f.y
+	y := f.y + fontHeight
+	switch f.aligny {
+	case "top":
+		// Do nothing
+	case "center":
+		y = y - totalHeight/2
+	case "bottom":
+		y = y - totalHeight
+	}
 
 	// Draw text line by line
 	for _, line := range finalLines {


Test patch:
diff --git a/resources/images/images_golden_integration_test.go b/resources/images/images_golden_integration_test.go
index c49de7bd191..5397bee232c 100644
--- a/resources/images/images_golden_integration_test.go
+++ b/resources/images/images_golden_integration_test.go
@@ -252,8 +252,8 @@ Home.
   "linespacing" 8
   "size" 28
   "x" (div $sunset.Width 2 | int)
+  "y" (div $sunset.Height 2 | int)
   "alignx" "center"
-  "y" 190
 }}
 
 {{ $text := "Pariatur deserunt sunt nisi sunt tempor quis eu. Sint et nulla enim officia sunt cupidatat. Eu amet ipsum qui velit cillum cillum ad Lorem in non ad aute." }}
@@ -262,6 +262,11 @@ Home.
 {{ template "filters" (dict "name" "text_alignx-right.jpg" "img" $sunset  "filters" (images.Text $text $textOpts )) }}
 {{ $textOpts = (dict "alignx" "left") | merge $textOpts }}
 {{ template "filters" (dict "name" "text_alignx-left.jpg" "img" $sunset  "filters" (images.Text $text $textOpts )) }}
+{{ $textOpts = (dict "alignx" "center" "aligny" "center") | merge $textOpts }}
+{{ $text = "Est exercitation deserunt exercitation nostrud magna. Eiusmod anim deserunt sit elit dolore ea incididunt nisi. Ea ullamco excepteur voluptate occaecat duis pariatur proident cupidatat.  Eu id esse qui consectetur commodo ad ex esse cupidatat velit duis cupidatat. Aliquip irure tempor consequat non amet in mollit ipsum officia tempor laborum." }}
+{{ template "filters" (dict "name" "text_alignx-center_aligny-center.jpg" "img" $sunset  "filters" (images.Text $text $textOpts )) }}
+{{ $textOpts = (dict "alignx" "center" "aligny" "bottom") | merge $textOpts }}
+{{ template "filters" (dict "name" "text_alignx-center_aligny-bottom.jpg" "img" $sunset  "filters" (images.Text $text $textOpts )) }}
 
 {{ define "filters"}}
 {{ if lt (len (path.Ext .name)) 4 }}
@@ -279,6 +284,8 @@ Home.
 	opts.T = t
 	opts.Name = name
 	opts.Files = files
+	// opts.WriteFiles = true
+	// opts.DevMode = true
 
 	imagetesting.RunGolden(opts)
 }
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg
index 090600f5f26..94bcb811a6f 100644
Binary files a/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg and b/resources/images/testdata/images_golden/filters/text/text_alignx-center.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg
new file mode 100644
index 00000000000..ca8893e4efb
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-bottom.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg
new file mode 100644
index 00000000000..828b6e6a33a
Binary files /dev/null and b/resources/images/testdata/images_golden/filters/text/text_alignx-center_aligny-center.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg
index d77e301df32..2894fae4276 100644
Binary files a/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg and b/resources/images/testdata/images_golden/filters/text/text_alignx-left.jpg differ
diff --git a/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg b/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg
index 3b727234a7e..207e88a496d 100644
Binary files a/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg and b/resources/images/testdata/images_golden/filters/text/text_alignx-right.jpg differ