Skip to content

Commit

Permalink
Add TIFF & BMP file formats, add parameter grouping for CLI tool
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronsor committed Jan 3, 2023
1 parent 4e225cf commit 0376136
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 13 deletions.
17 changes: 14 additions & 3 deletions format/png/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type encoder struct {
zs *zstd.Encoder
zsLevel zstd.EncoderLevel
bw *bufio.Writer
useZstd bool
}

// CompressionLevel indicates the compression level.
Expand Down Expand Up @@ -203,7 +204,7 @@ func (e *encoder) writePLTEAndTRNS(p color.Palette) {
// This method should only be called from writeIDATs (via writeImage).
// No other code should treat an encoder as an io.Writer.
func (e *encoder) Write(b []byte) (int, error) {
if e.enc.UseZstd {
if e.useZstd {
e.writeChunk(b, "ZDAT")
} else {
e.writeChunk(b, "IDAT")
Expand Down Expand Up @@ -316,7 +317,7 @@ func zeroMemory(v []uint8) {
func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) error {
var cw io.Writer

if !e.enc.UseZstd {
if !e.useZstd {
if e.zw == nil || e.zwLevel != level {
zw, err := zlib.NewWriterLevel(w, level)
if err != nil {
Expand Down Expand Up @@ -561,7 +562,7 @@ func (e *encoder) writeIDATorZDATs() {
} else {
e.bw.Reset(e)
}
if e.enc.UseZstd {
if e.useZstd {
e.err = e.writeImage(e.bw, e.m, e.cb, levelToZstd(e.enc.CompressionLevel))
} else {
e.err = e.writeImage(e.bw, e.m, e.cb, levelToZlib(e.enc.CompressionLevel))
Expand Down Expand Up @@ -623,6 +624,10 @@ type Chunk struct {
type EncodeOptions struct {
// CustomChunks specifies custom chunks to include in the PNG file.
CustomChunks []Chunk

// FallbackImage specifies a fallback image when Zstd compression is
// used.
FallbackImage image.Image
}

// Encode writes the Image m to w in PNG format. Any Image may be
Expand Down Expand Up @@ -662,6 +667,7 @@ func (enc *Encoder) EncodeWithOptions(w io.Writer, m image.Image, o *EncodeOptio
}

e.enc = enc
e.useZstd = enc.UseZstd
e.w = w
e.m = m

Expand Down Expand Up @@ -712,6 +718,11 @@ func (enc *Encoder) EncodeWithOptions(w io.Writer, m image.Image, o *EncodeOptio
e.writePLTEAndTRNS(pal)
}
e.writeIDATorZDATs()
if o.FallbackImage != nil && enc.UseZstd {
e.useZstd = false
e.m = o.FallbackImage
e.writeIDATorZDATs()
}
for _, c := range o.CustomChunks {
if c.AfterIDAT {
e.writeChunk(c.Data, c.Name)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ require (
github.com/pborman/getopt/v2 v2.1.0
github.com/spakin/netpbm v1.3.0
github.com/xfmoulet/qoi v0.2.0
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30
golang.org/x/image v0.2.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/xfmoulet/qoi v0.2.0/go.mod h1:uuPUygmV7o8qy7PhiaGAQX0iLiqoUvFEUKjwUFt
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws=
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ=
golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
Expand Down
57 changes: 57 additions & 0 deletions henshin/bmp_codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2023 Ronsor Labs. All rights reserved.

package henshin

import (
"image"
"io"

"golang.org/x/image/bmp"
)

func init() {
RegisterCodec(&BMPCodec{})
}

// BMPCodec is the BMP codec.
type BMPCodec struct{}

// New returns a new instance of BMPCodec.
func (c *BMPCodec) New() Codec { return &BMPCodec{} }

// Name returns the name of the BMP codec: "bmp"
func (c *BMPCodec) Name() string { return "bmp" }

// Aliases returns alternate names for the BMP codec
func (c *BMPCodec) Aliases() []string {
return []string{"dib"}
}

// Magic returns magic strings that identify BMP data.
func (c *BMPCodec) Magic() []string {
return []string{"BM????\x00\x00\x00\x00"}
}

// Decode decodes a BMP image according to the options specified.
func (c *BMPCodec) Decode(r io.Reader, d *DecodeOptions) (image.Image, error) {
return bmp.Decode(r)
}

// DecodeConfig returns the color model and dimensions of a BMP image
// without decoding the image.
func (c *BMPCodec) DecodeConfig(r io.Reader, d *DecodeOptions) (image.Config, error) {
return bmp.DecodeConfig(r)
}

// Encode encodes a BMP image according to the options specified.
func (c *BMPCodec) Encode(w io.Writer, i image.Image, o *EncodeOptions) error {
if o == nil { o = DefaultEncodeOptions() }

return bmp.Encode(w, i)
}

var (
_ Decoder = &BMPCodec{}
_ Encoder = &BMPCodec{}
_ CodecWithAliases = &BMPCodec{}
)
7 changes: 7 additions & 0 deletions henshin/codecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ type CodecWithAliases interface {
Aliases() []string
}

// CodecWithParamParser is an image codec that allows getting
// codec-specific parameters from a string.
type CodecWithParamParser interface {
Codec
ParseParams(opt string) (any, error)
}

// Decoder is an image codec that can decode.
type Decoder interface {
Codec
Expand Down
14 changes: 14 additions & 0 deletions henshin/resize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2023 Ronsor Labs. All rights reserved.

package henshin

import (
"testing"
)

func TestAreaFit(t *testing.T) {
w, h := areaFit(1920, 1080, 1024*1024)
if w != 1365 && h != 768 {
t.Errorf("Expected (1365, 768) but got (%d, %d)", w, h)
}
}
60 changes: 60 additions & 0 deletions henshin/tiff_codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2023 Ronsor Labs. All rights reserved.

package henshin

import (
"image"
"io"

"golang.org/x/image/tiff"
)

func init() {
RegisterCodec(&TIFFCodec{})
}

// TIFFCodec is the TIFF codec.
type TIFFCodec struct{}

// New returns a new instance of TIFFCodec.
func (c *TIFFCodec) New() Codec { return &TIFFCodec{} }

// Name returns the name of the TIFF codec: "tiff"
func (c *TIFFCodec) Name() string { return "tiff" }

// Aliases returns alternate names for the TIFF codec
func (c *TIFFCodec) Aliases() []string {
return []string{"tif"}
}

// Magic returns magic strings that identify TIFF data.
func (c *TIFFCodec) Magic() []string {
return []string{"II\x2A\x00", "MM\x00\x2A"}
}

// Decode decodes a TIFF image according to the options specified.
func (c *TIFFCodec) Decode(r io.Reader, d *DecodeOptions) (image.Image, error) {
return tiff.Decode(r)
}

// DecodeConfig returns the color model and dimensions of a TIFF image
// without decoding the image.
func (c *TIFFCodec) DecodeConfig(r io.Reader, d *DecodeOptions) (image.Config, error) {
return tiff.DecodeConfig(r)
}

// Encode encodes a TIFF image according to the options specified.
func (c *TIFFCodec) Encode(w io.Writer, i image.Image, o *EncodeOptions) error {
if o == nil { o = DefaultEncodeOptions() }

var tiffOpt tiff.Options
// TODO: allow setting these options

return tiff.Encode(w, i, &tiffOpt)
}

var (
_ Decoder = &TIFFCodec{}
_ Encoder = &TIFFCodec{}
_ CodecWithAliases = &TIFFCodec{}
)
1 change: 1 addition & 0 deletions henshin/wand.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ func (w *Wand) property(key string) (val string) {
val = w.md.Comments[0]
}
case "%": val = "%"
case ";": val = ","
}
if val == "" {
if strings.HasPrefix(key, "C:") || strings.HasPrefix(key, "comment:") {
Expand Down
86 changes: 76 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ var (
identifyFormatString = "%wx%h, hash: %H, comment: %c"

filterArgs FilterArgs

groupParamOpen, groupParamClose *int
)

// FilterArgs is a set of filtering arguments.
Expand All @@ -50,20 +52,69 @@ func init() {

getopt.FlagLong(&identifyFormatString, "identify-format", 0, "Format string for --identify output")

getopt.FlagLong(&filterArgs.Strip, "strip", 'S', "Strip metadata from image")
getopt.FlagLong(&filterArgs.AddComments, "comment", 'C', "Add comment to image metadata")
getopt.FlagLong(&filterArgs.SetComments, "set-comment", 0, "Set comments for image metadata")
getopt.FlagLong(&filterArgs.Crop, "crop", 'c', "Crop image")
getopt.FlagLong(&filterArgs.Resize, "resize", 'r', "Resize image")
getopt.FlagLong(&filterArgs.CompressionLevel, "compress", 0, "Compression level, if applicable (0-100)")
filterArgs.CompressionLevel = -1 // Set to default
groupParamOpen = getopt.CounterLong("group", '(', "Open filter parameter group")
groupParamClose = getopt.CounterLong("end-group", ')', "Close filter parameter group")

initFilterArgs(&filterArgs, getopt.CommandLine)

getopt.SetParameters("[images ...] [output path]")
}

func processFilterArgs(wand *henshin.Wand, fa *FilterArgs) {
wand.ForceRGBA()
func initFilterArgs(filterArgs *FilterArgs, optSet *getopt.Set) {
optSet.FlagLong(&filterArgs.Strip, "strip", 'S', "Strip metadata from image")
optSet.FlagLong(&filterArgs.AddComments, "comment", 'C', "Add comment to image metadata")
optSet.FlagLong(&filterArgs.SetComments, "set-comment", 0, "Set comments for image metadata")
optSet.FlagLong(&filterArgs.Crop, "crop", 'c', "Crop image")
optSet.FlagLong(&filterArgs.Resize, "resize", 'r', "Resize image")
optSet.FlagLong(&filterArgs.CompressionLevel, "compress", 0, "Compression level, if applicable (0-100)")
filterArgs.CompressionLevel = -1 // Set to default
}

func parseFilterArgs(args []string) (ret []*FilterArgs) {
groupStartIdx := -1
groupEndIdx := -1
groupCount := 1

for i, arg := range args {
if arg == "--group" || arg == "-(" {
groupStartIdx = i
} else if arg == "--end-group" || arg == "-)" {
groupEndIdx = i
}
if groupStartIdx != -1 && groupEndIdx != -1 {
if (groupEndIdx - groupStartIdx) == -1 {
groupStartIdx = -1
groupEndIdx = -1
groupCount++
continue
}

optSet := getopt.New()
filterArgs := &FilterArgs{}
initFilterArgs(filterArgs, optSet)

section := args[groupStartIdx:groupEndIdx]
err := optSet.Getopt(section, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Filter group %d: %v\n", groupCount, err)
optSet.SetProgram("majokko -(")
optSet.SetParameters("-)")
optSet.PrintUsage(os.Stderr)
os.Exit(1)
}

ret = append(ret, filterArgs)

groupStartIdx = -1
groupEndIdx = -1
groupCount++
}
}

return
}

func processFilterArgs(wand *henshin.Wand, fa *FilterArgs) {
if fa.Strip {
wand.Strip()
}
Expand Down Expand Up @@ -134,7 +185,16 @@ func actionConvert(wand *henshin.Wand, logPrefix string, maxArg int, args []stri
outFile = filepath.Join(outFile, filepath.Base(inFile))
}

processFilterArgs(wand, &filterArgs)
wand.ForceRGBA()

faGroups := parseFilterArgs(os.Args)
if faGroups == nil {
processFilterArgs(wand, &filterArgs)
} else {
for _, fa := range faGroups {
processFilterArgs(wand, fa)
}
}

err := wand.WriteImage(outFile)
if err != nil {
Expand Down Expand Up @@ -189,6 +249,12 @@ func main() {
return
}

if *groupParamOpen != *groupParamClose {
fmt.Fprintln(os.Stderr, "Unbalanced filter groups.")
getopt.Usage()
os.Exit(1)
}

if !doConvert {
doConvert = !doIdentify
}
Expand Down

0 comments on commit 0376136

Please sign in to comment.