From 61ebd3524080a1b4040794aefe60f11215db46d5 Mon Sep 17 00:00:00 2001 From: Nicolas F Date: Mon, 4 Mar 2019 17:04:09 +0100 Subject: [PATCH] Add WebP image format support Since Firefox 65 added support for WebP, users may be interested in having maps that use WebP images. Support for this is added in this commit, along with documentation for it. A new option, "imglossless", controls whether we write out lossless or lossy WebP images. The generic name "imglossless" as opposed to a more specific "webplossless" was chosen in case future image formats we also implement also support lossless/lossy modes in the same format (JPEG-XL? AV1 image format?). It's an okay meme but lossy mode really falls apart on our sorts of images on the more zoomed out composite tiles, resulting in pretty blurry messes. Might be due to a PSNR bias in the encoder, which is to be expected from Google. --- docs/config.rst | 19 ++++++++++++++++--- overviewer.py | 6 +++++- overviewer_core/settingsDefinition.py | 2 ++ overviewer_core/settingsValidators.py | 7 ++++++- overviewer_core/tileset.py | 23 +++++++++++++++++------ 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index d41d42a..6325683 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -694,16 +694,29 @@ Image options ``imgformat`` This is which image format to render the tiles into. Its value should be a - string containing "png", "jpg", or "jpeg". + string containing "png", "jpg", "jpeg" or "webp". + + .. note:: + For WebP, your PIL/Pillow needs to be built with WebP support. Do + keep in mind that not all browsers support WebP images. **Default:** ``"png"`` ``imgquality`` - This is the image quality used when saving the tiles into the JPEG image - format. Its value should be an integer between 0 and 100. + This is the image quality used when saving the tiles into the JPEG or WebP + image format. Its value should be an integer between 0 and 100. + + For WebP images in lossless mode, it determines how much effort is spent + on compressing the image. **Default:** ``95`` +``imglossless`` + Determines whether a WebP image is saved in lossless or lossy mode. Has + no effect on other image formats. + + **Default:** ``True`` + ``optimizeimg`` .. warning:: diff --git a/overviewer.py b/overviewer.py index 79a1626..f48be1e 100755 --- a/overviewer.py +++ b/overviewer.py @@ -533,7 +533,11 @@ dir but you forgot to put quotes around the directory, since it contains spaces. # only pass to the TileSet the options it really cares about render['name'] = render_name # perhaps a hack. This is stored here for the asset manager - tileSetOpts = util.dict_subset(render, ["name", "imgformat", "renderchecks", "rerenderprob", "bgcolor", "defaultzoom", "imgquality", "optimizeimg", "rendermode", "worldname_orig", "title", "dimension", "changelist", "showspawn", "overlay", "base", "poititle", "maxzoom", "showlocationmarker", "minzoom"]) + tileSetOpts = util.dict_subset(render, [ + "name", "imgformat", "renderchecks", "rerenderprob", "bgcolor", "defaultzoom", + "imgquality", "imglossless", "optimizeimg", "rendermode", "worldname_orig", "title", + "dimension", "changelist", "showspawn", "overlay", "base", "poititle", "maxzoom", + "showlocationmarker", "minzoom"]) tileSetOpts.update({"spawn": w.find_true_spawn()}) # TODO find a better way to do this for rset in rsets: tset = tileset.TileSet(w, rset, assetMrg, tex, tileSetOpts, tileset_dir) diff --git a/overviewer_core/settingsDefinition.py b/overviewer_core/settingsDefinition.py index ef956ae..8e62f98 100644 --- a/overviewer_core/settingsDefinition.py +++ b/overviewer_core/settingsDefinition.py @@ -71,6 +71,8 @@ renders = Setting(required=True, default=util.OrderedDict(), "forcerender": Setting(required=False, validator=validateBool, default=None), "imgformat": Setting(required=True, validator=validateImgFormat, default="png"), "imgquality": Setting(required=False, validator=validateImgQuality, default=95), + "imglossless": Setting(required=False, validator=validateBool, + default=True), "bgcolor": Setting(required=True, validator=validateBGColor, default="1a1a1a"), "defaultzoom": Setting(required=True, validator=validateDefaultZoom, default=1), "optimizeimg": Setting(required=True, validator=validateOptImg, default=[]), diff --git a/overviewer_core/settingsValidators.py b/overviewer_core/settingsValidators.py index bf5ad5e..f674286 100644 --- a/overviewer_core/settingsValidators.py +++ b/overviewer_core/settingsValidators.py @@ -115,9 +115,14 @@ def validateRerenderprob(s): return val def validateImgFormat(fmt): - if fmt not in ("png", "jpg", "jpeg"): + if fmt not in ("png", "jpg", "jpeg", "webp"): raise ValidationException("%r is not a valid image format" % fmt) if fmt == "jpeg": fmt = "jpg" + if fmt == "webp": + try: + from PIL import _webp + except ImportError: + raise ValidationException("WebP is not supported by your PIL/Pillow installation") return fmt def validateImgQuality(qual): diff --git a/overviewer_core/tileset.py b/overviewer_core/tileset.py index 640caf5..5dbac43 100644 --- a/overviewer_core/tileset.py +++ b/overviewer_core/tileset.py @@ -261,12 +261,15 @@ class TileSet(object): rest of this discussion. imgformat - A string indicating the output format. Must be one of 'png' or - 'jpeg' + A string indicating the output format. Must be one of 'png', + 'jpeg' or 'webp' imgquality An integer 1-100 indicating the quality of the jpeg output. Only - relevant in jpeg mode. + relevant in jpeg and webp mode. + + imglossless + A boolean indicating whether to save a webp image in lossless mode. optimizeimg A list of optimizer instances to use. @@ -387,8 +390,10 @@ class TileSet(object): self.imgextension = 'png' elif self.options['imgformat'] in ('jpeg', 'jpg'): self.imgextension = 'jpg' + elif self.options['imgformat'] == 'webp': + self.imgextension = 'webp' else: - raise ValueError("imgformat must be one of: 'png' or 'jpg'") + raise ValueError("imgformat must be one of: 'png', 'jpg' or 'webp'") # This sets self.treedepth, self.xradius, and self.yradius self._set_map_size() @@ -1000,8 +1005,11 @@ class TileSet(object): if imgformat == 'jpg': img.convert('RGB').save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0) - else: # PNG + elif imgformat == 'png': # PNG img.save(tmppath, "png") + elif imgformat == 'webp': + img.save(tmppath, "webp", quality=self.options['imgquality'], + lossless=self.options['imglossless']) if self.options['optimizeimg']: optimize_image(tmppath, imgformat, self.options['optimizeimg']) @@ -1101,8 +1109,11 @@ class TileSet(object): if self.imgextension == 'jpg': tileimg.convert('RGB').save(tmppath, "jpeg", quality=self.options['imgquality'], subsampling=0) - else: # PNG + elif self.imgextension == 'png': # PNG tileimg.save(tmppath, "png") + elif self.imgextension == 'webp': + tileimg.save(tmppath, "webp", quality=self.options['imgquality'], + lossless=self.options['imglossless']) if self.options['optimizeimg']: optimize_image(tmppath, self.imgextension, self.options['optimizeimg']) os.utime(tmppath, (max_chunk_mtime, max_chunk_mtime))