فهرست منبع

JPEG XL (JXL) support

DarthSim 4 ماه پیش
والد
کامیت
3f4edb91f7
9فایلهای تغییر یافته به همراه320 افزوده شده و 2 حذف شده
  1. 2 0
      CHANGELOG.md
  2. 17 2
      config/config.go
  3. 257 0
      imagemeta/jxl.go
  4. 5 0
      imagetype/imagetype.go
  5. 7 0
      options/processing_options.go
  6. 4 0
      processing/processing.go
  7. 16 0
      vips/vips.c
  8. 10 0
      vips/vips.go
  9. 2 0
      vips/vips.h

+ 2 - 0
CHANGELOG.md

@@ -2,6 +2,8 @@
 
 ## [Unreleased]
 ### Add
+- Add JPEL XL (JXL) support.
+- Add [IMGPROXY_ENABLE_JXL_DETECTION](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_ENABLE_JXL_DETECTION), [IMGPROXY_ENFORCE_JXL](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_ENFORCE_JXL), and [IMGPROXY_JXL_EFFORT](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_JXL_EFFORT) configs.
 - (pro) Add [objects_position](https://docs.imgproxy.net/latest/usage/processing#objects-position) processing and info options.
 - (pro) Add [IMGPROXY_OBJECT_DETECTION_SWAP_RB](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_OBJECT_DETECTION_SWAP_RB) config.
 

+ 17 - 2
config/config.go

@@ -54,6 +54,7 @@ var (
 	PngQuantize           bool
 	PngQuantizationColors int
 	AvifSpeed             int
+	JxlEffort             int
 	Quality               int
 	FormatQuality         map[imagetype.Type]int
 	StripMetadata         bool
@@ -68,6 +69,8 @@ var (
 	EnforceWebp         bool
 	EnableAvifDetection bool
 	EnforceAvif         bool
+	EnableJxlDetection  bool
+	EnforceJxl          bool
 	EnableClientHints   bool
 
 	PreferredFormats []imagetype.Type
@@ -253,8 +256,9 @@ func Reset() {
 	PngQuantize = false
 	PngQuantizationColors = 256
 	AvifSpeed = 9
+	JxlEffort = 4
 	Quality = 80
-	FormatQuality = map[imagetype.Type]int{imagetype.AVIF: 65}
+	FormatQuality = map[imagetype.Type]int{imagetype.AVIF: 65, imagetype.JXL: 77}
 	StripMetadata = true
 	KeepCopyright = true
 	StripColorProfile = true
@@ -267,6 +271,8 @@ func Reset() {
 	EnforceWebp = false
 	EnableAvifDetection = false
 	EnforceAvif = false
+	EnableJxlDetection = false
+	EnforceJxl = false
 	EnableClientHints = false
 
 	PreferredFormats = []imagetype.Type{
@@ -475,6 +481,7 @@ func Configure() error {
 	configurators.Bool(&PngQuantize, "IMGPROXY_PNG_QUANTIZE")
 	configurators.Int(&PngQuantizationColors, "IMGPROXY_PNG_QUANTIZATION_COLORS")
 	configurators.Int(&AvifSpeed, "IMGPROXY_AVIF_SPEED")
+	configurators.Int(&JxlEffort, "IMGPROXY_JXL_EFFORT")
 	configurators.Int(&Quality, "IMGPROXY_QUALITY")
 	if err := configurators.ImageTypesQuality(FormatQuality, "IMGPROXY_FORMAT_QUALITY"); err != nil {
 		return err
@@ -491,6 +498,8 @@ func Configure() error {
 	configurators.Bool(&EnforceWebp, "IMGPROXY_ENFORCE_WEBP")
 	configurators.Bool(&EnableAvifDetection, "IMGPROXY_ENABLE_AVIF_DETECTION")
 	configurators.Bool(&EnforceAvif, "IMGPROXY_ENFORCE_AVIF")
+	configurators.Bool(&EnableJxlDetection, "IMGPROXY_ENABLE_JXL_DETECTION")
+	configurators.Bool(&EnforceJxl, "IMGPROXY_ENFORCE_JXL")
 	configurators.Bool(&EnableClientHints, "IMGPROXY_ENABLE_CLIENT_HINTS")
 
 	configurators.URLPath(&HealthCheckPath, "IMGPROXY_HEALTH_CHECK_PATH")
@@ -706,11 +715,17 @@ func Configure() error {
 	}
 
 	if AvifSpeed < 0 {
-		return fmt.Errorf("Avif speed should be greater than 0, now - %d\n", AvifSpeed)
+		return fmt.Errorf("Avif speed should be greater than or equal to 0, now - %d\n", AvifSpeed)
 	} else if AvifSpeed > 9 {
 		return fmt.Errorf("Avif speed can't be greater than 9, now - %d\n", AvifSpeed)
 	}
 
+	if JxlEffort < 1 {
+		return fmt.Errorf("JXL effort should be greater than 0, now - %d\n", JxlEffort)
+	} else if JxlEffort > 9 {
+		return fmt.Errorf("JXL effort can't be greater than 9, now - %d\n", JxlEffort)
+	}
+
 	if Quality <= 0 {
 		return fmt.Errorf("Quality should be greater than 0, now - %d\n", Quality)
 	} else if Quality > 100 {

+ 257 - 0
imagemeta/jxl.go

@@ -0,0 +1,257 @@
+package imagemeta
+
+import (
+	"bytes"
+	"encoding/binary"
+	"io"
+
+	"github.com/imgproxy/imgproxy/v3/imagetype"
+)
+
+const (
+	jxlCodestreamHeaderMinSize = 4
+	jxlCodestreamHeaderMaxSize = 11
+)
+
+var jxlCodestreamMarker = []byte{0xff, 0x0a}
+var jxlISOBMFFMarker = []byte{0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A}
+
+var jxlSizeSizes = []uint64{9, 13, 18, 30}
+
+var jxlRatios = [][]uint64{
+	{1, 1},
+	{12, 10},
+	{4, 3},
+	{3, 2},
+	{16, 9},
+	{5, 4},
+	{2, 1},
+}
+
+type jxlBitReader struct {
+	buf    uint64
+	bufLen uint64
+}
+
+func NewJxlBitReader(data []byte) *jxlBitReader {
+	return &jxlBitReader{
+		buf:    binary.LittleEndian.Uint64(data),
+		bufLen: uint64(len(data) * 8),
+	}
+}
+
+func (br *jxlBitReader) Read(n uint64) (uint64, error) {
+	if n > br.bufLen {
+		return 0, io.EOF
+	}
+
+	mask := uint64(1<<n) - 1
+	res := br.buf & mask
+
+	br.buf >>= n
+	br.bufLen -= n
+
+	return res, nil
+}
+
+type JxlFormatError string
+
+func (e JxlFormatError) Error() string { return "invalid JPEG XL format: " + string(e) }
+
+func jxlReadJxlc(r io.Reader, boxDataSize uint64) ([]byte, error) {
+	if boxDataSize < jxlCodestreamHeaderMinSize {
+		return nil, JxlFormatError("invalid codestream box")
+	}
+
+	toRead := boxDataSize
+	if toRead > jxlCodestreamHeaderMaxSize {
+		toRead = jxlCodestreamHeaderMaxSize
+	}
+
+	return heifReadN(r, toRead)
+}
+
+func jxlReadJxlp(r io.Reader, boxDataSize uint64, codestream []byte) ([]byte, bool, error) {
+	if boxDataSize < 4 {
+		return nil, false, JxlFormatError("invalid jxlp box")
+	}
+
+	jxlpInd, err := heifReadN(r, 4)
+	if err != nil {
+		return nil, false, err
+	}
+
+	last := jxlpInd[0] == 0x80
+
+	readLeft := jxlCodestreamHeaderMaxSize - len(codestream)
+	if readLeft <= 0 {
+		return codestream, last, nil
+	}
+
+	toRead := boxDataSize - 4
+	if uint64(readLeft) < toRead {
+		toRead = uint64(readLeft)
+	}
+
+	data, err := heifReadN(r, toRead)
+	if err != nil {
+		return nil, last, err
+	}
+
+	if codestream == nil {
+		codestream = make([]byte, 0, jxlCodestreamHeaderMaxSize)
+	}
+
+	return append(codestream, data...), last, nil
+}
+
+// We can reuse HEIF functions to read ISO BMFF boxes
+func jxlFindCodestream(r io.Reader) ([]byte, error) {
+	var (
+		codestream []byte
+		last       bool
+	)
+
+	for {
+		boxType, boxDataSize, err := heifReadBoxHeader(r)
+		if err != nil {
+			return nil, err
+		}
+
+		switch boxType {
+		// jxlc box contins full codestream.
+		// We can just read and return its header
+		case "jxlc":
+			codestream, err = jxlReadJxlc(r, boxDataSize)
+			return codestream, err
+
+		// jxlp partial codestream.
+		// We should read its data until we read jxlCodestreamHeaderSize bytes
+		case "jxlp":
+			codestream, last, err = jxlReadJxlp(r, boxDataSize, codestream)
+			if err != nil {
+				return nil, err
+			}
+
+			csLen := len(codestream)
+			if csLen >= jxlCodestreamHeaderMaxSize || (last && csLen >= jxlCodestreamHeaderMinSize) {
+				return codestream, nil
+			}
+
+			if last {
+				return nil, JxlFormatError("invalid codestream box")
+			}
+
+		// Skip other boxes
+		default:
+			if err := heifDiscardN(r, boxDataSize); err != nil {
+				return nil, err
+			}
+		}
+	}
+}
+
+func jxlParseSize(br *jxlBitReader, small bool) (uint64, error) {
+	if small {
+		size, err := br.Read(5)
+		return (size + 1) * 8, err
+	} else {
+		selector, err := br.Read(2)
+		if err != nil {
+			return 0, err
+		}
+
+		sizeSize := jxlSizeSizes[selector]
+		size, err := br.Read(sizeSize)
+
+		return size + 1, err
+	}
+}
+
+func jxlDecodeCodestreamHeader(buf []byte) (width, height uint64, err error) {
+	if len(buf) < jxlCodestreamHeaderMinSize {
+		return 0, 0, JxlFormatError("invalid codestream header")
+	}
+
+	if !bytes.Equal(buf[0:2], jxlCodestreamMarker) {
+		return 0, 0, JxlFormatError("missing codestream marker")
+	}
+
+	br := NewJxlBitReader(buf[2:])
+
+	smallBit, sbErr := br.Read(1)
+	if sbErr != nil {
+		return 0, 0, sbErr
+	}
+
+	small := smallBit == 1
+
+	height, err = jxlParseSize(br, small)
+	if err != nil {
+		return 0, 0, err
+	}
+
+	ratioIdx, riErr := br.Read(3)
+	if riErr != nil {
+		return 0, 0, riErr
+	}
+
+	if ratioIdx == 0 {
+		width, err = jxlParseSize(br, small)
+	} else {
+		ratio := jxlRatios[ratioIdx-1]
+		width = height * ratio[0] / ratio[1]
+	}
+
+	return
+}
+
+func DecodeJxlMeta(r io.Reader) (Meta, error) {
+	var (
+		tmp           [12]byte
+		codestream    []byte
+		width, height uint64
+		err           error
+	)
+
+	if _, err = io.ReadFull(r, tmp[:2]); err != nil {
+		return nil, err
+	}
+
+	if bytes.Equal(tmp[0:2], jxlCodestreamMarker) {
+		if _, err = io.ReadFull(r, tmp[2:]); err != nil {
+			return nil, err
+		}
+
+		codestream = tmp[:]
+	} else {
+		if _, err = io.ReadFull(r, tmp[2:12]); err != nil {
+			return nil, err
+		}
+
+		if !bytes.Equal(tmp[0:12], jxlISOBMFFMarker) {
+			return nil, JxlFormatError("invalid header")
+		}
+
+		codestream, err = jxlFindCodestream(r)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	width, height, err = jxlDecodeCodestreamHeader(codestream)
+	if err != nil {
+		return nil, err
+	}
+
+	return &meta{
+		format: imagetype.JXL,
+		width:  int(width),
+		height: int(height),
+	}, nil
+}
+
+func init() {
+	RegisterFormat(string(jxlCodestreamMarker), DecodeJxlMeta)
+	RegisterFormat(string(jxlISOBMFFMarker), DecodeJxlMeta)
+}

+ 5 - 0
imagetype/imagetype.go

@@ -12,6 +12,7 @@ type Type int
 const (
 	Unknown Type = iota
 	JPEG
+	JXL
 	PNG
 	WEBP
 	GIF
@@ -32,6 +33,7 @@ var (
 	Types = map[string]Type{
 		"jpeg": JPEG,
 		"jpg":  JPEG,
+		"jxl":  JXL,
 		"png":  PNG,
 		"webp": WEBP,
 		"gif":  GIF,
@@ -45,6 +47,7 @@ var (
 
 	mimes = map[Type]string{
 		JPEG: "image/jpeg",
+		JXL:  "image/jxl",
 		PNG:  "image/png",
 		WEBP: "image/webp",
 		GIF:  "image/gif",
@@ -58,6 +61,7 @@ var (
 
 	extensions = map[Type]string{
 		JPEG: ".jpg",
+		JXL:  ".jxl",
 		PNG:  ".png",
 		WEBP: ".webp",
 		GIF:  ".gif",
@@ -149,6 +153,7 @@ func (it Type) SupportsAnimation() bool {
 
 func (it Type) SupportsColourProfile() bool {
 	return it == JPEG ||
+		it == JXL ||
 		it == PNG ||
 		it == WEBP ||
 		it == HEIC ||

+ 7 - 0
options/processing_options.go

@@ -108,6 +108,8 @@ type ProcessingOptions struct {
 	EnforceWebP bool
 	PreferAvif  bool
 	EnforceAvif bool
+	PreferJxl   bool
+	EnforceJxl  bool
 
 	Filename         string
 	ReturnAttachment bool
@@ -1088,6 +1090,11 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
 		po.EnforceAvif = config.EnforceAvif
 	}
 
+	if strings.Contains(headerAccept, "image/jxl") {
+		po.PreferJxl = config.EnableJxlDetection || config.EnforceJxl
+		po.EnforceJxl = config.EnforceJxl
+	}
+
 	if config.EnableClientHints {
 		headerDPR := headers.Get("Sec-CH-DPR")
 		if len(headerDPR) == 0 {

+ 4 - 0
processing/processing.go

@@ -281,6 +281,8 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
 	switch {
 	case po.Format == imagetype.Unknown:
 		switch {
+		case po.PreferJxl && !animated:
+			po.Format = imagetype.JXL
 		case po.PreferAvif && !animated:
 			po.Format = imagetype.AVIF
 		case po.PreferWebP:
@@ -290,6 +292,8 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options
 		default:
 			po.Format = findBestFormat(imgdata.Type, animated, expectAlpha)
 		}
+	case po.EnforceJxl && !animated:
+		po.Format = imagetype.JXL
 	case po.EnforceAvif && !animated:
 		po.Format = imagetype.AVIF
 	case po.EnforceWebP:

+ 16 - 0
vips/vips.c

@@ -63,6 +63,12 @@ vips_jpegload_go(void *buf, size_t len, int shrink, VipsImage **out)
   return vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
 }
 
+int
+vips_jxlload_go(void *buf, size_t len, VipsImage **out)
+{
+  return vips_jxlload_buffer(buf, len, out, "access", VIPS_ACCESS_SEQUENTIAL, NULL);
+}
+
 int
 vips_pngload_go(void *buf, size_t len, VipsImage **out, int unlimited)
 {
@@ -913,6 +919,16 @@ vips_jpegsave_go(VipsImage *in, void **buf, size_t *len, int quality, int interl
       NULL);
 }
 
+int
+vips_jxlsave_go(VipsImage *in, void **buf, size_t *len, int quality, int effort)
+{
+  return vips_jxlsave_buffer(
+      in, buf, len,
+      "Q", quality,
+      "effort", effort,
+      NULL);
+}
+
 int
 vips_pngsave_go(VipsImage *in, void **buf, size_t *len, int interlace, int quantize, int colors)
 {

+ 10 - 0
vips/vips.go

@@ -49,6 +49,7 @@ var vipsConf struct {
 	PngQuantize           C.int
 	PngQuantizationColors C.int
 	AvifSpeed             C.int
+	JxlEffort             C.int
 	PngUnlimited          C.int
 	SvgUnlimited          C.int
 }
@@ -98,6 +99,7 @@ func Init() error {
 	vipsConf.PngQuantize = gbool(config.PngQuantize)
 	vipsConf.PngQuantizationColors = C.int(config.PngQuantizationColors)
 	vipsConf.AvifSpeed = C.int(config.AvifSpeed)
+	vipsConf.JxlEffort = C.int(config.JxlEffort)
 	vipsConf.PngUnlimited = gbool(config.PngUnlimited)
 	vipsConf.SvgUnlimited = gbool(config.SvgUnlimited)
 
@@ -231,6 +233,8 @@ func SupportsLoad(it imagetype.Type) bool {
 	switch it {
 	case imagetype.JPEG:
 		sup = hasOperation("jpegload_buffer")
+	case imagetype.JXL:
+		sup = hasOperation("jxlload_buffer")
 	case imagetype.PNG:
 		sup = hasOperation("pngload_buffer")
 	case imagetype.WEBP:
@@ -262,6 +266,8 @@ func SupportsSave(it imagetype.Type) bool {
 	switch it {
 	case imagetype.JPEG:
 		sup = hasOperation("jpegsave_buffer")
+	case imagetype.JXL:
+		sup = hasOperation("jxlsave_buffer")
 	case imagetype.PNG, imagetype.ICO:
 		sup = hasOperation("pngsave_buffer")
 	case imagetype.WEBP:
@@ -330,6 +336,8 @@ func (img *Image) Load(imgdata *imagedata.ImageData, shrink int, scale float64,
 	switch imgdata.Type {
 	case imagetype.JPEG:
 		err = C.vips_jpegload_go(data, dataSize, C.int(shrink), &tmp)
+	case imagetype.JXL:
+		err = C.vips_jxlload_go(data, dataSize, &tmp)
 	case imagetype.PNG:
 		err = C.vips_pngload_go(data, dataSize, &tmp, vipsConf.PngUnlimited)
 	case imagetype.WEBP:
@@ -401,6 +409,8 @@ func (img *Image) Save(imgtype imagetype.Type, quality int) (*imagedata.ImageDat
 	switch imgtype {
 	case imagetype.JPEG:
 		err = C.vips_jpegsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality), vipsConf.JpegProgressive)
+	case imagetype.JXL:
+		err = C.vips_jxlsave_go(img.VipsImage, &ptr, &imgsize, C.int(quality), vipsConf.JxlEffort)
 	case imagetype.PNG:
 		err = C.vips_pngsave_go(img.VipsImage, &ptr, &imgsize, vipsConf.PngInterlaced, vipsConf.PngQuantize, vipsConf.PngQuantizationColors)
 	case imagetype.WEBP:

+ 2 - 0
vips/vips.h

@@ -16,6 +16,7 @@ int gif_resolution_limit();
 int vips_health();
 
 int vips_jpegload_go(void *buf, size_t len, int shrink, VipsImage **out);
+int vips_jxlload_go(void *buf, size_t len, VipsImage **out);
 int vips_pngload_go(void *buf, size_t len, VipsImage **out, int unlimited);
 int vips_webpload_go(void *buf, size_t len, double scale, int pages, VipsImage **out);
 int vips_gifload_go(void *buf, size_t len, int pages, VipsImage **out);
@@ -81,6 +82,7 @@ int vips_strip(VipsImage *in, VipsImage **out, int keep_exif_copyright);
 int vips_strip_all(VipsImage *in, VipsImage **out);
 
 int vips_jpegsave_go(VipsImage *in, void **buf, size_t *len, int quality, int interlace);
+int vips_jxlsave_go(VipsImage *in, void **buf, size_t *len, int quality, int effort);
 int vips_pngsave_go(VipsImage *in, void **buf, size_t *len, int interlace, int quantize,
     int colors);
 int vips_webpsave_go(VipsImage *in, void **buf, size_t *len, int quality);