package imageprocessor import ( "bytes" "io" "math" "os" "testing" "github.com/davidbyttow/govips/v2/vips" ) // 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) } } } // TestImageProcessor_JPEGXLAppliesEXIFOrientation verifies that a JPEG XL // output is turned upright, as TestImageProcessor_AppliesEXIFOrientation does // for PNG. func TestImageProcessor_JPEGXLAppliesEXIFOrientation(t *testing.T) { t.Parallel() // orientation-6.jpg is stored 16x8, red on the left and blue on the // right, with EXIF orientation 6 (turn 90 degrees clockwise to view). // Upright it is 8x16, red on top and blue below. input, err := os.ReadFile("testdata/orientation-6.jpg") if err != nil { t.Fatalf("failed to read test JPEG: %v", err) } output := processAndDecode(t, input, &Request{Format: FormatJXL}) if output.Width() != 8 || output.Height() != 16 { t.Fatalf("output is %dx%d, want 8x16", output.Width(), output.Height()) } top, err := output.GetPoint(4, 0) if err != nil { t.Fatalf("GetPoint() error = %v", err) } bottom, err := output.GetPoint(4, 15) if err != nil { t.Fatalf("GetPoint() error = %v", err) } if top[0] <= top[2] || bottom[2] <= bottom[0] { t.Errorf("top pixel = %v, bottom pixel = %v, want red above blue", top, bottom) } } // TestImageProcessor_JPEGXLConvertsWideGamutToSRGB verifies that a JPEG XL // output is converted to sRGB, as TestImageProcessor_ConvertsWideGamutToSRGB // does for PNG. It does not check for an ICC profile, as libvips reports one // for every JPEG XL image it loads. func TestImageProcessor_JPEGXLConvertsWideGamutToSRGB(t *testing.T) { t.Parallel() // display-p3.jpg is a flat 8x8 image with the Display P3 profile // embedded, filled with Display P3 (234, 51, 35), which is sRGB red. input, err := os.ReadFile("testdata/display-p3.jpg") if err != nil { t.Fatalf("failed to read test JPEG: %v", err) } output := processAndDecode(t, input, &Request{Format: FormatJXL}) pixel, err := output.GetPoint(4, 4) if err != nil { t.Fatalf("GetPoint() error = %v", err) } want := []float64{255, 0, 0} for i := range want { if math.Abs(pixel[i]-want[i]) > 5 { t.Fatalf("pixel = %v, want within 5 of %v", pixel, want) } } } // TestImageProcessor_JPEGXLFromCMYK verifies that a CMYK JPEG with no ICC // profile can be served as JPEG XL, in sRGB. func TestImageProcessor_JPEGXLFromCMYK(t *testing.T) { t.Parallel() // cmyk.jpg is a flat 8x8 CMYK image with no ICC profile, filled with // full cyan and no magenta, yellow or black. input, err := os.ReadFile("testdata/cmyk.jpg") if err != nil { t.Fatalf("failed to read test JPEG: %v", err) } output := processAndDecode(t, input, &Request{Format: FormatJXL}) if output.Bands() != 3 { t.Fatalf("output has %d bands, want 3", output.Bands()) } pixel, err := output.GetPoint(4, 4) if err != nil { t.Fatalf("GetPoint() error = %v", err) } // Cyan in sRGB: little red, much green and blue. if pixel[0] > 50 || pixel[1] < 100 || pixel[2] < 200 { t.Errorf("pixel = %v, want cyan", pixel) } } // 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) } } }