Skip to content

Image and text watermark

Copy page

Watermarking marks ownership of an image: a semi-transparent text label, a brand chip, or a logo placed over the photograph. Under the hood it is a two-layer composite — the base image and an overlay rendered with alpha — positioned with a fixed margin. This recipe shows a text watermark and a logo watermark, both anchored to the bottom-right corner.

Both variants start from the same input photograph and the project-generated WebP Cloud mark.

Gate of the Orient night photograph used as the watermark base
Base: JPEG1938 × 1292, 3 bands
Semi-transparent WebP Cloud mark used as the logo watermark
WebP Cloud overlay: SVG220 × 72, transparent background

Render © WebP Cloud in larger dark type on a white rounded backing pill, then composite that pill over the base at the bottom-right corner with a 24-pixel margin. Backing the text with a light pill keeps the label legible over any photograph luminance. The result is flattened to plain RGB and exported as WebP.

Gate of the Orient night photograph with a semi-transparent text watermark at the bottom-right
Text watermark · WEBP1938 × 1292, 399,498 bytes, 3 bands

Generated with govips v2.19.0 / libvips 8.17.3.

textImg, err := vips.Text(&vips.TextParams{
Text: "<span foreground='#12202b' font-weight='700'>© WebP Cloud</span>",
Font: "DejaVu Sans",
DPI: 144,
RGBA: true,
Wrap: vips.TextWrapWord,
})
if err != nil {
return err
}
// White rounded backing pill sized to the rendered label.
pill, err := vips.NewImageFromBuffer([]byte(fmt.Sprintf(
`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d"><rect width="%d" height="%d" rx="16" fill="#ffffff" fill-opacity="0.94"/></svg>`,
textImg.Width()+52, textImg.Height()+32, textImg.Width()+52, textImg.Height()+32)))
if err != nil {
return err
}
if err := textImg.PremultiplyAlpha(); err != nil {
return err
}
if err := pill.Composite(textImg, vips.BlendModeOver, 26, 16); err != nil {
return err
}
base, err := vips.NewImageFromFile(inputPath)
if err != nil {
return err
}
x := base.Width() - pill.Width() - 24
y := base.Height() - pill.Height() - 24
if err := base.Composite(pill, vips.BlendModeOver, x, y); err != nil {
return err
}
if err := base.Flatten(&vips.Color{R: 29, G: 45, B: 53}); err != nil {
return err
}
data, _, err := base.ExportWebp(vips.NewWebpExportParams())

Composite the semi-transparent WebP Cloud mark over the base at the same bottom-right position. Because the SVG overlay carries its own alpha, it is premultiplied before compositing so the blend is correct.

Gate of the Orient night photograph with the WebP Cloud mark at the bottom-right
WebP Cloud logo watermark · WEBP1938 × 1292, 399,632 bytes, 3 bands

Generated with govips v2.19.0 / libvips 8.17.3.

logo, err := vips.NewImageFromFile(overlayPath)
if err != nil {
return err
}
// Premultiply the RGBA overlay so the blend is correct.
if err := logo.PremultiplyAlpha(); err != nil {
return err
}
base, err := vips.NewImageFromFile(inputPath)
if err != nil {
return err
}
x := base.Width() - logo.Width() - 24
y := base.Height() - logo.Height() - 24
if err := base.Composite(logo, vips.BlendModeOver, x, y); err != nil {
return err
}
if err := base.Flatten(&vips.Color{R: 29, G: 45, B: 53}); err != nil {
return err
}
data, _, err := base.ExportWebp(vips.NewWebpExportParams())

Capability boundary: drawing is not uniform across bindings

Section titled “Capability boundary: drawing is not uniform across bindings”

Watermarking here is built on text rendering + two-layer compositing, which every binding supports. Raw drawing operations are a different story:

  • pyvips and the vips CLI expose the full draw suite: draw_line, draw_circle, draw_rect, draw_flood, and draw_image.
  • govips exposes only DrawRect (ink, left, top, width, height, fill); there is no generated or hand-written DrawLine or DrawCircle in govips v2.19.0.
  • sharp ships no drawing primitives at all. To draw a line or shape in sharp you rasterize an SVG or text overlay and composite it.

So a “draw a red circle” product intent is not directly expressible in govips or sharp without going through SVG/text. This recipe deliberately routes every binding through the compositing path they all support, and treats raw drawing as a capability boundary rather than a portable operation.