diff --git a/setup.py b/setup.py index 0030eec85a..fded48bd31 100755 --- a/setup.py +++ b/setup.py @@ -2292,6 +2292,9 @@ def nvcc_compile(cmd): add_cython_ext("xpra.codecs.avif.encoder", ["xpra/codecs/avif/encoder.pyx"], **avif_pkgconfig) + add_cython_ext("xpra.codecs.avif.decoder", + ["xpra/codecs/avif/decoder.pyx"], + **avif_pkgconfig) #swscale and avcodec2 use libav_common/av_log: diff --git a/xpra/client/mixins/encodings.py b/xpra/client/mixins/encodings.py index 3fbf0ce69e..4e995a4da0 100644 --- a/xpra/client/mixins/encodings.py +++ b/xpra/client/mixins/encodings.py @@ -38,7 +38,7 @@ def get_core_encodings(): """ #we always support rgb: core_encodings = ["rgb24", "rgb32"] - for codec in ("dec_pillow", "dec_webp", "dec_jpeg"): + for codec in ("dec_pillow", "dec_webp", "dec_jpeg", "dec_avif"): if has_codec(codec): c = get_codec(codec) encs = c.get_encodings() @@ -107,6 +107,8 @@ def init(self, opts): if "webp" in ae: #try to load the fast webp decoder: load_codec("dec_webp") + if "avif" in ae: + load_codec("dec_avif") vh = getVideoHelper() vh.set_modules(video_decoders=opts.video_decoders, csc_modules=opts.csc_modules or NO_GFX_CSC_OPTIONS) vh.init() diff --git a/xpra/client/window_backing_base.py b/xpra/client/window_backing_base.py index b3ec67b047..afd8c18465 100644 --- a/xpra/client/window_backing_base.py +++ b/xpra/client/window_backing_base.py @@ -152,6 +152,7 @@ def __init__(self, wid : int, window_alpha : bool): self.jpeg_decoder = get_codec("dec_jpeg") self.webp_decoder = get_codec("dec_webp") self.spng_decoder = get_codec("dec_spng") + self.avif_decoder = get_codec("dec_avif") self.draw_needs_refresh = True self.repaint_all = REPAINT_ALL self.mmap = None @@ -451,6 +452,15 @@ def do_paint_jpeg(self, rgb_format, img_data, x, y, width, height, options, call self.idle_add(self.do_paint_rgb, rgb_format, img_data, x, y, w, h, width, height, rowstride, options, callbacks) + def paint_avif(self, img_data, x, y, width, height, options, callbacks): + img = self.avif_decoder.decompress(img_data, options) + rgb_format = img.get_pixel_format() + img_data = img.get_pixels() + rowstride = img.get_rowstride() + w = img.get_width() + h = img.get_height() + self.idle_add(self.do_paint_rgb, rgb_format, img_data, + x, y, w, h, width, height, rowstride, options, callbacks) def paint_image(self, coding, img_data, x, y, width, height, options, callbacks): # can be called from any thread @@ -806,6 +816,8 @@ def draw_region(self, x, y, width, height, coding, img_data, rowstride, options, self.paint_jpeg(img_data, x, y, width, height, options, callbacks) elif self.jpeg_decoder and coding=="jpega": self.paint_jpega(img_data, x, y, width, height, options, callbacks) + elif self.avif_decoder and coding=="avif": + self.paint_avif(img_data, x, y, width, height, options, callbacks) elif coding == "webp": self.paint_webp(img_data, x, y, width, height, options, callbacks) elif self.spng_decoder and coding=="png": diff --git a/xpra/codecs/avif/avif.pxd b/xpra/codecs/avif/avif.pxd index 2d98307093..81791294d4 100644 --- a/xpra/codecs/avif/avif.pxd +++ b/xpra/codecs/avif/avif.pxd @@ -224,14 +224,6 @@ cdef extern from "avif/avif.h": void avifImageFreePlanes(avifImage * image, uint32_t planes) #Ignores already-freed planes void avifImageStealPlanes(avifImage * dstImage, avifImage * srcImage, uint32_t planes) - ctypedef enum avifRGBFormat: - AVIF_RGB_FORMAT_RGB - AVIF_RGB_FORMAT_RGBA - AVIF_RGB_FORMAT_ARGB - AVIF_RGB_FORMAT_BGR - AVIF_RGB_FORMAT_BGRA - AVIF_RGB_FORMAT_ABGR - uint32_t avifRGBFormatChannelCount(avifRGBFormat format) avifBool avifRGBFormatHasAlpha(avifRGBFormat format) @@ -331,6 +323,76 @@ cdef extern from "avif/avif.h": uint32_t addImageFlags) avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) + ctypedef struct avifDecoderData: + pass + enum avifDecoderSource: + AVIF_DECODER_SOURCE_AUTO + AVIF_DECODER_SOURCE_PRIMARY_ITEM + AVIF_DECODER_SOURCE_TRACKS + ctypedef struct avifImageTiming: + uint64_t timescale # timescale of the media (Hz) + double pts # presentation timestamp in seconds (ptsInTimescales / timescale) + uint64_t ptsInTimescales # presentation timestamp in "timescales" + double duration # in seconds (durationInTimescales / timescale) + uint64_t durationInTimescales # duration in "timescales" + ctypedef struct avifIOStats: + size_t colorOBUSize + size_t alphaOBUSize + ctypedef struct avifIO: + pass + ctypedef struct avifDecoder: + # Defaults to AVIF_CODEC_CHOICE_AUTO: Preference determined by order in availableCodecs table (avif.c) + avifCodecChoice codecChoice + int maxThreads # Defaults to 1 + # avifs can have multiple sets of images in them. This specifies which to decode. + # Set this via avifDecoderSetSource(). + avifDecoderSource requestedSource + + # All decoded image data; owned by the decoder. All information in this image is incrementally + # added and updated as avifDecoder*() functions are called. After a successful call to + # avifDecoderParse(), all values in decoder->image (other than the planes/rowBytes themselves) + # will be pre-populated with all information found in the outer AVIF container, prior to any + # AV1 decoding. If the contents of the inner AV1 payload disagree with the outer container, + # these values may change after calls to avifDecoderRead*(),avifDecoderNextImage(), or + # avifDecoderNthImage(). + # + # The YUV and A contents of this image are likely owned by the decoder, so be sure to copy any + # data inside of this image before advancing to the next image or reusing the decoder. It is + # legal to call avifImageYUVToRGB() on this in between calls to avifDecoderNextImage(), but use + # avifImageCopy() if you want to make a complete, permanent copy of this image's YUV content or + # metadata. + avifImage * image + # Counts and timing for the current image in an image sequence. Uninteresting for single image files. + int imageIndex # 0-based + int imageCount # Always 1 for non-sequences + avifImageTiming imageTiming + uint64_t timescale # timescale of the media (Hz) + double duration # in seconds (durationInTimescales / timescale) + uint64_t durationInTimescales # duration in "timescales" + # This is true when avifDecoderParse() detects an alpha plane. Use this to find out if alpha is + # present after a successful call to avifDecoderParse(), but prior to any call to + # avifDecoderNextImage() or avifDecoderNthImage(), as decoder->image->alphaPlane won't exist yet. + avifBool alphaPresent + # Enable any of these to avoid reading and surfacing specific data to the decoded avifImage. + # These can be useful if your avifIO implementation heavily uses AVIF_RESULT_WAITING_ON_IO for + # streaming data, as some of these payloads are (unfortunately) packed at the end of the file, + # which will cause avifDecoderParse() to return AVIF_RESULT_WAITING_ON_IO until it finds them. + # If you don't actually leverage this data, it is best to ignore it here. + avifBool ignoreExif + avifBool ignoreXMP + # stats from the most recent read, possibly 0s if reading an image sequence + avifIOStats ioStats + # Use one of the avifDecoderSetIO*() functions to set this + avifIO * io + # Internals used by the decoder + avifDecoderData * data + + avifDecoder * avifDecoderCreate() + avifResult avifDecoderSetIOMemory(avifDecoder * decoder, const uint8_t * data, size_t size) + avifResult avifDecoderParse(avifDecoder * decoder) + avifResult avifDecoderNextImage(avifDecoder * decoder) + void avifDecoderDestroy(avifDecoder * decoder) + ctypedef enum avifResult: AVIF_RESULT_OK AVIF_RESULT_UNKNOWN_ERROR diff --git a/xpra/codecs/avif/encoder.pyx b/xpra/codecs/avif/encoder.pyx index da574c5cce..f5ddee6ee9 100644 --- a/xpra/codecs/avif/encoder.pyx +++ b/xpra/codecs/avif/encoder.pyx @@ -122,7 +122,7 @@ def encode(coding, image, options=None): log("avifEncoderFinish()=%i", r) check(r, "Failed to finish encode") - client_options = {} + client_options = {"alpha" : pixel_format.find("A")>=0} cdata = avifOutput.data[:avifOutput.size] log("avif: got %i bytes", avifOutput.size) may_save_image("avif", cdata)