diff options
Diffstat (limited to 'image/webp/client.go')
-rw-r--r-- | image/webp/client.go | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/image/webp/client.go b/image/webp/client.go new file mode 100644 index 0000000..6b08b72 --- /dev/null +++ b/image/webp/client.go | |||
@@ -0,0 +1,117 @@ | |||
1 | package webp | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "context" | ||
6 | "fmt" | ||
7 | "image" | ||
8 | "io" | ||
9 | "net" | ||
10 | "net/http" | ||
11 | "os" | ||
12 | "os/exec" | ||
13 | "path" | ||
14 | "strconv" | ||
15 | "syscall" | ||
16 | |||
17 | "golang.org/x/image/tiff" | ||
18 | ) | ||
19 | |||
20 | // WebPConvertClient starts a webp conversion server and manages | ||
21 | // its lifecycle. Call Convert to convert an image.Image to a webp | ||
22 | // bytestream. This struct is only threadsafe after Start has been | ||
23 | // called. | ||
24 | type WebPConvertClient struct { | ||
25 | p *exec.Cmd | ||
26 | c *http.Client | ||
27 | } | ||
28 | |||
29 | // Start starts the server, cancelling the passed context will stop | ||
30 | // the server process. It is important to pass a real context into the | ||
31 | // process and not just context.Background. This function will return an | ||
32 | // error if setup fails but does not block. | ||
33 | func (c *WebPConvertClient) Start(ctx context.Context) error { | ||
34 | sockDir, err := os.MkdirTemp("", "webp-*") | ||
35 | if err != nil { | ||
36 | return err | ||
37 | } | ||
38 | |||
39 | sockPath := path.Join(sockDir, "webp.sock") | ||
40 | |||
41 | c.c = &http.Client{ | ||
42 | Transport: &http.Transport{ | ||
43 | DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { | ||
44 | var d net.Dialer | ||
45 | return d.DialContext(ctx, "unix", sockPath) | ||
46 | }, | ||
47 | }, | ||
48 | } | ||
49 | |||
50 | c.p = exec.CommandContext(ctx, "webp-convert-server", sockPath) | ||
51 | if c.p.Err != nil { | ||
52 | return c.p.Err | ||
53 | } | ||
54 | |||
55 | c.p.Cancel = func() error { | ||
56 | if err := c.p.Process.Signal(syscall.SIGTERM); err != nil { | ||
57 | return err | ||
58 | } | ||
59 | return os.ErrProcessDone | ||
60 | } | ||
61 | |||
62 | ec := make(chan error, 1) | ||
63 | go func() { | ||
64 | ec <- c.p.Run() | ||
65 | os.RemoveAll(sockDir) | ||
66 | }() | ||
67 | |||
68 | select { | ||
69 | case err = <-ec: | ||
70 | return err | ||
71 | default: | ||
72 | return nil | ||
73 | } | ||
74 | } | ||
75 | |||
76 | // Convert encodes an image as TIFF and sends it to the conversion | ||
77 | // server. It returns either an error or the webp encoded bytes from the | ||
78 | // input image. | ||
79 | func (c *WebPConvertClient) Convert(ctx context.Context, img image.Image, quality int, lossless, exact bool) ([]byte, error) { | ||
80 | if c.c == nil { | ||
81 | return nil, fmt.Errorf("WebPConvertClient has not been started") | ||
82 | } | ||
83 | |||
84 | bb := &bytes.Buffer{} | ||
85 | if err := tiff.Encode(bb, img, &tiff.Options{Compression: tiff.Uncompressed}); err != nil { | ||
86 | return nil, err | ||
87 | } | ||
88 | |||
89 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://unix/", bb) | ||
90 | if err != nil { | ||
91 | return nil, err | ||
92 | } | ||
93 | req.Header.Add("quality", strconv.Itoa(quality)) | ||
94 | req.Header.Add("lossless", fmt.Sprintf("%t", lossless)) | ||
95 | req.Header.Add("exact", fmt.Sprintf("%t", exact)) | ||
96 | |||
97 | res, err := c.c.Do(req) | ||
98 | if err != nil { | ||
99 | return nil, err | ||
100 | } | ||
101 | defer res.Body.Close() | ||
102 | |||
103 | if res.StatusCode != 200 { | ||
104 | e, err := io.ReadAll(res.Body) | ||
105 | if err != nil { | ||
106 | return nil, err | ||
107 | } | ||
108 | return nil, fmt.Errorf("WebPConvertClient server error: %s", e) | ||
109 | } | ||
110 | |||
111 | bb.Reset() | ||
112 | if _, err := io.Copy(bb, res.Body); err != nil { | ||
113 | return nil, err | ||
114 | } | ||
115 | |||
116 | return bb.Bytes(), nil | ||
117 | } | ||