Skip to content

Commit

Permalink
#2052 use the pillow encoder and downscale the image before compressi…
Browse files Browse the repository at this point in the history
…on if we know that the client will be rendering at a lower resolution, client side also has to use the pillow decoder (for now) to do the upscaling

git-svn-id: https://xpra.org/svn/Xpra/trunk@26656 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Jun 9, 2020
1 parent 7f0a1d2 commit 61118a9
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 16 deletions.
8 changes: 6 additions & 2 deletions src/xpra/client/window_backing_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@ def process_delta(self, raw_data, width, height, rowstride, options):
if comp:
assert len(comp)==1, "more than one compressor specified: %s" % str(comp)
img_data = compression.decompress_by_name(raw_data, algo=comp[0])
scaled_size = options.intpair("scaled-size")
if scaled_size:
return img_data
if len(img_data)!=rowstride * height:
deltalog.error("Error: invalid img data length: expected %s but got %s (%s: %s)",
rowstride * height, len(img_data), type(img_data), repr_ellipsized(img_data))
Expand Down Expand Up @@ -694,6 +697,7 @@ def draw_region(self, x, y, width, height, coding, img_data, rowstride, options,
hd = h.hexdigest()
assert chksum==hd, "pixel data failed compressed chksum integrity check: expected %s but got %s" % (chksum, hd)
deltalog("passed compressed data integrity checks: len=%s, chksum=%s (type=%s)", l, chksum, type(img_data))
unscaled_size = options.intpair("unscaled-size")
if coding == "mmap":
self.idle_add(self.paint_mmap, img_data, x, y, width, height, rowstride, options, callbacks)
elif coding in ("rgb24", "rgb32"):
Expand All @@ -711,9 +715,9 @@ def draw_region(self, x, y, width, height, coding, img_data, rowstride, options,
self.paint_with_video_decoder(VIDEO_DECODERS.get(coding),
coding,
img_data, x, y, width, height, options, callbacks)
elif self.jpeg_decoder and coding=="jpeg":
elif self.jpeg_decoder and coding=="jpeg" and not unscaled_size:
self.paint_jpeg(img_data, x, y, width, height, options, callbacks)
elif coding == "webp":
elif coding == "webp" and not unscaled_size:
self.paint_webp(img_data, x, y, width, height, options, callbacks)
elif coding in self._PIL_encodings:
self.paint_image(coding, img_data, x, y, width, height, options, callbacks)
Expand Down
6 changes: 6 additions & 0 deletions src/xpra/codecs/pillow/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ def nomask_value(a):
else:
img = img.convert("RGB")

unscaled_size = options.intpair("unscaled-size")
if unscaled_size:
resample = options.strget("resample", "NEAREST")
resample_value = getattr(Image, resample, 0)
img = img.resize(unscaled_size, resample=resample_value)

width = img.size[0]
if img.mode=="RGB":
#PIL flattens the data to a continuous straightforward RGB format:
Expand Down
17 changes: 15 additions & 2 deletions src/xpra/codecs/pillow/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def get_info() -> dict:
}


def encode(coding : str, image, quality : int, speed : int, supports_transparency : bool):
log("pillow.encode%s", (coding, image, quality, speed, supports_transparency))
def encode(coding : str, image, quality : int, speed : int, supports_transparency : bool, resize=None):
log("pillow.encode%s", (coding, image, quality, speed, supports_transparency, resize))
pixel_format = bytestostr(image.get_pixel_format())
palette = None
w = image.get_width()
Expand Down Expand Up @@ -126,6 +126,19 @@ def encode(coding : str, image, quality : int, speed : int, supports_transparenc
(w, h, coding, "%s bytes" % image.get_size(), pixel_format, image.get_rowstride()), type(pixels), pixel_format, rgb, exc_info=True)
raise
client_options = {}
if resize:
if speed>=95:
resample = "NEAREST"
elif speed>80:
resample = "BILINEAR"
elif speed>=30:
resample = "BICUBIC"
else:
resample = "LANCZOS"
resample_value = getattr(Image, resample, 0)
im = im.resize(resize, resample=resample_value)
client_options["unscaled-size"] = w, h
client_options["resample"] = resample
if coding in ("jpeg", "webp"):
#newer versions of pillow require explicit conversion to non-alpha:
if pixel_format.find("A")>=0:
Expand Down
56 changes: 46 additions & 10 deletions src/xpra/server/window/window_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,27 +307,33 @@ def __repr__(self):
return "WindowSource(%s : %s)" % (self.wid, self.window_dimensions)


def add_encoder(self, encoding, encoder):
log("add_encoder(%s, %s)", encoding, encoder)
self._all_encoders.setdefault(encoding, []).insert(0, encoder)
self._encoders[encoding] = encoder

def init_encoders(self):
self._encoders = {
"rgb24" : self.rgb_encode,
"rgb32" : self.rgb_encode,
}
self._all_encoders = {}
self._encoders = {}
self.add_encoder("rgb24", self.rgb_encode)
self.add_encoder("rgb32", self.rgb_encode)
self.enc_pillow = get_codec("enc_pillow")
if self.enc_pillow:
for x in self.enc_pillow.get_encodings():
if x in self.server_core_encodings:
self._encoders[x] = self.pillow_encode
self.add_encoder(x, self.pillow_encode)
#prefer these native encoders over the Pillow version:
if "webp" in self.server_core_encodings:
self._encoders["webp"] = self.webp_encode
self.add_encoder("webp", self.webp_encode)
self.enc_jpeg = get_codec("enc_jpeg")
if "jpeg" in self.server_core_encodings and self.enc_jpeg:
self._encoders["jpeg"] = self.jpeg_encode
self.add_encoder("jpeg", self.jpeg_encode)
if self._mmap and self._mmap_size>0:
self._encoders["mmap"] = self.mmap_encode
self.add_encoder("mmap", self.mmap_encode)
self.full_csc_modes = typedict()
self.parse_csc_modes(self.encoding_options.dictget("full_csc_modes", default_value=None))


def init_vars(self):
self.server_core_encodings = ()
self.server_encodings = ()
Expand Down Expand Up @@ -690,6 +696,19 @@ def do_set_client_properties(self, properties):
self.rgb_formats = rgb_formats
self.send_window_size = properties.boolget("encoding.send-window-size", self.send_window_size)
self.parse_csc_modes(properties.dictget("encoding.full_csc_modes", default_value=None))
#select the defaults encoders:
#(in case pillow was selected previously and the client side scaling changed)
for encoding, encoders in self._all_encoders.items():
self._encoders[encoding] = encoders[0]
#we may now want to downscale server-side,
#for that we need to use the pillow encoder:
if self.enc_pillow and self.client_render_size:
crsw, crsh = self.client_render_size
ww, wh = self.window_dimensions
if crsw<ww and crsh<wh:
for x in self.enc_pillow.get_encodings():
if x in self.server_core_encodings:
self.add_encoder(x, self.pillow_encode)
self.update_encoding_selection(self.encoding, [])


Expand Down Expand Up @@ -817,6 +836,13 @@ def update_encoding_options(self, force_reload=False):
if bwl:
max_rgb_threshold = min(max_rgb_threshold, max(bwl//1000, 1024))
v = int(MAX_PIXELS_PREFER_RGB * pcmult * smult * qmult * (1 + int(self.is_OR or self.is_tray or self.is_shadow)*2))
crs = self.client_render_size
if crs:
ww, wh = self.window_dimensions
if crs[0]<ww or crs[1]<wh:
#client will downscale, best to avoid sending rgb,
#so we can more easily downscale at this end:
max_rgb_threshold = 1024
self._rgb_auto_threshold = min(max_rgb_threshold, max(min_rgb_threshold, v))
self.assign_encoding_getter()
log("update_encoding_options(%s) wid=%i, want_alpha=%s, speed=%i, quality=%i, bandwidth-limit=%i, lossless threshold: %s / %s, rgb auto threshold=%i (min=%i, max=%i), get_best_encoding=%s",
Expand Down Expand Up @@ -2314,7 +2340,7 @@ def make_data_packet(self, damage_time, process_damage_time, image, coding, sequ
#* the pixel format is supported by the client
# (if we have to rgb_reformat the buffer, it really complicates things)
if self.delta_buckets>0 and (coding in self.supports_delta) and self.min_delta_size<isize<self.max_delta_size and \
pixel_format in self.rgb_formats:
pixel_format in self.rgb_formats and not options.get("unscaled-size"):
#this may save space (and lower the cost of xoring):
image.may_restride()
#we need to copy the pixels because some encodings
Expand Down Expand Up @@ -2485,7 +2511,17 @@ def pillow_encode(self, coding, image, options):
q = options.get("quality") or self.get_quality(coding)
s = options.get("speed") or self.get_speed(coding)
transparency = self.supports_transparency and options.get("transparency", True)
return self.enc_pillow.encode(coding, image, q, s, transparency)
resize = None
w, h = image.get_width(), image.get_height()
ww, wh = self.window_dimensions
crs = self.client_render_size
if crs:
crsw, crsh = crs
#resize if the render size is smaller
if ww>crsw and wh>crsh:
#keep the same proportions:
resize = w*crsw//ww, h*crsh//wh
return self.enc_pillow.encode(coding, image, q, s, transparency, resize)

def mmap_encode(self, coding, image, _options):
assert coding=="mmap"
Expand Down
4 changes: 2 additions & 2 deletions src/xpra/server/window/window_video_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ def init_encoders(self):
self.video_encodings = self.video_helper.get_encodings()
for x in self.video_encodings:
if x in self.server_core_encodings:
self._encoders[x] = self.video_encode
self._encoders["auto"] = self.video_encode
self.add_encoder(x, self.video_encode)
self.add_encoder("auto", self.video_encode)
#these are used for non-video areas, ensure "jpeg" is used if available
#as we may be dealing with large areas still, and we want speed:
nv_common = (set(self.server_core_encodings) & set(self.core_encodings)) - set(self.video_encodings)
Expand Down

0 comments on commit 61118a9

Please sign in to comment.