diff --git a/xpra/client/gl/gl_window_backing_base.py b/xpra/client/gl/gl_window_backing_base.py index c4d3688e45..3cea7d1b32 100644 --- a/xpra/client/gl/gl_window_backing_base.py +++ b/xpra/client/gl/gl_window_backing_base.py @@ -1071,6 +1071,20 @@ def paint_webp(self, img_data, x : int, y : int, width : int, height : int, opti return super().paint_webp(img_data, x, y, width, height, options, callbacks) + def paint_avif(self, img_data, x, y, width, height, options, callbacks): + alpha = options.boolget("alpha") + img = self.avif_decoder.decompress(img_data, options, yuv=not alpha) + pixel_format = img.get_pixel_format() + flush = options.intget("flush", 0) + w = img.get_width() + h = img.get_height() + if pixel_format.startswith("YUV"): + self.idle_add(self.gl_paint_planar, YUV2RGB_FULL_SHADER, flush, "avif", img, + x, y, w, h, width, height, options, callbacks) + else: + self.idle_add(self.do_paint_rgb, pixel_format, img.get_pixels(), x, y, w, h, width, height, + img.get_rowstride(), options, callbacks) + def do_paint_rgb(self, rgb_format, img_data, x : int, y : int, width : int, height : int, render_width : int, render_height : int, rowstride, options, callbacks): diff --git a/xpra/codecs/avif/decoder.pyx b/xpra/codecs/avif/decoder.pyx index 1d03a9b160..79d6d81bbe 100644 --- a/xpra/codecs/avif/decoder.pyx +++ b/xpra/codecs/avif/decoder.pyx @@ -4,6 +4,7 @@ # later version. See the file COPYING for details. from xpra.codecs.image_wrapper import ImageWrapper +from xpra.codecs.codec_constants import get_subsampling_divs from xpra.codecs.codec_debug import may_save_image from libc.string cimport memset #pylint: disable=syntax-error @@ -68,7 +69,7 @@ cdef check(avifResult r, message): err = avifResultToString(r).decode("latin1") or AVIF_RESULT.get(r, r) raise Exception("%s : %s" % (message, err)) -def decompress(data, options=None): +def decompress(data, options=None, yuv=False): cdef avifRGBImage rgb memset(&rgb, 0, sizeof(avifRGBImage)) cdef avifDecoder * decoder = avifDecoderCreate() @@ -82,7 +83,7 @@ def decompress(data, options=None): cdef avifResult r cdef size_t data_len cdef const uint8_t* data_buf - cdef uint32_t width, height, stride + cdef uint32_t width, height, stride, ydiv, size cdef uint8_t bpp = 32 cdef MemBuf pixels cdef avifImage *image = NULL @@ -108,12 +109,26 @@ def decompress(data, options=None): # * overall image sequence timing (including per-frame timing with avifDecoderNthImageTiming()) width = image.width height = image.height - log("avif parsed: %ux%u (%ubpc)\n", width, height, image.depth) r = avifDecoderNextImage(decoder) check(r, "failed to get next image") # Now available (for this frame): # * All decoder->image YUV pixel data (yuvFormat, yuvPlanes, yuvRange, yuvChromaSamplePosition, yuvRowBytes) - log("yuvFormat=%s, yuvRange=%s", AVIF_PIXEL_FORMAT.get(image.yuvFormat), AVIF_RANGE.get(image.yuvRange)) + log("avif parsed: %ux%u (%ubpc) yuvFormat=%s, yuvRange=%s", + width, height, image.depth, AVIF_PIXEL_FORMAT.get(image.yuvFormat), AVIF_RANGE.get(image.yuvRange)) + if yuv: + #do we still need to copy if image.imageOwnsYUVPlanes? + #could we keep the decoder context alive until the image is freed instead? + yuv_format = "%sP" % AVIF_PIXEL_FORMAT.get(image.yuvFormat) #ie: YUV420P + divs = get_subsampling_divs(yuv_format) + planes = [] + strides = [] + for i in range(3): + ydiv = divs[i][1] + size = image.yuvRowBytes[i]*height//ydiv + planes.append(image.yuvPlanes[i][:size]) + strides.append(image.yuvRowBytes[i]) + return ImageWrapper(0, 0, width, height, planes, yuv_format, 24, strides, ImageWrapper.PLANAR_3) + # * decoder->image alpha data (alphaRange, alphaPlane, alphaRowBytes) # * this frame's sequence timing avifRGBImageSetDefaults(&rgb, image) diff --git a/xpra/codecs/avif/encoder.pyx b/xpra/codecs/avif/encoder.pyx index 5ab72a4a75..5b17c179c9 100644 --- a/xpra/codecs/avif/encoder.pyx +++ b/xpra/codecs/avif/encoder.pyx @@ -127,21 +127,25 @@ def encode(coding, image, options=None): qrange = 20 cdef int minq = AVIF_QUANTIZER_WORST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY*100//quality cdef int maxq = max(0, minq-qrange) + client_options = {"quality" : quality} with buffer_context(pixels) as bc: rgb.pixels = ( int(bc)) log("avif.encode(%s, %s, %s) pixels=%#x", coding, image, options, int(bc)) - try: avif_image.yuvRange = AVIF_RANGE_FULL if grayscale: avif_image.yuvFormat = AVIF_PIXEL_FORMAT_YUV400 + client_options["subsampling"] = "YUV400" elif quality>80: avif_image.yuvFormat = AVIF_PIXEL_FORMAT_YUV444 + client_options["subsampling"] = "YUV444" elif quality>50: avif_image.yuvFormat = AVIF_PIXEL_FORMAT_YUV422 + client_options["subsampling"] = "YUV422" else: avif_image.yuvFormat = AVIF_PIXEL_FORMAT_YUV420 + client_options["subsampling"] = "YUV420" r = avifImageRGBToYUV(avif_image, &rgb) log("avifImageRGBToYUV()=%i for %s", r, AVIF_PIXEL_FORMAT.get(avif_image.yuvFormat)) check(r, "Failed to convert to YUV(A)") @@ -156,6 +160,8 @@ def encode(coding, image, options=None): encoder.minQuantizer = minq encoder.maxQuantizer = maxq if alpha>0: + #we need this client side to know if we can use planar gl paint: + client_options["alpha"] = alpha encoder.minQuantizerAlpha = minq encoder.maxQuantizerAlpha = maxq # * tileRowsLog2 @@ -170,9 +176,6 @@ def encode(coding, image, options=None): log("avifEncoderFinish()=%i", r) check(r, "Failed to finish encode") - client_options = {"quality" : quality} - if alpha: - client_options["alpha"] = alpha cdata = avifOutput.data[:avifOutput.size] log("avif: got %i bytes", avifOutput.size) may_save_image("avif", cdata)