You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

292 lines
9.2 KiB

package main
import (
"errors"
"fmt"
"os"
"strings"
"github.com/urfave/cli/v2"
)
// Set by compiler, see Makefile
var (
version = "v1.0.0"
commit = "unknown"
builtBy = "unknown"
)
func main() {
var (
matrixTypesDesc = "This only takes one argument, but there a few types available:\n" +
helpList(1,
"A preprogrammed matrix name",
"Inline JSON of a custom matrix",
"Or a path to JSON for your custom matrix. '-' means stdin.",
) + "\n"
decimalOrPercent = ", using a decimal or percentage."
caseInsensitiveNames = "\nTheir names are case-insensitive, and hyphens and underscores are treated the same."
description = `
Colors (for --palette and --recolor) are entered as a single quoted argument.
They can be separated by spaces and commas. Colors can be formatted as hex
codes (case-insensitive, with or without the '#'), a single number from 0-255
for grayscale, or a color name from the SVG 1.1 spec (aka the HTML or W3C
color names). All colors are interpreted in the sRGB colorspace.
Color names: https://www.w3.org/TR/SVG11/types.html#ColorKeywords
Images are converted to grayscale automatically if the palette is grayscale.
This produces more correct results.
Decimal range is -1.0 to 1.0. Percentage range is -100% or 100%.
The input file path can also be parsed as a glob. This will only happen if the
path contains an asterisk. For example -i '*.jpg' will select all the .jpg
files in the current directory as input. See this page for more info on glob
pattern matching: https://golang.org/pkg/path/filepath/#Match`
)
app := &cli.App{
Name: "didder",
Usage: "dither images with a variety of algorithms and processing options.",
Description: description,
UseShortOptionHandling: true,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "strength",
Aliases: []string{"s"},
Usage: "set strength of dithering" + decimalOrPercent + " Exceeding the range will work. A zero value will be ignored.",
},
&cli.UintFlag{
Name: "threads",
Aliases: []string{"j"},
Usage: "set number of threads for ordered dithering",
},
&cli.StringFlag{
Name: "palette",
Aliases: []string{"p"},
Usage: "set color palette used for dithering",
Required: true,
},
&cli.BoolFlag{
Name: "grayscale",
Usage: "make input image(s) grayscale before dithering",
},
&cli.StringFlag{
Name: "saturation",
Usage: "change input image(s) saturation before dithering" + decimalOrPercent,
},
&cli.StringFlag{
Name: "brightness",
Usage: "change input image(s) brightness before dithering" + decimalOrPercent,
},
&cli.StringFlag{
Name: "contrast",
Usage: "change input image(s) contrast before dithering" + decimalOrPercent,
},
&cli.StringFlag{
Name: "recolor",
Aliases: []string{"r"},
Usage: "set color palette used for replacing the dithered color palette after dithering",
},
&cli.BoolFlag{
Name: "no-exif-rotation",
Usage: "disable using the EXIF rotation flag to rotate the image before processing",
},
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Usage: "set output file format. Valid options are png and gif. It will auto detect from filename when possible.",
Value: "png",
},
&cli.StringFlag{
Name: "out",
Aliases: []string{"o"},
Usage: "set output file path or directory",
Required: true,
},
&cli.StringSliceFlag{
Name: "in",
Aliases: []string{"i"},
Usage: "set input file path, specify multiple times for multiple inputs",
Required: true,
},
&cli.BoolFlag{
Name: "no-overwrite",
Usage: "the command will stop before overwriting an existing file. Files written before this one was encountered will stay in place.",
},
&cli.StringFlag{
Name: "compression",
Aliases: []string{"c"},
Usage: "PNG compression type. Options: 'default', 'no', 'speed', 'size'",
Value: "default",
},
&cli.Float64Flag{
Name: "fps",
Usage: "set frames per second for animated GIF output",
},
&cli.UintFlag{
Name: "loop",
Usage: "number of times the animated GIF output should loop, 0 is infinite",
},
&cli.UintFlag{
Name: "width",
Aliases: []string{"x"},
Usage: "set the width the input image(s) will be resized to, BEFORE dithering. Aspect ratio will be maintained if --height is not specified",
},
&cli.UintFlag{
Name: "height",
Aliases: []string{"y"},
Usage: "set the height the input image(s) will be resized to, BEFORE dithering. Aspect ratio will be maintained if --width is not specified",
},
&cli.UintFlag{
Name: "upscale",
Aliases: []string{"u"},
Usage: "scale image up AFTER dithering. So '2' will make the output 2 times as big as the input. Integer only.",
Value: 1,
},
&cli.BoolFlag{
Name: "version",
Aliases: []string{"v"},
Usage: "get version info",
},
},
Commands: []*cli.Command{
{
Name: "random",
Usage: "grayscale and RGB random dithering",
Description: "Specify two arguments (min and max) for RGB or grayscale, or 6 (min/max for each channel) to control each RGB channel.\nArguments can be separated by commas or spaces. -0.5,0.5 is a good default.",
Flags: []cli.Flag{
&cli.Int64Flag{
Name: "seed",
Aliases: []string{"s"},
Usage: "set the seed for randomization. This will also only use one thread, to keep output deterministic",
},
},
UseShortOptionHandling: true,
Action: random,
SkipFlagParsing: true, // Allow for numbers that start with a negative
},
{
Name: "bayer",
Usage: "Bayer matrix ordered dithering",
Description: "Two arguments, for the X and Y dimension of the matrix. They can be separated by a space, comma, or 'x'.\nBoth arguments must be a power of two, with the exception of: 3x5, 5x3, and 3x3.",
UseShortOptionHandling: true,
Action: bayer,
},
{
Name: "odm",
Usage: "Ordered Dither Matrix",
Description: "Select or provide an ordered dithering matrix. " + matrixTypesDesc +
"Here are all the built-in ordered dithering matrices. You can find details on these matrices here:\nhttps://github.com/makeworld-the-better-one/dither/blob/v2.0.0/ordered_ditherers.go\n\n" +
helpList(1,
"ClusteredDot4x4",
"ClusteredDotDiagonal8x8",
"Vertical5x3",
"Horizontal3x5",
"ClusteredDotDiagonal6x6",
"ClusteredDotDiagonal8x8_2",
"ClusteredDotDiagonal16x16",
"ClusteredDot6x6",
"ClusteredDotSpiral5x5",
"ClusteredDotHorizontalLine",
"ClusteredDotVerticalLine",
"ClusteredDot8x8",
"ClusteredDot6x6_2",
"ClusteredDot6x6_3",
"ClusteredDotDiagonal8x8_3",
) + caseInsensitiveNames,
UseShortOptionHandling: true,
Action: odm,
},
{
Name: "edm",
Usage: "Error Diffusion Matrix",
Description: "Select or provide an error diffusion matrix. " + matrixTypesDesc +
"Here are all the built-in error diffusion matrices. You can find details on these matrices here:\nhttps://github.com/makeworld-the-better-one/dither/blob/v2.0.0/error_diffusers.go\n\n" +
helpList(1,
"Simple2D",
"FloydSteinberg",
"FalseFloydSteinberg",
"JarvisJudiceNinke",
"Atkinson",
"Stucki",
"Burkes",
"Sierra (or Sierra3)",
"TwoRowSierra (or Sierra2)",
"SierraLite (or Sierra2_4A)",
"StevenPigeon",
) + caseInsensitiveNames,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "serpentine",
Aliases: []string{"s"},
Usage: "enable serpentine dithering",
},
},
UseShortOptionHandling: true,
Action: edm,
},
},
Before: preProcess,
Action: func(c *cli.Context) error {
return errors.New("no command specified")
},
}
// Handle version flag
if len(os.Args) == 2 && (os.Args[1] == "-v" || os.Args[1] == "--version") {
fmt.Println("didder", version)
fmt.Println("Commit:", commit)
fmt.Println("Built by:", builtBy)
return
}
// Hack around issue where required flags are still required even for help
// https://github.com/urfave/cli/issues/1247
if len(os.Args) == 3 {
if os.Args[1] == "h" || os.Args[1] == "help" {
// Like: didder help bayer
for _, c := range app.Commands {
if c.Name == os.Args[2] {
cli.HelpPrinter(os.Stdout, cli.CommandHelpTemplate, c)
return
}
}
fmt.Println("no command with that name")
os.Exit(1)
} else if os.Args[len(os.Args)-1] == "-h" || os.Args[len(os.Args)-1] == "--help" {
// Like: didder bayer --help
for _, c := range app.Commands {
if c.Name == os.Args[1] {
cli.HelpPrinter(os.Stdout, cli.CommandHelpTemplate, c)
return
}
}
fmt.Println("no command with that name")
os.Exit(1)
}
}
err := app.Run(os.Args)
if err != nil {
if len(os.Args) == 1 {
// Just ran the command with no flags
return
}
fmt.Println(err)
os.Exit(1)
}
}
// helpList creates an indented list for the CLI help/usage info.
func helpList(level int, items ...string) string {
ret := ""
for _, item := range items {
ret += strings.Repeat(" ", level) + "- " + item + "\n"
}
return ret
}