1
0
mirror of https://github.com/coalaura/ffwebp.git synced 2025-09-08 13:59:54 +00:00
This commit is contained in:
2025-08-12 02:38:01 +02:00
parent e092ac7bb1
commit d3d23b1dc4
9 changed files with 259 additions and 4 deletions

View File

@@ -6,7 +6,7 @@ FFWebP is a small, single-binary CLI for converting images between formats, thin
- Single binary: no external tools required
- Auto-detects input codec and infers output from the file extension
- Supports AVIF, BMP, GIF, ICO, JPEG, JPEG XL, PCX, PNG, PNM (PBM/PGM/PPM/PAM), PSD (decode-only), QOI, TGA, TIFF, WebP, and XBM
- Supports AVIF, BMP, GIF, ICO, JPEG, JPEG XL, PCX, PNG, PNM (PBM/PGM/PPM/PAM), PSD (decode-only), QOI, SVG (decode-only), TGA, TIFF, WebP, and XBM
- Lossy or lossless output with configurable quality
- Thumbnail generation via Lanczos3 resampling
- Per-codec flags for fine-grained control (see `ffwebp --help`)

4
go.mod
View File

@@ -17,6 +17,8 @@ require (
github.com/samuel/go-pcx v0.0.0-20210515040514-6a5ce4d132f7
github.com/sergeymakinen/go-ico v1.0.0-beta.0
github.com/spakin/netpbm v1.3.2
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
github.com/xyproto/xbm v1.0.0
golang.org/x/image v0.18.0
)
@@ -26,4 +28,6 @@ require (
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
golang.org/x/text v0.16.0 // indirect
)

8
go.sum
View File

@@ -33,6 +33,10 @@ github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3
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/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
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=
@@ -43,6 +47,10 @@ github.com/xyproto/xbm v1.0.0 h1:R5/A2+yhyy4V2c626bdIVfS1UNwE3efhiEBuKXU6u3A=
github.com/xyproto/xbm v1.0.0/go.mod h1:m2xrjsNmxuzBrx6gs4rSgxgpJWXS01j9KxYgjxhcnoc=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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

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

View File

@@ -101,7 +101,7 @@ func (impl) Decode(reader io.Reader) (image.Image, error) {
}
func (impl) Encode(writer io.Writer, img image.Image, options opts.Common) error {
logx.Printf("avif: quality=%d, quality-alpha=%d, speed=%d, chroma=%d\n", options.Quality, qualityA, speed, chroma)
logx.Printf("avif: quality=%d quality-alpha=%d speed=%d chroma=%d\n", options.Quality, qualityA, speed, chroma)
return avif.Encode(writer, img, avif.Options{
Quality: options.Quality,

View File

@@ -40,7 +40,7 @@ func (impl) Flags(flags []cli.Flag) []cli.Flag {
return append(flags,
&cli.IntFlag{
Name: "jpegxl.effort",
Usage: "JPEG XL: encode effort (1=fast .. 10=slow). Default 7",
Usage: "JPEGXL: encode effort (1=fast .. 10=slow). Default 7",
Value: 7,
Destination: &effort,
Validator: func(value int) error {

View File

@@ -64,7 +64,7 @@ func (impl) Sniff(reader io.ReaderAt) (int, []byte, error) {
}
func (impl) Decode(reader io.Reader) (image.Image, error) {
logx.Printf("psd: skipMerged=%t\n", skipMerged)
logx.Printf("psd: skip-merged=%t\n", skipMerged)
img, _, err := psd.Decode(reader, &psd.DecodeOptions{
SkipMergedImage: skipMerged,

163
internal/codec/svg/svg.go Normal file
View File

@@ -0,0 +1,163 @@
package svg
import (
"errors"
"fmt"
"image"
"image/draw"
"io"
"strings"
"github.com/coalaura/ffwebp/internal/codec"
"github.com/coalaura/ffwebp/internal/logx"
"github.com/coalaura/ffwebp/internal/opts"
"github.com/srwiley/oksvg"
"github.com/srwiley/rasterx"
"github.com/urfave/cli/v3"
)
var (
svgWidth int
svgHeight int
svgBackground string
)
func init() {
codec.Register(impl{})
}
type impl struct{}
func (impl) String() string {
return "svg"
}
func (impl) Extensions() []string {
return []string{"svg"}
}
func (impl) CanEncode() bool {
return false
}
func (impl) Flags(flags []cli.Flag) []cli.Flag {
return append(flags,
&cli.IntFlag{
Name: "svg.width",
Usage: "SVG: output width in pixels (0 = auto)",
Value: 0,
Destination: &svgWidth,
},
&cli.IntFlag{
Name: "svg.height",
Usage: "SVG: output height in pixels (0 = auto)",
Value: 0,
Destination: &svgHeight,
},
&cli.StringFlag{
Name: "svg.background",
Usage: "SVG: background color",
Value: "",
Destination: &svgBackground,
},
)
}
func (impl) Sniff(reader io.ReaderAt) (int, []byte, error) {
buf := make([]byte, 128)
n, err := reader.ReadAt(buf, 0)
if err != nil && err != io.EOF {
return 0, nil, err
}
buf = buf[:n]
if strings.Contains(strings.ToLower(string(buf)), "<svg") {
sniff := buf
if len(sniff) > 64 {
sniff = sniff[:64]
}
return 80, sniff, nil
}
return 0, nil, nil
}
func (impl) Decode(r io.Reader) (image.Image, error) {
icon, err := oksvg.ReadIconStream(r)
if err != nil {
return nil, err
}
vbw := int(icon.ViewBox.W)
vbh := int(icon.ViewBox.H)
var w, h int
switch {
case svgWidth > 0 && svgHeight > 0:
w, h = svgWidth, svgHeight
case svgWidth > 0 && svgHeight == 0:
w = svgWidth
if vbw > 0 && vbh > 0 {
h = int(float64(svgWidth) * float64(vbh) / float64(vbw))
} else {
h = svgWidth
}
case svgHeight > 0 && svgWidth == 0:
h = svgHeight
if vbw > 0 && vbh > 0 {
w = int(float64(svgHeight) * float64(vbw) / float64(vbh))
} else {
w = svgHeight
}
default:
if vbw > 0 && vbh > 0 {
w, h = vbw, vbh
} else {
w, h = 256, 256
}
}
if w <= 0 {
w = 256
}
if h <= 0 {
h = 256
}
logx.Printf("svg: size=%dx%d background=%s\n", w, h, svgBackground)
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
if svgBackground != "" && !strings.EqualFold(svgBackground, "transparent") {
bgc, err := oksvg.ParseSVGColor(svgBackground)
if err != nil {
return nil, err
} else if bgc == nil {
return nil, fmt.Errorf("invalid svg.background: %s", svgBackground)
}
draw.Draw(rgba, rgba.Bounds(), &image.Uniform{bgc}, image.Point{}, draw.Src)
}
icon.SetTarget(0, 0, float64(w), float64(h))
scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())
d := rasterx.NewDasher(w, h, scanner)
icon.Draw(d, 1.0)
return rgba, nil
}
func (impl) Encode(w io.Writer, img image.Image, options opts.Common) error {
return errors.New("svg: encoding not supported")
}

72
test/image.svg Normal file
View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg id="svg2" width="620" height="472" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs id="defs4">
<path id="box1" d="m0 0h77v210h-77z" stroke="#000" stroke-width="2"/>
<path id="box2" d="m0 0h77v60h-77z" stroke="#000" stroke-width="2"/>
</defs>
<path id="bg" d="m0 0h620v472h-620z" fill="#fff"/>
<g id="g9" transform="translate(2 1)">
<use id="use11" fill="#fff" xlink:href="#box1"/>
<use id="use13" x="77" fill="#ff0" xlink:href="#box1"/>
<use id="use15" x="154" fill="#0ff" xlink:href="#box1"/>
<use id="use17" x="231" fill="#0f0" xlink:href="#box1"/>
<use id="use19" x="308" fill="#f0f" xlink:href="#box1"/>
<use id="use21" x="385" fill="red" xlink:href="#box1"/>
<use id="use23" x="462" fill="#00f" xlink:href="#box1"/>
<use id="use25" x="539" xlink:href="#box1"/>
</g>
<g id="g45" transform="translate(2 220)">
<use id="use47" fill="#0f0" xlink:href="#box2"/>
<use id="use49" x="77" fill="#0f0" xlink:href="#box2"/>
<use id="use51" x="154" fill="#0f0" xlink:href="#box2"/>
<use id="use53" x="231" fill="#0f0" xlink:href="#box2"/>
<use id="use55" x="308" fill="#fff" xlink:href="#box2"/>
<use id="use57" x="385" fill="#fff" xlink:href="#box2"/>
<use id="use59" x="462" fill="#fff" xlink:href="#box2"/>
<use id="use61" x="539" fill="#fff" xlink:href="#box2"/>
<text id="green100" x="30" y="35" fill="#fff" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">0.59</text>
</g>
<g id="g27" transform="translate(2 280)">
<use id="use29" fill="red" xlink:href="#box2"/>
<use id="use31" x="77" fill="red" xlink:href="#box2"/>
<use id="use33" x="154" fill="#fff" xlink:href="#box2"/>
<use id="use35" x="231" fill="#fff" xlink:href="#box2"/>
<use id="use37" x="308" fill="red" xlink:href="#box2"/>
<use id="use39" x="385" fill="red" xlink:href="#box2"/>
<use id="use41" x="462" fill="#fff" xlink:href="#box2"/>
<use id="use43" x="539" fill="#fff" xlink:href="#box2"/>
<text id="red100" x="20" y="35" fill="#fff" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">+0.30</text>
</g>
<g id="g63" transform="translate(2 340)">
<use id="use65" fill="#00f" xlink:href="#box2"/>
<use id="use67" x="77" fill="#fff" xlink:href="#box2"/>
<use id="use69" x="154" fill="#00f" xlink:href="#box2"/>
<use id="use71" x="231" fill="#fff" xlink:href="#box2"/>
<use id="use73" x="308" fill="#00f" xlink:href="#box2"/>
<use id="use75" x="385" fill="#fff" xlink:href="#box2"/>
<use id="use77" x="462" fill="#00f" xlink:href="#box2"/>
<use id="use79" x="539" fill="#fff" xlink:href="#box2"/>
<text id="blue100" x="20" y="35" fill="#fff" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">+0.11</text>
</g>
<g id="g63" transform="translate(2 410)">
<use id="grey100" fill="#fff" xlink:href="#box2"/>
<use id="grey89" x="77" fill="#e3e3e3" xlink:href="#box2"/>
<use id="grey70" x="154" fill="#b2b2b2" xlink:href="#box2"/>
<use id="grey59" x="231" fill="#969696" xlink:href="#box2"/>
<use id="grey41" x="308" fill="#696969" xlink:href="#box2"/>
<use id="grey30" x="385" fill="#4d4d4d" xlink:href="#box2"/>
<use id="grey11" x="462" fill="#1c1c1c" xlink:href="#box2"/>
<use id="grey0" x="539" fill="#000" xlink:href="#box2"/>
<text id="txgrey100" x="20" y="35" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">100%</text>
<text id="txgrey89" x="102" y="35" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">89%</text>
<text id="txgrey70" x="179" y="35" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">70%</text>
<text id="txgrey59" x="256" y="35" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">59%</text>
<text id="txgrey41" x="333" y="35" fill="#fff" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">41%</text>
<text id="txgrey30" x="408" y="35" fill="#fff" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">30%</text>
<text id="txgrey11" x="487" y="35" fill="#fff" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">11%</text>
<text id="txgrey0" x="569" y="35" fill="#fff" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="1" xml:space="preserve">0%</text>
</g>
<text id="text3446-0" x="90" y="184" fill="#fff" font-size="180" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="4" xml:space="preserve">TEST</text>
<text id="text3446" x="80" y="174" font-family="DejaVu Sans, Arial, Helvetica" stroke-width="4" xml:space="preserve"><tspan id="tspan3448" x="80" y="174" font-size="180">TEST</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB