Skip to content

Commit

Permalink
Merge pull request #41 from Pixboost/feature/jxl
Browse files Browse the repository at this point in the history
Feature/jxl
  • Loading branch information
dooman87 authored Oct 23, 2023
2 parents 6096d7b + ff267f6 commit 8dc2ee7
Show file tree
Hide file tree
Showing 14 changed files with 461 additions and 337 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM dpokidov/imagemagick:7.1.1-10-bullseye AS build
FROM dpokidov/imagemagick:7.1.1-17-bullseye AS build

RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
g++ \
Expand Down Expand Up @@ -134,7 +134,7 @@ WORKDIR /go/src/github.com/Pixboost/transformimgs/cmd

RUN go build -o /transformimgs

FROM dpokidov/imagemagick:7.1.1-10-bullseye
FROM dpokidov/imagemagick:7.1.1-17-bullseye

ENV IM_HOME /usr/local/bin

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM dpokidov/imagemagick:7.1.1-10-bullseye
FROM dpokidov/imagemagick:7.1.1-17-bullseye

RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
g++ \
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
[![Docker Automated build](https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg)](https://hub.docker.com/r/pixboost/transformimgs/)

Open Source [Image CDN](https://web.dev/image-cdns/) that provides image transformation API and supports
the latest image formats, such as WebP, AVIF and network client hints.
the latest image formats, such as WebP, AVIF, Jpeg XL, and network client hints.


## Table of Contents
Expand Down Expand Up @@ -157,6 +157,11 @@ $ jmeter -n -t perf-test-webp.jmx -l ./results-webp.jmx -e -o ./results-webp
$ jmeter -n -t perf-test-avif.jmx -l ./results-avif.jmx -e -o ./results-avif
```

* Run JMeter JPEG XL test:
```
$ jmeter -n -t perf-test-jxl.jmx -l ./results-jxl.jmx -e -o ./results-jxl
```


## Opened tickets for images related features

Expand Down
64 changes: 47 additions & 17 deletions img/processor/imagemagick.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,14 @@ const (
// MaxAVIFTargetSize is a maximum size in pixels of the result image
// that could be converted to AVIF.
//
// There are two aspects to this:
// * Encoding to AVIF consumes a lot of memory
// * On big sizes quality of Webp is better (could be a codec thing rather than a format)
// This is mainly done because encoding to AVIF consumes a lot of memory, and CPU time
MaxAVIFTargetSize = 2000 * 2000

MaxJxlLossyTargetSize = 1000 * 1000

JxlMime = "image/jxl"
WebpMime = "image/webp"
AvifMime = "image/avif"
)

func init() {
Expand Down Expand Up @@ -449,22 +453,29 @@ func (p *ImageMagick) isIllustration(src *img.Image, info *img.Info) (bool, erro
func getOutputFormat(src *img.Info, target *img.Info, supportedFormats []string) (string, string) {
webP := false
avif := false
jxl := false
for _, f := range supportedFormats {
if f == "image/webp" && src.Height < MaxWebpHeight && src.Width < MaxWebpWidth {
if f == WebpMime && src.Height < MaxWebpHeight && src.Width < MaxWebpWidth {
webP = true
}

targetSize := target.Width * target.Height
if f == "image/avif" && src.Format != "GIF" && !src.Illustration && targetSize < MaxAVIFTargetSize && targetSize != 0 {
if f == AvifMime && src.Format != "GIF" && targetSize < MaxAVIFTargetSize && targetSize != 0 {
avif = true
}
}

if avif {
return "avif:-", "image/avif"
if f == JxlMime && src.Format != "GIF" && (src.Illustration || targetSize < MaxJxlLossyTargetSize) {
jxl = true
}
}
if webP {
return "webp:-", "image/webp"

switch {
case (src.Illustration && jxl) || (jxl && !avif):
return "jxl:-", JxlMime
case avif && !src.Illustration:
return "avif:-", AvifMime
case webP:
return "webp:-", WebpMime
}

return "-", ""
Expand All @@ -473,7 +484,9 @@ func getOutputFormat(src *img.Info, target *img.Info, supportedFormats []string)
func getConvertFormatOptions(source *img.Info) []string {
var opts []string
if source.Illustration {
opts = append(opts, "-define", "webp:lossless=true")
opts = append(opts, "-define", "webp:lossless=true", "-quality", "100", "-define", "jxl:effort=9")
} else {
opts = append(opts, "-define", "jxl:effort=7")
}
if source.Format != "GIF" {
opts = append(opts, "-define", "webp:method=6")
Expand All @@ -497,23 +510,40 @@ func getQualityOptions(source *img.Info, config *img.TransformationConfig, outpu

img.Log.Printf("[%s] Getting quality for the image, source quality: %d, quality: %d, output type: %s", config.Src.Id, source.Quality, config.Quality, outputMimeType)

if outputMimeType == "image/avif" {
if source.Quality > 85 {
if source.Illustration {
return []string{}
}

switch {
case outputMimeType == AvifMime:
switch {
case source.Quality > 85:
quality = 70
} else if source.Quality > 75 {
case source.Quality > 75:
quality = 60
} else {
default:
quality = 50
}
} else if source.Quality == 100 {
case outputMimeType == JxlMime:
switch {
case source.Quality > 85:
quality = 82
case source.Quality > 75:
quality = 72
default:
quality = 62
}
case source.Quality == 100:
quality = 82
} else if config.Quality != img.DEFAULT {
case config.Quality != img.DEFAULT:
quality = source.Quality
}

if quality == 0 {
return []string{}
}

// If using lossy compression, then we can go lower
if quality != 100 {
switch config.Quality {
case img.LOW:
Expand Down
122 changes: 115 additions & 7 deletions img/processor/imagemagick_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ func BenchmarkImageMagickProcessor_Optimise_Avif(b *testing.B) {
benchmarkWithFormats(b, []string{"image/avif"})
}

func BenchmarkImageMagickProcessor_Optimise_Jxl(b *testing.B) {
benchmarkWithFormats(b, []string{"image/jxl"})
}

func benchmarkWithFormats(b *testing.B, formats []string) {
f := fmt.Sprintf("%s/%s", "./test_files/transformations", "medium-jpeg.jpg")

Expand Down Expand Up @@ -319,6 +323,29 @@ func TestImageMagickProcessor_Optimise_Avif(t *testing.T) {
})
}

func TestImageMagickProcessor_Optimise_Jxl(t *testing.T) {
testImages(t, func(orig []byte, imgId string) (*img.Image, error) {
return proc.Optimise(&img.TransformationConfig{
Src: &img.Image{
Id: imgId,
Data: orig,
},
SupportedFormats: []string{"image/jxl"},
})
},
[]*testTransformation{
{"big-jpeg.jpg", ""},
{"medium-jpeg.jpg", "image/jxl"},
{"opaque-png.png", "image/jxl"},
{"animated.gif", ""},
{"animated-coalesce.gif", ""},
{"transparent-png.png", "image/jxl"},
{"small-transparent-png.png", "image/jxl"},
{"transparent-png-use-original.png", "image/jxl"},
{"logo.png", "image/jxl"},
})
}

func TestImageMagickProcessor_Optimise_Avif_Webp(t *testing.T) {
qualities := []img.Quality{img.DEFAULT, img.LOW, img.LOWER}

Expand Down Expand Up @@ -349,6 +376,66 @@ func TestImageMagickProcessor_Optimise_Avif_Webp(t *testing.T) {
}
}

func TestImageMagickProcessor_Optimise_Jxl_Avif_Webp(t *testing.T) {
qualities := []img.Quality{img.DEFAULT, img.LOW, img.LOWER}

for _, q := range qualities {
t.Run(fmt.Sprintf("Quality_%d", q), func(t *testing.T) {
testImages(t, func(orig []byte, imgId string) (*img.Image, error) {
return proc.Optimise(&img.TransformationConfig{
Src: &img.Image{
Id: imgId,
Data: orig,
},
Quality: q,
SupportedFormats: []string{"image/jxl", "image/avif", "image/webp"},
})
},
[]*testTransformation{
{"big-jpeg.jpg", "image/webp"},
{"medium-jpeg.jpg", "image/avif"},
{"opaque-png.png", "image/avif"},
{"animated.gif", "image/webp"},
{"animated-coalesce.gif", "image/webp"},
{"transparent-png.png", "image/avif"},
{"small-transparent-png.png", "image/jxl"},
{"transparent-png-use-original.png", "image/jxl"},
{"logo.png", "image/jxl"},
})
})
}
}

func TestImageMagickProcessor_Optimise_Jxl_Webp(t *testing.T) {
qualities := []img.Quality{img.DEFAULT, img.LOW, img.LOWER}

for _, q := range qualities {
t.Run(fmt.Sprintf("Quality_%d", q), func(t *testing.T) {
testImages(t, func(orig []byte, imgId string) (*img.Image, error) {
return proc.Optimise(&img.TransformationConfig{
Src: &img.Image{
Id: imgId,
Data: orig,
},
Quality: q,
SupportedFormats: []string{"image/jxl", "image/webp"},
})
},
[]*testTransformation{
{"big-jpeg.jpg", "image/webp"},
{"medium-jpeg.jpg", "image/jxl"},
{"opaque-png.png", "image/jxl"},
{"animated.gif", "image/webp"},
{"animated-coalesce.gif", "image/webp"},
{"transparent-png.png", "image/jxl"},
{"small-transparent-png.png", "image/jxl"},
{"transparent-png-use-original.png", "image/jxl"},
{"logo.png", "image/jxl"},
})
})
}
}

func TestImageMagickProcessor_Resize_Avif(t *testing.T) {
testImages(t, func(orig []byte, imgId string) (*img.Image, error) {
return proc.Resize(&img.TransformationConfig{
Expand All @@ -371,25 +458,46 @@ func TestImageMagickProcessor_Resize_Avif(t *testing.T) {
})
}

func TestImageMagickProcessor_FitToSize_Avif(t *testing.T) {
func TestImageMagickProcessor_Resize_Jxl(t *testing.T) {
testImages(t, func(orig []byte, imgId string) (*img.Image, error) {
return proc.Resize(&img.TransformationConfig{
Src: &img.Image{
Id: imgId,
Data: orig,
},
SupportedFormats: []string{"image/jxl"},
Config: &img.ResizeConfig{Size: "50"},
})
},
[]*testTransformation{
{"big-jpeg.jpg", "image/jxl"},
{"medium-jpeg.jpg", "image/jxl"},
{"opaque-png.png", "image/jxl"},
{"animated.gif", ""},
{"transparent-png-use-original.png", "image/jxl"},
{"logo.png", "image/jxl"},
})
}

func TestImageMagickProcessor_FitToSize_Jxl(t *testing.T) {
testImages(t, func(orig []byte, imgId string) (*img.Image, error) {
return proc.FitToSize(&img.TransformationConfig{
Src: &img.Image{
Id: imgId,
Data: orig,
},
SupportedFormats: []string{"image/avif"},
SupportedFormats: []string{"image/jxl"},
Config: &img.ResizeConfig{Size: "50x50"},
})
},
[]*testTransformation{
{"big-jpeg.jpg", "image/avif"},
{"medium-jpeg.jpg", "image/avif"},
{"opaque-png.png", "image/avif"},
{"big-jpeg.jpg", "image/jxl"},
{"medium-jpeg.jpg", "image/jxl"},
{"opaque-png.png", "image/jxl"},
{"animated.gif", ""},
{"animated-coalesce.gif", ""},
{"transparent-png-use-original.png", ""},
{"logo.png", ""},
{"transparent-png-use-original.png", "image/jxl"},
{"logo.png", "image/jxl"},
})
}

Expand Down
26 changes: 11 additions & 15 deletions perf-test-avif.jmx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.3">
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
Expand All @@ -14,8 +14,8 @@
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Users" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">10</stringProp>
<boolProp name="LoopController.continue_forever">false</boolProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">50</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
Expand All @@ -25,6 +25,7 @@
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
<boolProp name="ThreadGroup.delayedStart">false</boolProp>
</ThreadGroup>
<hashTree>
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
Expand All @@ -33,31 +34,26 @@
</elementProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/img</stringProp>
<stringProp name="HTTPSampler.concurrentPool">6</stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</ConfigTestElement>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/img/http%3A%2F%2Fnginx/HT_Paper.png/optimise</stringProp>
<stringProp name="HTTPSampler.path">/img/http%3A%2F%2Fnginx/transformations/big-jpeg.jpg/fit?size=1900x1900</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
<boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">false</boolProp>
<boolProp name="HTTPSampler.image_parser">false</boolProp>
<boolProp name="HTTPSampler.concurrentDwn">false</boolProp>
<stringProp name="HTTPSampler.concurrentPool">6</stringProp>
<boolProp name="HTTPSampler.md5">false</boolProp>
<intProp name="HTTPSampler.ipSourceType">0</intProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
Expand Down
Loading

0 comments on commit 8dc2ee7

Please sign in to comment.