mirror of
https://github.com/coalaura/ffwebp.git
synced 2025-09-08 05:49:54 +00:00
pnm
This commit is contained in:
@@ -5,7 +5,7 @@ FFWebP is a command line utility for converting images between multiple formats.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Pure Go implementation with no external runtime dependencies
|
- 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
|
- Lossy or lossless output with configurable quality
|
||||||
- Output codec selected from the output file extension when `--codec` is omitted
|
- 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`)
|
- Full set of format-specific flags for every supported format (see `ffwebp --help`)
|
||||||
|
1
go.mod
1
go.mod
@@ -10,6 +10,7 @@ require (
|
|||||||
github.com/gen2brain/webp v0.5.5
|
github.com/gen2brain/webp v0.5.5
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0
|
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
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
|
||||||
)
|
)
|
||||||
|
|
||||||
|
2
go.sum
2
go.sum
@@ -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-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 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
|
||||||
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||||
|
8
internal/builtins/pnm.go
Normal file
8
internal/builtins/pnm.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build pnm || core || full
|
||||||
|
// +build pnm core full
|
||||||
|
|
||||||
|
package builtins
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/coalaura/ffwebp/internal/codec/pnm"
|
||||||
|
)
|
@@ -49,6 +49,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
|
|||||||
if v < 0 || v > 100 {
|
if v < 0 || v > 100 {
|
||||||
return fmt.Errorf("invalid avif.quality-alpha: %d", v)
|
return fmt.Errorf("invalid avif.quality-alpha: %d", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -61,6 +62,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
|
|||||||
if v < 0 || v > 10 {
|
if v < 0 || v > 10 {
|
||||||
return fmt.Errorf("invalid avif.speed: %d", v)
|
return fmt.Errorf("invalid avif.speed: %d", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -73,6 +75,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
|
|||||||
if v != 444 && v != 422 && v != 420 {
|
if v != 444 && v != 422 && v != 420 {
|
||||||
return fmt.Errorf("invalid avif.chroma: %d", v)
|
return fmt.Errorf("invalid avif.chroma: %d", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -47,6 +47,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
|
|||||||
if value < 1 || value > 10 {
|
if value < 1 || value > 10 {
|
||||||
return fmt.Errorf("invalid jpegxl.effort: %d", value)
|
return fmt.Errorf("invalid jpegxl.effort: %d", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
184
internal/codec/pnm/pnm.go
Normal file
184
internal/codec/pnm/pnm.go
Normal 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
|
||||||
|
}
|
Reference in New Issue
Block a user