blob: 980fb91e9ce8c7693f8d58fa52608875e5d4e4fc [file]
// Copyright 2025 The Wuffs Authors.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
// ----------------
// handsum decodes and encodes the Handsum lossy image file format.
package main
import (
"bytes"
"encoding/base64"
"errors"
"flag"
"image"
"image/draw"
"image/png"
"io"
"os"
"github.com/google/wuffs/lib/handsum"
"github.com/google/wuffs/lib/parsecolor"
_ "image/gif"
_ "image/jpeg"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
)
var (
decodeFlag = flag.Bool("decode", false, "")
encodeFlag = flag.Bool("encode", false, "")
roundtripFlag = flag.Bool("roundtrip", false, "")
bFlag = flag.String("b", "", "")
base64Flag = flag.String("base64", "", "")
bgFlag = flag.String("bg", "", "")
cFlag = flag.String("c", "rgb", "")
qFlag = flag.Int("q", 4, "")
)
func getBFlag() string {
if *bFlag != "" {
return *bFlag
}
return *bgFlag
}
const usageStr = `handsum decodes and encodes the Handsum lossy image file format.
Usage: choose one of
handsum -decode [path]
handsum -encode [path]
handsum -roundtrip [path]
The path to the input image file is optional. If omitted, stdin is read.
The output image (in Handsum or PNG format) is written to stdout.
Decode inputs Handsum and outputs PNG.
Encode inputs BMP, GIF, JPEG, PNG, TIFF or WEBP and outputs Handsum.
Roundtrip is equivalent to encode (to an ephemeral file) and then decode.
For encode or roundtrip, the default color and quality is -c=rgb -q=4 (best
quality; 147 bytes per file) but you can choose a lower setting. The -b or -bg
flag sets a background color (in case the input is transparent). For example:
handsum -encode -bg=darkblue -c=rgb -q=2 foo.png > foo.handsum
----
Flags:
-decode Whether to decode the input.
-encode Whether to encode the input.
-roundtrip Whether to encode-and-decode the input.
-b or -bg Encoding background color: e.g. red, white, 9c27b0 or #ff8.
The default is ff9e9e9e (for -c=1 or -c=3) or 00000000 (for -c=4).
-base64=std or -base64=url Decode or encode base64, not raw bytes.
-c Encoding color: gray (1), rgb (3, default) or rgba (4).
-q Encoding quality: 1, 2, 3 or 4 (default).
`
func main() {
if err := main1(); err != nil {
os.Stderr.WriteString(err.Error() + "\n")
os.Exit(1)
}
}
func main1() error {
flag.Usage = func() { os.Stderr.WriteString(usageStr) }
flag.Parse()
b64e := (*base64.Encoding)(nil)
if !*roundtripFlag {
switch *base64Flag {
case "STD", "Std", "std":
b64e = base64.StdEncoding
case "URL", "Url", "url":
b64e = base64.URLEncoding
}
}
color := handsum.ColorRGB
switch *cFlag {
case "1", "GRAY", "Gray", "gray":
color = handsum.ColorGray
case "4", "RGBA", "Rgba", "rgba":
color = handsum.ColorRGBA
}
if (color <= handsum.ColorRGB) && (getBFlag() == "") {
*bFlag = "ff9e9e9e"
}
quality := handsum.Quality(max(1, min(4, *qFlag)))
// ----
r := (io.Reader)(os.Stdin)
switch flag.NArg() {
case 0:
// No-op.
case 1:
f, err := os.Open(flag.Arg(0))
if err != nil {
return err
}
defer f.Close()
r = f
default:
return errors.New("too many filenames; the maximum is one")
}
if *decodeFlag && (b64e != nil) {
buf := [1024]byte{}
n, err := io.ReadFull(r, buf[:])
if (err != nil) && (err != io.ErrUnexpectedEOF) {
return err
}
dec, err := b64e.AppendDecode(nil, buf[:n])
if err != nil {
return err
}
r = bytes.NewReader(dec)
}
// ----
if *decodeFlag && !*encodeFlag && !*roundtripFlag {
return decode(r)
}
if !*decodeFlag && *encodeFlag && !*roundtripFlag {
return encode(os.Stdout, b64e, r, color, quality)
}
if !*decodeFlag && !*encodeFlag && *roundtripFlag {
return roundtrip(r, color, quality)
}
return errors.New("must specify exactly one of -decode, -encode, -roundtrip or -help")
}
func decode(r io.Reader) error {
src, err := handsum.Decode(r)
if err != nil {
return err
}
return png.Encode(os.Stdout, src)
}
func encode(w io.Writer, b64e *base64.Encoding, r io.Reader, color handsum.Color, quality handsum.Quality) error {
src, _, err := image.Decode(r)
if err != nil {
return err
}
if o, ok := src.(interface{ Opaque() bool }); !ok || !o.Opaque() {
src = applyBackgroundColor(src)
}
w1, buf := w, (*bytes.Buffer)(nil)
if b64e != nil {
buf = &bytes.Buffer{}
w1 = buf
}
if err := handsum.Encode(w1, src, &handsum.EncodeOptions{
Color: color,
Quality: quality,
}); err != nil {
return err
}
if b64e != nil {
enc := b64e.AppendEncode(nil, buf.Bytes())
enc = append(enc, '\n')
if _, err := w.Write(enc); err != nil {
return err
}
}
return nil
}
func roundtrip(r io.Reader, color handsum.Color, quality handsum.Quality) error {
buf := &bytes.Buffer{}
err := encode(buf, nil, r, color, quality)
if err != nil {
return err
}
dst, err := handsum.Decode(buf)
if err != nil {
return err
}
return png.Encode(os.Stdout, dst)
}
func applyBackgroundColor(src image.Image) image.Image {
backgroundColor, _ := parsecolor.Parse(getBFlag())
r16, g16, b16, a16 := backgroundColor.RGBA()
if (r16 == 0) && (g16 == 0) && (b16 == 0) && (a16 == 0) {
return src
}
r8, g8, b8 := uint8(r16>>8), uint8(g16>>8), uint8(b16>>8)
bounds := src.Bounds()
ret := image.NewRGBA(bounds)
pix := ret.Pix
n4 := len(pix) / 4
for i4 := range n4 {
i := i4 * 4
pix[i+0] = r8
pix[i+1] = g8
pix[i+2] = b8
pix[i+3] = 0xff
}
draw.Draw(ret, bounds, src, bounds.Min, draw.Over)
return ret
}