package application import ( "bytes" "image" "image/color" "image/draw" "image/png" "testing" ) func TestApplyAnnotationsDrawsIntoPNG(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 40, 40)) draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotations(buf.Bytes(), []Annotation{ { Tool: "rect", Color: "#ef4444", Points: []Point{ {X: 5, Y: 5}, {X: 30, Y: 30}, }, }, }, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } if got := color.RGBAModel.Convert(img.At(5, 5)).(color.RGBA); got.R != 0xef || got.G != 0x44 || got.B != 0x44 { t.Fatalf("expected annotated pixel at rectangle edge, got %#v", got) } } func TestApplyAnnotationsNoAnnotationsReturnsOriginalBytes(t *testing.T) { original := []byte("not decoded when no annotations") out, err := ApplyAnnotations(original, nil, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } if !bytes.Equal(out, original) { t.Fatalf("expected original bytes") } } func TestApplyAnnotationsDrawsTextIntoPNG(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 120, 80)) draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotations(buf.Bytes(), []Annotation{ { Tool: "text", Color: "#ef4444", Points: []Point{ {X: 10, Y: 10}, }, Text: "T", }, }, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } found := false for y := 8; y < 40 && !found; y++ { for x := 8; x < 40; x++ { got := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) if got.R > 180 && got.G < 180 && got.B < 180 { found = true break } } } if !found { t.Fatalf("expected red text pixels near annotation point") } } func TestApplyAnnotationsWithScaleDrawsTextAtDevicePosition(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 220, 120)) draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotationsWithScale(buf.Bytes(), []Annotation{ { Tool: "text", Color: "#ef4444", Points: []Point{{X: 50, Y: 20}}, Text: "T", FontSize: 20, }, }, 2, 2) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } minX := 999 found := false for y := 0; y < img.Bounds().Dy(); y++ { for x := 0; x < img.Bounds().Dx(); x++ { got := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) if got.R > 180 && got.G < 180 && got.B < 180 { found = true if x < minX { minX = x } } } } if !found { t.Fatalf("expected red text pixels") } if minX < 90 { t.Fatalf("expected text to be drawn near scaled x=100, min red x=%d", minX) } } func TestApplyAnnotationsUsesStrokeWidth(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 80, 80)) draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotations(buf.Bytes(), []Annotation{ { Tool: "rect", Color: "#ef4444", Points: []Point{{X: 20, Y: 20}, {X: 60, Y: 60}}, StrokeWidth: 8, }, }, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } got := color.RGBAModel.Convert(img.At(20, 24)).(color.RGBA) if got.R < 180 || got.G > 180 || got.B > 180 { t.Fatalf("expected thick rectangle stroke to cover y=24, got %#v", got) } } func TestApplyAnnotationsPixelatesMosaicRectangle(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 40, 30)) for y := 0; y < 30; y++ { for x := 0; x < 40; x++ { src.SetRGBA(x, y, color.RGBA{R: uint8(x * 6), G: uint8(y * 8), B: uint8((x + y) * 3), A: 255}) } } var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotations(buf.Bytes(), []Annotation{{ Tool: "mosaic-rect", Points: []Point{{X: 10, Y: 5}, {X: 30, Y: 25}}, }}, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } if img.At(12, 7) != img.At(18, 13) { t.Fatalf("expected pixels in one mosaic block to share a color") } if img.At(5, 5) != src.At(5, 5) { t.Fatalf("expected pixels outside mosaic rectangle to remain unchanged") } } func TestApplyAnnotationsPixelatesOnlyMosaicBrushStroke(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 50, 30)) for y := 0; y < 30; y++ { for x := 0; x < 50; x++ { src.SetRGBA(x, y, color.RGBA{R: uint8(x * 5), G: uint8(y * 8), A: 255}) } } var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotations(buf.Bytes(), []Annotation{{ Tool: "mosaic-brush", StrokeWidth: 12, Points: []Point{{X: 5, Y: 15}, {X: 45, Y: 15}}, }}, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } if img.At(12, 15) == src.At(12, 15) { t.Fatalf("expected brush path to be pixelated") } if img.At(12, 2) != src.At(12, 2) { t.Fatalf("expected pixels outside brush path to remain unchanged") } } func TestApplyAnnotationsDrawsStraightLine(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 60, 40)) draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotations(buf.Bytes(), []Annotation{{ Tool: "line", Color: "#ef4444", StrokeWidth: 4, Points: []Point{{X: 5, Y: 20}, {X: 50, Y: 20}}, }}, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } got := color.RGBAModel.Convert(img.At(30, 20)).(color.RGBA) if got.R < 180 || got.G > 180 || got.B > 180 { t.Fatalf("expected red line at center, got %#v", got) } } func TestApplyAnnotationsDrawsArrowHead(t *testing.T) { src := image.NewRGBA(image.Rect(0, 0, 80, 60)) draw.Draw(src, src.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src) var buf bytes.Buffer if err := png.Encode(&buf, src); err != nil { t.Fatalf("encode source: %v", err) } out, err := ApplyAnnotations(buf.Bytes(), []Annotation{{ Tool: "arrow", Color: "#3b82f6", StrokeWidth: 3, Points: []Point{{X: 10, Y: 30}, {X: 60, Y: 30}}, }}, 1) if err != nil { t.Fatalf("apply annotations: %v", err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { t.Fatalf("decode output: %v", err) } shaft := color.RGBAModel.Convert(img.At(35, 30)).(color.RGBA) head := color.RGBAModel.Convert(img.At(52, 25)).(color.RGBA) if shaft.B < 180 || shaft.R > 140 || head.B < 180 || head.R > 140 { t.Fatalf("expected blue arrow shaft and head, got shaft=%#v head=%#v", shaft, head) } }