Initial Release
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,5 @@
|
||||
module github.com/bakonpancakz/clitools/crunchy
|
||||
|
||||
go 1.25.2
|
||||
|
||||
require golang.org/x/image v0.33.0
|
||||
@@ -0,0 +1,2 @@
|
||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// ----- Parse Arguments -----
|
||||
var optionQuality = 0
|
||||
var optionNoise = 25
|
||||
var optionGenerations = 5
|
||||
var optionFilename string
|
||||
|
||||
flags := make([]string, 0, len(os.Args))
|
||||
for i := 1; i < len(os.Args); i++ {
|
||||
segments := strings.SplitN(os.Args[i], "=", 2)
|
||||
if len(segments) == 2 {
|
||||
n := segments[0]
|
||||
s := segments[1]
|
||||
switch {
|
||||
case strings.EqualFold(n, "--generations"):
|
||||
v := parseInteger(n, s, 0, math.MaxInt)
|
||||
fmt.Printf("Flag: Generation(s) %d\n", v)
|
||||
optionGenerations = v
|
||||
|
||||
case strings.EqualFold(n, "--quality"):
|
||||
v := parseInteger(n, s, 0, 100)
|
||||
fmt.Printf("Flag: Quality %d\n", v)
|
||||
optionQuality = v
|
||||
|
||||
case strings.EqualFold(n, "--noise"):
|
||||
v := parseInteger(n, s, 0, 100)
|
||||
fmt.Printf("Flag: Noise Level %d\n", v)
|
||||
optionQuality = v
|
||||
|
||||
default:
|
||||
fmt.Printf("%s: Unknown Argument", n)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
} else {
|
||||
flags = append(flags, segments[0])
|
||||
}
|
||||
}
|
||||
if len(flags) < 1 {
|
||||
fmt.Println("crunchy")
|
||||
fmt.Println(" --noise=<value> - Noise Level (Default: 25, Range: 0-100)")
|
||||
fmt.Println(" --quality=<value> - JPEG Quality (Default: 0, Range: 0-100)")
|
||||
fmt.Println(" --generations=<count> - Iterations (Default: 5)")
|
||||
fmt.Println(" <Filename> - Input Filename")
|
||||
os.Exit(0)
|
||||
}
|
||||
optionFilename = flags[0]
|
||||
noiseInteger := int(float32(optionNoise)*2.56) + 1
|
||||
noiseHalved := noiseInteger / 2
|
||||
|
||||
// ----- Decode Image Contents -----
|
||||
content := bytes.Buffer{}
|
||||
f, err := os.Open(optionFilename)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open file: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := io.Copy(&content, f); err != nil {
|
||||
fmt.Printf("Failed to read file: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
img, err := decodeImage(content.Bytes())
|
||||
if err != nil {
|
||||
fmt.Printf("Decoding Error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
ycc := image.NewYCbCr(bounds, image.YCbCrSubsampleRatio444)
|
||||
rgb := image.NewRGBA(bounds)
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
for x := 0; x < bounds.Dx(); x++ {
|
||||
rgb.Set(x, y, img.At(x, y)) // copy generic img to rgba
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Apply Generation Loss -----
|
||||
for i := 0; i < optionGenerations; i++ {
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
for x := 0; x < bounds.Dx(); x++ {
|
||||
// Rounding Error via Colorspace Conversion
|
||||
r, g, b, _ := rgb.At(x, y).RGBA()
|
||||
cy, cb, cr := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||
|
||||
// Random Noise
|
||||
noise := rand.Intn(noiseInteger) - noiseHalved
|
||||
cb = uint8(int(cb) + noise)
|
||||
cr = uint8(int(cr) + noise)
|
||||
|
||||
// Apply Changes
|
||||
ycc.Y[ycc.YOffset(x, y)] = cy
|
||||
ycc.Cb[ycc.COffset(x, y)] = cb
|
||||
ycc.Cr[ycc.COffset(x, y)] = cr
|
||||
}
|
||||
}
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
for x := 0; x < bounds.Dx(); x++ {
|
||||
rgb.Set(x, y, ycc.At(x, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Write Output -----
|
||||
content.Reset()
|
||||
if err := jpeg.Encode(&content, rgb, &jpeg.Options{Quality: optionQuality}); err != nil {
|
||||
fmt.Printf("Encoding Error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
cleanname := path.Base(optionFilename)
|
||||
emptyname := strings.TrimSuffix(cleanname, path.Ext(cleanname))
|
||||
finalname := fmt.Sprintf("%s_n%d_g%d_q%d.jpeg", emptyname, optionNoise, optionGenerations, optionQuality)
|
||||
if err := os.WriteFile(finalname, content.Bytes(), 0660); err != nil {
|
||||
fmt.Printf("Failed to write file '%s': %s\n", finalname, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Integer for CLI Arguments
|
||||
func parseInteger(n string, s string, min int, max int) int {
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
fmt.Printf("%s: Not A Number\n", n)
|
||||
os.Exit(1)
|
||||
}
|
||||
if v < min {
|
||||
fmt.Printf("%s: Value cannot be less than %d\n", n, min)
|
||||
os.Exit(1)
|
||||
}
|
||||
if v > max {
|
||||
fmt.Printf("%s: Value cannot be more than %d\n", n, max)
|
||||
os.Exit(1)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Decode Image with the appropriate decoder based on it's starting bytes
|
||||
// https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files)
|
||||
func decodeImage(d []byte) (image.Image, error) {
|
||||
var (
|
||||
decoderImage image.Image
|
||||
decoderError error
|
||||
)
|
||||
switch {
|
||||
case len(d) > 3 && // JPEG
|
||||
d[0] == 0xFF && d[1] == 0xD8 && d[2] == 0xFF:
|
||||
decoderImage, decoderError = jpeg.Decode(bytes.NewReader(d))
|
||||
|
||||
case len(d) > 8 && // PNG
|
||||
d[0] == 0x89 && d[1] == 0x50 && d[2] == 0x4E && d[3] == 0x47 &&
|
||||
d[4] == 0x0D && d[5] == 0x0A && d[6] == 0x1A && d[7] == 0x0A:
|
||||
decoderImage, decoderError = png.Decode(bytes.NewReader(d))
|
||||
|
||||
case len(d) > 4 && // GIF
|
||||
d[0] == 0x47 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x38:
|
||||
decoderImage, decoderError = gif.Decode(bytes.NewReader(d))
|
||||
|
||||
case len(d) > 12 && // WEBP
|
||||
d[0] == 0x52 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x46 &&
|
||||
d[8] == 0x57 && d[9] == 0x45 && d[10] == 0x42 && d[11] == 0x50:
|
||||
decoderImage, decoderError = webp.Decode(bytes.NewReader(d))
|
||||
|
||||
default:
|
||||
return decoderImage, errors.New("unsupported file type")
|
||||
}
|
||||
if decoderError != nil {
|
||||
return nil, decoderError
|
||||
}
|
||||
|
||||
return decoderImage, nil
|
||||
}
|
||||
Reference in New Issue
Block a user