1# This file is part of Xpra.
2# Copyright (C) 2021 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
5
6#cython: wraparound=False
7
8from time import time
9from xpra.log import Logger
10log = Logger("decoder", "spng")
11
12from libc.stdint cimport uintptr_t, uint32_t, uint8_t
13from xpra.buffers.membuf cimport makebuf, MemBuf, buffer_context #pylint: disable=syntax-error
14from xpra.util import envbool
15from xpra.net.compression import Compressed
16
17SAVE_TO_FILE = envbool("XPRA_SAVE_TO_FILE", False)
18
19
20cdef extern from "zconf.h":
21    int MAX_MEM_LEVEL
22
23cdef extern from "zlib.h":
24    int Z_HUFFMAN_ONLY
25
26cdef extern from "spng.h":
27    int SPNG_VERSION_MAJOR
28    int SPNG_VERSION_MINOR
29    int SPNG_VERSION_PATCH
30
31    int SPNG_CTX_ENCODER
32
33    int SPNG_FILTER_NONE
34    int SPNG_INTERLACE_NONE
35
36    enum spng_encode_flags:
37        SPNG_ENCODE_PROGRESSIVE
38        SPNG_ENCODE_FINALIZE
39
40    enum spng_filter_choice:
41        SPNG_DISABLE_FILTERING
42        SPNG_FILTER_CHOICE_NONE
43        SPNG_FILTER_CHOICE_SUB
44        SPNG_FILTER_CHOICE_UP
45        SPNG_FILTER_CHOICE_AVG
46        SPNG_FILTER_CHOICE_PAETH
47        SPNG_FILTER_CHOICE_ALL
48
49    enum spng_option:
50        SPNG_ENCODE_TO_BUFFER
51        SPNG_KEEP_UNKNOWN_CHUNKS
52
53        SPNG_IMG_COMPRESSION_LEVEL
54
55        SPNG_IMG_WINDOW_BITS
56        SPNG_IMG_MEM_LEVEL
57        SPNG_IMG_COMPRESSION_STRATEGY
58
59        SPNG_TEXT_COMPRESSION_LEVEL
60        SPNG_TEXT_WINDOW_BITS
61        SPNG_TEXT_MEM_LEVEL
62        SPNG_TEXT_COMPRESSION_STRATEGY
63
64        SPNG_FILTER_CHOICE
65
66    enum spng_format:
67        SPNG_FMT_RGBA8
68        SPNG_FMT_RGBA16
69        SPNG_FMT_RGB8
70        # Partially implemented, see documentation
71        SPNG_FMT_GA8
72        SPNG_FMT_GA16
73        SPNG_FMT_G8
74        SPNG_FMT_PNG        #host-endian
75        SPNG_FMT_RAW        #big-endian
76
77    enum spng_color_type:
78        SPNG_COLOR_TYPE_GRAYSCALE
79        SPNG_COLOR_TYPE_TRUECOLOR
80        SPNG_COLOR_TYPE_INDEXED
81        SPNG_COLOR_TYPE_GRAYSCALE_ALPHA
82        SPNG_COLOR_TYPE_TRUECOLOR_ALPHA
83
84    ctypedef struct spng_ctx:
85        pass
86
87    cdef struct spng_ihdr:
88        uint32_t height
89        uint32_t width
90        uint8_t bit_depth
91        uint8_t color_type
92        uint8_t compression_method
93        uint8_t filter_method
94        uint8_t interlace_method
95
96    const char *spng_version_string()
97    const char *spng_strerror(int err)
98    spng_ctx *spng_ctx_new(int flags)
99    void spng_ctx_free(spng_ctx *ctx)
100    int spng_set_option(spng_ctx *ctx, spng_option option, int value)
101    int spng_set_ihdr(spng_ctx *ctx, spng_ihdr *ihdr)
102    int spng_encode_image(spng_ctx *ctx, const void *img, size_t len, int fmt, int flags) nogil
103    void *spng_get_png_buffer(spng_ctx *ctx, size_t *len, int *error)
104
105
106
107COLOR_TYPE_STR = {
108    SPNG_COLOR_TYPE_GRAYSCALE       : "GRAYSCALE",
109    SPNG_COLOR_TYPE_TRUECOLOR       : "TRUECOLOR",
110    SPNG_COLOR_TYPE_INDEXED         : "INDEXED",
111    SPNG_COLOR_TYPE_GRAYSCALE_ALPHA : "GRAYSCALE_ALPHA",
112    SPNG_COLOR_TYPE_TRUECOLOR_ALPHA : "TRUECOLOR_ALPHA",
113    }
114
115
116def get_version():
117    return (SPNG_VERSION_MAJOR, SPNG_VERSION_MINOR, SPNG_VERSION_PATCH)
118
119def get_type():
120    return "spng"
121
122def get_encodings():
123    return ("png", )
124
125def get_error_str(int r):
126    b = spng_strerror(r)
127    return b.decode()
128
129def check_error(int r, msg):
130    if r:
131        log_error(r, msg)
132    return r
133
134def log_error(int r, msg):
135    log.error("Error: %s", msg)
136    log.error(" code %i: %s", r, get_error_str(r))
137
138INPUT_FORMATS = "RGBA", "RGB"
139
140def encode(coding, image, options=None):
141    assert coding=="png"
142    options = options or {}
143    cdef int grayscale = options.get("grayscale", 0)
144    cdef int speed = options.get("speed", 50)
145    cdef int width = image.get_width()
146    cdef int height = image.get_height()
147    cdef int rowstride = image.get_rowstride()
148    cdef int scaled_width = options.get("scaled-width", width)
149    cdef int scaled_height = options.get("scaled-height", height)
150    cdef char resize = scaled_width!=width or scaled_height!=height
151
152    rgb_format = image.get_pixel_format()
153    alpha = options.get("alpha", True)
154    if rgb_format not in INPUT_FORMATS or (resize and len(rgb_format)!=4) or rowstride!=width*len(rgb_format):
155        #best to restride before byte-swapping to trim extra unused data:
156        if rowstride!=width*len(rgb_format):
157            image.restride(width*len(rgb_format))
158        from xpra.codecs.argb.argb import argb_swap         #@UnresolvedImport
159        if not argb_swap(image, INPUT_FORMATS, supports_transparency=alpha):
160            log("spng: argb_swap failed to convert %s to a suitable format: %s" % (
161                rgb_format, INPUT_FORMATS))
162        log("spng converted %s to %s", rgb_format, image)
163        rgb_format = image.get_pixel_format()
164        rowstride = image.get_rowstride()
165
166    if resize:
167        from xpra.codecs.argb.scale import scale_image
168        image = scale_image(image, scaled_width, scaled_height)
169        log("jpeg scaled image: %s", image)
170
171    assert rgb_format in ("RGB", "RGBA", "RGBX"), "unsupported input pixel format %s" % rgb_format
172
173    cdef spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER)
174    if ctx==NULL:
175        raise Exception("failed to instantiate an spng context")
176
177    cdef spng_ihdr ihdr
178    ihdr.width = scaled_width
179    ihdr.height = scaled_height
180    ihdr.bit_depth = 8
181    ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR
182    if rgb_format=="RGBA":
183        ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA
184    ihdr.compression_method = 0
185    ihdr.filter_method = 0
186    ihdr.interlace_method = SPNG_INTERLACE_NONE
187    if check_error(spng_set_ihdr(ctx, &ihdr),
188                   "failed to set encode-to-buffer option"):
189        spng_ctx_free(ctx)
190        return None
191
192    cdef int clevel = 1
193    if check_error(spng_set_option(ctx, SPNG_IMG_COMPRESSION_LEVEL, clevel),
194                   "failed to set compression level"):
195        spng_ctx_free(ctx)
196        return None
197    if check_error(spng_set_option(ctx, SPNG_TEXT_WINDOW_BITS, 15),
198                   "failed to set window bits"):
199        spng_ctx_free(ctx)
200        return None
201    if check_error(spng_set_option(ctx, SPNG_IMG_COMPRESSION_STRATEGY, Z_HUFFMAN_ONLY),
202                   "failed to set compression strategy"):
203        spng_ctx_free(ctx)
204        return None
205    if check_error(spng_set_option(ctx, SPNG_IMG_MEM_LEVEL, MAX_MEM_LEVEL),
206                   "failed to set mem level"):
207        spng_ctx_free(ctx)
208        return None
209    cdef int filter = SPNG_FILTER_CHOICE_NONE
210    if speed<30:
211        filter |= SPNG_FILTER_CHOICE_SUB
212    if check_error(spng_set_option(ctx, SPNG_FILTER_CHOICE, filter),
213                   "failed to set filter choice"):
214        spng_ctx_free(ctx)
215        return None
216
217    if check_error(spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1),
218                   "failed to set encode-to-buffer option"):
219        spng_ctx_free(ctx)
220        return None
221
222    cdef spng_format fmt = SPNG_FMT_PNG #SPNG_FMT_PNG
223    cdef int flags = SPNG_ENCODE_FINALIZE
224    cdef size_t data_len = 0
225    cdef uintptr_t data_ptr
226    cdef int r = 0
227    pixels = image.get_pixels()
228    assert len(pixels)>=ihdr.width*ihdr.height*len(rgb_format), \
229        "pixel buffer is too small, expected %i bytes but got %i" % (
230        ihdr.width*ihdr.height*4, len(pixels))
231    with buffer_context(pixels) as bc:
232        data_ptr = <uintptr_t> int(bc)
233        data_len = len(bc)
234        #log("spng encode buffer %#x, len=%#x", <uintptr_t> data_ptr, data_len)
235        with nogil:
236            r = spng_encode_image(ctx, <const void*> data_ptr, data_len, fmt, flags)
237    if check_error(r, "failed to encode image"):
238        log.error(" %i bytes of %s pixel data", data_len, rgb_format)
239        log.error(" for %s", image)
240        spng_ctx_free(ctx)
241        return None
242
243    cdef int error
244    cdef size_t png_len
245    cdef void *png_data = spng_get_png_buffer(ctx, &png_len, &error)
246    if check_error(error,
247                   "failed get png buffer"):
248        spng_ctx_free(ctx)
249        return None
250    if png_data==NULL:
251        log.error("Error: spng buffer is NULL")
252        spng_ctx_free(ctx)
253        return None
254
255    cdef membuf = makebuf(png_data, png_len)
256    spng_ctx_free(ctx)
257    cdata = memoryview(membuf)
258    if SAVE_TO_FILE:    # pragma: no cover
259        filename = "./%s.png" % time()
260        with open(filename, "wb") as f:
261            f.write(cdata)
262        log.info("saved png to %s", filename)
263    return coding, Compressed(coding, cdata), {}, width, height, 0, len(rgb_format)*8
264
265
266def selftest(full=False):
267    log("spng version %s selftest" % (get_version(),))
268    from xpra.codecs.codec_checks import make_test_image
269    for rgb_format in ("RGBA", "RGB", "BGRA", "BGRX"):
270        image = make_test_image(rgb_format, 1024, 768)
271        assert encode("png", image)
272