diff --git a/resources/jxl-8bit-grey-icc-dot-gain.jxl b/resources/jxl-8bit-grey-icc-dot-gain.jxl new file mode 100644 index 00000000..e84177fb Binary files /dev/null and b/resources/jxl-8bit-grey-icc-dot-gain.jxl differ diff --git a/vips/foreign.c b/vips/foreign.c index 1e1b723c..f374f8ec 100644 --- a/vips/foreign.c +++ b/vips/foreign.c @@ -153,6 +153,11 @@ int set_jp2kload_options(VipsOperation *operation, LoadParams *params) { return 0; } +int set_jxlload_options(VipsOperation *operation, LoadParams *params) { + // nothing need to do + return 0; +} + int set_magickload_options(VipsOperation *operation, LoadParams *params) { MAYBE_SET_INT(operation, params->page, "page"); MAYBE_SET_INT(operation, params->n, "n"); @@ -399,6 +404,19 @@ int set_jp2ksave_options(VipsOperation *operation, SaveParams *params) { return ret; } +int set_jxlsave_options(VipsOperation *operation, SaveParams *params) { + int ret = vips_object_set( + VIPS_OBJECT(operation), "tier", params->jxlTier, + "distance", params->jxlDistance, "effort", params->jxlEffort, + "lossless", params->jxlLossless, NULL); + + if (!ret && params->quality) { + ret = vips_object_set(VIPS_OBJECT(operation), "Q", params->quality, NULL); + } + + return ret; +} + int load_from_buffer(LoadParams *params, void *buf, size_t len) { switch (params->inputFormat) { case JPEG: @@ -431,9 +449,12 @@ int load_from_buffer(LoadParams *params, void *buf, size_t len) { case AVIF: return load_buffer("heifload_buffer", buf, len, params, set_heifload_options); - case JP2K: + case JP2K: return load_buffer("jp2kload_buffer", buf, len, params, set_jp2kload_options); + case JXL: + return load_buffer("jxlload_buffer", buf, len, params, + set_jxlload_options); default: g_warning("Unsupported input type given: %d", params->inputFormat); } @@ -462,6 +483,8 @@ int save_to_buffer(SaveParams *params) { return save_buffer("heifsave_buffer", params, set_avifsave_options); case JP2K: return save_buffer("jp2ksave_buffer", params, set_jp2ksave_options); + case JXL: + return save_buffer("jxlsave_buffer", params, set_jxlsave_options); default: g_warning("Unsupported output type given: %d", params->outputFormat); } diff --git a/vips/foreign.go b/vips/foreign.go index 035dec43..a8e22ae5 100644 --- a/vips/foreign.go +++ b/vips/foreign.go @@ -44,6 +44,7 @@ const ( ImageTypeBMP ImageType = C.BMP ImageTypeAVIF ImageType = C.AVIF ImageTypeJP2K ImageType = C.JP2K + ImageTypeJXL ImageType = C.JXL ) var imageTypeExtensionMap = map[ImageType]string{ @@ -59,6 +60,7 @@ var imageTypeExtensionMap = map[ImageType]string{ ImageTypeBMP: ".bmp", ImageTypeAVIF: ".avif", ImageTypeJP2K: ".jp2", + ImageTypeJXL: ".jxl", } // ImageTypes defines the various image types supported by govips @@ -75,6 +77,7 @@ var ImageTypes = map[ImageType]string{ ImageTypeBMP: "bmp", ImageTypeAVIF: "heif", ImageTypeJP2K: "jp2k", + ImageTypeJXL: "jxl", } // TiffCompression represents method for compressing a tiff at export @@ -157,6 +160,8 @@ func DetermineImageType(buf []byte) ImageType { return ImageTypeBMP } else if isJP2K(buf) { return ImageTypeJP2K + } else if isJXL(buf) { + return ImageTypeJXL } else { return ImageTypeUnknown } @@ -252,6 +257,12 @@ func isJP2K(buf []byte) bool { return bytes.HasPrefix(buf, jp2kHeader) } +var jxlHeader = []byte("\xff\x0a") + +func isJXL(buf []byte) bool { + return bytes.HasPrefix(buf, jxlHeader) +} + func vipsLoadFromBuffer(buf []byte, params *ImportParams) (*C.VipsImage, ImageType, ImageType, error) { src := buf // Reference src here so it's not garbage collected during image initialization. @@ -461,6 +472,21 @@ func vipsSaveGIFToBuffer(in *C.VipsImage, params GifExportParams) ([]byte, error return vipsSaveToBuffer(p) } +func vipsSaveJxlToBuffer(in *C.VipsImage, params JxlExportParams) ([]byte, error) { + incOpCounter("save_jxl_buffer") + + p := C.create_save_params(C.JXL) + p.inputImage = in + p.outputFormat = C.JXL + p.quality = C.int(params.Quality) + p.jxlLossless = C.int(boolToInt(params.Lossless)) + p.jxlTier = C.int(params.Tier) + p.jxlDistance = C.double(params.Distance) + p.jxlEffort = C.int(params.Effort) + + return vipsSaveToBuffer(p) +} + func vipsSaveToBuffer(params C.struct_SaveParams) ([]byte, error) { if err := C.save_to_buffer(¶ms); err != 0 { return nil, handleSaveBufferError(params.outputBuffer) diff --git a/vips/foreign.h b/vips/foreign.h index fcbd1bc8..f9d0fd8b 100644 --- a/vips/foreign.h +++ b/vips/foreign.h @@ -25,7 +25,8 @@ typedef enum types { HEIF, BMP, AVIF, - JP2K + JP2K, + JXL } ImageType; typedef enum ParamType { @@ -124,6 +125,12 @@ typedef struct SaveParams { BOOL jp2kLossless; int jp2kTileWidth; int jp2kTileHeight; + + // JXL + int jxlTier; + double jxlDistance; + int jxlEffort; + BOOL jxlLossless; } SaveParams; SaveParams create_save_params(ImageType outputFormat); diff --git a/vips/foreign_test.go b/vips/foreign_test.go index 9a49e154..5cd62d63 100644 --- a/vips/foreign_test.go +++ b/vips/foreign_test.go @@ -138,3 +138,14 @@ func Test_DetermineImageType__JP2K(t *testing.T) { imageType := DetermineImageType(buf) assert.Equal(t, ImageTypeJP2K, imageType) } + +func Test_DetermineImageType__JXL(t *testing.T) { + Startup(&Config{}) + + buf, err := ioutil.ReadFile(resources + "jxl-8bit-grey-icc-dot-gain.jxl") + assert.NoError(t, err) + assert.NotNil(t, buf) + + imageType := DetermineImageType(buf) + assert.Equal(t, ImageTypeJXL, imageType) +} diff --git a/vips/header.go b/vips/header.go index f5e58bc5..a85e47c5 100644 --- a/vips/header.go +++ b/vips/header.go @@ -173,6 +173,9 @@ func vipsDetermineImageTypeFromMetaLoader(in *C.VipsImage) ImageType { if strings.HasPrefix(vipsLoader, "jp2k") { return ImageTypeJP2K } + if strings.HasPrefix(vipsLoader, "jxl") { + return ImageTypeJXL + } if strings.HasPrefix(vipsLoader, "magick") { return ImageTypeMagick } diff --git a/vips/image.go b/vips/image.go index 7d4339f5..42e334e1 100644 --- a/vips/image.go +++ b/vips/image.go @@ -385,6 +385,24 @@ func NewJp2kExportParams() *Jp2kExportParams { } } +// JxlExportParams are options when exporting an JXL to file or buffer. +type JxlExportParams struct { + Quality int + Lossless bool + Tier int + Distance float64 + Effort int +} + +// NewJxlExportParams creates default values for an export of an JXL image. +func NewJxlExportParams() *JxlExportParams { + return &JxlExportParams{ + Quality: 75, + Lossless: false, + Effort: 7, + } +} + // NewImageFromReader loads an ImageRef from the given reader func NewImageFromReader(r io.Reader) (*ImageRef, error) { buf, err := ioutil.ReadAll(r) @@ -844,6 +862,12 @@ func (r *ImageRef) Export(params *ExportParams) ([]byte, *ImageMetadata, error) Lossless: params.Lossless, Speed: params.Speed, }) + case ImageTypeJXL: + return r.ExportJxl(&JxlExportParams{ + Quality: params.Quality, + Lossless: params.Lossless, + Effort: params.Effort, + }) default: format = ImageTypeJPEG return r.ExportJpeg(&JpegExportParams{ @@ -879,6 +903,8 @@ func (r *ImageRef) ExportNative() ([]byte, *ImageMetadata, error) { return r.ExportJp2k(NewJp2kExportParams()) case ImageTypeGIF: return r.ExportGIF(NewGifExportParams()) + case ImageTypeJXL: + return r.ExportJxl(NewJxlExportParams()) default: return r.ExportJpeg(NewJpegExportParams()) } @@ -999,6 +1025,20 @@ func (r *ImageRef) ExportJp2k(params *Jp2kExportParams) ([]byte, *ImageMetadata, return buf, r.newMetadata(ImageTypeJP2K), nil } +// ExportJxl exports the image as JPEG XL to a buffer. +func (r *ImageRef) ExportJxl(params *JxlExportParams) ([]byte, *ImageMetadata, error) { + if params == nil { + params = NewJxlExportParams() + } + + buf, err := vipsSaveJxlToBuffer(r.image, *params) + if err != nil { + return nil, nil, err + } + + return buf, r.newMetadata(ImageTypeJXL), nil +} + // CompositeMulti composites the given overlay image on top of the associated image with provided blending mode. func (r *ImageRef) CompositeMulti(ins []*ImageComposite) error { out, err := vipsComposite(toVipsCompositeStructs(r, ins))