fix: align text annotations in captures

This commit is contained in:
2026-07-08 14:46:57 +08:00
parent f90612976f
commit 7b290d9300
7 changed files with 533 additions and 130 deletions
+41 -26
View File
@@ -22,10 +22,11 @@ import (
// Annotation describes a user-drawn mark relative to the selected screenshot.
// Coordinates are logical pixels in the same coordinate system as the overlay.
type Annotation struct {
Tool string `json:"tool"`
Color string `json:"color"`
Points []Point `json:"points"`
Text string `json:"text,omitempty"`
Tool string `json:"tool"`
Color string `json:"color"`
Points []Point `json:"points"`
Text string `json:"text,omitempty"`
FontSize float64 `json:"fontSize,omitempty"`
}
type Point struct {
@@ -35,12 +36,22 @@ type Point struct {
// ApplyAnnotations decodes a PNG, draws all annotations, and re-encodes it.
func ApplyAnnotations(pngBytes []byte, annotations []Annotation, scale float64) ([]byte, error) {
return ApplyAnnotationsWithScale(pngBytes, annotations, scale, scale)
}
// ApplyAnnotationsWithScale decodes a PNG, draws all annotations using the
// actual device-pixel scale of the captured image, and re-encodes it.
func ApplyAnnotationsWithScale(pngBytes []byte, annotations []Annotation, scaleX, scaleY float64) ([]byte, error) {
if len(annotations) == 0 {
return pngBytes, nil
}
if scale <= 0 {
scale = 1
if scaleX <= 0 {
scaleX = 1
}
if scaleY <= 0 {
scaleY = scaleX
}
strokeScale := math.Max(scaleX, scaleY)
src, err := png.Decode(bytes.NewReader(pngBytes))
if err != nil {
@@ -55,16 +66,16 @@ func ApplyAnnotations(pngBytes []byte, annotations []Annotation, scale float64)
if err != nil {
c = color.RGBA{R: 59, G: 130, B: 246, A: 255}
}
width := int(math.Max(2, math.Round(3*scale)))
width := int(math.Max(2, math.Round(3*strokeScale)))
switch ann.Tool {
case "pen":
drawPolyline(dst, ann.Points, scale, width, c)
drawPolyline(dst, ann.Points, scaleX, scaleY, width, c)
case "rect":
drawRectOutline(dst, ann.Points, scale, width, c)
drawRectOutline(dst, ann.Points, scaleX, scaleY, width, c)
case "ellipse":
drawEllipseOutline(dst, ann.Points, scale, width, c)
drawEllipseOutline(dst, ann.Points, scaleX, scaleY, width, c)
case "text":
drawTextAnnotation(dst, ann, scale, c)
drawTextAnnotation(dst, ann, scaleX, scaleY, c)
}
}
@@ -92,22 +103,22 @@ func parseHexColor(hex string) (color.RGBA, error) {
}, nil
}
func drawPolyline(img *image.RGBA, points []Point, scale float64, width int, c color.RGBA) {
func drawPolyline(img *image.RGBA, points []Point, scaleX, scaleY float64, width int, c color.RGBA) {
if len(points) == 1 {
drawDot(img, scalePoint(points[0], scale), width, c)
drawDot(img, scalePoint(points[0], scaleX, scaleY), width, c)
return
}
for i := 1; i < len(points); i++ {
drawLine(img, scalePoint(points[i-1], scale), scalePoint(points[i], scale), width, c)
drawLine(img, scalePoint(points[i-1], scaleX, scaleY), scalePoint(points[i], scaleX, scaleY), width, c)
}
}
func drawRectOutline(img *image.RGBA, points []Point, scale float64, width int, c color.RGBA) {
func drawRectOutline(img *image.RGBA, points []Point, scaleX, scaleY float64, width int, c color.RGBA) {
if len(points) < 2 {
return
}
a := scalePoint(points[0], scale)
b := scalePoint(points[len(points)-1], scale)
a := scalePoint(points[0], scaleX, scaleY)
b := scalePoint(points[len(points)-1], scaleX, scaleY)
x1, x2 := ordered(a.X, b.X)
y1, y2 := ordered(a.Y, b.Y)
drawLine(img, Point{X: x1, Y: y1}, Point{X: x2, Y: y1}, width, c)
@@ -116,12 +127,12 @@ func drawRectOutline(img *image.RGBA, points []Point, scale float64, width int,
drawLine(img, Point{X: x1, Y: y2}, Point{X: x1, Y: y1}, width, c)
}
func drawEllipseOutline(img *image.RGBA, points []Point, scale float64, width int, c color.RGBA) {
func drawEllipseOutline(img *image.RGBA, points []Point, scaleX, scaleY float64, width int, c color.RGBA) {
if len(points) < 2 {
return
}
a := scalePoint(points[0], scale)
b := scalePoint(points[len(points)-1], scale)
a := scalePoint(points[0], scaleX, scaleY)
b := scalePoint(points[len(points)-1], scaleX, scaleY)
x1, x2 := ordered(a.X, b.X)
y1, y2 := ordered(a.Y, b.Y)
rx := (x2 - x1) / 2
@@ -180,8 +191,8 @@ func drawDot(img *image.RGBA, p Point, width int, c color.RGBA) {
}
}
func scalePoint(p Point, scale float64) Point {
return Point{X: p.X * scale, Y: p.Y * scale}
func scalePoint(p Point, scaleX, scaleY float64) Point {
return Point{X: p.X * scaleX, Y: p.Y * scaleY}
}
func ordered(a, b float64) (float64, float64) {
@@ -191,7 +202,7 @@ func ordered(a, b float64) (float64, float64) {
return b, a
}
func drawTextAnnotation(img *image.RGBA, ann Annotation, scale float64, c color.RGBA) {
func drawTextAnnotation(img *image.RGBA, ann Annotation, scaleX, scaleY float64, c color.RGBA) {
if len(ann.Points) == 0 {
return
}
@@ -199,16 +210,20 @@ func drawTextAnnotation(img *image.RGBA, ann Annotation, scale float64, c color.
if text == "" {
return
}
face := annotationFontFace(20 * scale)
fontSize := ann.FontSize
if fontSize <= 0 {
fontSize = 20
}
face := annotationFontFace(fontSize * scaleY)
if face == nil {
face = basicfont.Face7x13
}
origin := scalePoint(ann.Points[0], scale)
origin := scalePoint(ann.Points[0], scaleX, scaleY)
metrics := face.Metrics()
lineHeight := metrics.Height
if lineHeight <= 0 {
lineHeight = fixed.I(int(math.Ceil(24 * scale)))
lineHeight = fixed.I(int(math.Ceil(fontSize * 1.2 * scaleY)))
}
d := &xfont.Drawer{
Dst: img,