Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions pkg/graphics/graph_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ func (p *Pipeline) applyAtkinson(gray *image.Gray) *MonochromeBitmap {
mono := NewMonochromeBitmap(width, height)

// Create a working copy for error diffusion
work := make([][]int, height)
work := make([]int, width*height)
for y := 0; y < height; y++ {
work[y] = make([]int, width)
rowOffset := y * width
for x := 0; x < width; x++ {
work[y][x] = int(gray.GrayAt(x, y).Y)
work[rowOffset+x] = int(gray.GrayAt(x, y).Y)
}
}

Expand All @@ -180,8 +180,13 @@ func (p *Pipeline) applyAtkinson(gray *image.Gray) *MonochromeBitmap {
// Error is distributed as 1/8 to each neighbor (total 6/8 = 3/4)

for y := 0; y < height; y++ {
rowOffset := y * width
nextRowOffset := (y + 1) * width
nextNextRowOffset := (y + 2) * width

for x := 0; x < width; x++ {
oldPixel := work[y][x]
idx := rowOffset + x
oldPixel := work[idx]
newPixel := 0
if oldPixel > int(p.opts.Threshold) {
newPixel = 255
Expand All @@ -199,22 +204,22 @@ func (p *Pipeline) applyAtkinson(gray *image.Gray) *MonochromeBitmap {

// Distribute to neighbors
if x+1 < width {
work[y][x+1] += diffusedError
work[idx+1] += diffusedError
}
if x+2 < width {
work[y][x+2] += diffusedError
work[idx+2] += diffusedError
}
if y+1 < height {
if x-1 >= 0 {
work[y+1][x-1] += diffusedError
work[nextRowOffset+(x-1)] += diffusedError
}
work[y+1][x] += diffusedError
work[nextRowOffset+x] += diffusedError
if x+1 < width {
work[y+1][x+1] += diffusedError
work[nextRowOffset+(x+1)] += diffusedError
}
}
if y+2 < height {
work[y+2][x] += diffusedError
work[nextNextRowOffset+x] += diffusedError
}
}
}
Expand Down
54 changes: 54 additions & 0 deletions pkg/graphics/graph_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,57 @@ func TestPipeline_Resize_Limit(t *testing.T) {
t.Errorf("Output width = %d, want 576 (capped)", mono.Width)
}
}

func TestPipeline_Process_Atkinson_DiffusionLogic(t *testing.T) {
opts := graphics.DefaultOptions()
opts.Dithering = graphics.Atkinson
opts.Threshold = 128
// Use small width to force manual checking and avoid resize
opts.PixelWidth = 3
p := graphics.NewPipeline(opts)

// We test each neighbor independently to avoid cascade interference
tests := []struct {
name string
source image.Point
target image.Point
}{
{"Right_x+1", image.Point{0, 0}, image.Point{1, 0}},
{"Right_x+2", image.Point{0, 0}, image.Point{2, 0}},
{"Below_y+1_x", image.Point{0, 0}, image.Point{0, 1}},
{"Diag_y+1_x+1", image.Point{0, 0}, image.Point{1, 1}},
{"Below_y+2_x", image.Point{0, 0}, image.Point{0, 2}},
{"Diag_y+1_x-1", image.Point{1, 0}, image.Point{0, 1}},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
img := image.NewGray(image.Rect(0, 0, 3, 3))
// Clear
for i := 0; i < 9; i++ {
img.SetGray(i%3, i/3, color.Gray{Y: 0})
}

// Set source to 100. Error = 100-0 = 100. Diff = 12.
img.SetGray(tc.source.X, tc.source.Y, color.Gray{Y: 100})

// Set target to 120. 120+12 = 132 > 128 -> White.
img.SetGray(tc.target.X, tc.target.Y, color.Gray{Y: 120})

mono, err := p.Process(img)
if err != nil {
t.Fatalf("Process failed: %v", err)
}

// Source should be Black
if !mono.GetPixel(tc.source.X, tc.source.Y) {
t.Errorf("Pixel(%d,%d) should be Black", tc.source.X, tc.source.Y)
}

// Target should be White
if mono.GetPixel(tc.target.X, tc.target.Y) {
t.Errorf("Pixel(%d,%d) should be White (received diffusion)", tc.target.X, tc.target.Y)
}
})
}
}
Loading