aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2023-10-18 12:41:57 -0700
committerMike Crute <mike@crute.us>2023-10-18 12:41:57 -0700
commite8178ad95305a3096cef410626164559693052f6 (patch)
tree5c9d915d2a1c679511b4ad00db37c378a0edef6d
parent552663ed296f4ab0b3902f296efc4900eba1a060 (diff)
downloadgolib-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.go158
-rw-r--r--image/webp/client.go117
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 @@
1package jpeg
2
3import (
4 "fmt"
5 "io"
6)
7
8var 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
31func 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
42func 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 @@
1package webp
2
3import (
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.
24type 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.
33func (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.
79func (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}