From 401dd70f6274ad0f1705ab21b73e1786b5181320 Mon Sep 17 00:00:00 2001 From: esimov Date: Thu, 29 Oct 2026 17:43:33 +0200 Subject: [PATCH 1/2] Code refactoring and restructuring, adapting to latest Go version --- image/image.go | 228 +++++++++++++++++++++++ image/image_test.go | 220 +++++++++++++++++++++++ image/sobel.go | 73 ++++++++ image/sobel_test.go | 58 ++++++ image/stackblur.go | 430 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1009 insertions(+) create mode 100644 image/image.go create mode 100644 image/image_test.go create mode 100644 image/sobel.go create mode 100644 image/sobel_test.go create mode 100644 image/stackblur.go diff --git a/image/image.go b/image/image.go new file mode 100644 index 00000000..961eb09f --- /dev/null +++ b/image/image.go @@ -0,0 +1,228 @@ +// Package image can be perceived as an utilitary library for all kind of +// image processing operations, like image conversion, image decoding and encoding, +// pixel manipulation, color conversion etc. +package image + +import ( + "fmt" + "image" + "image/color" + "io" + "os" + "strings" + + "github.com/esimov/caire/utils" +) + +func DecodeContent(src string) (image.Image, error) { + file, err := os.Open(src) + if err != nil { + return nil, fmt.Errorf("could not open the mask file: %v", err) + } + + ctype, err := utils.DetectContentType(file.Name()) + if err != nil { + return nil, err + } + + if !strings.Contains(ctype.(string), "image") { + return nil, fmt.Errorf("the mask should be an image file") + } + + img, err := Decode(file) + if err != nil { + return nil, fmt.Errorf("cannot decode content: %w", err) + } + + return img, nil +} + +// Decode decodes an image file to type image.Image +func Decode(r io.Reader) (image.Image, error) { + img, _, err := image.Decode(r) + if err != nil { + return nil, fmt.Errorf("could not decode the mask file: %v", err) + } + + return img, nil +} + +// ImgToNRGBA converts any image type to *image.NRGBA with min-point at (0, 0). +func ImgToNRGBA(img image.Image) *image.NRGBA { + srcBounds := img.Bounds() + if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 { + if src0, ok := img.(*image.NRGBA); ok { + return src0 + } + } + srcMinX := srcBounds.Min.X + srcMinY := srcBounds.Min.Y + + dstBounds := srcBounds.Sub(srcBounds.Min) + dstW := dstBounds.Dx() + dstH := dstBounds.Dy() + dst := image.NewNRGBA(dstBounds) + + switch src := img.(type) { + case *image.NRGBA: + rowSize := srcBounds.Dx() * 4 + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + si := src.PixOffset(srcMinX, srcMinY+dstY) + for dstX := 0; dstX < dstW; dstX++ { + copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize]) + } + } + case *image.YCbCr: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + srcX := srcMinX + dstX + srcY := srcMinY + dstY + siy := src.YOffset(srcX, srcY) + sic := src.COffset(srcX, srcY) + r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic]) + dst.Pix[di+0] = r + dst.Pix[di+1] = g + dst.Pix[di+2] = b + dst.Pix[di+3] = 0xff + di += 4 + } + } + default: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA) + dst.Pix[di+0] = c.R + dst.Pix[di+1] = c.G + dst.Pix[di+2] = c.B + dst.Pix[di+3] = c.A + di += 4 + } + } + } + + return dst +} + +// RgbToGrayscale converts an image to grayscale mode and +// returns the pixel values as an one dimensional array. +func RgbToGrayscale(img *image.NRGBA) []uint8 { + width, height := img.Bounds().Dx(), img.Bounds().Dy() + gray := make([]uint8, width*height) + + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + r, g, b, _ := img.At(x, y).RGBA() + gray[y*width+x] = uint8( + (0.299*float64(r) + + 0.587*float64(g) + + 0.114*float64(b)) / 256, + ) + } + } + + return gray +} + +// Dither converts an image to black and white image, where the white is fully transparent. +func Dither(img image.Image) *image.NRGBA { + bounds := img.Bounds() + dithered := image.NewNRGBA(bounds) + dx, dy := bounds.Dx(), bounds.Dy() + + for x := 0; x < dx; x++ { + for y := 0; y < dy; y++ { + r, g, b, _ := img.At(x, y).RGBA() + threshold := func() color.Color { + if r > 127 && g > 127 && b > 127 { + return color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + } + return color.NRGBA{A: 0x00} + } + dithered.Set(x, y, threshold()) + } + } + + return dithered +} + +// ImageToPix converts an image to a pixel array. +func ImageToPix(img *image.NRGBA) []uint8 { + bounds := img.Bounds() + pixels := make([]uint8, 0, bounds.Max.X*bounds.Max.Y*4) + + for x := bounds.Min.X; x < bounds.Max.X; x++ { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + r, g, b, _ := img.At(y, x).RGBA() + pixels = append(pixels, uint8(r>>8), uint8(g>>8), uint8(b>>8), 255) + } + } + + return pixels +} + +// PixToImage converts an array buffer to an image. +func PixToImage(pixels []uint8, width, height int) image.Image { + dst := image.NewNRGBA(image.Rect(0, 0, width, height)) + bounds := dst.Bounds() + dx, dy := bounds.Max.X, bounds.Max.Y + col := color.NRGBA{ + R: uint8(0), + G: uint8(0), + B: uint8(0), + A: uint8(255), + } + + for x := bounds.Min.X; x < dx; x++ { + for y := bounds.Min.Y; y < dy*4; y += 4 { + col.R = uint8(pixels[y+x*dy*4]) + col.G = uint8(pixels[y+x*dy*4+1]) + col.B = uint8(pixels[y+x*dy*4+2]) + col.A = uint8(pixels[y+x*dy*4+3]) + + dst.SetNRGBA(x, int(y/4), col) + } + } + + return dst +} + +// RotateImage90 rotate the image by 90 degree counter clockwise. +func RotateImage90(img *image.NRGBA) *image.NRGBA { + bounds := img.Bounds() + dst := image.NewNRGBA(image.Rect(0, 0, bounds.Max.Y, bounds.Max.X)) + + for dstY := 0; dstY < bounds.Max.X; dstY++ { + for dstX := 0; dstX < bounds.Max.Y; dstX++ { + srcX := bounds.Max.X - dstY - 1 + srcY := dstX + + srcOff := srcY*img.Stride + srcX*4 + dstOff := dstY*dst.Stride + dstX*4 + copy(dst.Pix[dstOff:dstOff+4], img.Pix[srcOff:srcOff+4]) + } + } + + return dst +} + +// RotateImage270 rotate the image by 270 degree counter clockwise. +func RotateImage270(img *image.NRGBA) *image.NRGBA { + bounds := img.Bounds() + dst := image.NewNRGBA(image.Rect(0, 0, bounds.Max.Y, bounds.Max.X)) + + for dstY := 0; dstY < bounds.Max.X; dstY++ { + for dstX := 0; dstX < bounds.Max.Y; dstX++ { + srcX := dstY + srcY := bounds.Max.Y - dstX - 1 + + srcOff := srcY*img.Stride + srcX*4 + dstOff := dstY*dst.Stride + dstX*4 + copy(dst.Pix[dstOff:dstOff+4], img.Pix[srcOff:srcOff+4]) + } + } + + return dst +} diff --git a/image/image_test.go b/image/image_test.go new file mode 100644 index 00000000..9fee1aae --- /dev/null +++ b/image/image_test.go @@ -0,0 +1,220 @@ +package image + +import ( + "image" + "image/color" + "image/color/palette" + "image/draw" + "testing" + + "github.com/esimov/caire/utils" +) + +func TestImage_ImgToNRGBA(t *testing.T) { + rect := image.Rect(-1, -1, 15, 15) + colors := palette.Plan9 + testCases := []struct { + name string + img image.Image + }{ + { + name: "NRGBA", + img: makeNRGBAImage(rect, colors), + }, + { + name: "YCbCr-444", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio444), + }, + { + name: "YCbCr-422", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio422), + }, + { + name: "YCbCr-420", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio420), + }, + { + name: "YCbCr-440", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio440), + }, + { + name: "YCbCr-410", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio410), + }, + { + name: "YCbCr-411", + img: makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio411), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := tc.img.Bounds() + for y := r.Min.Y; y < r.Max.Y; y++ { + buf := make([]byte, r.Dx()*4) + scan(tc.img, 0, y-r.Min.Y, r.Dx(), y+1-r.Min.Y, buf) + wantBuf := readRow(tc.img, y) + if !compareBytes(buf, wantBuf, 1) { + t.Errorf("scan horizontal line (y=%d): got %v want %v", y, buf, wantBuf) + } + } + for x := r.Min.X; x < r.Max.X; x++ { + buf := make([]byte, r.Dy()*4) + scan(tc.img, x-r.Min.X, 0, x+1-r.Min.X, r.Dy(), buf) + wantBuf := readColumn(tc.img, x) + if !compareBytes(buf, wantBuf, 1) { + t.Errorf("scan vertical line (x=%d): got %v want %v", x, buf, wantBuf) + } + } + }) + } +} + +func scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) { + switch img := img.(type) { + case *image.NRGBA: + size := (x2 - x1) * 4 + j := 0 + i := y1*img.Stride + x1*4 + for y := y1; y < y2; y++ { + copy(dst[j:j+size], img.Pix[i:i+size]) + j += size + i += img.Stride + } + case *image.YCbCr: + j := 0 + x1 += img.Rect.Min.X + x2 += img.Rect.Min.X + y1 += img.Rect.Min.Y + y2 += img.Rect.Min.Y + for y := y1; y < y2; y++ { + iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X) + for x := x1; x < x2; x++ { + var ic int + switch img.SubsampleRatio { + case image.YCbCrSubsampleRatio444: + ic = (y-img.Rect.Min.Y)*img.CStride + (x - img.Rect.Min.X) + case image.YCbCrSubsampleRatio422: + ic = (y-img.Rect.Min.Y)*img.CStride + (x/2 - img.Rect.Min.X/2) + case image.YCbCrSubsampleRatio420: + ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x/2 - img.Rect.Min.X/2) + case image.YCbCrSubsampleRatio440: + ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x - img.Rect.Min.X) + default: + ic = img.COffset(x, y) + } + + yy := int(img.Y[iy]) + cb := int(img.Cb[ic]) - 128 + cr := int(img.Cr[ic]) - 128 + + r := (yy<<16 + 91881*cr + 1<<15) >> 16 + if r > 0xff { + r = 0xff + } else if r < 0 { + r = 0 + } + + g := (yy<<16 - 22554*cb - 46802*cr + 1<<15) >> 16 + if g > 0xff { + g = 0xff + } else if g < 0 { + g = 0 + } + + b := (yy<<16 + 116130*cb + 1<<15) >> 16 + if b > 0xff { + b = 0xff + } else if b < 0 { + b = 0 + } + + dst[j+0] = uint8(r) + dst[j+1] = uint8(g) + dst[j+2] = uint8(b) + dst[j+3] = 0xff + + iy++ + j += 4 + } + } + } +} + +func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image.YCbCrSubsampleRatio) *image.YCbCr { + img := image.NewYCbCr(rect, sr) + j := 0 + for y := rect.Min.Y; y < rect.Max.Y; y++ { + for x := rect.Min.X; x < rect.Max.X; x++ { + iy := img.YOffset(x, y) + ic := img.COffset(x, y) + c := color.NRGBAModel.Convert(colors[j]).(color.NRGBA) + img.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(c.R, c.G, c.B) + j++ + } + } + return img +} + +func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.NRGBA { + img := image.NewNRGBA(rect) + fillDrawImage(img, colors) + return img +} + +func fillDrawImage(img draw.Image, colors []color.Color) { + colorsNRGBA := make([]color.NRGBA, len(colors)) + for i, c := range colors { + nrgba := color.NRGBAModel.Convert(c).(color.NRGBA) + nrgba.A = uint8(i % 256) + colorsNRGBA[i] = nrgba + } + rect := img.Bounds() + i := 0 + for y := rect.Min.Y; y < rect.Max.Y; y++ { + for x := rect.Min.X; x < rect.Max.X; x++ { + img.Set(x, y, colorsNRGBA[i]) + i++ + } + } +} + +func readRow(img image.Image, y int) []uint8 { + row := make([]byte, img.Bounds().Dx()*4) + i := 0 + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + row[i+0] = c.R + row[i+1] = c.G + row[i+2] = c.B + row[i+3] = c.A + i += 4 + } + return row +} + +func readColumn(img image.Image, x int) []uint8 { + column := make([]byte, img.Bounds().Dy()*4) + i := 0 + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + column[i+0] = c.R + column[i+1] = c.G + column[i+2] = c.B + column[i+3] = c.A + i += 4 + } + return column +} + +func compareBytes(a, b []uint8, delta int) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if utils.Abs(int(a[i])-int(b[i])) > delta { + return false + } + } + return true +} diff --git a/image/sobel.go b/image/sobel.go new file mode 100644 index 00000000..8171a84f --- /dev/null +++ b/image/sobel.go @@ -0,0 +1,73 @@ +package image + +import ( + "image" + "math" +) + +type kernel [][]int32 + +var ( + kernelX = kernel{ + {-1, 0, 1}, + {-2, 0, 2}, + {-1, 0, 1}, + } + + kernelY = kernel{ + {-1, -2, -1}, + {0, 0, 0}, + {1, 2, 1}, + } +) + +// SobelDetector uses the sobel filter operator for detecting image edges. +// See https://en.wikipedia.org/wiki/Sobel_operator +func SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA { + var sumX, sumY int32 + + bounds := img.Bounds() + dx, dy := bounds.Dx(), bounds.Dy() + dst := image.NewNRGBA(bounds) + grayscale := RgbToGrayscale(img) + + // at returns the brightness of the pixel at (x, y). Coordinates outside + // the image are clamped to the nearest border pixel, so the borders + // of a uniform image produce no edge. + at := func(x, y int) int32 { + x = min(max(x, 0), dx-1) + y = min(max(y, 0), dy-1) + return int32(grayscale[y*dx+x]) + } + + for y := 0; y < dy; y++ { + for x := 0; x < dx; x++ { + // Apply the 3x3 kernels centered on the current pixel. + sumX, sumY = 0, 0 + for ky := 0; ky < len(kernelY); ky++ { + for kx := 0; kx < len(kernelX); kx++ { + px := at(x+kx-1, y+ky-1) + sumX += px * kernelX[ky][kx] + sumY += px * kernelY[ky][kx] + } + } + magnitude := math.Sqrt(float64(sumX*sumX) + float64(sumY*sumY)) + if magnitude > 255 { + magnitude = 255 + } + // Set magnitude to 0 if doesn't exceed threshold, else set to magnitude + if magnitude <= threshold { + magnitude = 0 + } + + idx := (y*dx + x) * 4 + m := uint8(magnitude) + dst.Pix[idx] = m + dst.Pix[idx+1] = m + dst.Pix[idx+2] = m + dst.Pix[idx+3] = 0xff + } + } + + return dst +} diff --git a/image/sobel_test.go b/image/sobel_test.go new file mode 100644 index 00000000..976db44b --- /dev/null +++ b/image/sobel_test.go @@ -0,0 +1,58 @@ +package image + +import ( + "image" + "image/color" + "image/draw" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + imgWidth = 10 + imgHeight = 10 +) + +// energyRow returns one character per pixel of a row: "#" for an edge, "." otherwise. +func energyRow(sobel *image.NRGBA, y int) string { + var row strings.Builder + for x := 0; x < sobel.Bounds().Dx(); x++ { + if sobel.Pix[sobel.PixOffset(x, y)] > 0 { + row.WriteByte('#') + } else { + row.WriteByte('.') + } + } + return row.String() +} + +func TestSobel_UniformImageShouldHaveNoEnergy(t *testing.T) { + img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight)) + draw.Draw(img, img.Bounds(), &image.Uniform{color.Gray{Y: 0x80}}, image.Point{}, draw.Src) + + sobel := SobelDetector(img, 0) + + // The detector used to read past the end of the data in the last rows, + // which marked the bottom of every image as an edge. + for y := 0; y < imgHeight; y++ { + assert.Equal(t, strings.Repeat(".", imgWidth), energyRow(sobel, y), "row %d", y) + } +} + +func TestSobel_EdgeShouldBeCenteredOnTheColorStep(t *testing.T) { + // The left half is black and the right half is white. + img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight)) + draw.Draw(img, image.Rect(imgWidth/2, 0, imgWidth, imgHeight), image.White, image.Point{}, draw.Src) + + out := SobelDetector(img, 0) + + // Only the two columns on each side of the step are edges. + // The window used to be anchored at the top left corner, which moved + // every edge one pixel up and to the left. + want := strings.Repeat(".", imgWidth/2-1) + "##" + strings.Repeat(".", imgWidth/2-1) + for y := 0; y < imgHeight; y++ { + assert.Equal(t, want, energyRow(out, y), "row %d", y) + } +} diff --git a/image/stackblur.go b/image/stackblur.go new file mode 100644 index 00000000..309e661c --- /dev/null +++ b/image/stackblur.go @@ -0,0 +1,430 @@ +// Go implementation of the StackBlur algorithm +// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php + +package image + +import ( + "errors" + "image" + "image/color" +) + +// blurStack is a linked list containing the color value and a pointer to the next struct. +type blurStack struct { + r, g, b, a uint32 + next *blurStack +} + +var mulTable = []uint32{ + 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, + 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, + 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, + 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, + 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, + 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, + 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, + 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, + 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, + 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, + 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, + 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, + 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, + 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, + 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, + 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259, +} + +var shgTable = []uint32{ + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, + 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, +} + +// Stackblur takes the source image and returns it's blurred version by applying the blur radius defined as parameter. The destination image must be a image.NRGBA. +func Stackblur(dst, src image.Image, radius uint32) error { + // Limit the maximum blur radius to 255 to avoid overflowing the multable. + if int(radius) >= len(mulTable) { + radius = uint32(len(mulTable) - 1) + } + + if radius < 1 { + return errors.New("blur radius must be greater than 0") + } + + img, ok := dst.(*image.NRGBA) + if !ok { + return errors.New("the destination image must be image.NRGBA") + } + + process(img, src, radius) + return nil +} + +func process(dst *image.NRGBA, src image.Image, radius uint32) { + srcBounds := src.Bounds() + srcMinX := srcBounds.Min.X + srcMinY := srcBounds.Min.Y + + dstBounds := srcBounds.Sub(srcBounds.Min) + dstW := dstBounds.Dx() + dstH := dstBounds.Dy() + + switch src0 := src.(type) { + case *image.NRGBA: + rowSize := srcBounds.Dx() * 4 + for dstY := 0; dstY < dstH; dstY++ { + di := src0.PixOffset(0, dstY) + si := src0.PixOffset(srcMinX, srcMinY+dstY) + for dstX := 0; dstX < dstW; dstX++ { + copy(dst.Pix[di:di+rowSize], src0.Pix[si:si+rowSize]) + } + } + case *image.YCbCr: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + srcX := srcMinX + dstX + srcY := srcMinY + dstY + siy := src0.YOffset(srcX, srcY) + sic := src0.COffset(srcX, srcY) + r, g, b := color.YCbCrToRGB(src0.Y[siy], src0.Cb[sic], src0.Cr[sic]) + dst.Pix[di+0] = r + dst.Pix[di+1] = g + dst.Pix[di+2] = b + dst.Pix[di+3] = 0xff + di += 4 + } + } + default: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + c := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA) + dst.Pix[di+0] = c.R + dst.Pix[di+1] = c.G + dst.Pix[di+2] = c.B + dst.Pix[di+3] = c.A + di += 4 + } + } + } + + blurImage(dst, radius) +} + +func blurImage(src *image.NRGBA, radius uint32) { + var ( + stackEnd *blurStack + stackIn *blurStack + stackOut *blurStack + ) + + var width, height = uint32(src.Bounds().Dx()), uint32(src.Bounds().Dy()) + var ( + div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32 + x, y, i, p, yp, yi, yw, + rSum, gSum, bSum, aSum, + rOutSum, gOutSum, bOutSum, aOutSum, + rInSum, gInSum, bInSum, aInSum, + pr, pg, pb, pa uint32 + ) + + div = radius + radius + 1 + widthMinus1 = width - 1 + heightMinus1 = height - 1 + radiusPlus1 = radius + 1 + sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2 + + stackStart := new(blurStack) + stack := stackStart + + for i = 1; i < div; i++ { + stack.next = new(blurStack) + stack = stack.next + if i == radiusPlus1 { + stackEnd = stack + } + } + stack.next = stackStart + + mulSum := mulTable[radius] + shgSum := shgTable[radius] + + for y = 0; y < height; y++ { + rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 + + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) + + rOutSum = radiusPlus1 * pr + gOutSum = radiusPlus1 * pg + bOutSum = radiusPlus1 * pb + aOutSum = radiusPlus1 * pa + + rSum += sumFactor * pr + gSum += sumFactor * pg + bSum += sumFactor * pb + aSum += sumFactor * pa + + stack = stackStart + + for i = 0; i < radiusPlus1; i++ { + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + stack = stack.next + } + + for i = 1; i < radiusPlus1; i++ { + var diff uint32 + if widthMinus1 < i { + diff = widthMinus1 + } else { + diff = i + } + p = yi + (diff << 2) + pr = uint32(src.Pix[p]) + pg = uint32(src.Pix[p+1]) + pb = uint32(src.Pix[p+2]) + pa = uint32(src.Pix[p+3]) + + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + + rSum += stack.r * (radiusPlus1 - i) + gSum += stack.g * (radiusPlus1 - i) + bSum += stack.b * (radiusPlus1 - i) + aSum += stack.a * (radiusPlus1 - i) + + rInSum += pr + gInSum += pg + bInSum += pb + aInSum += pa + + stack = stack.next + } + stackIn = stackStart + stackOut = stackEnd + + for x = 0; x < width; x++ { + pa = (aSum * mulSum) >> shgSum + src.Pix[yi+3] = uint8(pa) + + if pa != 0 { + src.Pix[yi] = uint8((rSum * mulSum) >> shgSum) + src.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum) + src.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum) + } else { + src.Pix[yi] = 0 + src.Pix[yi+1] = 0 + src.Pix[yi+2] = 0 + } + + rSum -= rOutSum + gSum -= gOutSum + bSum -= bOutSum + aSum -= aOutSum + + rOutSum -= stackIn.r + gOutSum -= stackIn.g + bOutSum -= stackIn.b + aOutSum -= stackIn.a + + p = x + radius + 1 + + if p > widthMinus1 { + p = widthMinus1 + } + p = (yw + p) << 2 + + stackIn.r = uint32(src.Pix[p]) + stackIn.g = uint32(src.Pix[p+1]) + stackIn.b = uint32(src.Pix[p+2]) + stackIn.a = uint32(src.Pix[p+3]) + + rInSum += stackIn.r + gInSum += stackIn.g + bInSum += stackIn.b + aInSum += stackIn.a + + rSum += rInSum + gSum += gInSum + bSum += bInSum + aSum += aInSum + + stackIn = stackIn.next + + pr = stackOut.r + pg = stackOut.g + pb = stackOut.b + pa = stackOut.a + + rOutSum += pr + gOutSum += pg + bOutSum += pb + aOutSum += pa + + rInSum -= pr + gInSum -= pg + bInSum -= pb + aInSum -= pa + + stackOut = stackOut.next + + yi += 4 + } + yw += width + } + + for x = 0; x < width; x++ { + rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 + + yi = x << 2 + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) + + rOutSum = radiusPlus1 * pr + gOutSum = radiusPlus1 * pg + bOutSum = radiusPlus1 * pb + aOutSum = radiusPlus1 * pa + + rSum += sumFactor * pr + gSum += sumFactor * pg + bSum += sumFactor * pb + aSum += sumFactor * pa + + stack = stackStart + + for i = 0; i < radiusPlus1; i++ { + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + stack = stack.next + } + + yp = width + + for i = 1; i <= radius; i++ { + yi = (yp + x) << 2 + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) + + stack.r = pr + stack.g = pg + stack.b = pb + stack.a = pa + + rSum += stack.r * (radiusPlus1 - i) + gSum += stack.g * (radiusPlus1 - i) + bSum += stack.b * (radiusPlus1 - i) + aSum += stack.a * (radiusPlus1 - i) + + rInSum += pr + gInSum += pg + bInSum += pb + aInSum += pa + + stack = stack.next + + if i < heightMinus1 { + yp += width + } + } + + yi = x + stackIn = stackStart + stackOut = stackEnd + + for y = 0; y < height; y++ { + p = yi << 2 + pa = (aSum * mulSum) >> shgSum + src.Pix[p+3] = uint8(pa) + + if pa > 0 { + src.Pix[p] = uint8((rSum * mulSum) >> shgSum) + src.Pix[p+1] = uint8((gSum * mulSum) >> shgSum) + src.Pix[p+2] = uint8((bSum * mulSum) >> shgSum) + } else { + src.Pix[p] = 0 + src.Pix[p+1] = 0 + src.Pix[p+2] = 0 + } + + rSum -= rOutSum + gSum -= gOutSum + bSum -= bOutSum + aSum -= aOutSum + + rOutSum -= stackIn.r + gOutSum -= stackIn.g + bOutSum -= stackIn.b + aOutSum -= stackIn.a + + p = y + radiusPlus1 + + if p > heightMinus1 { + p = heightMinus1 + } + p = (x + (p * width)) << 2 + + stackIn.r = uint32(src.Pix[p]) + stackIn.g = uint32(src.Pix[p+1]) + stackIn.b = uint32(src.Pix[p+2]) + stackIn.a = uint32(src.Pix[p+3]) + + rInSum += stackIn.r + gInSum += stackIn.g + bInSum += stackIn.b + aInSum += stackIn.a + + rSum += rInSum + gSum += gInSum + bSum += bInSum + aSum += aInSum + + stackIn = stackIn.next + + pr = stackOut.r + pg = stackOut.g + pb = stackOut.b + pa = stackOut.a + + rOutSum += pr + gOutSum += pg + bOutSum += pb + aOutSum += pa + + rInSum -= pr + gInSum -= pg + bInSum -= pb + aInSum -= pa + + stackOut = stackOut.next + + yi += width + } + } +} From ac67ba16a420be88c1eb552e27401975e6927538 Mon Sep 17 00:00:00 2001 From: esimov Date: Thu, 29 Oct 2026 17:53:59 +0200 Subject: [PATCH 2/2] Fix non constant format --- cmd/caire/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/caire/main.go b/cmd/caire/main.go index 2c568b8a..42b9dbb9 100644 --- a/cmd/caire/main.go +++ b/cmd/caire/main.go @@ -53,7 +53,7 @@ func main() { log.SetFlags(0) flag.Usage = func() { - fmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version)) + fmt.Fprintf(os.Stderr, "%s", fmt.Sprintf(HelpBanner, Version)) flag.PrintDefaults() } flag.Parse()