Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of Xpra. 

2# Copyright (C) 2014-2020 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 

6import os 

7import time 

8from io import BytesIO 

9import PIL 

10from PIL import Image, ImagePalette #@UnresolvedImport 

11 

12from xpra.util import envbool 

13from xpra.os_util import bytestostr 

14from xpra.net.compression import Compressed 

15from xpra.log import Logger 

16 

17log = Logger("encoder", "pillow") 

18 

19SAVE_TO_FILE = envbool("XPRA_SAVE_TO_FILE") 

20ENCODE_FORMATS = os.environ.get("XPRA_PILLOW_ENCODE_FORMATS", "png,png/L,png/P,jpeg,webp").split(",") 

21 

22 

23def get_version(): 

24 return PIL.__version__ 

25 

26def get_type() -> str: 

27 return "pillow" 

28 

29def do_get_encodings(): 

30 log("PIL.Image.SAVE=%s", Image.SAVE) 

31 encodings = [] 

32 for encoding in ENCODE_FORMATS: 

33 #strip suffix (so "png/L" -> "png") 

34 stripped = encoding.split("/")[0].upper() 

35 if stripped in Image.SAVE: 

36 encodings.append(encoding) 

37 log("do_get_encodings()=%s", encodings) 

38 return encodings 

39 

40def get_encodings(): 

41 return ENCODINGS 

42 

43ENCODINGS = do_get_encodings() 

44 

45def get_info() -> dict: 

46 return { 

47 "version" : get_version(), 

48 "encodings" : get_encodings(), 

49 } 

50 

51 

52def encode(coding : str, image, quality : int, speed : int, supports_transparency : bool, grayscale : bool=False, resize=None): 

53 log("pillow.encode%s", (coding, image, quality, speed, supports_transparency, grayscale, resize)) 

54 assert coding in ("jpeg", "webp", "png", "png/P", "png/L"), "unsupported encoding: %s" % coding 

55 assert image, "no image to encode" 

56 pixel_format = bytestostr(image.get_pixel_format()) 

57 palette = None 

58 w = image.get_width() 

59 h = image.get_height() 

60 rgb = { 

61 "RLE8" : "P", 

62 "XRGB" : "RGB", 

63 "BGRX" : "RGB", 

64 "RGBX" : "RGB", 

65 "RGBA" : "RGBA", 

66 "BGRA" : "RGBA", 

67 "BGR" : "RGB", 

68 }.get(pixel_format, pixel_format) 

69 bpp = 32 

70 pixels = image.get_pixels() 

71 assert pixels, "failed to get pixels from %s" % image 

72 #remove transparency if it cannot be handled, 

73 #and deal with non 24-bit formats: 

74 if pixel_format=="r210": 

75 stride = image.get_rowstride() 

76 from xpra.codecs.argb.argb import r210_to_rgba, r210_to_rgb #@UnresolvedImport 

77 if supports_transparency: 

78 pixels = r210_to_rgba(pixels, w, h, stride, w*4) 

79 pixel_format = "RGBA" 

80 rgb = "RGBA" 

81 else: 

82 image.set_rowstride(image.get_rowstride()*3//4) 

83 pixels = r210_to_rgb(pixels, w, h, stride, w*3) 

84 pixel_format = "RGB" 

85 rgb = "RGB" 

86 bpp = 24 

87 elif pixel_format=="BGR565": 

88 from xpra.codecs.argb.argb import bgr565_to_rgbx, bgr565_to_rgb #@UnresolvedImport 

89 if supports_transparency: 

90 image.set_rowstride(image.get_rowstride()*2) 

91 pixels = bgr565_to_rgbx(pixels) 

92 pixel_format = "RGBA" 

93 rgb = "RGBA" 

94 else: 

95 image.set_rowstride(image.get_rowstride()*3//2) 

96 pixels = bgr565_to_rgb(pixels) 

97 pixel_format = "RGB" 

98 rgb = "RGB" 

99 bpp = 24 

100 elif pixel_format=="RLE8": 

101 pixel_format = "P" 

102 palette = [] 

103 #pillow requires 8 bit palette values, 

104 #but we get 16-bit values from the image wrapper (X11 palettes are 16-bit): 

105 for r, g, b in image.get_palette(): 

106 palette.append((r>>8) & 0xFF) 

107 palette.append((g>>8) & 0xFF) 

108 palette.append((b>>8) & 0xFF) 

109 bpp = 8 

110 else: 

111 assert pixel_format in ("RGBA", "RGBX", "BGRA", "BGRX", "BGR", "RGB"), "invalid pixel format '%s'" % pixel_format 

112 try: 

113 #PIL cannot use the memoryview directly: 

114 if isinstance(pixels, memoryview): 

115 pixels = pixels.tobytes() 

116 #it is safe to use frombuffer() here since the convert() 

117 #calls below will not convert and modify the data in place 

118 #and we save the compressed data then discard the image 

119 im = Image.frombuffer(rgb, (w, h), pixels, "raw", pixel_format, image.get_rowstride(), 1) 

120 if palette: 

121 im.putpalette(palette) 

122 im.palette = ImagePalette.ImagePalette("RGB", palette = palette, size = len(palette)) 

123 if coding!="png/L" and grayscale: 

124 if rgb.find("A")>=0 and supports_transparency and coding!="jpeg": 

125 im = im.convert("LA") 

126 else: 

127 im = im.convert("L") 

128 rgb = "L" 

129 bpp = 8 

130 elif coding.startswith("png") and not supports_transparency and rgb=="RGBA": 

131 im = im.convert("RGB") 

132 rgb = "RGB" 

133 bpp = 24 

134 except Exception: 

135 log.error("PIL_encode%s converting %s pixels from %s to %s failed", 

136 (w, h, coding, "%s bytes" % image.get_size(), pixel_format, image.get_rowstride()), type(pixels), pixel_format, rgb, exc_info=True) 

137 raise 

138 client_options = {} 

139 if resize: 

140 if speed>=95: 

141 resample = "NEAREST" 

142 elif speed>80: 

143 resample = "BILINEAR" 

144 elif speed>=30: 

145 resample = "BICUBIC" 

146 else: 

147 resample = "LANCZOS" 

148 resample_value = getattr(Image, resample, 0) 

149 im = im.resize(resize, resample=resample_value) 

150 client_options["resample"] = resample 

151 if coding in ("jpeg", "webp"): 

152 #newer versions of pillow require explicit conversion to non-alpha: 

153 if pixel_format.find("A")>=0: 

154 im = im.convert("RGB") 

155 q = int(min(100, max(1, quality))) 

156 kwargs = im.info 

157 kwargs["quality"] = q 

158 client_options["quality"] = q 

159 if coding=="jpeg" and speed<50: 

160 #(optimizing jpeg is pretty cheap and worth doing) 

161 kwargs["optimize"] = True 

162 client_options["optimize"] = True 

163 elif coding=="webp" and q>=100: 

164 kwargs["lossless"] = 1 

165 pil_fmt = coding.upper() 

166 else: 

167 assert coding in ("png", "png/P", "png/L"), "unsupported encoding: %s" % coding 

168 if coding in ("png/L", "png/P") and supports_transparency and rgb=="RGBA": 

169 #grab alpha channel (the last one): 

170 #we use the last channel because we know it is RGBA, 

171 #otherwise we should do: alpha_index= image.getbands().index('A') 

172 alpha = im.split()[-1] 

173 #convert to simple on or off mask: 

174 #set all pixel values below 128 to 255, and the rest to 0 

175 def mask_value(a): 

176 if a<=128: 

177 return 255 

178 return 0 

179 mask = Image.eval(alpha, mask_value) 

180 else: 

181 #no transparency 

182 mask = None 

183 if coding=="png/L": 

184 im = im.convert("L", palette=Image.ADAPTIVE, colors=255) 

185 bpp = 8 

186 elif coding=="png/P": 

187 #convert to 255 indexed colour if: 

188 # * we're not in palette mode yet (source is >8bpp) 

189 # * we need space for the mask (256 -> 255) 

190 if palette is None or mask: 

191 #I wanted to use the "better" adaptive method, 

192 #but this does NOT work (produces a black image instead): 

193 #im.convert("P", palette=Image.ADAPTIVE) 

194 im = im.convert("P", palette=Image.WEB, colors=255) 

195 bpp = 8 

196 kwargs = im.info 

197 if mask: 

198 # paste the alpha mask to the color of index 255 

199 im.paste(255, mask) 

200 client_options["transparency"] = 255 

201 kwargs["transparency"] = 255 

202 if speed==0: 

203 #optimizing png is very rarely worth doing 

204 kwargs["optimize"] = True 

205 client_options["optimize"] = True 

206 #level can range from 0 to 9, but anything above 5 is way too slow for small gains: 

207 #76-100 -> 1 

208 #51-76 -> 2 

209 #etc 

210 level = max(1, min(5, (125-speed)//25)) 

211 kwargs["compress_level"] = level 

212 #no need to expose to the client: 

213 #client_options["compress_level"] = level 

214 #default is good enough, no need to override, other options: 

215 #DEFAULT_STRATEGY, FILTERED, HUFFMAN_ONLY, RLE, FIXED 

216 #kwargs["compress_type"] = Image.DEFAULT_STRATEGY 

217 pil_fmt = "PNG" 

218 buf = BytesIO() 

219 im.save(buf, pil_fmt, **kwargs) 

220 if SAVE_TO_FILE: # pragma: no cover 

221 filename = "./%s.%s" % (time.time(), pil_fmt) 

222 im.save(filename, pil_fmt) 

223 log.info("saved %s to %s", coding, filename) 

224 log("sending %sx%s %s as %s, mode=%s, options=%s", w, h, pixel_format, coding, im.mode, kwargs) 

225 data = buf.getvalue() 

226 buf.close() 

227 return coding, Compressed(coding, data), client_options, image.get_width(), image.get_height(), 0, bpp 

228 

229def selftest(full=False): 

230 global ENCODINGS 

231 from xpra.os_util import hexstr 

232 from xpra.codecs.codec_checks import make_test_image 

233 img = make_test_image("BGRA", 32, 32) 

234 if full: 

235 vrange = (0, 50, 100) 

236 else: 

237 vrange = (50, ) 

238 for encoding in tuple(ENCODINGS): 

239 try: 

240 for q in vrange: 

241 for s in vrange: 

242 for alpha in (True, False): 

243 v = encode(encoding, img, q, s, False, alpha) 

244 assert v, "encode output was empty!" 

245 cdata = v[1].data 

246 log("encode(%s)=%s", (encoding, img, q, s, alpha), hexstr(cdata)) 

247 except Exception as e: # pragma: no cover 

248 l = log.warn 

249 l("Pillow error saving %s with quality=%s, speed=%s, alpha=%s", encoding, q, s, alpha) 

250 l(" %s", e, exc_info=True) 

251 ENCODINGS.remove(encoding)