aboutsummaryrefslogtreecommitdiff
path: root/image/webp/client.go
blob: 6b08b7233fc9cd11340b738d51a23c36378c990d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package webp

import (
	"bytes"
	"context"
	"fmt"
	"image"
	"io"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path"
	"strconv"
	"syscall"

	"golang.org/x/image/tiff"
)

// WebPConvertClient starts a webp conversion server and manages
// its lifecycle. Call Convert to convert an image.Image to a webp
// bytestream. This struct is only threadsafe after Start has been
// called.
type WebPConvertClient struct {
	p *exec.Cmd
	c *http.Client
}

// Start starts the server, cancelling the passed context will stop
// the server process. It is important to pass a real context into the
// process and not just context.Background. This function will return an
// error if setup fails but does not block.
func (c *WebPConvertClient) Start(ctx context.Context) error {
	sockDir, err := os.MkdirTemp("", "webp-*")
	if err != nil {
		return err
	}

	sockPath := path.Join(sockDir, "webp.sock")

	c.c = &http.Client{
		Transport: &http.Transport{
			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
				var d net.Dialer
				return d.DialContext(ctx, "unix", sockPath)
			},
		},
	}

	c.p = exec.CommandContext(ctx, "webp-convert-server", sockPath)
	if c.p.Err != nil {
		return c.p.Err
	}

	c.p.Cancel = func() error {
		if err := c.p.Process.Signal(syscall.SIGTERM); err != nil {
			return err
		}
		return os.ErrProcessDone
	}

	ec := make(chan error, 1)
	go func() {
		ec <- c.p.Run()
		os.RemoveAll(sockDir)
	}()

	select {
	case err = <-ec:
		return err
	default:
		return nil
	}
}

// Convert encodes an image as TIFF and sends it to the conversion
// server. It returns either an error or the webp encoded bytes from the
// input image.
func (c *WebPConvertClient) Convert(ctx context.Context, img image.Image, quality int, lossless, exact bool) ([]byte, error) {
	if c.c == nil {
		return nil, fmt.Errorf("WebPConvertClient has not been started")
	}

	bb := &bytes.Buffer{}
	if err := tiff.Encode(bb, img, &tiff.Options{Compression: tiff.Uncompressed}); err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://unix/", bb)
	if err != nil {
		return nil, err
	}
	req.Header.Add("quality", strconv.Itoa(quality))
	req.Header.Add("lossless", fmt.Sprintf("%t", lossless))
	req.Header.Add("exact", fmt.Sprintf("%t", exact))

	res, err := c.c.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	if res.StatusCode != 200 {
		e, err := io.ReadAll(res.Body)
		if err != nil {
			return nil, err
		}
		return nil, fmt.Errorf("WebPConvertClient server error: %s", e)
	}

	bb.Reset()
	if _, err := io.Copy(bb, res.Body); err != nil {
		return nil, err
	}

	return bb.Bytes(), nil
}