~robertgzr/corrupter

b76ec21b3dcffe8c7a62f9cfe47ed956eb63bcbf — Robert Günzler 1 year, 3 months ago 0e0d962
Refactor into library

Signed-off-by: Robert Günzler <r@gnzler.io>
4 files changed, 387 insertions(+), 434 deletions(-)

A cmd/corrupter/main.go
M cmd/lock/lock.go
A corrupter.go
D main.go
A cmd/corrupter/main.go => cmd/corrupter/main.go +93 -0
@@ 0,0 1,93 @@
package main

import (
	"flag"
	"fmt"
	"image/png"
	"log"
	"math/rand"
	"os"
	"time"

	"github.com/r00tman/corrupter"
)

var seededRand = rand.New(rand.NewSource(1))

func check(e error) {
	if e != nil {
		log.Fatal(e)
	}
}

func main() {
	// command line parsing
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s [options] [input] [output]\n", os.Args[0])
		fmt.Fprintf(os.Stderr, "   or: %s [options] - (for stdin+stdout processing)\n", os.Args[0])
		flag.PrintDefaults()
	}

	var opts corrupter.Options

	flag.Float64Var(&opts.Mag, "mag", corrupter.DefaultOptions.Mag, "dissolve blur strength")
	flag.IntVar(&opts.Bheight, "bheight", corrupter.DefaultOptions.Bheight, "average distorted block height")
	flag.Float64Var(&opts.Boffset, "boffset", corrupter.DefaultOptions.Boffset, "distorted block offset strength")
	flag.Float64Var(&opts.Stride, "stride", corrupter.DefaultOptions.Stride, "distorted block stride strength")

	flag.Float64Var(&opts.Lag, "lag", corrupter.DefaultOptions.Lag, "per-channel scanline lag strength")
	flag.Float64Var(&opts.Lr, "lr", corrupter.DefaultOptions.Lr, "initial red scanline lag")
	flag.Float64Var(&opts.Lg, "lg", corrupter.DefaultOptions.Lg, "initial green scanline lag")
	flag.Float64Var(&opts.Lb, "lb", corrupter.DefaultOptions.Lb, "initial blue scanline lag")
	flag.Float64Var(&opts.StdOffset, "stdoffset", corrupter.DefaultOptions.StdOffset, "std. dev. of red-blue channel offset (non-destructive)")
	flag.UintVar(&opts.Add, "add", corrupter.DefaultOptions.Add, "additional brightness control (0-255)")

	flag.Float64Var(&opts.MeanAbber, "meanabber", corrupter.DefaultOptions.MeanAbber, "mean chromatic abberation offset")
	flag.Float64Var(&opts.StdAbber, "stdabber", corrupter.DefaultOptions.StdAbber, "std. dev. of chromatic abberation offset (lower values induce longer trails)")

	flag.Int64Var(&opts.Seed, "seed", corrupter.DefaultOptions.Seed, "random seed. set to -1 if you want to generate it from time. the old version has used seed=1")

	flag.Parse()

	if opts.Seed == -1 {
		seededRand = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
	} else if opts.Seed != 1 {
		seededRand = rand.New(rand.NewSource(opts.Seed))
	}

	// flag.Args() contain all non-option arguments, i.e., our input and output files
	reader := (*os.File)(nil)
	if len(flag.Args()) == 0 {
		flag.Usage()
		os.Exit(2)
	} else if flag.Args()[0] == "-" {
		// stdin/stdout processing
		reader = os.Stdin
	} else if len(flag.Args()) == 2 {
		err := error(nil)
		reader, err = os.Open(flag.Args()[0])
		check(err)
	} else {
		flag.Usage()
		os.Exit(2)
	}
	m, err := png.Decode(reader)
	check(err)
	reader.Close()

	new_img1, err := corrupter.Corrupt(m, opts)
	check(err)

	// write the image
	writer := (*os.File)(nil)
	if flag.Args()[0] == "-" {
		// stdin/stdout processing
		writer = os.Stdout
	} else {
		writer, err = os.Create(flag.Args()[1])
		check(err)
	}
	e := png.Encoder{CompressionLevel: png.NoCompression}
	e.Encode(writer, new_img1)
	writer.Close()
}

M cmd/lock/lock.go => cmd/lock/lock.go +127 -216
@@ 5,19 5,18 @@ import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"image"
	"image/png"
	"flag"
	"fmt"
	"io"
	"log"
	"math/rand"
	"os"
	"os/exec"
	"sync"
	"time"

	"golang.org/x/sys/unix"
	"github.com/r00tman/corrupter"
)

const (


@@ 27,29 26,17 @@ const (
	wrongColor = "#AF005F"
)

const (
	mag       = 7.
	bheight   = 10
	boffset   = 30.
	strideC   = .1
	lag       = .005
	lrC       = -7.
	lgC       = 0.
	lbC       = 3.
	stdOffset = 10
	add       = 10.
	meanAbber = 10.
	stdAbber  = 10.
)

func main() {
	log.SetOutput(os.Stderr)
	log.SetFlags(log.Ltime)

	swaylock := exec.Command("swaylock",
	runparts := flag.Bool("runparts", true, "run hooks under $XDG_DATA_DIR/lock/{un,}locked.d/")
	daemon := flag.Bool("daemonize", false, "don't block on the locking process")
	flag.Parse()

	swaylock := exec.Command("/home/robert/bin/swaylock",
		"--color=#000000EE",
		"--daemonize",
		"--font-size=16",
		"--font-size=18",
		"--font=Iosevka Aile",
		"--hide-keyboard-layout",
		"--ignore-empty-password",


@@ 72,91 59,95 @@ func main() {

	var wg sync.WaitGroup

	{
		swqOutput, err := exec.Command("swaymsg", "--type", "get_tree", "--raw").Output()
	forOutput(&wg, func(output string) {
		grim := exec.Command("grim", "-o", output, "-")
		rc, err := grim.StdoutPipe()
		if err != nil {
			log.Fatal(err)
			log.Fatalf("%s: %v", output, err)
		}
		var swqJson struct {
			Nodes []struct {
				Name string
			}
		defer rc.Close()

		// capture screenshot
		log.Printf("+ %v", grim.Args)
		if err := grim.Start(); err != nil {
			log.Fatalf("%s: %v", output, err)
		}
		if err := json.Unmarshal(swqOutput, &swqJson); err != nil {
			log.Fatal(err)

		var buf bytes.Buffer
		defer buf.Reset()

		// process screenshot
		log.Printf("%s: processing", output)
		opts := corrupter.DefaultOptions
		opts.Seed = time.Now().Unix()

		m, err := png.Decode(rc)
		if err != nil {
			log.Fatalf("%s: %v", output, err)
		}

		img, err := corrupter.Corrupt(m, opts)
		if err != nil {
			log.Fatalf("%s: %v", output, err)
		}

		for _, output := range swqJson.Nodes {
			if output.Name == "__i3" {
				continue
			}
			wg.Add(1)
			go func(output string) {
				defer wg.Done()

				grim := exec.Command("grim", "-o", output, "-")
				rc, err := grim.StdoutPipe()
				if err != nil {
					log.Fatalf("%s: %v", output, err)
				}
				defer rc.Close()

				// capture screenshot
				log.Printf("+ %v", grim.Args)
				if err := grim.Start(); err != nil {
					log.Fatalf("%s: %v", output, err)
				}

				var buf bytes.Buffer
				defer buf.Reset()

				// process screenshot
				log.Printf("%s: processing", output)
				if err := corrupter(rc, &buf, time.Now().Unix()); err != nil {
					log.Fatalf("%s: %v", output, err)
				}

				log.Printf("%s: processed %d bytes", output, buf.Len())

				// memfd to pass image from internal processing
				// to swaylock
				fd, err := unix.MemfdCreate("lock-"+output, 0)
				if err != nil {
					log.Fatalf("%s: %v", output, err)
				}
				if err := unix.Ftruncate(fd, int64(buf.Len())); err != nil {
					log.Fatal(err)
				}
				mem, err := unix.Mmap(fd, 0, buf.Len(), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
				if err != nil {
					log.Fatalf("%s: %v", output, err)
				}
				copy(mem, buf.Bytes())
				if err := unix.Munmap(mem); err != nil {
					log.Fatalf("%s: %v", output, err)
				}

				swaylock.ExtraFiles = append(swaylock.ExtraFiles, os.NewFile(uintptr(fd), fmt.Sprintf("/proc/self/fd/%d", fd)))
				swaylock.Args = append(swaylock.Args, "--image="+output+":"+fmt.Sprintf("/proc/self/fd/%d", 2+len(swaylock.ExtraFiles)))
			}(output.Name)
		e := png.Encoder{CompressionLevel: png.NoCompression}
		if err := e.Encode(&buf, img); err != nil {
			log.Fatalf("%s: %v", output, err)
		}
	}

	{
		wg.Add(1)
		go func() {
			defer wg.Done()
			runparts := exec.Command("run-parts", "-v", os.ExpandEnv("${XDG_DATA_HOME}/lock.d"))
			logCmd(runparts)
			if err := runparts.Run(); err != nil {
				log.Fatal(err)
			}
		}()
	}
		log.Printf("%s: processed %d bytes", output, buf.Len())

		// memfd to pass image from internal processing
		// to swaylock
		memfd, err := memFd(buf.Bytes())
		if err != nil {
			log.Fatalf("%s: failed to create mem fd: %v", output, err)
		}

		swaylock.ExtraFiles = append(swaylock.ExtraFiles, memfd)
		swaylock.Args = append(swaylock.Args, "--image="+output+":"+fmt.Sprintf("/proc/self/fd/%d", 2+len(swaylock.ExtraFiles)))
		swaylock.Args = append(swaylock.Args, flag.Args()...)
	})

	wg.Add(1)
	go func() {
		defer wg.Done()

		if !*runparts {
			return
		}

		runParts := exec.Command("run-parts", "-v", os.ExpandEnv("${XDG_DATA_HOME}/lock/locked.d"))
		logCmd(runParts)
		if err := runParts.Run(); err != nil {
			log.Print(err)
		}
	}()

	defer func() {
		if !*runparts {
			return
		}

		runParts := exec.Command("run-parts", "-v", os.ExpandEnv("${XDG_DATA_HOME}/lock/unlocked.d"))
		logCmd(runParts)
		if err := runParts.Run(); err != nil {
			log.Print(err)
		}
	}()

	wg.Wait()

	logCmd(swaylock)

	if *daemon {
		if err := swaylock.Start(); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}

	if err := swaylock.Run(); err != nil {
		log.Fatal(err)
	}


@@ 179,139 170,59 @@ func logCmd(c *exec.Cmd) {
	go handler(c.StderrPipe())
}

func corrupter(reader io.Reader, writer io.Writer, seed int64) error {
	seededRand := rand.New(rand.NewSource(seed))

	// flag.Args() contain all non-option arguments, i.e., our input and output files
	m, err := png.Decode(reader)
	if err != nil {
		return err
func forOutput(wg *sync.WaitGroup, fn func(name string)) {
	if wg == nil {
		var _wg sync.WaitGroup
		wg = &_wg
	}

	// trying to obtain raw pointers to color data, since .At(), .Set() are very slow
	m_raw_stride, m_raw_pix := 0, []uint8(nil)

	switch m.(type) {
	default:
		return errors.New("unknown image type")
	case *image.NRGBA:
		m_raw := m.(*image.NRGBA)
		m_raw_stride = m_raw.Stride
		m_raw_pix = m_raw.Pix
	case *image.RGBA:
		m_raw := m.(*image.RGBA)
		m_raw_stride = m_raw.Stride
		m_raw_pix = m_raw.Pix
	swqOutput, err := exec.Command("swaymsg", "--type", "get_tree", "--raw").Output()
	if err != nil {
		log.Fatal(err)
	}

	b := m.Bounds()

	// first stage is dissolve+block corruption
	new_img := image.NewNRGBA(b)
	line_off := 0
	stride := 0.
	yset := 0
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			// Every BHEIGHT lines in average a new distorted block begins
			if seededRand.Intn(bheight*b.Max.X) == 0 {
				line_off = offset(seededRand, boffset)
				stride = seededRand.NormFloat64() * strideC
				yset = y
			}
			// at the line where the block has begun, we don't want to offset the image
			// so stride_off is 0 on the block's line
			stride_off := int(stride * float64(y-yset))

			// offset is composed of the blur, block offset, and skew offset (stride)
			offx := offset(seededRand, mag) + line_off + stride_off
			offy := offset(seededRand, mag)

			// copy the corresponding pixel (4 bytes) to the new image
			src_idx := m_raw_stride*wrap(y+offy, b.Max.Y) + 4*wrap(x+offx, b.Max.X)
			dst_idx := new_img.Stride*y + 4*x

			copy(new_img.Pix[dst_idx:dst_idx+4], m_raw_pix[src_idx:src_idx+4])
	var swqJson struct {
		Nodes []struct {
			Name string
		}
	}

	// second stage is adding per-channel scan inconsistency and brightening
	new_img1 := image.NewNRGBA(b)

	lr, lg, lb := lrC, lgC, lbC
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			lr += seededRand.NormFloat64() * lag
			lg += seededRand.NormFloat64() * lag
			lb += seededRand.NormFloat64() * lag
			offx := offset(seededRand, stdOffset)

			// obtain source pixel base offsets. red/blue border is also smoothed by offx
			ra_idx := new_img.Stride*y + 4*wrap(x+int(lr)-offx, b.Max.X)
			g_idx := new_img.Stride*y + 4*wrap(x+int(lg), b.Max.X)
			b_idx := new_img.Stride*y + 4*wrap(x+int(lb)+offx, b.Max.X)

			// pixels are stored in (r, g, b, a) order in memory
			r := new_img.Pix[ra_idx]
			a := new_img.Pix[ra_idx+3]
			g := new_img.Pix[g_idx+1]
			b := new_img.Pix[b_idx+2]

			r, g, b = brighten(r, uint8(add)), brighten(g, uint8(add)), brighten(b, uint8(add))

			// copy the corresponding pixel (4 bytes) to the new image
			dst_idx := new_img1.Stride*y + 4*x
			copy(new_img1.Pix[dst_idx:dst_idx+4], []uint8{r, g, b, a})
		}
	if err := json.Unmarshal(swqOutput, &swqJson); err != nil {
		log.Fatal(err)
	}

	// third stage is to add chromatic abberation+chromatic trails
	// (trails happen because we're changing the same image we process)
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			offx := meanAbber + offset(seededRand, stdAbber) // lower offset arg = longer trails

			// obtain source pixel base offsets. only red and blue are distorted
			ra_idx := new_img1.Stride*y + 4*wrap(x+offx, b.Max.X)
			g_idx := new_img1.Stride*y + 4*x
			b_idx := new_img1.Stride*y + 4*wrap(x-offx, b.Max.X)

			// pixels are stored in (r, g, b, a) order in memory
			r := new_img1.Pix[ra_idx]
			a := new_img1.Pix[ra_idx+3]
			g := new_img1.Pix[g_idx+1]
			b := new_img1.Pix[b_idx+2]

			// copy the corresponding pixel (4 bytes) to the SAME image. this gets us nice colorful trails
			dst_idx := new_img1.Stride*y + 4*x
			copy(new_img1.Pix[dst_idx:dst_idx+4], []uint8{r, g, b, a})
	for _, output := range swqJson.Nodes {
		if output.Name == "__i3" {
			continue
		}
		wg.Add(1)
		go func(name string) {
			defer wg.Done()
			fn(name)
		}(output.Name)
	}

	// write the image
	e := png.Encoder{CompressionLevel: png.NoCompression}
	return e.Encode(writer, new_img1)
	if wg != nil {
		wg.Wait()
	}
}

// force x to stay in [0, b) range. x is assumed to be in [-b,2*b) range
func wrap(x, b int) int {
	if x < 0 {
		return x + b
func memFd(data []byte) (*os.File, error) {
	// memfd to pass image from internal processing
	// to swaylock
	fd, err := unix.MemfdCreate("", 0)
	if err != nil {
		return nil, err
	}
	if x >= b {
		return x - b
	if err := unix.Ftruncate(fd, int64(len(data))); err != nil {
		return nil, err
	}
	mem, err := unix.Mmap(fd, 0, len(data), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
	if err != nil {
		return nil, err
	}
	copy(mem, data)
	if err := unix.Munmap(mem); err != nil {
		return nil, err
	}
	return x
}

// get normally distributed (rounded to int) value with the specified std. dev.
func offset(seededRand *rand.Rand, stddev float64) int {
	sample := seededRand.NormFloat64() * stddev
	return int(sample)
}

// brighten the color safely, i.e., by simultaneously reducing contrast
func brighten(r uint8, add uint8) uint8 {
	r32, add32 := uint32(r), uint32(add)
	return uint8(r32 - r32*add32/255 + add32)
	return os.NewFile(uintptr(fd), fmt.Sprintf("/proc/self/fd/%d", fd)), nil
}

A corrupter.go => corrupter.go +167 -0
@@ 0,0 1,167 @@
package corrupter

import (
	"errors"
	"image"
	"math/rand"
)

var DefaultOptions = Options{
	Mag:       7.,
	Bheight:   10,
	Boffset:   30.,
	Stride:    .1,
	Lag:       .005,
	Lr:        -7.,
	Lg:        0.,
	Lb:        3.,
	StdOffset: 10,
	Add:       10,
	MeanAbber: 10.,
	StdAbber:  10.,
	Seed:      -1,
}

type Options struct {
	Mag       float64
	Bheight   int
	Boffset   float64
	Stride    float64
	Lag       float64
	Lr        float64
	Lg        float64
	Lb        float64
	StdOffset float64
	Add       uint
	MeanAbber float64
	StdAbber  float64
	Seed      int64
}

func Corrupt(m image.Image, opts Options) (*image.NRGBA, error) {
	seededRand := rand.New(rand.NewSource(opts.Seed))

	// trying to obtain raw pointers to color data, since .At(), .Set() are very slow
	m_raw_stride, m_raw_pix := 0, []uint8(nil)

	switch m.(type) {
	default:
		return nil, errors.New("unknown image type")
	case *image.NRGBA:
		m_raw := m.(*image.NRGBA)
		m_raw_stride = m_raw.Stride
		m_raw_pix = m_raw.Pix
	case *image.RGBA:
		m_raw := m.(*image.RGBA)
		m_raw_stride = m_raw.Stride
		m_raw_pix = m_raw.Pix
	}

	b := m.Bounds()

	// first stage is dissolve+block corruption
	new_img := image.NewNRGBA(b)
	line_off := 0
	stride := 0.
	yset := 0
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			// Every BHEIGHT lines in average a new distorted block begins
			if seededRand.Intn(opts.Bheight*b.Max.X) == 0 {
				line_off = offset(seededRand, opts.Boffset)
				stride = seededRand.NormFloat64() * opts.Stride
				yset = y
			}
			// at the line where the block has begun, we don't want to offset the image
			// so stride_off is 0 on the block's line
			stride_off := int(stride * float64(y-yset))

			// offset is composed of the blur, block offset, and skew offset (stride)
			offx := offset(seededRand, opts.Mag) + line_off + stride_off
			offy := offset(seededRand, opts.Mag)

			// copy the corresponding pixel (4 bytes) to the new image
			src_idx := m_raw_stride*wrap(y+offy, b.Max.Y) + 4*wrap(x+offx, b.Max.X)
			dst_idx := new_img.Stride*y + 4*x

			copy(new_img.Pix[dst_idx:dst_idx+4], m_raw_pix[src_idx:src_idx+4])
		}
	}

	// second stage is adding per-channel scan inconsistency and brightening
	new_img1 := image.NewNRGBA(b)

	lr, lg, lb := opts.Lr, opts.Lg, opts.Lb
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			lr += seededRand.NormFloat64() * opts.Lag
			lg += seededRand.NormFloat64() * opts.Lag
			lb += seededRand.NormFloat64() * opts.Lag
			offx := offset(seededRand, opts.StdOffset)

			// obtain source pixel base offsets. red/blue border is also smoothed by offx
			ra_idx := new_img.Stride*y + 4*wrap(x+int(lr)-offx, b.Max.X)
			g_idx := new_img.Stride*y + 4*wrap(x+int(lg), b.Max.X)
			b_idx := new_img.Stride*y + 4*wrap(x+int(lb)+offx, b.Max.X)

			// pixels are stored in (r, g, b, a) order in memory
			r := new_img.Pix[ra_idx]
			a := new_img.Pix[ra_idx+3]
			g := new_img.Pix[g_idx+1]
			b := new_img.Pix[b_idx+2]

			r, g, b = brighten(r, uint8(opts.Add)), brighten(g, uint8(opts.Add)), brighten(b, uint8(opts.Add))

			// copy the corresponding pixel (4 bytes) to the new image
			dst_idx := new_img1.Stride*y + 4*x
			copy(new_img1.Pix[dst_idx:dst_idx+4], []uint8{r, g, b, a})
		}
	}

	// third stage is to add chromatic abberation+chromatic trails
	// (trails happen because we're changing the same image we process)
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			offx := int(opts.MeanAbber) + offset(seededRand, opts.StdAbber) // lower offset arg = longer trails

			// obtain source pixel base offsets. only red and blue are distorted
			ra_idx := new_img1.Stride*y + 4*wrap(x+offx, b.Max.X)
			g_idx := new_img1.Stride*y + 4*x
			b_idx := new_img1.Stride*y + 4*wrap(x-offx, b.Max.X)

			// pixels are stored in (r, g, b, a) order in memory
			r := new_img1.Pix[ra_idx]
			a := new_img1.Pix[ra_idx+3]
			g := new_img1.Pix[g_idx+1]
			b := new_img1.Pix[b_idx+2]

			// copy the corresponding pixel (4 bytes) to the SAME image. this gets us nice colorful trails
			dst_idx := new_img1.Stride*y + 4*x
			copy(new_img1.Pix[dst_idx:dst_idx+4], []uint8{r, g, b, a})
		}
	}

	return new_img1, nil
}

// force x to stay in [0, b) range. x is assumed to be in [-b,2*b) range
func wrap(x, b int) int {
	if x < 0 {
		return x + b
	}
	if x >= b {
		return x - b
	}
	return x
}

// get normally distributed (rounded to int) value with the specified std. dev.
func offset(seededRand *rand.Rand, stddev float64) int {
	return int(seededRand.NormFloat64() * stddev)
}

// brighten the color safely, i.e., by simultaneously reducing contrast
func brighten(r uint8, add uint8) uint8 {
	r32, add32 := uint32(r), uint32(add)
	return uint8(r32 - r32*add32/255 + add32)
}

D main.go => main.go +0 -218
@@ 1,218 0,0 @@
package main

import (
	"flag"
	"fmt"
	"image"
	"image/png"
	"log"
	"math/rand"
	"os"
	"time"
)

var seededRand = rand.New(rand.NewSource(1))

func check(e error) {
	if e != nil {
		log.Fatal(e)
	}
}

// force x to stay in [0, b) range. x is assumed to be in [-b,2*b) range
func wrap(x, b int) int {
	if x < 0 {
		return x + b
	}
	if x >= b {
		return x - b
	}
	return x
}

// get normally distributed (rounded to int) value with the specified std. dev.
func offset(stddev float64) int {
	sample := seededRand.NormFloat64() * stddev
	return int(sample)
}

// brighten the color safely, i.e., by simultaneously reducing contrast
func brighten(r uint8, add uint8) uint8 {
	r32, add32 := uint32(r), uint32(add)
	return uint8(r32 - r32*add32/255 + add32)
}

func main() {
	// command line parsing
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s [options] [input] [output]\n", os.Args[0])
		fmt.Fprintf(os.Stderr, "   or: %s [options] - (for stdin+stdout processing)\n", os.Args[0])
		flag.PrintDefaults()
	}
	magPtr := flag.Float64("mag", 7.0, "dissolve blur strength")
	blockHeightPtr := flag.Int("bheight", 10, "average distorted block height")
	blockOffsetPtr := flag.Float64("boffset", 30., "distorted block offset strength")
	strideMagPtr := flag.Float64("stride", 0.1, "distorted block stride strength")

	lagPtr := flag.Float64("lag", 0.005, "per-channel scanline lag strength")
	lrPtr := flag.Float64("lr", -7, "initial red scanline lag")
	lgPtr := flag.Float64("lg", 0, "initial green scanline lag")
	lbPtr := flag.Float64("lb", 3, "initial blue scanline lag")
	stdOffsetPtr := flag.Float64("stdoffset", 10, "std. dev. of red-blue channel offset (non-destructive)")
	addPtr := flag.Int("add", 37, "additional brightness control (0-255)")

	meanAbberPtr := flag.Int("meanabber", 10, "mean chromatic abberation offset")
	stdAbberPtr := flag.Float64("stdabber", 10, "std. dev. of chromatic abberation offset (lower values induce longer trails)")

	seedPtr := flag.Int64("seed", -1, "random seed. set to -1 if you want to generate it from time. the old version has used seed=1")

	flag.Parse()

	if *seedPtr == -1 {
		seededRand = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
	} else if *seedPtr != 1 {
		seededRand = rand.New(rand.NewSource(*seedPtr))
	}

	// flag.Args() contain all non-option arguments, i.e., our input and output files
	reader := (*os.File)(nil)
	if len(flag.Args()) == 0 {
		flag.Usage()
		os.Exit(2)
	} else if flag.Args()[0] == "-" {
		// stdin/stdout processing
		reader = os.Stdin
	} else if len(flag.Args()) == 2 {
		err := error(nil)
		reader, err = os.Open(flag.Args()[0])
		check(err)
	} else {
		flag.Usage()
		os.Exit(2)
	}
	m, err := png.Decode(reader)
	check(err)
	reader.Close()

	// trying to obtain raw pointers to color data, since .At(), .Set() are very slow
	m_raw_stride, m_raw_pix := 0, []uint8(nil)

	switch m.(type) {
	default:
		log.Fatal("unknown image type")
	case *image.NRGBA:
		m_raw := m.(*image.NRGBA)
		m_raw_stride = m_raw.Stride
		m_raw_pix = m_raw.Pix
	case *image.RGBA:
		m_raw := m.(*image.RGBA)
		m_raw_stride = m_raw.Stride
		m_raw_pix = m_raw.Pix
	}

	b := m.Bounds()

	// first stage is dissolve+block corruption
	new_img := image.NewNRGBA(b)
	line_off := 0
	stride := 0.
	yset := 0
	MAG := *magPtr
	BHEIGHT := *blockHeightPtr
	BOFFSET := *blockOffsetPtr
	STRIDE_MAG := *strideMagPtr
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			// Every BHEIGHT lines in average a new distorted block begins
			if seededRand.Intn(BHEIGHT*b.Max.X) == 0 {
				line_off = offset(BOFFSET)
				stride = seededRand.NormFloat64() * STRIDE_MAG
				yset = y
			}
			// at the line where the block has begun, we don't want to offset the image
			// so stride_off is 0 on the block's line
			stride_off := int(stride * float64(y-yset))

			// offset is composed of the blur, block offset, and skew offset (stride)
			offx := offset(MAG) + line_off + stride_off
			offy := offset(MAG)

			// copy the corresponding pixel (4 bytes) to the new image
			src_idx := m_raw_stride*wrap(y+offy, b.Max.Y) + 4*wrap(x+offx, b.Max.X)
			dst_idx := new_img.Stride*y + 4*x

			copy(new_img.Pix[dst_idx:dst_idx+4], m_raw_pix[src_idx:src_idx+4])
		}
	}

	// second stage is adding per-channel scan inconsistency and brightening
	new_img1 := image.NewNRGBA(b)

	lr, lg, lb := *lrPtr, *lgPtr, *lbPtr
	LAG := *lagPtr
	ADD := uint8(*addPtr)
	STD_OFFSET := *stdOffsetPtr
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			lr += seededRand.NormFloat64() * LAG
			lg += seededRand.NormFloat64() * LAG
			lb += seededRand.NormFloat64() * LAG
			offx := offset(STD_OFFSET)

			// obtain source pixel base offsets. red/blue border is also smoothed by offx
			ra_idx := new_img.Stride*y + 4*wrap(x+int(lr)-offx, b.Max.X)
			g_idx := new_img.Stride*y + 4*wrap(x+int(lg), b.Max.X)
			b_idx := new_img.Stride*y + 4*wrap(x+int(lb)+offx, b.Max.X)

			// pixels are stored in (r, g, b, a) order in memory
			r := new_img.Pix[ra_idx]
			a := new_img.Pix[ra_idx+3]
			g := new_img.Pix[g_idx+1]
			b := new_img.Pix[b_idx+2]

			r, g, b = brighten(r, ADD), brighten(g, ADD), brighten(b, ADD)

			// copy the corresponding pixel (4 bytes) to the new image
			dst_idx := new_img1.Stride*y + 4*x
			copy(new_img1.Pix[dst_idx:dst_idx+4], []uint8{r, g, b, a})
		}
	}

	// third stage is to add chromatic abberation+chromatic trails
	// (trails happen because we're changing the same image we process)
	MEAN_ABBER := *meanAbberPtr
	STD_ABBER := *stdAbberPtr
	for y := 0; y < b.Max.Y; y++ {
		for x := 0; x < b.Max.X; x++ {
			offx := MEAN_ABBER + offset(STD_ABBER) // lower offset arg = longer trails

			// obtain source pixel base offsets. only red and blue are distorted
			ra_idx := new_img1.Stride*y + 4*wrap(x+offx, b.Max.X)
			g_idx := new_img1.Stride*y + 4*x
			b_idx := new_img1.Stride*y + 4*wrap(x-offx, b.Max.X)

			// pixels are stored in (r, g, b, a) order in memory
			r := new_img1.Pix[ra_idx]
			a := new_img1.Pix[ra_idx+3]
			g := new_img1.Pix[g_idx+1]
			b := new_img1.Pix[b_idx+2]

			// copy the corresponding pixel (4 bytes) to the SAME image. this gets us nice colorful trails
			dst_idx := new_img1.Stride*y + 4*x
			copy(new_img1.Pix[dst_idx:dst_idx+4], []uint8{r, g, b, a})
		}
	}

	// write the image
	writer := (*os.File)(nil)
	if flag.Args()[0] == "-" {
		// stdin/stdout processing
		writer = os.Stdout
	} else {
		writer, err = os.Create(flag.Args()[1])
		check(err)
	}
	e := png.Encoder{CompressionLevel: png.NoCompression}
	e.Encode(writer, new_img1)
	writer.Close()
}