Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/codecs/pillow/encoder.py : 98%
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.
6import os
7import time
8from io import BytesIO
9import PIL
10from PIL import Image, ImagePalette #@UnresolvedImport
12from xpra.util import envbool
13from xpra.os_util import bytestostr
14from xpra.net.compression import Compressed
15from xpra.log import Logger
17log = Logger("encoder", "pillow")
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(",")
23def get_version():
24 return PIL.__version__
26def get_type() -> str:
27 return "pillow"
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
40def get_encodings():
41 return ENCODINGS
43ENCODINGS = do_get_encodings()
45def get_info() -> dict:
46 return {
47 "version" : get_version(),
48 "encodings" : get_encodings(),
49 }
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
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)