1
0
mirror of https://github.com/coalaura/ffwebp.git synced 2025-09-07 05:35:30 +00:00

multi input/output support

This commit is contained in:
Laura
2025-08-13 03:12:14 +02:00
parent 16c48dc046
commit 09a41d0e8e
3 changed files with 320 additions and 23 deletions

2
.gitignore vendored
View File

@@ -2,4 +2,4 @@ test.*
encoded.*
decoded.*
ffwebp.exe
ffwebp
/ffwebp

View File

@@ -2,9 +2,11 @@ package main
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
_ "github.com/coalaura/ffwebp/internal/builtins"
@@ -23,15 +25,20 @@ func main() {
&cli.StringFlag{
Name: "input",
Aliases: []string{"i"},
Usage: "input file (\"-\" = stdin)",
Usage: "input file or pattern (\"-\" = stdin, supports globs and %d sequences)",
Value: "-",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "output file (\"-\" = stdout)",
Usage: "output file, directory, or pattern (\"-\" = stdout)",
Value: "-",
},
&cli.IntFlag{
Name: "start-number",
Usage: "starting number for %%d output patterns",
Value: 1,
},
&cli.StringFlag{
Name: "codec",
Aliases: []string{"c"},
@@ -97,17 +104,113 @@ func run(_ context.Context, cmd *cli.Command) error {
banner()
var (
n int
input string
output string
common opts.Common
)
common.Quality = cmd.Int("quality")
common.Lossless = cmd.Bool("lossless")
common.FillDefaults()
input = cmd.String("input")
output = cmd.String("output")
var inputs []string
if input == "-" {
inputs = []string{"-"}
} else if hasSeq(input) {
files, err := expandSeq(input)
if err != nil {
return err
}
if len(files) == 0 {
return fmt.Errorf("no inputs match sequence: %s", filepath.ToSlash(input))
}
inputs = files
} else if isGlob(input) {
files, err := expandGlob(input)
if err != nil {
return err
}
if len(files) == 0 {
return fmt.Errorf("no inputs match glob: %s", filepath.ToSlash(input))
}
inputs = files
} else {
inputs = []string{input}
}
startNum := cmd.Int("start-number")
var outputs []string
if len(inputs) == 1 {
outputs = []string{output}
} else {
switch {
case output == "-":
return fmt.Errorf("multiple inputs require an output pattern or directory, not '-' ")
case hasSeq(output):
outs := make([]string, len(inputs))
for i := range inputs {
outs[i] = formatSeq(output, i, startNum)
}
outputs = outs
default:
outDir := output
if outDir == "" {
outDir = "."
}
if fi, err := os.Stat(outDir); err == nil {
if !fi.IsDir() {
return fmt.Errorf("output must be a directory or pattern when multiple inputs: %s", filepath.ToSlash(output))
}
} else {
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}
}
outs := make([]string, len(inputs))
for i := range inputs {
outs[i] = output
}
outputs = outs
}
}
for i := range inputs {
in := inputs[i]
out := outputs[i]
if err := processOne(in, out, cmd, &common); err != nil {
return err
}
}
return nil
}
func processOne(input, output string, cmd *cli.Command, common *opts.Common) error {
var (
reader io.Reader = os.Stdin
writer *countWriter = &countWriter{w: os.Stdout}
)
if input = cmd.String("input"); input != "-" {
if input != "-" {
logx.Printf("opening input file %q\n", filepath.ToSlash(input))
file, err := os.OpenFile(input, os.O_RDONLY, 0)
@@ -122,16 +225,60 @@ func run(_ context.Context, cmd *cli.Command) error {
logx.Printf("reading input from <stdin>\n")
}
sniffed, reader, err := codec.Sniff(reader, input, cmd.Bool("sniff"))
sniffed, reader2, err := codec.Sniff(reader, input, cmd.Bool("sniff"))
if err != nil {
return err
}
reader = reader2
logx.Printf("sniffed codec: %s (%q)\n", sniffed.Codec, sniffed)
if output = cmd.String("output"); output != "-" {
var mappedFromDir bool
if output != "-" {
if fi, err := os.Stat(output); err == nil && fi.IsDir() {
name := filepath.Base(input)
output = filepath.Join(output, name)
mappedFromDir = true
} else if strings.HasSuffix(output, string(os.PathSeparator)) {
if err := os.MkdirAll(output, 0755); err != nil {
return err
}
name := filepath.Base(input)
output = filepath.Join(output, name)
mappedFromDir = true
}
}
oCodec, oExt, err := codec.Detect(output, cmd.String("codec"))
if err != nil {
return err
}
common.OutputExtension = oExt
logx.Printf("output codec: %s (forced=%v)\n", oCodec, cmd.IsSet("codec"))
if output != "-" {
curExt := strings.TrimPrefix(filepath.Ext(output), ".")
if mappedFromDir || curExt == "" || curExt != oExt {
base := strings.TrimSuffix(output, filepath.Ext(output))
output = base + "." + oExt
}
}
if output != "-" {
logx.Printf("opening output file %q\n", filepath.ToSlash(output))
if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
return err
}
file, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
@@ -144,20 +291,6 @@ func run(_ context.Context, cmd *cli.Command) error {
logx.Printf("writing output to <stdout>\n")
}
common.Quality = cmd.Int("quality")
common.Lossless = cmd.Bool("lossless")
common.FillDefaults()
oCodec, oExt, err := codec.Detect(output, cmd.String("codec"))
if err != nil {
return err
}
common.OutputExtension = oExt
logx.Printf("output codec: %s (forced=%v)\n", oCodec, cmd.IsSet("codec"))
t0 := time.Now()
img, err := sniffed.Codec.Decode(reader)
@@ -177,6 +310,8 @@ func run(_ context.Context, cmd *cli.Command) error {
t1 := time.Now()
var n int
img, n, err = effects.ApplyAll(img)
if err != nil {
return err
@@ -186,8 +321,7 @@ func run(_ context.Context, cmd *cli.Command) error {
t2 := time.Now()
err = oCodec.Encode(writer, img, common)
if err != nil {
if err := oCodec.Encode(writer, img, *common); err != nil {
return err
}

163
cmd/ffwebp/pattern.go Normal file
View File

@@ -0,0 +1,163 @@
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
)
func hasSeq(s string) bool {
re := regexp.MustCompile(`%0?\d*d`)
return re.FindStringIndex(s) != nil
}
func seqSpec(s string) (start, end, width int, zeroPad bool, ok bool) {
re := regexp.MustCompile(`%0?(\d*)d`)
loc := re.FindStringSubmatchIndex(s)
if loc == nil {
return 0, 0, 0, false, false
}
start = loc[0]
end = loc[1]
if loc[2] != -1 && loc[3] != -1 {
wstr := s[loc[2]:loc[3]]
if wstr != "" {
if w, err := strconv.Atoi(wstr); err == nil && w > 0 {
width = w
}
}
}
zeroPad = width > 0
return start, end, width, zeroPad, true
}
func formatSeq(pattern string, idx, startNum int) string {
s, e, width, zero, ok := seqSpec(pattern)
if !ok {
return pattern
}
n := idx + startNum - 1
var num string
if width > 0 && zero {
num = fmt.Sprintf("%0*d", width, n)
} else {
num = fmt.Sprintf("%d", n)
}
return pattern[:s] + num + pattern[e:]
}
func isGlob(s string) bool {
for i := 0; i < len(s); i++ {
switch s[i] {
case '*', '?', '[':
return true
}
}
return false
}
func expandGlob(pat string) ([]string, error) {
matches, err := filepath.Glob(pat)
if err != nil {
return nil, err
}
sort.Strings(matches)
return matches, nil
}
func expandSeq(pat string) ([]string, error) {
dir := filepath.Dir(pat)
if dir == "." || dir == "" {
dir = ""
}
base := filepath.Base(pat)
s, e, width, _, ok := seqSpec(base)
if !ok {
return nil, nil
}
pre := regexp.QuoteMeta(base[:s])
suf := regexp.QuoteMeta(base[e:])
num := `\\d+`
if width > 0 {
num = fmt.Sprintf(`\\d{%d}`, width)
}
re := regexp.MustCompile("^" + pre + "(" + num + ")" + suf + "$")
scanDir := filepath.Dir(pat)
if scanDir == "." || scanDir == "" {
scanDir = "."
}
entries, err := os.ReadDir(scanDir)
if err != nil {
return nil, err
}
type match struct {
path string
n int
}
var out []match
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
m := re.FindStringSubmatch(name)
if m == nil {
continue
}
n, err := strconv.Atoi(m[1])
if err != nil {
continue
}
full := name
if dir != "" {
full = filepath.Join(filepath.Dir(pat), name)
}
out = append(out, match{path: full, n: n})
}
sort.Slice(out, func(i, j int) bool { return out[i].n < out[j].n })
result := make([]string, len(out))
for i := range out {
result[i] = out[i].path
}
return result, nil
}