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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,4 +2,4 @@ test.*
|
||||
encoded.*
|
||||
decoded.*
|
||||
ffwebp.exe
|
||||
ffwebp
|
||||
/ffwebp
|
@@ -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
163
cmd/ffwebp/pattern.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user