From ef045f18aa7cf27aeb461229946a1f754273ae1c Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Wed, 29 Oct 2025 01:18:40 -0400 Subject: [PATCH] Finally have Atkinson or FloydSteinberg dithering with the index palette and sharpening, so that's primarily what I need. --- Makefile | 5 ++ filters/posterize.go | 108 +++++++++++++++++++++++++++++++++++++------ main.go | 14 +++++- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index eb0d984..91f1a26 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ build: go build . +compare: + ./jankifier -input ./tests/images/bash_test.png -output none.png -pixel-width 4 -color-depth 16 -dither 0 + ./jankifier -input ./tests/images/bash_test.png -output floyd.png -pixel-width 4 -color-depth 16 -dither 1 + ./jankifier -input ./tests/images/bash_test.png -output atkinson.png -pixel-width 4 -color-depth 16 -dither 2 + test: go test MY/webapp/tests -c ./tests.test diff --git a/filters/posterize.go b/filters/posterize.go index 98e2ecd..9845b7a 100644 --- a/filters/posterize.go +++ b/filters/posterize.go @@ -4,40 +4,120 @@ import ( "image" "image/draw" "image/color" + "math" "github.com/disintegration/gift" ) +const ( + NoDither=0 + FloydSteinDither=1 + AtkinsonDither=2 +) + + type PosterizeFilter struct { - Depth uint8 + Depth uint16 + Bins map[uint16]uint16 + DitherType int } -func Posterize(depth uint8) gift.Filter { - return PosterizeFilter{depth} +func Posterize(depth uint16, dither_type int) gift.Filter { + var i uint16 + bins := make(map[uint16]uint16) + chunk := uint16((math.MaxUint16 + 1) / int(depth)) + + // BUG: this was fine with uint8, now it's dumb as hell + for i = 0; i < math.MaxUint16; i++ { + bins[i] = (i / chunk) * chunk + } + + return PosterizeFilter{depth, bins, dither_type} } func (filter PosterizeFilter) Bounds(src image.Rectangle) (dst_bounds image.Rectangle) { return src } -func bin(c uint8) uint8 { - if c <= 64 { - return 0 - } else { - return 255 +func (filter PosterizeFilter) Quantize(c color.NRGBA64) (color.NRGBA64, color.NRGBA64) { + + newpixel := color.NRGBA64{ + filter.Bins[c.R], + filter.Bins[c.G], + filter.Bins[c.B], + c.A, + } + + err := color.NRGBA64{ + c.R - newpixel.R, + c.G - newpixel.G, + c.B - newpixel.B, + c.A, + } + + return newpixel, err +} + +func DistError(old color.Color, quant_err color.NRGBA64, term float32) color.Color { + dumb := old.(color.NRGBA64) + + c := color.NRGBA64{ + dumb.R + uint16(float32(quant_err.R) * term), + dumb.G + uint16(float32(quant_err.G) * term), + dumb.B + uint16(float32(quant_err.B) * term), + dumb.A, + } + + return c +} + +func (filter PosterizeFilter) FloydStein(x int, y int, dst draw.Image, quant_err color.NRGBA64) { + dst.Set(x+1, y, + DistError(dst.At(x+1, y), quant_err, 7.0/16.0)) + dst.Set(x-1, y+1, + DistError(dst.At(x-1, y+1), quant_err, 3.0/16.0)) + dst.Set(x, y+1, + DistError(dst.At(x, y+1), quant_err, 5.0/16.0)) + dst.Set(x+1, y+1, + DistError(dst.At(x+1, y+1), quant_err, 1.0/16.0)) +} + +func (filter PosterizeFilter) Atkinson(x int, y int, dst draw.Image, quant_err color.NRGBA64) { + delta := [6]image.Point{ + {x+1, y+0}, + {x+2, y+0}, + {x-1, y+1}, + {x+0, y+1}, + {x+1, y+1}, + {x+0, y+2}, + } + bounds := dst.Bounds() + + for _, d := range(delta) { + if d.In(bounds) { + dst.Set(d.X, d.Y, DistError(dst.At(d.X, d.Y), quant_err, 1.0/8.0)) + } } } func (filter PosterizeFilter) Draw(dst draw.Image, src image.Image, _ *gift.Options) { bounds := src.Bounds() + gift.New().Draw(dst, src) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { - from := src.At(x, y) - c := from.(color.NRGBA) - c.R = bin(c.R) - c.G = bin(c.G) - c.B = bin(c.B) - dst.Set(x, y, c) + from := dst.At(x, y) + c := from.(color.NRGBA64) + newpixel, quant_err := filter.Quantize(c) + + dst.Set(x, y, newpixel) + + switch filter.DitherType { + case FloydSteinDither: + filter.FloydStein(x, y, dst, quant_err) + case AtkinsonDither: + filter.Atkinson(x, y, dst, quant_err) + } } } } diff --git a/main.go b/main.go index 0a2dbc9..93a2955 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "image/png" "github.com/disintegration/gift" "flag" + "math" "lcthw.dev/go/jankifier/filters" ) @@ -38,6 +39,8 @@ type Opts struct { InFile string OutFile string PixelWidth int + ColorDepth int + DitherType int } func ParseOpts() Opts { @@ -46,8 +49,14 @@ func ParseOpts() Opts { flag.StringVar(&opts.InFile, "input", "", "input file.png") flag.StringVar(&opts.OutFile, "output", "", "output file.png") flag.IntVar(&opts.PixelWidth, "pixel-width", 4, "pixel width") + flag.IntVar(&opts.ColorDepth, "color-depth", 16, "number of colors in the palette") + flag.IntVar(&opts.DitherType, "dither", 0, "0=none, 1=floyd, 2=atkinson") flag.Parse() + if opts.ColorDepth > math.MaxUint8 { + log.Fatalf("color-depth can't be greater than %d", math.MaxUint8); + } + return opts } @@ -58,10 +67,11 @@ func main() { bounds := src.Bounds() resize := gift.Resize(bounds.Max.X / opts.PixelWidth, 0, gift.NearestNeighborResampling) - posterize := filters.Posterize(16) + posterize := filters.Posterize(uint16(opts.ColorDepth), opts.DitherType) upscale := filters.Upscale(bounds, opts.PixelWidth) + sharpen := gift.UnsharpMask(1, 1, 0) - g := gift.New(posterize, resize, upscale) + g := gift.New(resize, posterize, upscale, sharpen) smaller := image.NewNRGBA(g.Bounds(bounds)) g.Draw(smaller, src)