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: added support for avif and "format" query arg to transform output format #232

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 97 additions & 18 deletions controller/get_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"io"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -45,6 +47,76 @@ func getQueryFloat(ctx *gin.Context, param string) (float64, *APIError) {
return x, nil
}

func mimeTypeToImageType(mimeType string) (image.ImageType, *APIError) {
switch mimeType {
case "image/webp":
return image.ImageTypeWEBP, nil
case "image/png":
return image.ImageTypePNG, nil
case "image/jpeg":
return image.ImageTypeJPEG, nil
case "image/avif":
return image.ImageTypeAVIF, nil
default:
return 0, BadDataError(
fmt.Errorf( //nolint: goerr113
"image manipulation features are not supported for '%s'", mimeType,
),
fmt.Sprintf("image manipulation features are not supported for '%s'", mimeType),
)
}
}

func chooseImageFormat( //nolint: cyclop
ctx *gin.Context,
mimeType string,
) (image.ImageType, image.ImageType, *APIError) {
format, found := ctx.GetQuery("f")
if !found {
format = "same"
}

originalFormat, err := mimeTypeToImageType(mimeType)
if err != nil {
return 0, 0, err
}

switch format {
case "", "same":
return originalFormat, originalFormat, nil
case "webp":
return originalFormat, image.ImageTypeWEBP, nil
case "png":
return originalFormat, image.ImageTypePNG, nil
case "jpeg":
return originalFormat, image.ImageTypeJPEG, nil
case "avif":
return originalFormat, image.ImageTypeAVIF, nil
case "auto":
acceptHeaders := ctx.Request.Header.Values("Accept")
for _, acceptHeader := range acceptHeaders {
acceptedTypes := strings.Split(acceptHeader, ",")
switch {
case slices.Contains(acceptedTypes, "image/avif"):
return originalFormat, image.ImageTypeAVIF, nil
case slices.Contains(acceptedTypes, "image/webp"):
return originalFormat, image.ImageTypeWEBP, nil
case slices.Contains(acceptedTypes, "image/jpeg"):
return originalFormat, image.ImageTypeJPEG, nil
case slices.Contains(acceptedTypes, "image/png"):
return originalFormat, image.ImageTypePNG, nil
}
}
return originalFormat, originalFormat, nil
default:
return 0, 0, BadDataError(
//nolint: goerr113
fmt.Errorf("format must be one of: same, webp, png, jpeg, avif, auto. Got: %s", format),
"format must be one of: same, webp, png, jpeg, avif, auto. Got: "+format,
)
}
}

func getImageManipulationOptions(ctx *gin.Context, mimeType string) (image.Options, *APIError) {
w, err := getQueryInt(ctx, "w")
if err != nil {
Expand All @@ -65,28 +137,20 @@ func getImageManipulationOptions(ctx *gin.Context, mimeType string) (image.Optio
return image.Options{}, err
}

_, outpufFormatFound := ctx.GetQuery("f")

opts := image.Options{
Height: h,
Width: w,
Blur: b,
Quality: q,
}
if !opts.IsEmpty() {
switch mimeType {
case "image/webp":
opts.Format = image.ImageTypeWEBP
case "image/png":
opts.Format = image.ImageTypePNG
case "image/jpeg":
opts.Format = image.ImageTypeJPEG
default:
return image.Options{},
BadDataError(
fmt.Errorf( //nolint: goerr113
"image manipulation features are not supported for '%s'", mimeType,
),
fmt.Sprintf("image manipulation features are not supported for '%s'", mimeType),
)
if !opts.IsEmpty() || outpufFormatFound {
orig, format, err := chooseImageFormat(ctx, mimeType)
opts.Format = format
opts.OriginalFormat = orig
if err != nil {
return image.Options{}, err
}
}

Expand Down Expand Up @@ -129,6 +193,18 @@ func (ctrl *Controller) manipulateImage(
return NewP(buf.Bytes()), int64(buf.Len()), etag, nil
}

func getFileNameAndMimeType(fileMetadata FileMetadata, opts image.Options) (string, string) {
filename := fileMetadata.Name
mimeType := fileMetadata.MimeType

if opts.FormatChanged() {
filename = fmt.Sprintf("%s.%s", filename, opts.FileExtension())
mimeType = opts.FormatMimeType()
}

return filename, mimeType
}

type getFileFunc func() (*File, *APIError)

func (ctrl *Controller) processFileToDownload( //nolint: funlen
Expand Down Expand Up @@ -191,16 +267,19 @@ func (ctrl *Controller) processFileToDownload( //nolint: funlen
return nil, apiErr
}
}

filename, mimeType := getFileNameAndMimeType(fileMetadata, opts)

return NewFileResponse(
fileMetadata.ID,
fileMetadata.MimeType,
mimeType,
contentLength,
etag,
cacheControl,
updateAt,
statusCode,
body,
fileMetadata.Name,
filename,
download.ExtraHeaders,
), nil
}
Expand Down
2 changes: 1 addition & 1 deletion controller/get_file_with_presigned_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (ctrl *Controller) getFileWithPresignedURLParse(
signature := make(url.Values, len(ctx.Request.URL.Query()))
for k, v := range ctx.Request.URL.Query() {
switch k {
case "w", "h", "q", "b":
case "w", "h", "q", "b", "f":
default:
signature[k] = v
}
Expand Down
14 changes: 14 additions & 0 deletions controller/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,20 @@ paths:
in: query
schema:
type: number
- name: f
description: Format to output the image in. Only applies to images. If `same` the image will be returned in the same format as it was uploaded. If `auto` the server will choose the first match in the Accept header from the client following the order `avif`, `webp`, `jpeg`, `png`.
in: query
schema:
type: string
default: same
enum:
- auto
- same
- jpeg
- webp
- png
- avif

responses:
'200':
description: File information gathered successfully
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 59 additions & 14 deletions image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,53 @@ const (
ImageTypeJPEG ImageType = iota
ImageTypePNG
ImageTypeWEBP
ImageTypeAVIF
)

type Options struct {
Height int
Width int
Blur float64
Quality int
Format ImageType
Height int
Width int
Blur float64
Quality int
OriginalFormat ImageType
Format ImageType
}

func (o Options) IsEmpty() bool {
return o.Height == 0 && o.Width == 0 && o.Blur == 0 && o.Quality == 0
return o.Height == 0 && o.Width == 0 && o.Blur == 0 && o.Quality == 0 &&
o.OriginalFormat == o.Format
}

func (o Options) FormatChanged() bool {
return o.OriginalFormat != o.Format
}

func (o Options) FormatMimeType() string {
switch o.Format {
case ImageTypeJPEG:
return "image/jpeg"
case ImageTypePNG:
return "image/png"
case ImageTypeWEBP:
return "image/webp"
case ImageTypeAVIF:
return "image/avif"
}
return ""
}

func (o Options) FileExtension() string {
switch o.Format {
case ImageTypeJPEG:
return "jpeg"
case ImageTypePNG:
return "png"
case ImageTypeWEBP:
return "webp"
case ImageTypeAVIF:
return "avif"
}
return ""
}

type Transformer struct {
Expand Down Expand Up @@ -67,19 +102,29 @@ func (t *Transformer) Shutdown() {
vips.Shutdown()
}

func getExportParams(opts Options) *vips.ExportParams {
var ep *vips.ExportParams
func export(image *vips.ImageRef, opts Options) ([]byte, error) {
var b []byte
var err error

switch opts.Format {
case ImageTypeJPEG:
ep = vips.NewDefaultJPEGExportParams()
ep := vips.NewJpegExportParams()
ep.Quality = opts.Quality
b, _, err = image.ExportJpeg(ep)
case ImageTypePNG:
ep = vips.NewDefaultPNGExportParams()
ep := vips.NewPngExportParams()
b, _, err = image.ExportPng(ep)
case ImageTypeWEBP:
ep = vips.NewDefaultWEBPExportParams()
ep := vips.NewWebpExportParams()
ep.Quality = opts.Quality
b, _, err = image.ExportWebp(ep)
case ImageTypeAVIF:
ep := vips.NewAvifExportParams()
ep.Quality = opts.Quality
b, _, err = image.ExportAvif(ep)
}
ep.Quality = opts.Quality

return ep
return b, err //nolint: wrapcheck
}

func processImage(image *vips.ImageRef, opts Options) error {
Expand Down Expand Up @@ -145,7 +190,7 @@ func (t *Transformer) Run(
return err
}

b, _, err := image.Export(getExportParams(opts))
b, err := export(image, opts)
if err != nil {
return fmt.Errorf("failed to export: %w", err)
}
Expand Down
18 changes: 17 additions & 1 deletion image/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestManipulate(t *testing.T) {
{
name: "png",
filename: "testdata/nhost.png",
sum: "58dd9460342dba786d9f1029198213fcd881433f6feb553b933f38fcbfc0a5f2",
sum: "d538212aa74ad1d17261bc2126e60964e6d2dc1c7898ea3b9f9bd3b5bc94b380",
size: 68307,
options: image.Options{Width: 300, Height: 100, Blur: 2, Format: image.ImageTypePNG},
},
Expand All @@ -62,6 +62,20 @@ func TestManipulate(t *testing.T) {
size: 33399,
options: image.Options{Blur: 2, Format: image.ImageTypeJPEG},
},
{
name: "webp to avif",
filename: "testdata/nhost.webp",
sum: "af84174b76ef23f44c4064d7c1a53e7097ebdbff7dc7283931c87f93ccad4bf3",
size: 17784,
options: image.Options{Width: 300, Height: 100, Blur: 2, Format: image.ImageTypeAVIF},
},
{
name: "jpeg to avif, no image manipulation",
filename: "testdata/nhost.jpg",
sum: "67fd59ef17e59420d40c079e81debf308e3f293754240db7579c1b4873acb1d2",
size: 17784,
options: image.Options{Format: image.ImageTypeAVIF},
},
}

transformer := image.NewTransformer()
Expand All @@ -78,6 +92,8 @@ func TestManipulate(t *testing.T) {

hasher := sha256.New()
// f, _ := os.OpenFile("/tmp/nhost-test."+tc.name, os.O_WRONLY|os.O_CREATE, 0o644)
// defer f.Close()
// if err := transformer.Run(orig, tc.size, f, tc.options); err != nil {
if err := transformer.Run(orig, tc.size, hasher, tc.options); err != nil {
t.Fatal(err)
}
Expand Down
3 changes: 2 additions & 1 deletion nix/overlay.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final: prev: rec {
final.pango
final.libarchive
final.libhwy
final.libheif
];
mesonFlags = [
"-Dgtk_doc=false"
Expand All @@ -31,11 +32,11 @@ final: prev: rec {
"-Dlcms=disabled"
"-Dopenexr=disabled"
"-Dorc=disabled"
"-Dheif=disabled"
"-Djpeg-xl=disabled"
"-Dpoppler=disabled"
"-Drsvg=disabled"
"-Dpangocairo=disabled"
"-Dheif=enabled"
];

});
Expand Down
Loading