mirror of
https://github.com/woodchen-ink/webp_server_go.git
synced 2025-07-18 05:32:02 +08:00
Adds JPEG XL support, max_height/max_width support (#321)
* WIP JXL * Fix test * Tries to fix autobuild * Tries to fix autobuild * Add setup go in codeql * Bump actions version * Do not print curl output in CI * Do not print curl output in CI * Remove Metadata on RAW image * Update sample config * better loop * Prefetch should also respect AllowedType * Better Export params and UA handle * Only do conversion on supported formats * CONVERT_TYPES default to webp only * CONVERT_TYPES default to webp only * Add GIF to AllowedTypes * Update README
This commit is contained in:
parent
ddcb5323b5
commit
43c275e3ec
2
.github/workflows/CI.yaml
vendored
2
.github/workflows/CI.yaml
vendored
@ -40,7 +40,7 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
|
26
.github/workflows/codeql-analysis.yml
vendored
26
.github/workflows/codeql-analysis.yml
vendored
@ -15,7 +15,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '32 20 * * 2'
|
- cron: '32 20 * * 2'
|
||||||
@ -33,38 +32,23 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'go' ]
|
language: [ 'go' ]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v2
|
||||||
|
4
.github/workflows/integration-test.yaml
vendored
4
.github/workflows/integration-test.yaml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
- name: Send Requests to Server
|
- name: Send Requests to Server
|
||||||
run: |
|
run: |
|
||||||
cd pics
|
cd pics
|
||||||
find * -type f -print | xargs -I {} curl -H "Accept: image/webp" http://localhost:3333/{}
|
find * -type f -print | xargs -I {} curl -o /dev/null -H "Accept: image/webp" http://localhost:3333/{}
|
||||||
|
|
||||||
- name: Get container RAM stats
|
- name: Get container RAM stats
|
||||||
run: |
|
run: |
|
||||||
@ -61,7 +61,7 @@ jobs:
|
|||||||
- name: Send Requests to Server
|
- name: Send Requests to Server
|
||||||
run: |
|
run: |
|
||||||
cd pics
|
cd pics
|
||||||
find * -type f -print | xargs -I {} curl -H "Accept: image/webp" http://localhost:3333/{}
|
find * -type f -print | xargs -I {} curl -o /dev/null -H "Accept: image/webp" http://localhost:3333/{}
|
||||||
|
|
||||||
- name: Get container RAM stats
|
- name: Get container RAM stats
|
||||||
run: |
|
run: |
|
||||||
|
4
.github/workflows/release_docker_image.yaml
vendored
4
.github/workflows/release_docker_image.yaml
vendored
@ -43,10 +43,10 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
|
@ -16,7 +16,7 @@ Currently supported image format: JPEG, PNG, BMP, GIF, SVG, HEIC, NEF, WEBP
|
|||||||
|
|
||||||
> e.g When you visit `https://your.website/pics/tsuki.jpg`,it will serve as `image/webp`/`image/avif` format without changing the URL.
|
> e.g When you visit `https://your.website/pics/tsuki.jpg`,it will serve as `image/webp`/`image/avif` format without changing the URL.
|
||||||
>
|
>
|
||||||
> GIF image will not be converted to AVIF format even with `ENABLE_AVIF` to `true`, because the converted AVIF image is not animated.
|
> GIF image will not be converted to AVIF format because the converted AVIF image is not animated.
|
||||||
|
|
||||||
## Usage with Docker(recommended)
|
## Usage with Docker(recommended)
|
||||||
|
|
||||||
@ -32,8 +32,6 @@ services:
|
|||||||
image: webpsh/webp-server-go
|
image: webpsh/webp-server-go
|
||||||
# image: ghcr.io/webp-sh/webp_server_go
|
# image: ghcr.io/webp-sh/webp_server_go
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
|
||||||
- MALLOC_ARENA_MAX=1
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./path/to/pics:/opt/pics
|
- ./path/to/pics:/opt/pics
|
||||||
- ./exhaust:/opt/exhaust
|
- ./exhaust:/opt/exhaust
|
||||||
@ -74,8 +72,6 @@ services:
|
|||||||
image: webpsh/webp-server-go
|
image: webpsh/webp-server-go
|
||||||
# image: ghcr.io/webp-sh/webp_server_go
|
# image: ghcr.io/webp-sh/webp_server_go
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
|
||||||
- MALLOC_ARENA_MAX=1
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./path/to/pics:/opt/pics
|
- ./path/to/pics:/opt/pics
|
||||||
- ./path/to/exhaust:/opt/exhaust
|
- ./path/to/exhaust:/opt/exhaust
|
||||||
|
@ -4,12 +4,13 @@
|
|||||||
"QUALITY": "80",
|
"QUALITY": "80",
|
||||||
"IMG_PATH": "./pics",
|
"IMG_PATH": "./pics",
|
||||||
"EXHAUST_PATH": "./exhaust",
|
"EXHAUST_PATH": "./exhaust",
|
||||||
"ALLOWED_TYPES": ["jpg","png","jpeg","bmp","gif","svg","heic"],
|
|
||||||
"IMG_MAP": {},
|
"IMG_MAP": {},
|
||||||
"ENABLE_AVIF": false,
|
"ALLOWED_TYPES": ["jpg","png","jpeg","gif","bmp","svg","heic","nef"],
|
||||||
|
"CONVERT_TYPES": ["webp"],
|
||||||
|
"STRIP_METADATA": true,
|
||||||
"ENABLE_EXTRA_PARAMS": false,
|
"ENABLE_EXTRA_PARAMS": false,
|
||||||
"READ_BUFFER_SIZE": 4096,
|
"READ_BUFFER_SIZE": 4096,
|
||||||
"CONCURRENCY": 262144,
|
"CONCURRENCY": 262144,
|
||||||
"DISABLE_KEEPALIVE": false,
|
"DISABLE_KEEPALIVE": false,
|
||||||
"CACHE_TTL": 259200
|
"CACHE_TTL": 259200
|
||||||
}
|
}
|
114
config/config.go
114
config/config.go
@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -28,13 +29,14 @@ const (
|
|||||||
"IMG_PATH": "./pics",
|
"IMG_PATH": "./pics",
|
||||||
"EXHAUST_PATH": "./exhaust",
|
"EXHAUST_PATH": "./exhaust",
|
||||||
"IMG_MAP": {},
|
"IMG_MAP": {},
|
||||||
"ALLOWED_TYPES": ["jpg","png","jpeg","bmp","svg","heic","nef"],
|
"ALLOWED_TYPES": ["jpg","png","jpeg","gif","bmp","svg","heic","nef"],
|
||||||
"ENABLE_AVIF": false,
|
"CONVERT_TYPES": ["webp"],
|
||||||
"ENABLE_EXTRA_PARAMS": false
|
"STRIP_METADATA": true,
|
||||||
|
"ENABLE_EXTRA_PARAMS": false,
|
||||||
"READ_BUFFER_SIZE": 4096,
|
"READ_BUFFER_SIZE": 4096,
|
||||||
"CONCURRENCY": 262144,
|
"CONCURRENCY": 262144,
|
||||||
"DISABLE_KEEPALIVE": false,
|
"DISABLE_KEEPALIVE": false,
|
||||||
"CACHE_TTL": 259200,
|
"CACHE_TTL": 259200
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ var (
|
|||||||
ProxyMode bool
|
ProxyMode bool
|
||||||
Prefetch bool
|
Prefetch bool
|
||||||
Config = NewWebPConfig()
|
Config = NewWebPConfig()
|
||||||
Version = "0.10.8"
|
Version = "0.11.0"
|
||||||
WriteLock = cache.New(5*time.Minute, 10*time.Minute)
|
WriteLock = cache.New(5*time.Minute, 10*time.Minute)
|
||||||
ConvertLock = cache.New(5*time.Minute, 10*time.Minute)
|
ConvertLock = cache.New(5*time.Minute, 10*time.Minute)
|
||||||
RemoteRaw = "./remote-raw"
|
RemoteRaw = "./remote-raw"
|
||||||
@ -63,32 +65,44 @@ type MetaFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WebpConfig struct {
|
type WebpConfig struct {
|
||||||
Host string `json:"HOST"`
|
Host string `json:"HOST"`
|
||||||
Port string `json:"PORT"`
|
Port string `json:"PORT"`
|
||||||
ImgPath string `json:"IMG_PATH"`
|
ImgPath string `json:"IMG_PATH"`
|
||||||
Quality int `json:"QUALITY,string"`
|
Quality int `json:"QUALITY,string"`
|
||||||
AllowedTypes []string `json:"ALLOWED_TYPES"`
|
AllowedTypes []string `json:"ALLOWED_TYPES"`
|
||||||
ImageMap map[string]string `json:"IMG_MAP"`
|
ConvertTypes []string `json:"CONVERT_TYPES"`
|
||||||
ExhaustPath string `json:"EXHAUST_PATH"`
|
ImageMap map[string]string `json:"IMG_MAP"`
|
||||||
EnableAVIF bool `json:"ENABLE_AVIF"`
|
ExhaustPath string `json:"EXHAUST_PATH"`
|
||||||
EnableExtraParams bool `json:"ENABLE_EXTRA_PARAMS"`
|
|
||||||
ReadBufferSize int `json:"READ_BUFFER_SIZE"`
|
EnableWebP bool `json:"ENABLE_WEBP"`
|
||||||
Concurrency int `json:"CONCURRENCY"`
|
EnableAVIF bool `json:"ENABLE_AVIF"`
|
||||||
DisableKeepalive bool `json:"DISABLE_KEEPALIVE"`
|
EnableJXL bool `json:"ENABLE_JXL"`
|
||||||
CacheTTL int `json:"CACHE_TTL"`
|
|
||||||
|
EnableExtraParams bool `json:"ENABLE_EXTRA_PARAMS"`
|
||||||
|
StripMetadata bool `json:"STRIP_METADATA"`
|
||||||
|
ReadBufferSize int `json:"READ_BUFFER_SIZE"`
|
||||||
|
Concurrency int `json:"CONCURRENCY"`
|
||||||
|
DisableKeepalive bool `json:"DISABLE_KEEPALIVE"`
|
||||||
|
CacheTTL int `json:"CACHE_TTL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebPConfig() *WebpConfig {
|
func NewWebPConfig() *WebpConfig {
|
||||||
return &WebpConfig{
|
return &WebpConfig{
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
Port: "3333",
|
Port: "3333",
|
||||||
ImgPath: "./pics",
|
ImgPath: "./pics",
|
||||||
Quality: 80,
|
Quality: 80,
|
||||||
AllowedTypes: []string{"jpg", "png", "jpeg", "bmp", "svg", "nef", "heic", "webp"},
|
AllowedTypes: []string{"jpg", "png", "jpeg", "bmp", "gif", "svg", "nef", "heic", "webp"},
|
||||||
ImageMap: map[string]string{},
|
ConvertTypes: []string{"webp"},
|
||||||
ExhaustPath: "./exhaust",
|
ImageMap: map[string]string{},
|
||||||
EnableAVIF: false,
|
ExhaustPath: "./exhaust",
|
||||||
|
|
||||||
|
EnableWebP: false,
|
||||||
|
EnableAVIF: false,
|
||||||
|
EnableJXL: false,
|
||||||
|
|
||||||
EnableExtraParams: false,
|
EnableExtraParams: false,
|
||||||
|
StripMetadata: true,
|
||||||
ReadBufferSize: 4096,
|
ReadBufferSize: 4096,
|
||||||
Concurrency: 262144,
|
Concurrency: 262144,
|
||||||
DisableKeepalive: false,
|
DisableKeepalive: false,
|
||||||
@ -115,6 +129,16 @@ func LoadConfig() {
|
|||||||
switchProxyMode()
|
switchProxyMode()
|
||||||
Config.ImageMap = parseImgMap(Config.ImageMap)
|
Config.ImageMap = parseImgMap(Config.ImageMap)
|
||||||
|
|
||||||
|
if slices.Contains(Config.ConvertTypes, "webp") {
|
||||||
|
Config.EnableWebP = true
|
||||||
|
}
|
||||||
|
if slices.Contains(Config.ConvertTypes, "avif") {
|
||||||
|
Config.EnableAVIF = true
|
||||||
|
}
|
||||||
|
if slices.Contains(Config.ConvertTypes, "jxl") {
|
||||||
|
Config.EnableJXL = true
|
||||||
|
}
|
||||||
|
|
||||||
// Read from ENV for override
|
// Read from ENV for override
|
||||||
if os.Getenv("WEBP_HOST") != "" {
|
if os.Getenv("WEBP_HOST") != "" {
|
||||||
Config.Host = os.Getenv("WEBP_HOST")
|
Config.Host = os.Getenv("WEBP_HOST")
|
||||||
@ -139,16 +163,24 @@ func LoadConfig() {
|
|||||||
if os.Getenv("WEBP_ALLOWED_TYPES") != "" {
|
if os.Getenv("WEBP_ALLOWED_TYPES") != "" {
|
||||||
Config.AllowedTypes = strings.Split(os.Getenv("WEBP_ALLOWED_TYPES"), ",")
|
Config.AllowedTypes = strings.Split(os.Getenv("WEBP_ALLOWED_TYPES"), ",")
|
||||||
}
|
}
|
||||||
if os.Getenv("WEBP_ENABLE_AVIF") != "" {
|
|
||||||
enableAVIF := os.Getenv("WEBP_ENABLE_AVIF")
|
// Override enabled convert types
|
||||||
if enableAVIF == "true" {
|
if os.Getenv("WEBP_CONVERT_TYPES") != "" {
|
||||||
|
Config.ConvertTypes = strings.Split(os.Getenv("WEBP_CONVERT_TYPES"), ",")
|
||||||
|
Config.EnableWebP = false
|
||||||
|
Config.EnableAVIF = false
|
||||||
|
Config.EnableJXL = false
|
||||||
|
if slices.Contains(Config.ConvertTypes, "webp") {
|
||||||
|
Config.EnableWebP = true
|
||||||
|
}
|
||||||
|
if slices.Contains(Config.ConvertTypes, "avif") {
|
||||||
Config.EnableAVIF = true
|
Config.EnableAVIF = true
|
||||||
} else if enableAVIF == "false" {
|
}
|
||||||
Config.EnableAVIF = false
|
if slices.Contains(Config.ConvertTypes, "jxl") {
|
||||||
} else {
|
Config.EnableJXL = true
|
||||||
log.Warnf("WEBP_ENABLE_AVIF is not a valid boolean, using value in config.json %t", Config.EnableAVIF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("WEBP_ENABLE_EXTRA_PARAMS") != "" {
|
if os.Getenv("WEBP_ENABLE_EXTRA_PARAMS") != "" {
|
||||||
enableExtraParams := os.Getenv("WEBP_ENABLE_EXTRA_PARAMS")
|
enableExtraParams := os.Getenv("WEBP_ENABLE_EXTRA_PARAMS")
|
||||||
if enableExtraParams == "true" {
|
if enableExtraParams == "true" {
|
||||||
@ -159,6 +191,16 @@ func LoadConfig() {
|
|||||||
log.Warnf("WEBP_ENABLE_EXTRA_PARAMS is not a valid boolean, using value in config.json %t", Config.EnableExtraParams)
|
log.Warnf("WEBP_ENABLE_EXTRA_PARAMS is not a valid boolean, using value in config.json %t", Config.EnableExtraParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if os.Getenv("WEBP_STRIP_METADATA") != "" {
|
||||||
|
stripMetadata := os.Getenv("WEBP_STRIP_METADATA")
|
||||||
|
if stripMetadata == "true" {
|
||||||
|
Config.StripMetadata = true
|
||||||
|
} else if stripMetadata == "false" {
|
||||||
|
Config.StripMetadata = false
|
||||||
|
} else {
|
||||||
|
log.Warnf("WEBP_STRIP_METADATA is not a valid boolean, using value in config.json %t", Config.StripMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
if os.Getenv("WEBP_IMG_MAP") != "" {
|
if os.Getenv("WEBP_IMG_MAP") != "" {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
@ -223,8 +265,10 @@ func parseImgMap(imgMap map[string]string) map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExtraParams struct {
|
type ExtraParams struct {
|
||||||
Width int // in px
|
Width int // in px
|
||||||
Height int // in px
|
Height int // in px
|
||||||
|
MaxWidth int // in px
|
||||||
|
MaxHeight int // in px
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchProxyMode() {
|
func switchProxyMode() {
|
||||||
|
@ -33,7 +33,7 @@ func init() {
|
|||||||
intMinusOne.Set(-1)
|
intMinusOne.Set(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraParams, c chan int) {
|
func ConvertFilter(rawPath, jxlPath, avifPath, webpPath string, extraParams config.ExtraParams, supportedFormats map[string]bool, c chan int) {
|
||||||
// Wait for the conversion to complete and return the converted image
|
// Wait for the conversion to complete and return the converted image
|
||||||
retryDelay := 100 * time.Millisecond // Initial retry delay
|
retryDelay := 100 * time.Millisecond // Initial retry delay
|
||||||
|
|
||||||
@ -53,8 +53,8 @@ func ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraP
|
|||||||
defer config.ConvertLock.Delete(rawPath)
|
defer config.ConvertLock.Delete(rawPath)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(3)
|
||||||
if !helper.ImageExists(avifPath) && config.Config.EnableAVIF {
|
if !helper.ImageExists(avifPath) && config.Config.EnableAVIF && supportedFormats["avif"] {
|
||||||
go func() {
|
go func() {
|
||||||
err := convertImage(rawPath, avifPath, "avif", extraParams)
|
err := convertImage(rawPath, avifPath, "avif", extraParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -66,7 +66,7 @@ func ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraP
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !helper.ImageExists(webpPath) {
|
if !helper.ImageExists(webpPath) && config.Config.EnableWebP && supportedFormats["webp"] {
|
||||||
go func() {
|
go func() {
|
||||||
err := convertImage(rawPath, webpPath, "webp", extraParams)
|
err := convertImage(rawPath, webpPath, "webp", extraParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -77,6 +77,19 @@ func ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraP
|
|||||||
} else {
|
} else {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !helper.ImageExists(jxlPath) && config.Config.EnableJXL && supportedFormats["jxl"] {
|
||||||
|
go func() {
|
||||||
|
err := convertImage(rawPath, jxlPath, "jxl", extraParams)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
}
|
||||||
|
defer wg.Done()
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if c != nil {
|
if c != nil {
|
||||||
@ -123,11 +136,52 @@ func convertImage(rawPath, optimizedPath, imageType string, extraParams config.E
|
|||||||
err = webpEncoder(img, rawPath, optimizedPath)
|
err = webpEncoder(img, rawPath, optimizedPath)
|
||||||
case "avif":
|
case "avif":
|
||||||
err = avifEncoder(img, rawPath, optimizedPath)
|
err = avifEncoder(img, rawPath, optimizedPath)
|
||||||
|
case "jxl":
|
||||||
|
err = jxlEncoder(img, rawPath, optimizedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func jxlEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
quality = config.Config.Quality
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// If quality >= 100, we use lossless mode
|
||||||
|
if quality >= 100 {
|
||||||
|
buf, _, err = img.ExportJxl(&vips.JxlExportParams{
|
||||||
|
Effort: 1,
|
||||||
|
Tier: 4,
|
||||||
|
Lossless: true,
|
||||||
|
Distance: 1.0,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
buf, _, err = img.ExportJxl(&vips.JxlExportParams{
|
||||||
|
Effort: 1,
|
||||||
|
Tier: 4,
|
||||||
|
Quality: quality,
|
||||||
|
Lossless: false,
|
||||||
|
Distance: 1.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Can't encode source image: %v to JXL", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(optimizedPath, buf, 0600); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
convertLog("JXL", rawPath, optimizedPath, quality)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error {
|
func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error {
|
||||||
var (
|
var (
|
||||||
buf []byte
|
buf []byte
|
||||||
@ -139,13 +193,13 @@ func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error
|
|||||||
if quality >= 100 {
|
if quality >= 100 {
|
||||||
buf, _, err = img.ExportAvif(&vips.AvifExportParams{
|
buf, _, err = img.ExportAvif(&vips.AvifExportParams{
|
||||||
Lossless: true,
|
Lossless: true,
|
||||||
StripMetadata: true,
|
StripMetadata: config.Config.StripMetadata,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
buf, _, err = img.ExportAvif(&vips.AvifExportParams{
|
buf, _, err = img.ExportAvif(&vips.AvifExportParams{
|
||||||
Quality: quality,
|
Quality: quality,
|
||||||
Lossless: false,
|
Lossless: false,
|
||||||
StripMetadata: true,
|
StripMetadata: config.Config.StripMetadata,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +231,7 @@ func webpEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error
|
|||||||
// use_lossless_preset = 0; // disable -z option
|
// use_lossless_preset = 0; // disable -z option
|
||||||
buf, _, err = img.ExportWebp(&vips.WebpExportParams{
|
buf, _, err = img.ExportWebp(&vips.WebpExportParams{
|
||||||
Lossless: true,
|
Lossless: true,
|
||||||
StripMetadata: true,
|
StripMetadata: config.Config.StripMetadata,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// If some special images cannot encode with default ReductionEffort(0), then retry from 0 to 6
|
// If some special images cannot encode with default ReductionEffort(0), then retry from 0 to 6
|
||||||
@ -185,7 +239,7 @@ func webpEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error
|
|||||||
ep := vips.WebpExportParams{
|
ep := vips.WebpExportParams{
|
||||||
Quality: quality,
|
Quality: quality,
|
||||||
Lossless: false,
|
Lossless: false,
|
||||||
StripMetadata: true,
|
StripMetadata: config.Config.StripMetadata,
|
||||||
}
|
}
|
||||||
for i := range 7 {
|
for i := range 7 {
|
||||||
ep.ReductionEffort = i
|
ep.ReductionEffort = i
|
||||||
|
@ -33,12 +33,27 @@ func PrefetchImages() {
|
|||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if !helper.CheckAllowedType(picAbsPath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// RawImagePath string, ImgFilename string, reqURI string
|
// RawImagePath string, ImgFilename string, reqURI string
|
||||||
metadata := helper.ReadMetadata(picAbsPath, "", config.LocalHostAlias)
|
metadata := helper.ReadMetadata(picAbsPath, "", config.LocalHostAlias)
|
||||||
avif, webp := helper.GenOptimizedAbsPath(metadata, config.LocalHostAlias)
|
avifAbsPath, webpAbsPath, jxlAbsPath := helper.GenOptimizedAbsPath(metadata, config.LocalHostAlias)
|
||||||
_ = os.MkdirAll(path.Dir(avif), 0755)
|
|
||||||
|
// Using avifAbsPath here is the same as using webpAbsPath/jxlAbsPath
|
||||||
|
_ = os.MkdirAll(path.Dir(avifAbsPath), 0755)
|
||||||
|
|
||||||
log.Infof("Prefetching %s", picAbsPath)
|
log.Infof("Prefetching %s", picAbsPath)
|
||||||
go ConvertFilter(picAbsPath, avif, webp, config.ExtraParams{Width: 0, Height: 0}, finishChan)
|
|
||||||
|
// Allow all supported formats
|
||||||
|
supported := map[string]bool{
|
||||||
|
"raw": true,
|
||||||
|
"webp": true,
|
||||||
|
"avif": true,
|
||||||
|
"jxl": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
go ConvertFilter(picAbsPath, jxlAbsPath, avifAbsPath, webpAbsPath, config.ExtraParams{Width: 0, Height: 0}, supported, finishChan)
|
||||||
_ = bar.Add(<-finishChan)
|
_ = bar.Add(<-finishChan)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -12,18 +12,69 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error {
|
func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error {
|
||||||
imgHeightWidthRatio := float32(img.Metadata().Height) / float32(img.Metadata().Width)
|
imageHeight := img.Height()
|
||||||
|
imageWidth := img.Width()
|
||||||
|
|
||||||
|
imgHeightWidthRatio := float32(imageHeight) / float32(imageWidth)
|
||||||
|
|
||||||
|
// Here we have width, height and max_width, max_height
|
||||||
|
// Both pairs cannot be used at the same time
|
||||||
|
|
||||||
|
// max_height and max_width are used to make sure bigger images are resized to max_height and max_width
|
||||||
|
// e.g, 500x500px image with max_width=200,max_height=100 will be resized to 100x100
|
||||||
|
// while smaller images are untouched
|
||||||
|
|
||||||
|
// If both are used, we will use width and height
|
||||||
|
|
||||||
|
if extraParams.MaxHeight > 0 && extraParams.MaxWidth > 0 {
|
||||||
|
// If any of it exceeds
|
||||||
|
if imageHeight > extraParams.MaxHeight || imageWidth > extraParams.MaxWidth {
|
||||||
|
// Check which dimension exceeds most
|
||||||
|
heightExceedRatio := float32(imageHeight) / float32(extraParams.MaxHeight)
|
||||||
|
widthExceedRatio := float32(imageWidth) / float32(extraParams.MaxWidth)
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := img.Thumbnail(extraParams.MaxWidth, int(float32(extraParams.MaxWidth)*imgHeightWidthRatio), 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if extraParams.MaxHeight > 0 && imageHeight > extraParams.MaxHeight && extraParams.MaxWidth == 0 {
|
||||||
|
err := img.Thumbnail(int(float32(extraParams.MaxHeight)/imgHeightWidthRatio), extraParams.MaxHeight, 0)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if extraParams.Width > 0 && extraParams.Height > 0 {
|
if extraParams.Width > 0 && extraParams.Height > 0 {
|
||||||
err := img.Thumbnail(extraParams.Width, extraParams.Height, vips.InterestingAttention)
|
err := img.Thumbnail(extraParams.Width, extraParams.Height, vips.InterestingAttention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if extraParams.Width > 0 && extraParams.Height == 0 {
|
}
|
||||||
|
if extraParams.Width > 0 && extraParams.Height == 0 {
|
||||||
err := img.Thumbnail(extraParams.Width, int(float32(extraParams.Width)*imgHeightWidthRatio), 0)
|
err := img.Thumbnail(extraParams.Width, int(float32(extraParams.Width)*imgHeightWidthRatio), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if extraParams.Height > 0 && extraParams.Width == 0 {
|
}
|
||||||
|
if extraParams.Height > 0 && extraParams.Width == 0 {
|
||||||
err := img.Thumbnail(int(float32(extraParams.Height)/imgHeightWidthRatio), extraParams.Height, 0)
|
err := img.Thumbnail(int(float32(extraParams.Height)/imgHeightWidthRatio), extraParams.Height, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -49,6 +100,9 @@ func ResizeItself(raw, dest string, extraParams config.ExtraParams) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = resizeImage(img, extraParams)
|
_ = resizeImage(img, extraParams)
|
||||||
|
if config.Config.StripMetadata {
|
||||||
|
img.RemoveMetadata()
|
||||||
|
}
|
||||||
buf, _, _ := img.ExportNative()
|
buf, _, _ := img.ExportNative()
|
||||||
_ = os.WriteFile(dest, buf, 0600)
|
_ = os.WriteFile(dest, buf, 0600)
|
||||||
img.Close()
|
img.Close()
|
||||||
|
90
encoder/process_test.go
Normal file
90
encoder/process_test.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package encoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"webp_server_go/config"
|
||||||
|
|
||||||
|
"github.com/davidbyttow/govips/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResizeImage(t *testing.T) {
|
||||||
|
img, _ := vips.Black(500, 500)
|
||||||
|
|
||||||
|
// Define the parameters for the test cases
|
||||||
|
testCases := []struct {
|
||||||
|
extraParams config.ExtraParams // Extra parameters
|
||||||
|
expectedH int // Expected height
|
||||||
|
expectedW int // Expected width
|
||||||
|
}{
|
||||||
|
// Tests for MaxHeight and MaxWidth
|
||||||
|
// Both extraParams.MaxHeight and extraParams.MaxWidth are 0
|
||||||
|
{
|
||||||
|
extraParams: config.ExtraParams{
|
||||||
|
MaxHeight: 0,
|
||||||
|
MaxWidth: 0,
|
||||||
|
},
|
||||||
|
expectedH: 500,
|
||||||
|
expectedW: 500,
|
||||||
|
},
|
||||||
|
// Both extraParams.MaxHeight and extraParams.MaxWidth are greater than 0, but the image size is smaller than the limits
|
||||||
|
{
|
||||||
|
extraParams: config.ExtraParams{
|
||||||
|
MaxHeight: 1000,
|
||||||
|
MaxWidth: 1000,
|
||||||
|
},
|
||||||
|
expectedH: 500,
|
||||||
|
expectedW: 500,
|
||||||
|
},
|
||||||
|
// Both extraParams.MaxHeight and extraParams.MaxWidth are greater than 0, and the image exceeds the limits
|
||||||
|
{
|
||||||
|
extraParams: config.ExtraParams{
|
||||||
|
MaxHeight: 200,
|
||||||
|
MaxWidth: 200,
|
||||||
|
},
|
||||||
|
expectedH: 200,
|
||||||
|
expectedW: 200,
|
||||||
|
},
|
||||||
|
// Only MaxHeight is set to 200
|
||||||
|
{
|
||||||
|
extraParams: config.ExtraParams{
|
||||||
|
MaxHeight: 200,
|
||||||
|
MaxWidth: 0,
|
||||||
|
},
|
||||||
|
expectedH: 200,
|
||||||
|
expectedW: 200,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test for Width and Height
|
||||||
|
{
|
||||||
|
extraParams: config.ExtraParams{
|
||||||
|
Width: 200,
|
||||||
|
Height: 200,
|
||||||
|
},
|
||||||
|
expectedH: 200,
|
||||||
|
expectedW: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extraParams: config.ExtraParams{
|
||||||
|
Width: 200,
|
||||||
|
Height: 500,
|
||||||
|
},
|
||||||
|
expectedH: 500,
|
||||||
|
expectedW: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through the test cases and perform the tests
|
||||||
|
for _, tc := range testCases {
|
||||||
|
err := resizeImage(img, tc.extraParams)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("resizeImage failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify if the adjusted image height and width match the expected values
|
||||||
|
actualH := img.Height()
|
||||||
|
actualW := img.Width()
|
||||||
|
if actualH != tc.expectedH || actualW != tc.expectedW {
|
||||||
|
t.Errorf("resizeImage failed: expected (%d, %d), got (%d, %d)", tc.expectedH, tc.expectedW, actualH, actualW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
@ -36,3 +36,5 @@ require (
|
|||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/jeremytorres/rawparser v1.0.2 => github.com/webp-sh/rawparser v0.0.0-20240311121240-15117cd3320a
|
||||||
|
4
go.sum
4
go.sum
@ -17,8 +17,6 @@ github.com/h2non/filetype v1.1.4-0.20230123234534-cfcd7d097bc4 h1:k7FGP5I7raiaC3
|
|||||||
github.com/h2non/filetype v1.1.4-0.20230123234534-cfcd7d097bc4/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
github.com/h2non/filetype v1.1.4-0.20230123234534-cfcd7d097bc4/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc=
|
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc=
|
||||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE=
|
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE=
|
||||||
github.com/jeremytorres/rawparser v1.0.2 h1:xUHpDBSQv+wZhmi5Dc3zEdlpqQj1X8IPIs8ys78NI/A=
|
|
||||||
github.com/jeremytorres/rawparser v1.0.2/go.mod h1:X0j2dOqH3ecGRuWvkThgDy+NKAfIwSN9wAOQlMcFOfY=
|
|
||||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||||
@ -61,6 +59,8 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g
|
|||||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/webp-sh/rawparser v0.0.0-20240311121240-15117cd3320a h1:yFNUYbDL81wQZ7AQmBhkS+ZDfTugwepVI4LUQ/tQBAc=
|
||||||
|
github.com/webp-sh/rawparser v0.0.0-20240311121240-15117cd3320a/go.mod h1:X0j2dOqH3ecGRuWvkThgDy+NKAfIwSN9wAOQlMcFOfY=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"webp_server_go/config"
|
"webp_server_go/config"
|
||||||
"webp_server_go/encoder"
|
"webp_server_go/encoder"
|
||||||
@ -40,11 +39,15 @@ func Convert(c *fiber.Ctx) error {
|
|||||||
proxyMode = config.ProxyMode
|
proxyMode = config.ProxyMode
|
||||||
mapMode = false
|
mapMode = false
|
||||||
|
|
||||||
width, _ = strconv.Atoi(c.Query("width")) // Extra Params
|
width, _ = strconv.Atoi(c.Query("width")) // Extra Params
|
||||||
height, _ = strconv.Atoi(c.Query("height")) // Extra Params
|
height, _ = strconv.Atoi(c.Query("height")) // Extra Params
|
||||||
extraParams = config.ExtraParams{
|
maxHeight, _ = strconv.Atoi(c.Query("max_height")) // Extra Params
|
||||||
Width: width,
|
maxWidth, _ = strconv.Atoi(c.Query("max_width")) // Extra Params
|
||||||
Height: height,
|
extraParams = config.ExtraParams{
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
MaxWidth: maxWidth,
|
||||||
|
MaxHeight: maxHeight,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -133,8 +136,11 @@ func Convert(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
supportedFormats := helper.GuessSupportedFormat(reqHeader)
|
supportedFormats := helper.GuessSupportedFormat(reqHeader)
|
||||||
// resize itself and return if only one format(raw) is supported
|
// resize itself and return if only raw(original format) is supported
|
||||||
if len(supportedFormats) == 1 {
|
if supportedFormats["raw"] == true &&
|
||||||
|
supportedFormats["webp"] == false &&
|
||||||
|
supportedFormats["avif"] == false &&
|
||||||
|
supportedFormats["jxl"] == false {
|
||||||
dest := path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id)
|
dest := path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id)
|
||||||
if !helper.ImageExists(dest) {
|
if !helper.ImageExists(dest) {
|
||||||
encoder.ResizeItself(rawImageAbs, dest, extraParams)
|
encoder.ResizeItself(rawImageAbs, dest, extraParams)
|
||||||
@ -152,16 +158,20 @@ func Convert(c *fiber.Ctx) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
avifAbs, webpAbs := helper.GenOptimizedAbsPath(metadata, targetHostName)
|
avifAbs, webpAbs, jxlAbs := helper.GenOptimizedAbsPath(metadata, targetHostName)
|
||||||
encoder.ConvertFilter(rawImageAbs, avifAbs, webpAbs, extraParams, nil)
|
// Do the convertion based on supported formats and config
|
||||||
|
encoder.ConvertFilter(rawImageAbs, jxlAbs, avifAbs, webpAbs, extraParams, supportedFormats, nil)
|
||||||
|
|
||||||
var availableFiles = []string{rawImageAbs}
|
var availableFiles = []string{rawImageAbs}
|
||||||
if slices.Contains(supportedFormats, "avif") {
|
if supportedFormats["avif"] {
|
||||||
availableFiles = append(availableFiles, avifAbs)
|
availableFiles = append(availableFiles, avifAbs)
|
||||||
}
|
}
|
||||||
if slices.Contains(supportedFormats, "webp") {
|
if supportedFormats["webp"] {
|
||||||
availableFiles = append(availableFiles, webpAbs)
|
availableFiles = append(availableFiles, webpAbs)
|
||||||
}
|
}
|
||||||
|
if supportedFormats["jxl"] {
|
||||||
|
availableFiles = append(availableFiles, jxlAbs)
|
||||||
|
}
|
||||||
|
|
||||||
finalFilename := helper.FindSmallestFiles(availableFiles)
|
finalFilename := helper.FindSmallestFiles(availableFiles)
|
||||||
contentType := helper.GetFileContentType(finalFilename)
|
contentType := helper.GetFileContentType(finalFilename)
|
||||||
|
@ -18,12 +18,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
chromeUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36"
|
chromeUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36"
|
||||||
|
safariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15"
|
||||||
|
safari17UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15" // <- Mac with Safari 17
|
||||||
|
curlUA = "curl/7.64.1"
|
||||||
|
|
||||||
acceptWebP = "image/webp,image/apng,image/*,*/*;q=0.8"
|
acceptWebP = "image/webp,image/apng,image/*,*/*;q=0.8"
|
||||||
acceptAvif = "image/avif,image/*,*/*;q=0.8"
|
acceptAvif = "image/avif,image/*,*/*;q=0.8"
|
||||||
acceptLegacy = "image/jpeg"
|
acceptLegacy = "image/jpeg,image/png"
|
||||||
safariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15"
|
|
||||||
curlUA = "curl/7.64.1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupParam() {
|
func setupParam() {
|
||||||
@ -34,6 +36,7 @@ func setupParam() {
|
|||||||
config.Metadata = "../metadata"
|
config.Metadata = "../metadata"
|
||||||
config.RemoteRaw = "../remote-raw"
|
config.RemoteRaw = "../remote-raw"
|
||||||
config.ProxyMode = false
|
config.ProxyMode = false
|
||||||
|
config.Config.EnableWebP = true
|
||||||
config.Config.EnableAVIF = false
|
config.Config.EnableAVIF = false
|
||||||
config.Config.Quality = 80
|
config.Config.Quality = 80
|
||||||
config.Config.ImageMap = map[string]string{}
|
config.Config.ImageMap = map[string]string{}
|
||||||
@ -103,7 +106,7 @@ func TestConvertDuplicates(t *testing.T) {
|
|||||||
|
|
||||||
// test Chrome
|
// test Chrome
|
||||||
for url, respType := range testLink {
|
for url, respType := range testLink {
|
||||||
for _ = range N {
|
for range N {
|
||||||
resp, data := requestToServer(url, app, chromeUA, acceptWebP)
|
resp, data := requestToServer(url, app, chromeUA, acceptWebP)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
contentType := helper.GetContentType(data)
|
contentType := helper.GetContentType(data)
|
||||||
|
@ -96,12 +96,14 @@ func CheckAllowedType(imgFilename string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenOptimizedAbsPath(metadata config.MetaFile, subdir string) (string, string) {
|
func GenOptimizedAbsPath(metadata config.MetaFile, subdir string) (string, string, string) {
|
||||||
webpFilename := fmt.Sprintf("%s.webp", metadata.Id)
|
webpFilename := fmt.Sprintf("%s.webp", metadata.Id)
|
||||||
avifFilename := fmt.Sprintf("%s.avif", metadata.Id)
|
avifFilename := fmt.Sprintf("%s.avif", metadata.Id)
|
||||||
|
jxlFilename := fmt.Sprintf("%s.jxl", metadata.Id)
|
||||||
webpAbsolutePath := path.Clean(path.Join(config.Config.ExhaustPath, subdir, webpFilename))
|
webpAbsolutePath := path.Clean(path.Join(config.Config.ExhaustPath, subdir, webpFilename))
|
||||||
avifAbsolutePath := path.Clean(path.Join(config.Config.ExhaustPath, subdir, avifFilename))
|
avifAbsolutePath := path.Clean(path.Join(config.Config.ExhaustPath, subdir, avifFilename))
|
||||||
return avifAbsolutePath, webpAbsolutePath
|
jxlAbsolutePath := path.Clean(path.Join(config.Config.ExhaustPath, subdir, jxlFilename))
|
||||||
|
return avifAbsolutePath, webpAbsolutePath, jxlAbsolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCompressionRate(RawImagePath string, optimizedImg string) string {
|
func GetCompressionRate(RawImagePath string, optimizedImg string) string {
|
||||||
@ -119,12 +121,13 @@ func GetCompressionRate(RawImagePath string, optimizedImg string) string {
|
|||||||
return fmt.Sprintf(`%.2f`, compressionRate)
|
return fmt.Sprintf(`%.2f`, compressionRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GuessSupportedFormat(header *fasthttp.RequestHeader) []string {
|
func GuessSupportedFormat(header *fasthttp.RequestHeader) map[string]bool {
|
||||||
var (
|
var (
|
||||||
supported = map[string]bool{
|
supported = map[string]bool{
|
||||||
"raw": true,
|
"raw": true,
|
||||||
"webp": false,
|
"webp": false,
|
||||||
"avif": false,
|
"avif": false,
|
||||||
|
"jxl": false,
|
||||||
}
|
}
|
||||||
|
|
||||||
ua = string(header.Peek("user-agent"))
|
ua = string(header.Peek("user-agent"))
|
||||||
@ -154,14 +157,20 @@ func GuessSupportedFormat(header *fasthttp.RequestHeader) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save true value's key to slice
|
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15 <- iPad
|
||||||
var accepted []string
|
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15 <- Mac
|
||||||
for k, v := range supported {
|
// Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1 <- iPhone @ Safari
|
||||||
if v {
|
supportedJXLs := []string{"iPhone OS 17", "CPU OS 17", "Version/17"}
|
||||||
accepted = append(accepted, k)
|
if strings.Contains(ua, "iPhone") || strings.Contains(ua, "Macintosh") {
|
||||||
|
for _, version := range supportedJXLs {
|
||||||
|
if strings.Contains(ua, version) {
|
||||||
|
supported["jxl"] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return accepted
|
|
||||||
|
return supported
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindSmallestFiles(files []string) string {
|
func FindSmallestFiles(files []string) string {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
"testing"
|
"testing"
|
||||||
"webp_server_go/config"
|
"webp_server_go/config"
|
||||||
|
|
||||||
@ -53,25 +52,41 @@ func TestGuessSupportedFormat(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
userAgent string
|
userAgent string
|
||||||
accept string
|
accept string
|
||||||
expected []string
|
expected map[string]bool
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "WebP/AVIF/JXL Supported",
|
||||||
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15", // iPad
|
||||||
|
accept: "image/webp, image/avif",
|
||||||
|
expected: map[string]bool{
|
||||||
|
"raw": true,
|
||||||
|
"webp": true,
|
||||||
|
"avif": true,
|
||||||
|
"jxl": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "WebP/AVIF Supported",
|
name: "WebP/AVIF Supported",
|
||||||
userAgent: "iPhone OS 16",
|
userAgent: "iPhone OS 16",
|
||||||
accept: "image/webp, image/png",
|
accept: "image/webp, image/png",
|
||||||
expected: []string{"raw", "webp", "avif"},
|
expected: map[string]bool{
|
||||||
|
"raw": true,
|
||||||
|
"webp": true,
|
||||||
|
"avif": true,
|
||||||
|
"jxl": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Both Supported",
|
name: "Both Supported",
|
||||||
userAgent: "iPhone OS 16",
|
userAgent: "iPhone OS 16",
|
||||||
accept: "image/webp, image/avif",
|
accept: "image/webp, image/avif",
|
||||||
expected: []string{"raw", "webp", "avif"},
|
expected: map[string]bool{"raw": true, "webp": true, "avif": true, "jxl": false},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No Supported Formats",
|
name: "No Supported Formats",
|
||||||
userAgent: "Unknown OS",
|
userAgent: "Unknown OS",
|
||||||
accept: "image/jpeg, image/gif",
|
accept: "image/jpeg, image/gif",
|
||||||
expected: []string{"raw"},
|
expected: map[string]bool{"raw": true, "webp": false, "avif": false, "jxl": false},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,10 +102,8 @@ func TestGuessSupportedFormat(t *testing.T) {
|
|||||||
t.Errorf("Expected %v, but got %v", test.expected, result)
|
t.Errorf("Expected %v, but got %v", test.expected, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, format := range test.expected {
|
for k, v := range test.expected {
|
||||||
if !slices.Contains(result, format) {
|
assert.Equal(t, v, result[k])
|
||||||
t.Errorf("Expected format %s is not in the result", format)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,11 @@ func getId(p string) (string, string, string) {
|
|||||||
parsed, _ := url.Parse(p)
|
parsed, _ := url.Parse(p)
|
||||||
width := parsed.Query().Get("width")
|
width := parsed.Query().Get("width")
|
||||||
height := parsed.Query().Get("height")
|
height := parsed.Query().Get("height")
|
||||||
// santizedPath will be /webp_server.jpg?width=200\u0026height= in local mode when requesting /webp_server.jpg?width=200
|
max_width := parsed.Query().Get("max_width")
|
||||||
|
max_height := parsed.Query().Get("max_height")
|
||||||
|
// santizedPath will be /webp_server.jpg?width=200\u0026height=\u0026max_width=\u0026max_height= in local mode when requesting /webp_server.jpg?width=200
|
||||||
// santizedPath will be https://docs.webp.sh/images/webp_server.jpg?width=400 in proxy mode when requesting /images/webp_server.jpg?width=400 with IMG_PATH = https://docs.webp.sh
|
// santizedPath will be https://docs.webp.sh/images/webp_server.jpg?width=400 in proxy mode when requesting /images/webp_server.jpg?width=400 with IMG_PATH = https://docs.webp.sh
|
||||||
santizedPath := parsed.Path + "?width=" + width + "&height=" + height
|
santizedPath := parsed.Path + "?width=" + width + "&height=" + height + "&max_width=" + max_width + "&max_height=" + max_height
|
||||||
id = HashString(santizedPath)
|
id = HashString(santizedPath)
|
||||||
|
|
||||||
return id, path.Join(config.Config.ImgPath, parsed.Path), santizedPath
|
return id, path.Join(config.Config.ImgPath, parsed.Path), santizedPath
|
||||||
|
@ -32,9 +32,9 @@ func TestGetId(t *testing.T) {
|
|||||||
|
|
||||||
// Verify the return values
|
// Verify the return values
|
||||||
parsed, _ := url.Parse(p)
|
parsed, _ := url.Parse(p)
|
||||||
expectedId := HashString(parsed.Path + "?width=400&height=500")
|
expectedId := HashString(parsed.Path + "?width=400&height=500&max_width=&max_height=")
|
||||||
expectedPath := path.Join(config.Config.ImgPath, parsed.Path)
|
expectedPath := path.Join(config.Config.ImgPath, parsed.Path)
|
||||||
expectedSantizedPath := parsed.Path + "?width=400&height=500"
|
expectedSantizedPath := parsed.Path + "?width=400&height=500&max_width=&max_height="
|
||||||
if id != expectedId || jointPath != expectedPath || santizedPath != expectedSantizedPath {
|
if id != expectedId || jointPath != expectedPath || santizedPath != expectedSantizedPath {
|
||||||
t.Errorf("Test case 2 failed: Expected (%s, %s, %s), but got (%s, %s, %s)",
|
t.Errorf("Test case 2 failed: Expected (%s, %s, %s), but got (%s, %s, %s)",
|
||||||
expectedId, expectedPath, expectedSantizedPath, id, jointPath, santizedPath)
|
expectedId, expectedPath, expectedSantizedPath, id, jointPath, santizedPath)
|
||||||
|
@ -47,7 +47,10 @@ func setupLogger() {
|
|||||||
TimeFormat: config.TimeDateFormat,
|
TimeFormat: config.TimeDateFormat,
|
||||||
}))
|
}))
|
||||||
app.Use(recover.New(recover.Config{}))
|
app.Use(recover.New(recover.Config{}))
|
||||||
log.Infoln("WebP Server Go ready.")
|
fmt.Println("Allowed file types as source:", config.Config.AllowedTypes)
|
||||||
|
fmt.Println("Convert to WebP Enabled:", config.Config.EnableWebP)
|
||||||
|
fmt.Println("Convert to AVIF Enabled:", config.Config.EnableAVIF)
|
||||||
|
fmt.Println("Convert to JXL Enabled:", config.Config.EnableJXL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -92,5 +95,4 @@ func main() {
|
|||||||
fmt.Println("WebP Server Go is Running on http://" + listenAddress)
|
fmt.Println("WebP Server Go is Running on http://" + listenAddress)
|
||||||
|
|
||||||
_ = app.Listen(listenAddress)
|
_ = app.Listen(listenAddress)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user