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_JPEGXLDropsSourceEXIF verifies that none of the source's // EXIF reaches a JPEG XL output. libvips 8.16 and later write an EXIF block of // their own into JPEG XL (orientation, resolution, size, colour space and // fixed defaults), and govips cannot ask them to leave it out, so the test // looks for the source's fields rather than for no EXIF at all. func TestImageProcessor_JPEGXLDropsSourceEXIF(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) } exif := processAndDecode(t, input, &Request{Format: FormatJXL}).GetExif() for _, field := range []string{ "exif-ifd0-Make", "exif-ifd0-Model", "exif-ifd2-BodySerialNumber", "exif-ifd2-DateTimeOriginal", "exif-ifd3-GPSLatitude", "exif-ifd3-GPSLongitude", } { if value, found := exif[field]; found { t.Errorf("output has %s: %s", field, value) } } } // 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) } } }