1
0
mirror of https://github.com/coalaura/ffwebp.git synced 2025-09-07 21:45:31 +00:00
This commit is contained in:
2025-08-11 03:56:07 +02:00
parent 20ac175f51
commit 278e3afeb3
7 changed files with 200 additions and 1 deletions

View File

@@ -5,7 +5,7 @@ FFWebP is a command line utility for converting images between multiple formats.
## Features
- Pure Go implementation with no external runtime dependencies
- Supports AVIF, BMP, GIF, ICO, JPEG, JPEGXL, PNG, TIFF and WebP
- Supports AVIF, BMP, GIF, ICO, JPEG, JPEGXL, PNG, PNM (PBM/PGM/PPM/PAM), TIFF and WebP
- Lossy or lossless output with configurable quality
- Output codec selected from the output file extension when `--codec` is omitted
- Full set of format-specific flags for every supported format (see `ffwebp --help`)

1
go.mod
View File

@@ -10,6 +10,7 @@ require (
github.com/gen2brain/webp v0.5.5
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/sergeymakinen/go-ico v1.0.0-beta.0
github.com/spakin/netpbm v1.3.2
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
)

2
go.sum
View File

@@ -16,6 +16,8 @@ github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNW
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
github.com/spakin/netpbm v1.3.2 h1:ZAb16Sw/+b4QeO9NokEvejVvbrYdF6DdcYJe0dKONL0=
github.com/spakin/netpbm v1.3.2/go.mod h1:cVep9uXARFgAu2UU+0c+OE1J+eKmDN2hW0EN+tazkTA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=

8
internal/builtins/pnm.go Normal file
View File

@@ -0,0 +1,8 @@
//go:build pnm || core || full
// +build pnm core full
package builtins
import (
_ "github.com/coalaura/ffwebp/internal/codec/pnm"
)

View File

@@ -49,6 +49,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
if v < 0 || v > 100 {
return fmt.Errorf("invalid avif.quality-alpha: %d", v)
}
return nil
},
},
@@ -61,6 +62,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
if v < 0 || v > 10 {
return fmt.Errorf("invalid avif.speed: %d", v)
}
return nil
},
},
@@ -73,6 +75,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
if v != 444 && v != 422 && v != 420 {
return fmt.Errorf("invalid avif.chroma: %d", v)
}
return nil
},
},

View File

@@ -47,6 +47,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
if value < 1 || value > 10 {
return fmt.Errorf("invalid jpegxl.effort: %d", value)
}
return nil
},
},

184
internal/codec/pnm/pnm.go Normal file
View File

@@ -0,0 +1,184 @@
package pnm
import (
"fmt"
"image"
"io"
"strings"
"github.com/spakin/netpbm"
"github.com/coalaura/ffwebp/internal/codec"
"github.com/coalaura/ffwebp/internal/logx"
"github.com/coalaura/ffwebp/internal/opts"
"github.com/urfave/cli/v3"
)
var (
plain bool
maxValue uint16
formatStr string
tupleType string
)
func init() {
codec.Register(impl{})
}
type impl struct{}
func (impl) String() string {
return "pnm"
}
func (impl) Extensions() []string {
return []string{"ppm", "pgm", "pnm", "pbm", "pam"}
}
func (impl) CanEncode() bool {
return true
}
func (impl) Flags(flags []cli.Flag) []cli.Flag {
return append(flags,
&cli.BoolFlag{
Name: "pnm.plain",
Usage: "PNM: produce plain (ASCII) format (P1/P2/P3/P7 with ASCII raster)",
Value: false,
Destination: &plain,
},
&cli.Uint16Flag{
Name: "pnm.maxval",
Usage: "PNM: maximum sample value (1..65535). Controls 1- or 2-byte samples for binary formats",
Value: 255,
Destination: &maxValue,
},
&cli.StringFlag{
Name: "pnm.format",
Usage: "PNM: force output subformat (pbm, pgm, ppm, pam). If empty the codec will use the extension of the output file or infer it from the image.",
Value: "",
Destination: &formatStr,
},
&cli.StringFlag{
Name: "pnm.pam-tupletype",
Usage: "PNM: when writing PAM (P7), set TUPLETYPE (e.g. RGB, RGB_ALPHA, GRAYSCALE). If empty a sensible value is chosen.",
Value: "",
Destination: &tupleType,
},
)
}
func (impl) Sniff(reader io.ReaderAt) (int, []byte, error) {
buf := make([]byte, 2)
if _, err := reader.ReadAt(buf, 0); err != nil {
return 0, nil, err
}
if buf[0] == 'P' && buf[1] >= '1' && buf[1] <= '7' {
return 100, buf, nil
}
return 0, nil, nil
}
func (impl) Decode(r io.Reader) (image.Image, error) {
return netpbm.Decode(r, &netpbm.DecodeOptions{
Exact: true,
})
}
func (impl) Encode(w io.Writer, img image.Image, common opts.Common) error {
var (
format netpbm.Format
found bool
)
if formatStr != "" {
f, ok := pnmFormat(formatStr)
if !ok {
return fmt.Errorf("invalid pnm.format: %q", formatStr)
}
found = true
format = f
} else if common.OutputExtension != "" {
f, ok := pnmFormat(common.OutputExtension)
if ok {
found = true
format = f
}
}
if !found {
if imageHasAlpha(img) {
format = netpbm.PAM
} else if imageIsGrayscale(img) {
format = netpbm.PGM
} else {
format = netpbm.PPM
}
}
opts := &netpbm.EncodeOptions{
Format: format,
Plain: plain,
MaxValue: maxValue,
TupleType: tupleType,
}
if opts.Format == netpbm.PBM {
opts.MaxValue = 1
}
logx.Printf("pnm: format=%s plain=%t maxval=%d tupltype=%q\n", opts.Format.String(), opts.Plain, opts.MaxValue, opts.TupleType)
return netpbm.Encode(w, img, opts)
}
func pnmFormat(format string) (netpbm.Format, bool) {
switch strings.ToLower(format) {
case "pbm":
return netpbm.PBM, true
case "pgm":
return netpbm.PGM, true
case "ppm":
return netpbm.PPM, true
case "pam":
return netpbm.PAM, true
}
return 0, false
}
func imageHasAlpha(img image.Image) bool {
bounds := img.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
_, _, _, a := img.At(x, y).RGBA()
if a != 0xFFFF {
return true
}
}
}
return false
}
func imageIsGrayscale(img image.Image) bool {
bounds := img.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, _ := img.At(x, y).RGBA()
if r != g || r != b {
return false
}
}
}
return true
}