Coverage for gws-app/gws/lib/image/format.py: 100%

79 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +0200

1"""Image formats. 

2 

3``PNG8``, ``PNG24``, ``JPEG_LOW``, ``JPEG``, ``JPEG_HIGH``, ``WEBP`` and ``WEBP_LOSSLESS`` are predefined ``gws.ImageFormat`` objects. The first 

4MIME type of a format is its content type, the others are names under which services offer it 

5(``image/png; mode=8bit``). ``from_config`` creates a format from a ``Config``, and ``best_mime_type`` 

6selects the MIME type to request from a remote source (WMS, QGIS Server) for a format. 

7 

8Encoding options 

9---------------- 

10 

11The options of a format are passed to ``gws.Image.to_bytes``. ``mode``, ``background`` and ``quality`` 

12are also ``Config`` fields. ``mode`` and ``background`` are handled by ``gws.lib.image.core.Image``, 

13all other keys are passed to the PIL encoder (Pillow 12). Configured options replace those of a 

14predefined format, except its ``mode``, ``background`` and ``quality``. 

15 

16==================== ================================================================= ================ 

17option effect formats 

18==================== ================================================================= ================ 

19``mode`` convert to this PIL mode before encoding, with an adaptive all 

20 palette: ``P`` (8-bit palette), ``RGBA``, ``RGB``, ``L`` 

21 (grayscale); JPEG accepts only ``RGB``, ``L``, ``CMYK`` 

22``background`` color of transparent areas when RGBA is converted to RGB, JPEG 

23 default white 

24``quality`` JPEG 1-95 (default 75), WebP 0-100 (default 80); for lossless JPEG, WebP, TIFF 

25 WebP the encoder effort; TIFF with ``compression: jpeg`` only 

26``compress_level`` zlib level 0-9, default 6; predefined PNG formats use 3 PNG 

27``optimize`` optimized Huffman tables JPEG 

28``progressive`` progressive JPEG JPEG 

29``subsampling`` chroma subsampling: 0 = 4:4:4, 1 = 4:2:2, 2 = 4:2:0 JPEG 

30``lossless`` lossless WebP WebP 

31``method`` encoder effort 0-6, default 4 (higher = smaller, slower) WebP 

32``alpha_quality`` alpha channel quality 0-100, default 100 WebP 

33``compression`` ``raw``, ``tiff_lzw``, ``tiff_adobe_deflate``, ``jpeg``, TIFF 

34 ``packbits`` 

35==================== ================================================================= ================ 

36 

37Not to be used: PNG ``optimize`` and ``compress_level: 9`` (6-10x slower than level 6, at most 2% 

38smaller), PNG ``bits`` and ``transparency`` (break the palette created by ``mode: P``), WebP ``exact``, 

39JPEG ``quality`` above 95, ``smooth``, ``qtables``, ``restart_marker_*``, ``dpi`` (header only, no 

40effect on maps), metadata and animation options, ``mode: P`` with WebP (converted back to RGBA). 

41 

42Measured on map tiles and a 1024 px aerial image: ``compress_level: 3`` encodes PNG24 2-3x and PNG8 3x 

43faster than the default 6, at most 8% larger. Lossy WebP is 15-50% smaller than PNG8 and 6x slower to 

44encode; lossless WebP is 2-4x smaller than PNG24, but larger and much slower than PNG8. JPEG on a 

451024 px map: quality 50 is 35% smaller than 75, quality 90 is 70% larger; ``subsampling: 0`` adds 

46another 40% at quality 90. 

47""" 

48 

49from typing import Optional 

50 

51import gws 

52import gws.lib.mime 

53 

54from . import core 

55 

56 

57class ImageMode(gws.Enum): 

58 """Image mode the image is converted to before encoding.""" 

59 

60 P = 'P' 

61 """8-bit palette.""" 

62 RGBA = 'RGBA' 

63 """Full color with transparency.""" 

64 RGB = 'RGB' 

65 """Full color without transparency.""" 

66 L = 'L' 

67 """Grayscale.""" 

68 

69 

70class ImageFormatName(gws.Enum): 

71 """Predefined image format.""" 

72 

73 PNG8 = 'PNG8' 

74 """PNG with an 8-bit palette.""" 

75 PNG24 = 'PNG24' 

76 """PNG with full color and alpha.""" 

77 JPEG_LOW = 'JPEG_LOW' 

78 """JPEG with quality 50.""" 

79 JPEG = 'JPEG' 

80 """JPEG with quality 75.""" 

81 JPEG_HIGH = 'JPEG_HIGH' 

82 """JPEG with quality 90.""" 

83 WEBP = 'WEBP' 

84 """Lossy WebP.""" 

85 WEBP_LOSSLESS = 'WEBP_LOSSLESS' 

86 """Lossless WebP.""" 

87 

88 

89class Config(gws.Config): 

90 """Image format with its encoding options.""" 

91 

92 name: ImageFormatName 

93 """Predefined format.""" 

94 mimeTypes: Optional[list[gws.MimeType]] 

95 """MIME types for this format, replace those of the predefined format.""" 

96 mode: Optional[ImageMode] 

97 """Image mode.""" 

98 background: Optional[gws.Color] 

99 """Color of transparent areas in formats without transparency (JPEG).""" 

100 quality: Optional[int] 

101 """Encoding quality: JPEG 1-95, WebP 0-100.""" 

102 options: Optional[dict] 

103 """Further image encoding options.""" 

104 

105 

106PNG8 = gws.ImageFormat( 

107 name=ImageFormatName.PNG8, 

108 mimeTypes=[gws.lib.mime.PNG, gws.lib.mime.PNG8], 

109 options={'mode': 'P', 'compress_level': 3}, 

110) 

111PNG24 = gws.ImageFormat( 

112 name=ImageFormatName.PNG24, 

113 mimeTypes=[gws.lib.mime.PNG, gws.lib.mime.PNG24], 

114 options={'compress_level': 3}, 

115) 

116JPEG_LOW = gws.ImageFormat( 

117 name=ImageFormatName.JPEG_LOW, 

118 mimeTypes=[gws.lib.mime.JPEG], 

119 options={'quality': 50}, 

120) 

121JPEG = gws.ImageFormat( 

122 name=ImageFormatName.JPEG, 

123 mimeTypes=[gws.lib.mime.JPEG], 

124 options={'quality': 75}, 

125) 

126JPEG_HIGH = gws.ImageFormat( 

127 name=ImageFormatName.JPEG_HIGH, 

128 mimeTypes=[gws.lib.mime.JPEG], 

129 options={'quality': 90}, 

130) 

131WEBP = gws.ImageFormat( 

132 name=ImageFormatName.WEBP, 

133 mimeTypes=[gws.lib.mime.WEBP], 

134 options={}, 

135) 

136WEBP_LOSSLESS = gws.ImageFormat( 

137 name=ImageFormatName.WEBP_LOSSLESS, 

138 mimeTypes=[gws.lib.mime.WEBP], 

139 options={'lossless': True}, 

140) 

141 

142 

143_FORMATS = {f.name: f for f in [PNG8, PNG24, JPEG_LOW, JPEG, JPEG_HIGH, WEBP, WEBP_LOSSLESS]} 

144_CONFIG_KEYS = ['mode', 'background', 'quality'] 

145 

146 

147def from_config(cfg: Config) -> gws.ImageFormat: 

148 """Create an image format from its configuration. 

149 

150 The format is based on the predefined format ``name``. Configured MIME types replace those 

151 of the predefined format, and so do configured ``options``, except that the predefined ``mode``, 

152 ``background`` and ``quality`` are kept unless the configured ``options`` set them. The 

153 configured ``mode``, ``background`` and ``quality`` fields are applied last. 

154 

155 Args: 

156 cfg: Format configuration. 

157 

158 Returns: 

159 The image format. 

160 

161 Raises: 

162 ``Error``: If the name is not a predefined format or the configuration has no known MIME types. 

163 """ 

164 

165 name = cfg.get('name') or '' 

166 base = _FORMATS.get(name) 

167 if not base: 

168 raise core.Error(f'unknown image format {name!r}') 

169 

170 mimes = base.mimeTypes 

171 p = cfg.get('mimeTypes') 

172 if p is not None: 

173 mimes = [] 

174 for s in p: 

175 mime = gws.lib.mime.get(s) 

176 if not mime or not core.mime_to_pil_format(mime): 

177 gws.log.warning(f'image format {name!r}: unknown MIME type {s!r} ignored') 

178 continue 

179 mimes.append(mime) 

180 

181 if not mimes: 

182 raise core.Error(f'image format {name!r}: no known MIME types') 

183 

184 p = cfg.get('options') 

185 options = dict(gws.u.to_dict(p) if p is not None else base.options) 

186 

187 for key in _CONFIG_KEYS: 

188 p = cfg.get(key) 

189 if p is not None: 

190 options[key] = p 

191 elif key not in options and key in base.options: 

192 options[key] = base.options[key] 

193 

194 return gws.ImageFormat(name=name, mimeTypes=mimes, options=options) 

195 

196 

197def find_by_mime_type(mime: gws.MimeType, formats: list[gws.ImageFormat]) -> Optional[gws.ImageFormat]: 

198 """Find an image format by one of its MIME types. 

199 

200 Args: 

201 mime: MIME type, normalized with ``gws.lib.mime.get``. 

202 formats: Image formats. 

203 

204 Returns: 

205 The first format that has the MIME type, or ``None`` if there is none. 

206 """ 

207 

208 for ifo in formats: 

209 if mime in ifo.mimeTypes: 

210 return ifo 

211 

212 

213def best_mime_type(ifo: gws.ImageFormat, supported: list[str]) -> str: 

214 """Select the MIME type to request from a source for an image format. 

215 

216 The supported MIME types are tried longest first, so that a specific one (``image/png; mode=8bit``) 

217 wins over a plain one (``image/png``). The first one that is among the MIME types of the format 

218 is selected. 

219 

220 Args: 

221 ifo: Image format. 

222 supported: MIME types the source supports, normalized with ``gws.lib.mime.get``. 

223 

224 Returns: 

225 The selected MIME type, or an empty string if none matches. 

226 """ 

227 

228 for mime in sorted(supported, key=len, reverse=True): 

229 if mime in ifo.mimeTypes: 

230 return mime 

231 

232 return ''