check / check (push) Canceled after 0s
A JPEG XL source, a bare codestream or one in its container, is fetched, detected and decoded, and orig of one is JPEG XL. The format jxl works in plain and encrypted URLs and on the generator page, served as image/jxl, and auto chooses it first when Accept names image/jxl. govips sends libvips a JPEG XL distance with every save, and libvips then ignores the quality, so q is turned into a distance with libvips' formula, keeping 100 lossy. govips cannot have libvips strip metadata from JPEG XL, so it is removed from the image before saving; the conversion to sRGB moved ahead of the format switch to run first. Model: opus-5-5
163 lines
4.2 KiB
Go
163 lines
4.2 KiB
Go
package imageprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/davidbyttow/govips/v2/vips"
|
|
)
|
|
|
|
// TestCheckJPEGXLSupport fails when libvips lacks JPEG XL support, as on
|
|
// Alpine without the vips-jxl package.
|
|
func TestCheckJPEGXLSupport(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := CheckJPEGXLSupport()
|
|
if err != nil {
|
|
t.Fatalf("CheckJPEGXLSupport() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLibvipsSavesAndLoadsJPEGXL saves an image as JPEG XL with govips and
|
|
// loads it back. It fails when libvips lacks JPEG XL support, as on Alpine
|
|
// without the vips-jxl package.
|
|
func TestLibvipsSavesAndLoadsJPEGXL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
img, err := vips.NewImageFromBuffer(createTestJPEG(t, 64, 48))
|
|
if err != nil {
|
|
t.Fatalf("failed to load test JPEG: %v", err)
|
|
}
|
|
|
|
defer img.Close()
|
|
|
|
jxl, _, err := img.ExportJxl(vips.NewJxlExportParams())
|
|
if err != nil {
|
|
t.Fatalf("ExportJxl() error = %v", err)
|
|
}
|
|
|
|
loaded, err := vips.NewImageFromBuffer(jxl)
|
|
if err != nil {
|
|
t.Fatalf("failed to load the JPEG XL image: %v", err)
|
|
}
|
|
|
|
defer loaded.Close()
|
|
|
|
if loaded.Format() != vips.ImageTypeJXL {
|
|
t.Errorf("loaded format = %s, want jxl", vips.ImageTypes[loaded.Format()])
|
|
}
|
|
|
|
if loaded.Width() != 64 || loaded.Height() != 48 {
|
|
t.Errorf("loaded size = %dx%d, want 64x48", loaded.Width(), loaded.Height())
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_EncodeJPEGXL converts a JPEG to a smaller JPEG XL and
|
|
// checks the content type, and the format and size the output loads as.
|
|
func TestImageProcessor_EncodeJPEGXL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
req := &Request{Size: Size{Width: 100, Height: 75}, Format: FormatJXL}
|
|
|
|
result, err := New(Params{}).Process(
|
|
t.Context(), bytes.NewReader(createTestJPEG(t, 200, 150)), req,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Process() error = %v", err)
|
|
}
|
|
|
|
defer func() { _ = result.Content.Close() }()
|
|
|
|
if result.ContentType != "image/jxl" {
|
|
t.Errorf("ContentType = %q, want image/jxl", result.ContentType)
|
|
}
|
|
|
|
data, err := io.ReadAll(result.Content)
|
|
if err != nil {
|
|
t.Fatalf("failed to read result: %v", err)
|
|
}
|
|
|
|
output, err := vips.NewImageFromBuffer(data)
|
|
if err != nil {
|
|
t.Fatalf("failed to load the output: %v", err)
|
|
}
|
|
|
|
defer output.Close()
|
|
|
|
if output.Format() != vips.ImageTypeJXL {
|
|
t.Errorf("output format = %s, want jxl", vips.ImageTypes[output.Format()])
|
|
}
|
|
|
|
if output.Width() != 100 || output.Height() != 75 {
|
|
t.Errorf("output size = %dx%d, want 100x75", output.Width(), output.Height())
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_JPEGXLQuality verifies that the quality reaches the JPEG
|
|
// XL encoder: the same image comes out smaller at quality 30 than at 90.
|
|
func TestImageProcessor_JPEGXLQuality(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := createTestJPEG(t, 400, 300)
|
|
outputBytes := make(map[int]int64)
|
|
|
|
for _, quality := range []int{30, 90} {
|
|
result, err := New(Params{}).Process(t.Context(),
|
|
bytes.NewReader(input), &Request{Format: FormatJXL, Quality: quality})
|
|
if err != nil {
|
|
t.Fatalf("Process() at quality %d error = %v", quality, err)
|
|
}
|
|
|
|
_ = result.Content.Close()
|
|
outputBytes[quality] = result.ContentLength
|
|
}
|
|
|
|
if outputBytes[30] >= outputBytes[90] {
|
|
t.Errorf("%d bytes at quality 30, %d at 90, want fewer at 30",
|
|
outputBytes[30], outputBytes[90])
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_JPEGXLStripsEXIF verifies that a JPEG XL output carries
|
|
// no EXIF, like the other formats in TestImageProcessor_StripsEXIF.
|
|
func TestImageProcessor_JPEGXLStripsEXIF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input, err := os.ReadFile("testdata/gps-exif.jpg")
|
|
if err != nil {
|
|
t.Fatalf("failed to read test JPEG: %v", err)
|
|
}
|
|
|
|
output := processAndDecode(t, input, &Request{Format: FormatJXL})
|
|
|
|
if output.HasExif() {
|
|
t.Errorf("output has EXIF: %v", output.GetExif())
|
|
}
|
|
}
|
|
|
|
// TestJXLDistance verifies the JPEG XL distance for a few qualities: 90 is
|
|
// distance 1, the encoder's own default, and 100 stays lossy.
|
|
func TestJXLDistance(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
quality int
|
|
want float64
|
|
}{
|
|
{quality: 100, want: 0.1},
|
|
{quality: 90, want: 1},
|
|
{quality: 30, want: 6.4},
|
|
{quality: 20, want: 9.0667},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := jxlDistance(tt.quality)
|
|
if math.Abs(got-tt.want) > 0.0001 {
|
|
t.Errorf("jxlDistance(%d) = %v, want %v", tt.quality, got, tt.want)
|
|
}
|
|
}
|
|
}
|