diff --git a/go.mod b/go.mod index 799dca5..f0355d4 100644 --- a/go.mod +++ b/go.mod @@ -2,23 +2,20 @@ module github.com/gen2brain/cam2ip require ( github.com/abbot/go-http-auth v0.4.0 - github.com/anthonynsimon/bild v0.14.0 - github.com/coder/websocket v1.8.13 + github.com/coder/websocket v1.8.15 github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c github.com/gen2brain/jpegn v0.4.2 github.com/korandiz/v4l v1.1.0 github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d - go.senan.xyz/flagconf v0.1.9 - gocv.io/x/gocv v0.35.0 + go.senan.xyz/flagconf v0.1.11 + gocv.io/x/gocv v0.43.0 ) require ( - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect ) -go 1.23.0 - -toolchain go1.24.3 +go 1.26 diff --git a/go.sum b/go.sum index 460fa8f..c04fd9c 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,30 @@ github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= -github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync= -github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs= -github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= -github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c h1:TUjjeJ2rV4KZxH6hIEi/boEQB3v6aKvwdakUJR3AwiE= github.com/gen2brain/base64 v0.0.0-20221015184129-317a5c93030c/go.mod h1:VG58IUyxPWojCtGwqwoZ/6LLXwClu1tssqa5ktOxI9o= github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM= github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0= -github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs= github.com/korandiz/v4l v1.1.0 h1:VbzaWlhqNzVPfHEYEM+V8T7184ndiEzljJgDHSHc7pc= github.com/korandiz/v4l v1.1.0/go.mod h1:pftxPG7hkuUgepioAY6PAE81mShaVjzd95X/WF4Izus= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567 h1:pKjmNHL7BCXhgsnSlN6Ov3WAN2jbJMCx6IvrMN9GNfc= github.com/pbnjay/pixfont v0.0.0-20200714042608-33b744692567/go.mod h1:ytYavTmrpWG4s7UOfDhP6m4ASL5XA66nrOcUn1e2M78= github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d h1:ls+7AYarUlUSetfnN/DKVNcK6W8mQWc6VblmOm4XwX0= github.com/pixiv/go-libjpeg v0.0.0-20190822045933-3da21a74767d/go.mod h1:DO7ixpslN6XfbWzeNH9vkS5CF2FQUX81B85rYe9zDxU= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -go.senan.xyz/flagconf v0.1.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4= -go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ= -gocv.io/x/gocv v0.35.0 h1:Qaxb5KdVyy8Spl4S4K0SMZ6CVmKtbfoSGQAxRD3FZlw= -gocv.io/x/gocv v0.35.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +go.senan.xyz/flagconf v0.1.11 h1:ApA9DpoSrfVQlLt8spKJC3fOT7oTEZ2/MmBGMnb9mt4= +go.senan.xyz/flagconf v0.1.11/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ= +gocv.io/x/gocv v0.43.0 h1:PFNpRUcV8fgBRDbVHHN+4BDZjjPnVveo5N/+e15BTuA= +gocv.io/x/gocv v0.43.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= diff --git a/image/encode.go b/image/encode.go index 209d7cb..dd34311 100644 --- a/image/encode.go +++ b/image/encode.go @@ -1,4 +1,4 @@ -//go:build !libjpeg && !jpegli +//go:build !libjpeg // Package image. package image diff --git a/image/image.go b/image/image.go index e51dca6..1b788d4 100644 --- a/image/image.go +++ b/image/image.go @@ -6,32 +6,63 @@ import ( "image/draw" "time" - "github.com/anthonynsimon/bild/transform" "github.com/pbnjay/pixfont" ) +// Rotate rotates the image clockwise by 90, 180 or 270 degrees. func Rotate(img image.Image, angle int) image.Image { + b := img.Bounds() + w, h := b.Dx(), b.Dy() + + var dst *image.RGBA switch angle { - case 90: - img = transform.Rotate(img, 90, &transform.RotationOptions{ResizeBounds: true}) + case 90, 270: + dst = image.NewRGBA(image.Rect(0, 0, h, w)) case 180: - img = transform.Rotate(img, 180, &transform.RotationOptions{ResizeBounds: true}) - case 270: - img = transform.Rotate(img, 270, &transform.RotationOptions{ResizeBounds: true}) + dst = image.NewRGBA(image.Rect(0, 0, w, h)) + default: + return img } - return img + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(b.Min.X+x, b.Min.Y+y) + switch angle { + case 90: + dst.Set(h-1-y, x, c) + case 180: + dst.Set(w-1-x, h-1-y, c) + case 270: + dst.Set(y, w-1-x, c) + } + } + } + + return dst } +// Flip mirrors the image horizontally or vertically. func Flip(img image.Image, dir string) image.Image { - switch dir { - case "horizontal": - img = transform.FlipH(img) - case "vertical": - img = transform.FlipV(img) + if dir != "horizontal" && dir != "vertical" { + return img } - return img + b := img.Bounds() + w, h := b.Dx(), b.Dy() + dst := image.NewRGBA(image.Rect(0, 0, w, h)) + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(b.Min.X+x, b.Min.Y+y) + if dir == "horizontal" { + dst.Set(w-1-x, y, c) + } else { + dst.Set(x, h-1-y, c) + } + } + } + + return dst } func Timestamp(img image.Image, format string) image.Image { diff --git a/image/image_test.go b/image/image_test.go index a909cdb..0e3dae3 100644 --- a/image/image_test.go +++ b/image/image_test.go @@ -3,6 +3,8 @@ package image_test import ( "bytes" _ "embed" + stdimage "image" + "image/color" "image/jpeg" "io" "testing" @@ -41,3 +43,65 @@ func BenchmarkBase64(b *testing.B) { _ = image.EncodeToString(testJpg) } } + +// labeled returns a 3x2 image whose pixels encode their coordinates as R=x, G=y. +func labeled() *stdimage.RGBA { + m := stdimage.NewRGBA(stdimage.Rect(0, 0, 3, 2)) + for y := 0; y < 2; y++ { + for x := 0; x < 3; x++ { + m.Set(x, y, color.RGBA{uint8(x), uint8(y), 0, 0xFF}) + } + } + + return m +} + +func at(t *testing.T, img stdimage.Image, x, y int) (int, int) { + t.Helper() + r, g, _, _ := img.At(x, y).RGBA() + + return int(r >> 8), int(g >> 8) +} + +func TestRotate(t *testing.T) { + src := labeled() + + r90 := image.Rotate(src, 90) + if b := r90.Bounds(); b.Dx() != 2 || b.Dy() != 3 { + t.Fatalf("rotate 90 size = %v, want 2x3", b) + } + // Clockwise: source (0,0) ends up top-right of the 2-wide result. + if x, y := at(t, r90, 1, 0); x != 0 || y != 0 { + t.Errorf("rotate 90 top-right = (%d,%d), want source (0,0)", x, y) + } + + r180 := image.Rotate(src, 180) + if b := r180.Bounds(); b.Dx() != 3 || b.Dy() != 2 { + t.Fatalf("rotate 180 size = %v, want 3x2", b) + } + if x, y := at(t, r180, 0, 0); x != 2 || y != 1 { + t.Errorf("rotate 180 (0,0) = (%d,%d), want source (2,1)", x, y) + } + + if got := image.Rotate(src, 45); got != stdimage.Image(src) { + t.Error("rotate with unsupported angle should return the source unchanged") + } +} + +func TestFlip(t *testing.T) { + src := labeled() + + h := image.Flip(src, "horizontal") + if x, y := at(t, h, 0, 0); x != 2 || y != 0 { + t.Errorf("flipH (0,0) = (%d,%d), want source (2,0)", x, y) + } + + v := image.Flip(src, "vertical") + if x, y := at(t, v, 0, 0); x != 0 || y != 1 { + t.Errorf("flipV (0,0) = (%d,%d), want source (0,1)", x, y) + } + + if got := image.Flip(src, ""); got != stdimage.Image(src) { + t.Error("flip with empty direction should return the source unchanged") + } +}