diff --git a/.gitignore b/.gitignore index 064d9946..7206fb05 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,8 @@ coverage.txt /webp_server_go /metadata/* /.idea/inspectionProfiles/Project_Default.xml +/vips/ +/.idea/copilot.data.migration.agent.xml +/.idea/copilot.data.migration.ask.xml +/.idea/copilot.data.migration.ask2agent.xml +/.idea/copilot.data.migration.edit.xml diff --git a/Dockerfile b/Dockerfile index e54fb1e2..1a1455a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ COPY . /build RUN cd /build && sed -i "s|.\/pics|${IMG_PATH}|g" config.json \ && sed -i "s|\"\"|\"${EXHAUST_PATH}\"|g" config.json \ && sed -i 's/127.0.0.1/0.0.0.0/g' config.json \ + && make codegen \ && go build -ldflags="-s -w" -o webp-server . FROM debian:trixie-slim diff --git a/Makefile b/Makefile index 9b11f503..fb051ffc 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ endif default: make clean + make codegen go build -o builds/webp-server-$(OS)-$(ARCH) . ls builds @@ -41,3 +42,7 @@ clean: docker: DOCKER_BUILDKIT=1 docker build -t webpsh/webps . + +codegen: + go install github.com/cshum/vipsgen/cmd/vipsgen@latest + $$(go env GOPATH)/bin/vipsgen -out ./vips diff --git a/README.md b/README.md index 86d22753..a8676a13 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [Documentation](https://docs.webp.sh/) | [Website](https://webp.sh/) | [Blog](https://blog.webp.se/) -This is a Server based on Golang, which allows you to serve WebP images on the fly. +This is a Server based on Go, which allows you to serve WebP images on the fly. Currently supported image format: JPEG, PNG, BMP, GIF, SVG, HEIC, NEF, WEBP diff --git a/encoder/autorot_darwin.go b/encoder/autorot_darwin.go new file mode 100644 index 00000000..02e7c0df --- /dev/null +++ b/encoder/autorot_darwin.go @@ -0,0 +1,7 @@ +package encoder + +import "webp_server_go/vips" + +func autorot(img *vips.Image) error { + return img.Autorot() +} diff --git a/encoder/autorot_linux.go b/encoder/autorot_linux.go new file mode 100644 index 00000000..d9c08cc7 --- /dev/null +++ b/encoder/autorot_linux.go @@ -0,0 +1,7 @@ +package encoder + +import "webp_server_go/vips" + +func autorot(img *vips.Image) error { + return img.Autorot(nil) +} diff --git a/encoder/encoder.go b/encoder/encoder.go index 5b56e5ac..32ee1101 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -9,36 +9,25 @@ import ( "time" "webp_server_go/config" "webp_server_go/helper" + "webp_server_go/vips" - "github.com/davidbyttow/govips/v2/vips" log "github.com/sirupsen/logrus" ) var ( - boolFalse vips.BoolParameter - intMinusOne vips.IntParameter // Source image encoder ignore list for WebP and AVIF // We shouldn't convert Unknown and AVIF to WebP - webpIgnore = []vips.ImageType{vips.ImageTypeUnknown, vips.ImageTypeAVIF} + webpIgnore = []vips.ImageType{vips.ImageTypeUnknown, vips.ImageTypeAvif} // We shouldn't convert Unknown,AVIF and GIF to AVIF - avifIgnore = append(webpIgnore, vips.ImageTypeGIF) + avifIgnore = append(webpIgnore, vips.ImageTypeGif) ) func init() { - vips.LoggingSettings(nil, vips.LogLevelError) + + vips.SetLogging(nil, vips.LogLevelError) vips.Startup(&vips.Config{ ConcurrencyLevel: runtime.NumCPU(), }) - boolFalse.Set(false) - intMinusOne.Set(-1) -} - -func loadImage(filename string) (*vips.ImageRef, error) { - img, err := vips.LoadImageFromFile(filename, &vips.ImportParams{ - FailOnError: boolFalse, - NumPages: intMinusOne, - }) - return img, err } func ConvertFilter(rawPath, jxlPath, avifPath, webpPath string, extraParams config.ExtraParams, supportedFormats map[string]bool, c chan int) { @@ -129,9 +118,10 @@ func convertImage(rawPath, optimizedPath, imageType string, extraParams config.E } } - // Image is only opened here - img, err := loadImage(rawPath) - defer img.Close() + img, err := helper.LoadImage(rawPath) + if err == nil { + defer img.Close() + } // Pre-process image(auto rotate, resize, etc.) err = preProcessImage(img, imageType, extraParams) @@ -144,21 +134,21 @@ func convertImage(rawPath, optimizedPath, imageType string, extraParams config.E switch imageType { case "webp": - if imageFormat == vips.ImageTypeWEBP { + if imageFormat == vips.ImageTypeWebp { log.Infof("Image is already in WebP format, copying %s to %s", rawPath, optimizedPath) return helper.CopyFile(rawPath, optimizedPath) } else { err = webpEncoder(img, rawPath, optimizedPath) } case "avif": - if imageFormat == vips.ImageTypeAVIF { + if imageFormat == vips.ImageTypeAvif { log.Infof("Image is already in AVIF format, copying %s to %s", rawPath, optimizedPath) return helper.CopyFile(rawPath, optimizedPath) } else { err = avifEncoder(img, rawPath, optimizedPath) } case "jxl": - if imageFormat == vips.ImageTypeJXL { + if imageFormat == vips.ImageTypeJxl { log.Infof("Image is already in JXL format, copying %s to %s", rawPath, optimizedPath) return helper.CopyFile(rawPath, optimizedPath) } else { @@ -169,7 +159,7 @@ func convertImage(rawPath, optimizedPath, imageType string, extraParams config.E return err } -func jxlEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error { +func jxlEncoder(img *vips.Image, rawPath string, optimizedPath string) error { var ( buf []byte quality = config.Config.Quality @@ -178,17 +168,17 @@ func jxlEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error // If quality >= 100, we use lossless mode if quality >= 100 { - buf, _, err = img.ExportJxl(&vips.JxlExportParams{ + buf, err = img.JxlsaveBuffer(&vips.JxlsaveBufferOptions{ Effort: 1, Tier: 4, Lossless: true, Distance: 1.0, }) } else { - buf, _, err = img.ExportJxl(&vips.JxlExportParams{ + buf, err = img.JxlsaveBuffer(&vips.JxlsaveBufferOptions{ Effort: 1, Tier: 4, - Quality: quality, + Q: quality, Lossless: false, Distance: 1.0, }) @@ -208,24 +198,33 @@ func jxlEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error return nil } -func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error { +func avifEncoder(img *vips.Image, rawPath string, optimizedPath string) error { var ( buf []byte quality = config.Config.Quality err error ) + // encoder: HeifEncoderSvt - intel & netflix + // HeifEncoderRav1e: Mozilla Rust + // HeifEncoderAom: Official + //StripMetadata: config.Config.StripMetadata, // If quality >= 100, we use lossless mode + if config.Config.StripMetadata { + _ = img.RemoveExif() + } if quality >= 100 { - buf, _, err = img.ExportAvif(&vips.AvifExportParams{ - Lossless: true, - StripMetadata: config.Config.StripMetadata, + buf, err = img.HeifsaveBuffer(&vips.HeifsaveBufferOptions{ + Lossless: true, + Encoder: vips.HeifEncoderSvt, + Compression: vips.HeifCompressionAv1, }) } else { - buf, _, err = img.ExportAvif(&vips.AvifExportParams{ - Quality: quality, - Lossless: false, - StripMetadata: config.Config.StripMetadata, + buf, err = img.HeifsaveBuffer(&vips.HeifsaveBufferOptions{ + Q: quality, + Lossless: false, + Encoder: vips.HeifEncoderSvt, + Compression: vips.HeifCompressionAv1, }) } @@ -243,33 +242,36 @@ func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error return nil } -func webpEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error { +func webpEncoder(img *vips.Image, rawPath string, optimizedPath string) error { var ( buf []byte quality = config.Config.Quality err error ) - + if config.Config.StripMetadata { + _ = img.RemoveExif() + } // If quality >= 100, we use lossless mode if quality >= 100 { // Lossless mode will not encounter problems as below, because in libvips as code below // config.method = ExUtilGetInt(argv[++c], 0, &parse_error); // use_lossless_preset = 0; // disable -z option - buf, _, err = img.ExportWebp(&vips.WebpExportParams{ - Lossless: true, - StripMetadata: config.Config.StripMetadata, + + buf, err = img.WebpsaveBuffer(&vips.WebpsaveBufferOptions{ + Lossless: true, + //StripMetadata: config.Config.StripMetadata, }) } else { // If some special images cannot encode with default ReductionEffort(0), then retry from 0 to 6 // Example: https://github.com/webp-sh/webp_server_go/issues/234 - ep := vips.WebpExportParams{ - Quality: quality, - Lossless: false, - StripMetadata: config.Config.StripMetadata, + ep := vips.WebpsaveBufferOptions{ + Q: quality, + Lossless: false, + //StripMetadata: config.Config.StripMetadata, } for i := range 7 { - ep.ReductionEffort = i - buf, _, err = img.ExportWebp(&ep) + ep.Effort = i + buf, err = img.WebpsaveBuffer(&ep) if err != nil && strings.Contains(err.Error(), "unable to encode") { log.Warnf("Can't encode image to WebP with ReductionEffort %d, trying higher value...", i) } else if err != nil { @@ -278,7 +280,7 @@ func webpEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error break } } - buf, _, err = img.ExportWebp(&ep) + buf, err = img.WebpsaveBuffer(&ep) } if err != nil { diff --git a/encoder/process.go b/encoder/process.go index 6bb0fedf..3bc0989a 100644 --- a/encoder/process.go +++ b/encoder/process.go @@ -6,12 +6,13 @@ import ( "path" "slices" "webp_server_go/config" + "webp_server_go/helper" + "webp_server_go/vips" - "github.com/davidbyttow/govips/v2/vips" log "github.com/sirupsen/logrus" ) -func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error { +func resizeImage(img *vips.Image, extraParams config.ExtraParams) error { imageHeight := img.Height() imageWidth := img.Width() @@ -35,12 +36,18 @@ func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error { // If height exceeds more, like 500x500 -> 200x100 (2.5 < 5) // Take max_height as new height ,resize and retain ratio if heightExceedRatio > widthExceedRatio { - err := img.Thumbnail(int(float32(extraParams.MaxHeight)/imgHeightWidthRatio), extraParams.MaxHeight, 0) + err := img.ThumbnailImage(int(float32(extraParams.MaxHeight)/imgHeightWidthRatio), &vips.ThumbnailImageOptions{ + Height: extraParams.MaxHeight, + Crop: vips.InterestingNone, + }) if err != nil { return err } } else { - err := img.Thumbnail(extraParams.MaxWidth, int(float32(extraParams.MaxWidth)*imgHeightWidthRatio), 0) + err := img.ThumbnailImage(extraParams.MaxWidth, &vips.ThumbnailImageOptions{ + Height: int(float32(extraParams.MaxWidth) * imgHeightWidthRatio), + Crop: vips.InterestingNone, + }) if err != nil { return err } @@ -49,14 +56,22 @@ func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error { } if extraParams.MaxHeight > 0 && imageHeight > extraParams.MaxHeight && extraParams.MaxWidth == 0 { - err := img.Thumbnail(int(float32(extraParams.MaxHeight)/imgHeightWidthRatio), extraParams.MaxHeight, 0) + + err := img.ThumbnailImage(int(float32(extraParams.MaxHeight)/imgHeightWidthRatio), &vips.ThumbnailImageOptions{ + Height: extraParams.MaxHeight, + Crop: vips.InterestingNone, + }) if err != nil { return err } } if extraParams.MaxWidth > 0 && imageWidth > extraParams.MaxWidth && extraParams.MaxHeight == 0 { - err := img.Thumbnail(extraParams.MaxWidth, int(float32(extraParams.MaxWidth)*imgHeightWidthRatio), 0) + err := img.ThumbnailImage(extraParams.MaxWidth, + &vips.ThumbnailImageOptions{ + Height: int(float32(extraParams.MaxWidth) * imgHeightWidthRatio), + Crop: vips.InterestingNone, + }) if err != nil { return err } @@ -83,19 +98,28 @@ func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error { cropInteresting = vips.InterestingAttention } - err := img.Thumbnail(extraParams.Width, extraParams.Height, cropInteresting) + err := img.ThumbnailImage(extraParams.Width, &vips.ThumbnailImageOptions{ + Height: extraParams.Height, + Crop: cropInteresting, + }) if err != nil { return err } } if extraParams.Width > 0 && extraParams.Height == 0 { - err := img.Thumbnail(extraParams.Width, int(float32(extraParams.Width)*imgHeightWidthRatio), 0) + err := img.ThumbnailImage(extraParams.Width, &vips.ThumbnailImageOptions{ + Height: int(float32(extraParams.Width) * imgHeightWidthRatio), + Crop: vips.InterestingNone, + }) if err != nil { return err } } if extraParams.Height > 0 && extraParams.Width == 0 { - err := img.Thumbnail(int(float32(extraParams.Height)/imgHeightWidthRatio), extraParams.Height, 0) + err := img.ThumbnailImage(int(float32(extraParams.Height)/imgHeightWidthRatio), &vips.ThumbnailImageOptions{ + Height: extraParams.Height, + Crop: vips.InterestingNone, + }) if err != nil { return err } @@ -111,30 +135,53 @@ func ResizeItself(raw, dest string, extraParams config.ExtraParams) { if err != nil { log.Error(err.Error()) } - - img, err := vips.LoadImageFromFile(raw, &vips.ImportParams{ - FailOnError: boolFalse, - NumPages: intMinusOne, - }) + img, err := helper.LoadImage(raw) if err != nil { log.Warnf("Could not load %s: %s", raw, err) return } _ = resizeImage(img, extraParams) if config.Config.StripMetadata { - img.RemoveMetadata() + _ = img.RemoveExif() + } + + // ExportNative exports the image to a buffer based on its native format with default parameters. + var buf []byte + switch img.Format() { + case vips.ImageTypeJpeg: + buf, _ = img.JpegsaveBuffer(nil) + case vips.ImageTypePng: + buf, _ = img.PngsaveBuffer(nil) + case vips.ImageTypeWebp: + buf, _ = img.WebpsaveBuffer(nil) + case vips.ImageTypeHeif: + buf, _ = img.HeifsaveBuffer(nil) + case vips.ImageTypeTiff: + buf, _ = img.TiffsaveBuffer(nil) + case vips.ImageTypeAvif: + buf, _ = img.HeifsaveBuffer(&vips.HeifsaveBufferOptions{ + Encoder: vips.HeifEncoderSvt, + }) + case vips.ImageTypeJp2k: + buf, _ = img.Jp2ksaveBuffer(nil) + case vips.ImageTypeGif: + buf, _ = img.GifsaveBuffer(nil) + case vips.ImageTypeJxl: + buf, _ = img.JxlsaveBuffer(nil) + default: + buf, _ = img.PngsaveBuffer(nil) } - buf, _, _ := img.ExportNative() _ = os.WriteFile(dest, buf, 0600) img.Close() } // Pre-process image(auto rotate, resize, etc.) -func preProcessImage(img *vips.ImageRef, imageType string, extraParams config.ExtraParams) error { +func preProcessImage(img *vips.Image, imageType string, extraParams config.ExtraParams) error { // Check Width/Height and ignore image formats switch imageType { case "webp": - if img.Metadata().Width > config.WebpMax || img.Metadata().Height > config.WebpMax { + + if img.Width() > config.WebpMax || img.Height() > config.WebpMax { return errors.New("WebP: image too large") } imageFormat := img.Format() @@ -143,7 +190,7 @@ func preProcessImage(img *vips.ImageRef, imageType string, extraParams config.Ex return errors.New("WebP encoder: ignore image type") } case "avif": - if img.Metadata().Width > config.AvifMax || img.Metadata().Height > config.AvifMax { + if img.Width() > config.AvifMax || img.Height() > config.AvifMax { return errors.New("AVIF: image too large") } imageFormat := img.Format() @@ -160,11 +207,11 @@ func preProcessImage(img *vips.ImageRef, imageType string, extraParams config.Ex } } // Skip auto rotate for GIF/WebP - if img.Format() == vips.ImageTypeGIF || img.Format() == vips.ImageTypeWEBP { + if img.Format() == vips.ImageTypeGif || img.Format() == vips.ImageTypeWebp { return nil } else { // Auto rotate - err := img.AutoRotate() + err := autorot(img) if err != nil { return err } diff --git a/encoder/process_test.go b/encoder/process_test.go index 82ed925f..1691d1d7 100644 --- a/encoder/process_test.go +++ b/encoder/process_test.go @@ -3,12 +3,11 @@ package encoder import ( "testing" "webp_server_go/config" - - "github.com/davidbyttow/govips/v2/vips" + vips "webp_server_go/vips" ) func TestResizeImage(t *testing.T) { - img, _ := vips.Black(500, 500) + img, _ := vips.NewBlack(500, 500, nil) // Define the parameters for the test cases testCases := []struct { diff --git a/go.mod b/go.mod index d2456dd9..673c20e0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25 require ( github.com/buckket/go-blurhash v1.1.0 github.com/cespare/xxhash v1.1.0 - github.com/davidbyttow/govips/v2 v2.16.0 + github.com/cshum/vipsgen v1.1.2 github.com/gofiber/fiber/v2 v2.52.9 github.com/h2non/filetype v1.1.4-0.20230123234534-cfcd7d097bc4 github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c @@ -30,11 +30,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/image v0.18.0 // indirect - golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cd522a04..4697e8cd 100644 --- a/go.sum +++ b/go.sum @@ -8,14 +8,13 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= +github.com/cshum/vipsgen v1.1.2 h1:7kFUxlCBx4bAd69YwWagOGYC8/7vkXJuzCFV6tmPYvU= +github.com/cshum/vipsgen v1.1.2/go.mod h1:1GboZQcNmo4NwuNnGogM24m3O+1i6UpnvurqMcsFItE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davidbyttow/govips/v2 v2.16.0 h1:1nH/Rbx8qZP1hd+oYL9fYQjAnm1+KorX9s07ZGseQmo= -github.com/davidbyttow/govips/v2 v2.16.0/go.mod h1:clH5/IDVmG5eVyc23qYpyi7kmOT0B/1QNTKtci4RkyM= github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/h2non/filetype v1.1.4-0.20230123234534-cfcd7d097bc4 h1:k7FGP5I7raiaC3aAzCLddcoxzboIrOm6/FVRXjp/5JM= @@ -24,9 +23,6 @@ github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -37,8 +33,6 @@ github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNG github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -53,7 +47,6 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -65,80 +58,14 @@ github.com/webp-sh/rawparser v0.0.0-20240311121240-15117cd3320a h1:yFNUYbDL81wQZ github.com/webp-sh/rawparser v0.0.0-20240311121240-15117cd3320a/go.mod h1:X0j2dOqH3ecGRuWvkThgDy+NKAfIwSN9wAOQlMcFOfY= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helper/helper.go b/helper/helper.go index d26cbd9c..a188e809 100644 --- a/helper/helper.go +++ b/helper/helper.go @@ -8,10 +8,10 @@ import ( "strings" "time" "webp_server_go/config" + "webp_server_go/vips" "slices" - "github.com/davidbyttow/govips/v2/vips" "github.com/h2non/filetype" "github.com/mileusna/useragent" @@ -22,17 +22,33 @@ import ( log "github.com/sirupsen/logrus" ) -var ( - boolFalse vips.BoolParameter - intMinusOne vips.IntParameter -) - var _ = filetype.AddMatcher(filetype.NewType("svg", "image/svg+xml"), svgMatcher) func svgMatcher(buf []byte) bool { return svg.Is(buf) } +func LoadImage(filename string) (*vips.Image, error) { + var ( + img *vips.Image + err error + ) + + // N: N Number of pages to load, -1 for all, i.e. this is for gif + img, err = vips.NewImageFromFile(filename, &vips.LoadOptions{ + FailOnError: true, + N: -1, + }) + + if err != nil { + // all other images + img, err = vips.NewImageFromFile(filename, &vips.LoadOptions{ + FailOnError: true, + }) + } + return img, err +} + func GetFileContentType(filename string) string { // raw image, need to use filetype to determine buf, _ := os.ReadFile(filename) diff --git a/helper/metadata.go b/helper/metadata.go index 949e3ae0..648aaba6 100644 --- a/helper/metadata.go +++ b/helper/metadata.go @@ -1,14 +1,17 @@ package helper import ( + "bytes" "encoding/json" + "image" "net/url" "os" "path" "webp_server_go/config" + "webp_server_go/vips" + "github.com/buckket/go-blurhash" - "github.com/davidbyttow/govips/v2/vips" log "github.com/sirupsen/logrus" ) @@ -82,22 +85,18 @@ func WriteMetadata(p, etag string, subdir string) config.MetaFile { } func getImageMeta(filePath string) (metadata config.ImageMeta) { - boolFalse.Set(false) - intMinusOne.Set(-1) - img, err := vips.LoadImageFromFile(filePath, &vips.ImportParams{ - FailOnError: boolFalse, - NumPages: intMinusOne, - }) - if err != nil { - log.Warnf("Could not load %s: %s", filePath, err) - return metadata + img, err := LoadImage(filePath) + if err == nil { + defer img.Close() + } else { + return } - defer img.Close() + var colorspace string switch img.Interpretation() { - case vips.InterpretationSRGB: + case vips.InterpretationSrgb: colorspace = "sRGB" - case vips.InterpretationYXY: + case vips.InterpretationYxy: colorspace = "YXY" case vips.InterpretationFourier: colorspace = "Fourier" @@ -105,61 +104,77 @@ func getImageMeta(filePath string) (metadata config.ImageMeta) { colorspace = "Grey16" case vips.InterpretationMatrix: colorspace = "Matrix" - case vips.InterpretationScRGB: + case vips.InterpretationScrgb: colorspace = "scRGB" - case vips.InterpretationHSV: + case vips.InterpretationHsv: colorspace = "HSV" default: colorspace = "Unknown" } // Get image size - height := img.Metadata().Height - width := img.Metadata().Width - numPages := img.Metadata().Pages + height := img.Height() + width := img.Width() + numPages := img.Pages() if numPages > 1 { height = height / numPages } - var imgFormat string + var ( + imgFormat string + imgBytes []byte + ) switch img.Format() { - case vips.ImageTypeJPEG: + case vips.ImageTypeJpeg: imgFormat = "jpeg" - case vips.ImageTypePNG: + imgBytes, _ = img.JpegsaveBuffer(nil) + case vips.ImageTypePng: imgFormat = "png" - case vips.ImageTypeWEBP: + imgBytes, _ = img.PngsaveBuffer(nil) + + case vips.ImageTypeWebp: imgFormat = "webp" - case vips.ImageTypeAVIF: + imgBytes, _ = img.WebpsaveBuffer(nil) + + case vips.ImageTypeAvif: imgFormat = "avif" - case vips.ImageTypeGIF: + imgBytes, _ = img.HeifsaveBuffer(&vips.HeifsaveBufferOptions{ + Encoder: vips.HeifEncoderSvt, + }) + + case vips.ImageTypeGif: imgFormat = "gif" - case vips.ImageTypeBMP: + imgBytes, _ = img.GifsaveBuffer(nil) + + case vips.ImageTypeBmp: imgFormat = "bmp" + imgBytes, _ = img.MagicksaveBuffer(&vips.MagicksaveBufferOptions{Format: "bmp"}) + default: imgFormat = "unknown" } - imgBytes, err := img.ToBytes() - if err != nil { - log.Error("Error in img.ToBytes", err) - return - } - metadata = config.ImageMeta{ Width: width, Height: height, Format: imgFormat, Colorspace: colorspace, NumPages: numPages, - Size: len(imgBytes), + Size: len(imgBytes), //TODO old algorithm: wrong way to calculate size? } // Get blurhash - _ = img.Thumbnail(32, 32, vips.InterestingAttention) - imageImage, err := img.ToImage(vips.NewDefaultExportParams()) + _ = img.ThumbnailImage(32, &vips.ThumbnailImageOptions{ + Height: 32, + Crop: vips.InterestingAttention, + }) + + reader := bytes.NewReader(imgBytes) + imageImage, _, err := image.Decode(reader) if err != nil { log.Error("Error in img.ToImage", err) return } + // imageImage: image.Image blurHash, err := blurhash.Encode(4, 3, imageImage) if err != nil { log.Error("Error in blurhash", err)