From 12cdad0805711848f33d97050c4b5df7641df548 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Wed, 16 Sep 2026 09:55:24 +0300 Subject: [PATCH] text overlay positions preset --- cmd/mxl-pattern/main.go | 27 +++++++++++++++++++++++---- internal/generator/text.go | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/cmd/mxl-pattern/main.go b/cmd/mxl-pattern/main.go index d8f2039..04c7148 100644 --- a/cmd/mxl-pattern/main.go +++ b/cmd/mxl-pattern/main.go @@ -35,6 +35,7 @@ type appArgs struct { textOverlay string overlayX int overlayY int + overlayPos string videoWidth uint videoHeight uint @@ -277,9 +278,26 @@ func flagSetAddFlags(fs *pflag.FlagSet, args *appArgs) { fs.StringVarP(&args.pattern, "pattern", "p", "bars", "Video pattern type") fs.BoolVar(&args.listPatterns, "list-patterns", false, "List video available video patterns and exit") - fs.StringVarP(&args.textOverlay, "text", "t", "", "Text overlay above video pattern") - fs.IntVar(&args.overlayX, "text-x", 0, "Text overlay X position in px") - fs.IntVar(&args.overlayY, "text-y", 0, "Text overlay Y position in px") + fs.StringVarP(&args.textOverlay, "text", "t", "", "Text overlay above video pattern. Ignored if text-pos set") + fs.IntVar(&args.overlayX, "text-x", 0, "Text overlay X position in px. Ignored if text-pos set") + fs.IntVar( + &args.overlayY, + "text-y", + 0, + "Text overlay Y position in px") + fs.StringVar( + &args.overlayPos, + "text-pos", + "", + "Text overlay position with pre-defined values:\n"+ + "tl - top-left corner\n"+ + "tc - top-center\n"+ + "tr - top-right corner\n"+ + "cc - center of the frame\n"+ + "bl - bottom-left corner\n"+ + "bc - bottom-center\n"+ + "br - bottom-right corner", + ) fs.UintVar(&args.videoWidth, "width", 1920, "Video pattern width") fs.UintVar(&args.videoHeight, "height", 1080, "Video pattern height") @@ -376,7 +394,7 @@ func main() { args.overlayX > int(vi.width) || args.overlayY < 0 || args.overlayY > int(vi.height) { - log.Fatalf("Overlay X/Y pos can't be negative or greater, than video width/height") + log.Fatalf("overlay X/Y pos can't be negative or greater, than video width/height") } overlay, err = generator.NewTextOverlay( args.textOverlay, @@ -384,6 +402,7 @@ func main() { int(vi.height), args.overlayX, args.overlayY, + args.overlayPos, face, ) if err != nil { diff --git a/internal/generator/text.go b/internal/generator/text.go index 015ae34..e488c4f 100644 --- a/internal/generator/text.go +++ b/internal/generator/text.go @@ -8,6 +8,7 @@ import ( "fmt" "image" "os" + "slices" "golang.org/x/image/font" "golang.org/x/image/font/opentype" @@ -22,6 +23,8 @@ type TextOverlay struct { blocks []byte // pre-packed v210 tile: h rows x (w/6) blocks x 16 bytes } +var TextPosAvailable []string = []string{"tl", "tc", "tr", "cc", "bl", "bc", "br"} + // LoadFace parses a TTF/OTF file and builds a render-ready face at the given // pixel size (DPI 72, full hinting for crisp video text). Call once and // reuse for every overlay. @@ -48,7 +51,7 @@ func LoadFace(path string, size float64) (font.Face, error) { // NewTextOverlay rasterizes text (white, anti-aliased) onto a black backing box. // Box width and x are multiples of 6 so the box // covers whole v210 blocks and the per-frame copy is word-aligned. -func NewTextOverlay(text string, frameW, frameH, posX, posY int, face font.Face) (*TextOverlay, error) { +func NewTextOverlay(text string, frameW, frameH, posX, posY int, textPos string, face font.Face) (*TextOverlay, error) { if text == "" { return nil, fmt.Errorf("text: empty string") } @@ -89,8 +92,34 @@ func NewTextOverlay(text string, frameW, frameH, posX, posY int, face font.Face) return nil, fmt.Errorf("text: box %dpx does not fit frame height %dpx", topMargin+h, frameH) } - // posX = ((frameW - w) / 2 / 6) * 6 - // posY = topMargin + if textPos != "" { + if !slices.Contains(TextPosAvailable, textPos) { + return nil, fmt.Errorf("text: unknown text position preset '%s'", textPos) + } + switch textPos { + case "tl": + posX = 0 + posY = 0 + case "tc": + posX = ((frameW - w) / 2 / 6) * 6 + posY = 0 + case "tr": + posX = frameW - w + posY = 0 + case "cc": + posX = ((frameW - w) / 2 / 6) * 6 + posY = (frameH - h) / 2 + case "bl": + posX = 0 + posY = frameH - h + case "bc": + posX = ((frameW - w) / 2 / 6) * 6 + posY = frameH - h + case "br": + posX = frameW - w + posY = frameH - h + } + } o := &TextOverlay{ frameW: frameW, x: posX,