overlay

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package overlay draws the post processing overlays on an extracted screenshot: a colour gradient at the bottom edge and a watermark in one of the corners.

Everything here is pure Go image work. The package neither starts processes nor knows about ffmpeg; the geometry is integer arithmetic so the results are exact and testable.

Index

Constants

View Source
const MinWatermarkSize = 8

MinWatermarkSize is the smallest edge length a scaled logo may have. Below that nothing is recognisable any more, so the watermark is left out and reported.

Variables

View Source
var (
	// ErrUnsupportedFormat reports a format Go cannot write, which is webp.
	ErrUnsupportedFormat = errors.New("format cannot be written without ffmpeg")
	// ErrNotPNG reports a watermark file that is not a PNG.
	ErrNotPNG = errors.New("not a png file")
)

Errors of the image codecs used here.

View Source
var ErrInvalidColor = errors.New("invalid colour")

ErrInvalidColor reports an unparsable colour.

View Source
var ErrUnknownPosition = errors.New("unknown watermark position")

ErrUnknownPosition reports an unusable watermark position.

Positions lists the supported positions in the order they are offered.

Functions

func DecodeFile

func DecodeFile(path string) (image.Image, error)

DecodeFile reads an image file.

func DecodePNGFile

func DecodePNGFile(path string) (image.Image, error)

DecodePNGFile reads a PNG file and rejects every other format, so a mistyped watermark path fails with a clear message instead of silently working.

func DrawGradient

func DrawGradient(img *image.NRGBA, g Gradient)

DrawGradient blends the gradient into the image. The colour is mixed into the existing pixels rather than laid on top as a translucent layer, so the alpha channel of the frame is left exactly as it was.

The image is non premultiplied, which is what the blend formula expects: mixing premultiplied values while keeping the alpha would produce invalid pixels for frames that carry real transparency.

func DrawVignette

func DrawVignette(img *image.NRGBA, mask []uint8)

DrawVignette darkens the image with a mask from VignetteMask. A mask that does not match the image is ignored rather than drawn wrongly.

Like the gradient the colour is mixed into the existing pixels and the alpha channel is left exactly as it was, so frames with real transparency stay valid.

func DrawWatermark

func DrawWatermark(img *image.NRGBA, logo image.Image, marginPercent, opacity int, position Position)

DrawWatermark blends an already scaled logo into the frame. A nil logo or a zero opacity leaves the frame untouched.

Both images are non premultiplied, so the blend is a plain linear mix and the alpha channel of the frame survives unchanged.

func Encode

func Encode(w io.Writer, img image.Image, format clipper.Format, quality int) error

Encode writes the image in the given format.

func EncodeFile

func EncodeFile(path string, img image.Image, format clipper.Format, quality int) error

EncodeFile writes the image to path. It stages the data in a temporary file next to the target and moves it into place afterwards, so a failure never leaves a half written screenshot behind.

func Fit

func Fit(logo, frame image.Point, percent int) image.Point

Fit returns the size the logo is drawn at. The logo is fitted into a square box with an edge of percent per cent of the frame scale, keeping its aspect ratio. It is never enlarged: a logo that already fits keeps its original size.

Because the box is square, the longer edge of the logo is the one that binds, so the rule reads simply: the long edge of the logo becomes percent per cent of the frame scale, in every format.

The box is capped at the shorter frame edge, so a logo never grows beyond the picture. That cap only bites at very large percentages, well above anything useful for a watermark: on 16:9 it starts at about 75 per cent.

func FrameScale added in v0.2.0

func FrameScale(frame image.Point) int

FrameScale returns the size of a frame as a single number: the geometric mean of its two edges, rounded. It is the reference the watermark is measured against.

Using one edge would tie the watermark to the shape of the frame rather than to its size. At ten per cent of the width a logo comes out 192 pixels wide on 1920x1080 but only 108 on 1080x1080, so the same setting would look different in every aspect ratio. The geometric mean depends on both edges equally, which makes the share of the picture the logo covers the same for 1:1, 9:16 and 16:9 alike.

func GradientBand

func GradientBand(frame image.Point, percent int) int

GradientBand returns the height in pixels of the gradient band at the bottom of the frame, rounded down.

func ParseHexColor

func ParseHexColor(s string) (color.RGBA, error)

ParseHexColor reads a colour in hexadecimal notation. Accepted are #RRGGBB and the short #RGB form, with or without the leading hash. Transparency is not part of the colour; the gradient opacity has its own setting.

func Place

func Place(logo, frame image.Point, marginPercent int, position Position) image.Point

Place returns the top left corner the logo is drawn at. The margin is a percentage of the frame scale, the same reference Fit uses, and is applied as an equal pixel distance on both axes so the gap looks square. Measuring it against a single edge would make the distance depend on the aspect ratio while the size no longer does. The result always stays inside the frame.

func PositionList

func PositionList() string

PositionList returns the supported positions as a comma separated list, for help and error messages.

func ScaleLogo(logo image.Image, frame image.Point, sizePercent int) *image.NRGBA

ScaleLogo fits the logo into the size box of a frame and returns the scaled copy. It returns nil when the result would be smaller than MinWatermarkSize, and it never returns the source image itself, so the caller cannot reach the shared source pixels.

func VignetteMask

func VignetteMask(frame image.Point, v Vignette) []uint8

VignetteMask returns the coverage of every pixel of a frame, one byte per pixel in row major order, or nil when the vignette would draw nothing.

The mask depends only on the frame size and the settings, never on the picture, so it is the same for every screenshot of a resolution and worth caching. It is by far the expensive half of the effect; applying it is a table lookup.

Unlike the rest of the geometry this computes in floating point and rounds only once, at the end. The falloff has to be smooth across hundreds of pixels, and integer arithmetic would introduce exactly the steps the smoothstep is there to avoid.

Types

type Compositor

type Compositor struct {
	// contains filtered or unexported fields
}

Compositor applies a Spec to extracted frames. It implements clipper.Compositor.

Everything that depends only on the frame size is cached per size: the scaled logo and the vignette mask. A run with many screenshots therefore scales the logo and builds the mask once per resolution instead of once per image. The cache is shared between the parallel workers and therefore guarded.

func New

func New(spec Spec) *Compositor

New returns a Compositor for the given spec.

func (*Compositor) Active

func (c *Compositor) Active() bool

Active reports whether this compositor changes anything beyond re-encoding.

func (*Compositor) Compose

Compose implements clipper.Compositor. It always consumes the raw frame, also when it fails, so no intermediate files are left behind.

func (*Compositor) Discard

func (c *Compositor) Discard(raw string)

Discard implements clipper.Compositor. Removing a frame that is not there is not an error: the extraction may have failed before writing anything.

func (*Compositor) Staging

func (c *Compositor) Staging(dest string) string

Staging implements clipper.Compositor. The raw frame sits next to its screenshot, is hidden by the leading dot and always ends in .png so ffmpeg picks the png muxer. The name carries the process id and a random part, so two clipper runs writing into the same directory cannot delete each other's intermediate.

The random part is hexadecimal and 32 bits wide rather than a full decimal uint64, which keeps the addition to the name at 23 characters instead of 36. That matters because a file name may only be 255 bytes long and the destination name is already part of it. Thirty two bits is ample here: the destination name and the process id do most of the separating, and the random part only has to cover two runs that share a process id, as two containers on one mounted volume can.

type Gradient

type Gradient struct {
	// Color is the colour faded in, usually black.
	Color color.RGBA
	// HeightPercent is the band height as a percentage of the frame height.
	HeightPercent int
	// Opacity is the coverage at the bottom row, from 1 to 100.
	Opacity int
}

Gradient describes the colour band drawn at the bottom edge of a screenshot. The band is fully opaque at the very last row and fades linearly to fully transparent at its top edge, which keeps the transition invisible.

func (Gradient) Active

func (g Gradient) Active() bool

Active reports whether the gradient would change anything.

type Position

type Position int

Position selects the corner the watermark is placed in. The zero value is TopRight.

const (
	TopRight Position = iota
	TopLeft
	BottomRight
	BottomLeft
)

Supported watermark positions.

func ParsePosition

func ParsePosition(s string) (Position, error)

ParsePosition maps user input to a Position, case insensitively.

func (Position) String

func (p Position) String() string

String implements fmt.Stringer.

type Spec

type Spec struct {
	Vignette  Vignette
	Gradient  Gradient
	Watermark Watermark
}

Spec is the overlay configuration of a run.

func (Spec) Active

func (s Spec) Active() bool

Active reports whether the spec would change an image at all.

type Vignette

type Vignette struct {
	// RadiusPercent is the clear zone in the middle, measured as a percentage of
	// the distance from the centre to a corner. At 100 the clear zone reaches the
	// corner and nothing is darkened at all.
	RadiusPercent int
	// SoftnessPercent is the width of the transition in the same unit. Zero gives a
	// hard edge. Radius plus softness may exceed 100, which simply means the
	// falloff is still rising when it reaches the corner.
	SoftnessPercent int
	// Opacity is the coverage where the falloff has completed, from 1 to 100.
	Opacity int
}

Vignette describes the darkening towards the edges of a screenshot. It is always black: a vignette imitates a lens, and a coloured one would be a different effect, so there is no colour to configure.

The darkening follows the shape of the frame rather than a circle, and the falloff is a smoothstep, which is what separates a vignette from a plain radial gradient.

func (Vignette) Active

func (v Vignette) Active() bool

Active reports whether the vignette would change anything. A radius of a hundred or more leaves even the corners inside the clear zone.

type Watermark

type Watermark struct {
	Logo image.Image
	// SizePercent is the share of the frame the logo is fitted into.
	SizePercent int
	// MarginPercent is the distance to the frame edges, as a percentage of the
	// frame width.
	MarginPercent int
	// Opacity scales the alpha channel of the logo, from 1 to 100.
	Opacity int
	// Position is the corner the logo is placed in.
	Position Position
}

Watermark describes the logo overlay.

func (Watermark) Active

func (w Watermark) Active() bool

Active reports whether the watermark would change anything.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL