diff options
author | Mike Crute <mike@crute.us> | 2023-10-18 12:41:57 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2023-10-18 12:41:57 -0700 |
commit | e8178ad95305a3096cef410626164559693052f6 (patch) | |
tree | 5c9d915d2a1c679511b4ad00db37c378a0edef6d | |
parent | 552663ed296f4ab0b3902f296efc4900eba1a060 (diff) | |
download | golib-e8178ad95305a3096cef410626164559693052f6.tar.bz2 golib-e8178ad95305a3096cef410626164559693052f6.tar.xz golib-e8178ad95305a3096cef410626164559693052f6.zip |
image: add JPEG quality and web clientv0.8.0
-rw-r--r-- | image/jpeg/quality.go | 158 | ||||
-rw-r--r-- | image/webp/client.go | 117 |
2 files changed, 275 insertions, 0 deletions
diff --git a/image/jpeg/quality.go b/image/jpeg/quality.go new file mode 100644 index 0000000..674f0b8 --- /dev/null +++ b/image/jpeg/quality.go | |||
@@ -0,0 +1,158 @@ | |||
1 | package jpeg | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "io" | ||
6 | ) | ||
7 | |||
8 | var defaultTables = [][]int{ | ||
9 | { // Luminance | ||
10 | 16, 11, 12, 14, 12, 10, 16, 14, | ||
11 | 13, 14, 18, 17, 16, 19, 24, 40, | ||
12 | 26, 24, 22, 22, 24, 49, 35, 37, | ||
13 | 29, 40, 58, 51, 61, 60, 57, 51, | ||
14 | 56, 55, 64, 72, 92, 78, 64, 68, | ||
15 | 87, 69, 55, 56, 80, 109, 81, 87, | ||
16 | 95, 98, 103, 104, 103, 62, 77, 113, | ||
17 | 121, 112, 100, 120, 92, 101, 103, 99, | ||
18 | }, | ||
19 | { // Chrominance | ||
20 | 17, 18, 18, 24, 21, 24, 47, 26, | ||
21 | 26, 47, 99, 66, 56, 66, 99, 99, | ||
22 | 99, 99, 99, 99, 99, 99, 99, 99, | ||
23 | 99, 99, 99, 99, 99, 99, 99, 99, | ||
24 | 99, 99, 99, 99, 99, 99, 99, 99, | ||
25 | 99, 99, 99, 99, 99, 99, 99, 99, | ||
26 | 99, 99, 99, 99, 99, 99, 99, 99, | ||
27 | 99, 99, 99, 99, 99, 99, 99, 99, | ||
28 | }, | ||
29 | } | ||
30 | |||
31 | func read2Bytes(r io.Reader) ([]byte, error) { | ||
32 | b := make([]byte, 2) | ||
33 | if _, err := r.Read(b); err != nil { | ||
34 | return nil, err | ||
35 | } | ||
36 | return b, nil | ||
37 | } | ||
38 | |||
39 | // ReadJPEGQuality returns the JPEG quality estimate. | ||
40 | // | ||
41 | // Cleaned up version of github.com/liut/jpegquality | ||
42 | func ReadJPEGQuality(r io.ReadSeeker) (int, error) { | ||
43 | sign, err := read2Bytes(r) | ||
44 | if err != nil { | ||
45 | return 0, err | ||
46 | } | ||
47 | |||
48 | if sign[0] != 0xff && sign[1] != 0xd8 { | ||
49 | return 0, fmt.Errorf("invalid jpeg header") | ||
50 | } | ||
51 | |||
52 | for { | ||
53 | mark, err := read2Bytes(r) | ||
54 | if err != nil { | ||
55 | return 0, err | ||
56 | } | ||
57 | |||
58 | if mark[0] != 0xff || mark[1] == 0xff || mark[1] == 0x00 { | ||
59 | // Would also be valid to just keep re-reading to find the marker | ||
60 | return 0, fmt.Errorf("invalid marker") | ||
61 | } | ||
62 | |||
63 | marker := int(mark[0])<<8 + int(mark[1]) | ||
64 | if marker == 0 { | ||
65 | return 0, fmt.Errorf("invalid jpeg header") | ||
66 | } | ||
67 | |||
68 | sign, err := read2Bytes(r) | ||
69 | if err != nil { | ||
70 | return 0, err | ||
71 | } | ||
72 | |||
73 | length := int(sign[0])<<8 + int(sign[1]) - 2 | ||
74 | if length < 0 { | ||
75 | return 0, fmt.Errorf("short segment read") | ||
76 | } | ||
77 | |||
78 | if (marker & 0xff) != 0xdb { // not a quantization table | ||
79 | if _, err := r.Seek(int64(length), 1); err != nil { | ||
80 | return 0, err | ||
81 | } | ||
82 | continue | ||
83 | } | ||
84 | |||
85 | if length%65 != 0 { | ||
86 | return 0, fmt.Errorf("wrong size for quantization table") | ||
87 | } | ||
88 | |||
89 | tabuf := make([]byte, length) | ||
90 | n, err := r.Read(tabuf) | ||
91 | if err != nil { | ||
92 | return 0, err | ||
93 | } | ||
94 | tabuf = tabuf[0:n] | ||
95 | |||
96 | allones := 1 | ||
97 | |||
98 | var reftable []int | ||
99 | var cumsf, cumsf2 float64 | ||
100 | |||
101 | for a := 0; a < n; { | ||
102 | tableindex := int(tabuf[a] & 0x0f) | ||
103 | a++ | ||
104 | |||
105 | if tableindex < 2 { | ||
106 | reftable = defaultTables[tableindex] | ||
107 | } | ||
108 | |||
109 | // Read in the table, compute statistics relative to reference table | ||
110 | if a+64 > n { | ||
111 | return 0, fmt.Errorf("DQT segment too short") | ||
112 | } | ||
113 | |||
114 | for coefindex := 0; coefindex < 64 && a < n; coefindex++ { | ||
115 | var val int | ||
116 | |||
117 | if tableindex>>4 != 0 { | ||
118 | temp := int(tabuf[a]) | ||
119 | a++ | ||
120 | temp *= 256 | ||
121 | val = int(tabuf[a]) + temp | ||
122 | a++ | ||
123 | } else { | ||
124 | val = int(tabuf[a]) | ||
125 | a++ | ||
126 | } | ||
127 | |||
128 | // scaling factor in percent | ||
129 | x := 100.0 * float64(val) / float64(reftable[coefindex]) | ||
130 | cumsf += x | ||
131 | cumsf2 += x * x | ||
132 | |||
133 | // separate check for all-ones table (Q 100) | ||
134 | if val != 1 { | ||
135 | allones = 0 | ||
136 | } | ||
137 | } | ||
138 | |||
139 | if 0 != len(reftable) { // terse output includes quality | ||
140 | var qual float64 | ||
141 | cumsf /= 64.0 // mean scale factor | ||
142 | cumsf2 /= 64.0 | ||
143 | |||
144 | if allones == 1 { // special case for all-ones table | ||
145 | qual = 100.0 | ||
146 | } else if cumsf <= 100.0 { | ||
147 | qual = (200.0 - cumsf) / 2.0 | ||
148 | } else { | ||
149 | qual = 5000.0 / cumsf | ||
150 | } | ||
151 | |||
152 | if tableindex == 0 { | ||
153 | return (int)(qual + 0.5), nil | ||
154 | } | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | } | ||
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 | } | ||