Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(avif): add support avif #372

Merged
merged 1 commit into from Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 6 additions & 7 deletions .github/workflows/go.yml
Expand Up @@ -12,7 +12,7 @@ on:

jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04

strategy:
matrix:
Expand All @@ -35,7 +35,7 @@ jobs:
GO111MODULE: on
run: |
sudo apt-get update
sudo apt-get install -y libwebp-dev
sudo apt-get install -y libaom-dev libwebp-dev
go mod download

- name: Test
Expand All @@ -49,8 +49,7 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}

- name: GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --snapshot
- name: Build
env:
GO111MODULE: on
run: make
51 changes: 6 additions & 45 deletions convert.go → decode.go
@@ -1,4 +1,4 @@
// Copyright (c) 2017 Yamagishi Kazutoshi <ykzts@desire.sh>
// Copyright (c) 2021 Yamagishi Kazutoshi <ykzts@desire.sh>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
Expand All @@ -21,20 +21,19 @@
package manael // import "manael.org/x/manael"

import (
"bytes"
"errors"
"image"
"io"

// register jpeg
_ "image/jpeg"
// register png
_ "image/png"
"io"

"github.com/harukasan/go-libwebp/webp"
)

func decode(src io.Reader) (image.Image, error) {
img, _, err := image.Decode(src)
// Decode returns an image.Image.
func Decode(r io.Reader) (image.Image, error) {
img, _, err := image.Decode(r)
if err != nil {
return nil, err
}
Expand All @@ -56,41 +55,3 @@ func decode(src io.Reader) (image.Image, error) {

return nil, errors.New("Not supported image format")
}

func encode(src image.Image) (*bytes.Buffer, error) {
config, err := webp.ConfigPreset(webp.PresetDefault, 90)
if err != nil {
return nil, err
}

buf := bytes.NewBuffer(nil)
switch img := src.(type) {
case *image.Gray:
err = webp.EncodeGray(buf, img, config)
if err != nil {
return nil, err
}
case *image.RGBA, *image.NRGBA:
err = webp.EncodeRGBA(buf, img, config)
if err != nil {
return nil, err
}
}

return buf, nil
}

// Convert returns a image buffer converted to webp
func Convert(src io.Reader) (*bytes.Buffer, error) {
img, err := decode(src)
if err != nil {
return nil, err
}

buf, err := encode(img)
if err != nil {
return nil, err
}

return buf, nil
}
70 changes: 36 additions & 34 deletions convert_test.go → encode.go
@@ -1,4 +1,4 @@
// Copyright (c) 2018 Yamagishi Kazutoshi <ykzts@desire.sh>
// Copyright (c) 2021 Yamagishi Kazutoshi <ykzts@desire.sh>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
Expand All @@ -18,51 +18,53 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

package manael_test // import "manael.org/x/manael"
package manael // import "manael.org/x/manael"

import (
"os"
"testing"
"errors"
"image"
"io"

"manael.org/x/manael"
"manael.org/x/manael/internal/testutil"
"github.com/Kagami/go-avif"
"github.com/harukasan/go-libwebp/webp"
)

var convertTests = []struct {
name string
format string
}{
{
"testdata/logo.png",
"webp",
},
{
"testdata/gray.png",
"webp",
},
{
"testdata/photo.jpeg",
"webp",
},
}
// Encode writes the Image m to w in the format specified by Content-Type t.
func Encode(w io.Writer, m image.Image, t string) error {
switch t {
case "image/avif":
opts := avif.Options{
Quality: 20,
Speed: 8,
}

func TestConvert(t *testing.T) {
for _, tc := range convertTests {
f, err := os.Open(tc.name)
err := avif.Encode(w, m, &opts)
if err != nil {
t.Fatal(err)
return err
}
defer f.Close()

img, err := manael.Convert(f)
return nil
case "image/webp":
c, err := webp.ConfigPreset(webp.PresetDefault, 90)
if err != nil {
t.Fatal(err)
return err
}

format := testutil.DetectFormat(img)

if got, want := format, tc.format; got != want {
t.Errorf("Image format is %s, want %s", got, want)
switch img := m.(type) {
case *image.Gray:
err = webp.EncodeGray(w, img, c)
if err != nil {
return err
}
case *image.RGBA, *image.NRGBA:
err = webp.EncodeRGBA(w, img, c)
if err != nil {
return err
}
}

return nil
}

return errors.New("Not supported image type")
}
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -3,6 +3,7 @@ module manael.org/x/manael
go 1.12

require (
github.com/Kagami/go-avif v0.1.0
github.com/gorilla/handlers v1.5.1
github.com/harukasan/go-libwebp v0.0.0-20190703060927-68562c9c99af
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
Expand Down
2 changes: 2 additions & 0 deletions go.sum
@@ -1,3 +1,5 @@
github.com/Kagami/go-avif v0.1.0 h1:8GHAGLxCdFfhpd4Zg8j1EqO7rtcQNenxIDerC/uu68w=
github.com/Kagami/go-avif v0.1.0/go.mod h1:OPmPqzNdQq3+sXm0HqaUJQ9W/4k+Elbc3RSfJUemDKA=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
Expand Down
93 changes: 60 additions & 33 deletions transport.go
Expand Up @@ -71,77 +71,104 @@ func (t *Transport) makeRequest(r *http.Request) (*http.Request, error) {
return r2, nil
}

func shouldEncodeToWebP(resp *http.Response) bool {
if s := resp.Header.Get("Cache-Control"); s != "" {
func scanAcceptHeader(r *http.Request) string {
accepts := r.Header.Get("Accept")

for _, v := range strings.Split(accepts, ",") {
t := strings.TrimSpace(v)

if strings.HasPrefix(t, "image/avif") {
return "image/avif"
} else if strings.HasPrefix(t, "image/webp") {
return "image/webp"
}
}

return "*/*"
}

func check(w *http.Response, r *http.Request) string {
if r.Method != "GET" {
return "*/*"
}

if w.StatusCode != http.StatusOK && w.StatusCode != http.StatusNotModified {
return "*/*"
}

if s := w.Header.Get("Cache-Control"); s != "" {
for _, v := range strings.Split(s, ",") {
if strings.TrimSpace(v) == "no-transform" {
return false
return "*/*"
}
}
}

if !(resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotModified) {
return false
t := w.Header.Get("Content-Type")

if t != "image/jpeg" && t != "image/png" {
return "*/*"
}

contentType := resp.Header.Get("Content-Type")
return contentType == "image/jpeg" || contentType == "image/png"
return scanAcceptHeader(r)
}

func canDecodeWebP(r *http.Request) bool {
accepts := r.Header.Get("Accept")
func convert(src io.Reader, t string) (*bytes.Buffer, error) {
img, err := Decode(src)
if err != nil {
return nil, err
}

for _, v := range strings.Split(accepts, ",") {
t := strings.TrimSpace(v)
if strings.HasPrefix(t, "image/webp") {
return true
}
buf := bytes.NewBuffer(nil)

err = Encode(buf, img, t)
if err != nil {
return nil, err
}

return false
return buf, nil
}

// RoundTrip responds to an converted image.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
req2, err := t.makeRequest(req)
r, err := t.makeRequest(req)
if err != nil {
return nil, err
}

resp, err := t.Base.RoundTrip(req2)
w, err := t.Base.RoundTrip(r)
if err != nil {
return nil, err
}

if !(shouldEncodeToWebP(resp) && canDecodeWebP(req2)) {
return resp, nil
typ := check(w, r)
if typ == "*/*" {
return w, nil
}

defer resp.Body.Close()

var body io.Reader = resp.Body
defer w.Body.Close()

p := bytes.NewBuffer(nil)
r := io.TeeReader(body, p)
b := io.TeeReader(w.Body, p)

buf, err := Convert(r)
buf, err := convert(b, typ)
if err != nil {
body = io.MultiReader(p, body)
body := io.MultiReader(p, w.Body)

resp.Body = ioutil.NopCloser(body)
w.Body = ioutil.NopCloser(body)
log.Printf("error: %v\n", err)

return resp, nil
return w, nil
}

resp.Body = ioutil.NopCloser(buf)
w.Body = ioutil.NopCloser(buf)

resp.Header.Set("Content-Type", "image/webp")
resp.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
w.Header.Set("Content-Type", typ)
w.Header.Set("Content-Length", strconv.Itoa(buf.Len()))

if resp.Header.Get("Accept-Ranges") != "" {
resp.Header.Del("Accept-Ranges")
if w.Header.Get("Accept-Ranges") != "" {
w.Header.Del("Accept-Ranges")
}

return resp, nil
return w, nil
}