MapProxy 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (459) hide show
  1. MapProxy-2.1.0.dist-info/AUTHORS.txt +33 -0
  2. MapProxy-2.1.0.dist-info/COPYING.txt +60 -0
  3. MapProxy-2.1.0.dist-info/LICENSE.txt +202 -0
  4. MapProxy-2.1.0.dist-info/METADATA +165 -0
  5. MapProxy-2.1.0.dist-info/RECORD +459 -0
  6. MapProxy-2.1.0.dist-info/WHEEL +5 -0
  7. MapProxy-2.1.0.dist-info/entry_points.txt +4 -0
  8. MapProxy-2.1.0.dist-info/top_level.txt +1 -0
  9. mapproxy/__init__.py +0 -0
  10. mapproxy/cache/__init__.py +36 -0
  11. mapproxy/cache/azureblob.py +145 -0
  12. mapproxy/cache/base.py +120 -0
  13. mapproxy/cache/compact.py +665 -0
  14. mapproxy/cache/couchdb.py +301 -0
  15. mapproxy/cache/dummy.py +36 -0
  16. mapproxy/cache/file.py +200 -0
  17. mapproxy/cache/geopackage.py +647 -0
  18. mapproxy/cache/legend.py +87 -0
  19. mapproxy/cache/mbtiles.py +411 -0
  20. mapproxy/cache/meta.py +80 -0
  21. mapproxy/cache/path.py +261 -0
  22. mapproxy/cache/redis.py +152 -0
  23. mapproxy/cache/renderd.py +100 -0
  24. mapproxy/cache/riak.py +206 -0
  25. mapproxy/cache/s3.py +209 -0
  26. mapproxy/cache/tile.py +736 -0
  27. mapproxy/client/__init__.py +0 -0
  28. mapproxy/client/arcgis.py +82 -0
  29. mapproxy/client/cgi.py +141 -0
  30. mapproxy/client/http.py +295 -0
  31. mapproxy/client/log.py +33 -0
  32. mapproxy/client/tile.py +158 -0
  33. mapproxy/client/wms.py +255 -0
  34. mapproxy/compat/__init__.py +0 -0
  35. mapproxy/compat/image.py +86 -0
  36. mapproxy/config/__init__.py +22 -0
  37. mapproxy/config/config-schema.json +813 -0
  38. mapproxy/config/config.py +213 -0
  39. mapproxy/config/coverage.py +108 -0
  40. mapproxy/config/defaults.py +102 -0
  41. mapproxy/config/loader.py +2399 -0
  42. mapproxy/config/spec.py +657 -0
  43. mapproxy/config/validator.py +242 -0
  44. mapproxy/config_template/__init__.py +0 -0
  45. mapproxy/config_template/base_config/config.wsgi +10 -0
  46. mapproxy/config_template/base_config/full_example.yaml +598 -0
  47. mapproxy/config_template/base_config/full_seed_example.yaml +79 -0
  48. mapproxy/config_template/base_config/log.ini +35 -0
  49. mapproxy/config_template/base_config/mapproxy.yaml +60 -0
  50. mapproxy/config_template/base_config/seed.yaml +27 -0
  51. mapproxy/exception.py +149 -0
  52. mapproxy/featureinfo.py +251 -0
  53. mapproxy/grid.py +1199 -0
  54. mapproxy/image/__init__.py +549 -0
  55. mapproxy/image/fonts/DejaVuSans.ttf +0 -0
  56. mapproxy/image/fonts/DejaVuSansMono.ttf +0 -0
  57. mapproxy/image/fonts/LICENSE +99 -0
  58. mapproxy/image/fonts/__init__.py +0 -0
  59. mapproxy/image/mask.py +79 -0
  60. mapproxy/image/merge.py +323 -0
  61. mapproxy/image/message.py +357 -0
  62. mapproxy/image/opts.py +185 -0
  63. mapproxy/image/tile.py +171 -0
  64. mapproxy/image/transform.py +350 -0
  65. mapproxy/layer.py +489 -0
  66. mapproxy/multiapp.py +230 -0
  67. mapproxy/proj.py +309 -0
  68. mapproxy/request/__init__.py +18 -0
  69. mapproxy/request/arcgis.py +268 -0
  70. mapproxy/request/base.py +466 -0
  71. mapproxy/request/tile.py +131 -0
  72. mapproxy/request/wms/__init__.py +829 -0
  73. mapproxy/request/wms/exception.py +107 -0
  74. mapproxy/request/wmts.py +442 -0
  75. mapproxy/response.py +237 -0
  76. mapproxy/script/__init__.py +0 -0
  77. mapproxy/script/conf/__init__.py +0 -0
  78. mapproxy/script/conf/app.py +222 -0
  79. mapproxy/script/conf/caches.py +44 -0
  80. mapproxy/script/conf/geopackage.py +136 -0
  81. mapproxy/script/conf/layers.py +54 -0
  82. mapproxy/script/conf/seeds.py +36 -0
  83. mapproxy/script/conf/sources.py +88 -0
  84. mapproxy/script/conf/utils.py +148 -0
  85. mapproxy/script/defrag.py +187 -0
  86. mapproxy/script/export.py +337 -0
  87. mapproxy/script/grids.py +198 -0
  88. mapproxy/script/scales.py +134 -0
  89. mapproxy/script/util.py +410 -0
  90. mapproxy/script/wms_capabilities.py +160 -0
  91. mapproxy/seed/__init__.py +0 -0
  92. mapproxy/seed/cachelock.py +127 -0
  93. mapproxy/seed/cleanup.py +191 -0
  94. mapproxy/seed/config.py +481 -0
  95. mapproxy/seed/script.py +391 -0
  96. mapproxy/seed/seeder.py +551 -0
  97. mapproxy/seed/spec.py +66 -0
  98. mapproxy/seed/util.py +266 -0
  99. mapproxy/service/__init__.py +14 -0
  100. mapproxy/service/base.py +45 -0
  101. mapproxy/service/demo.py +364 -0
  102. mapproxy/service/kml.py +333 -0
  103. mapproxy/service/ows.py +39 -0
  104. mapproxy/service/template_helper.py +55 -0
  105. mapproxy/service/templates/demo/capabilities_demo.html +18 -0
  106. mapproxy/service/templates/demo/demo.html +181 -0
  107. mapproxy/service/templates/demo/openlayers-demo.cfg +16 -0
  108. mapproxy/service/templates/demo/static/img/blank.gif +0 -0
  109. mapproxy/service/templates/demo/static/img/east-mini.png +0 -0
  110. mapproxy/service/templates/demo/static/img/north-mini.png +0 -0
  111. mapproxy/service/templates/demo/static/img/south-mini.png +0 -0
  112. mapproxy/service/templates/demo/static/img/west-mini.png +0 -0
  113. mapproxy/service/templates/demo/static/img/zoom-minus-mini.png +0 -0
  114. mapproxy/service/templates/demo/static/img/zoom-plus-mini.png +0 -0
  115. mapproxy/service/templates/demo/static/img/zoom-world-mini.png +0 -0
  116. mapproxy/service/templates/demo/static/logo.png +0 -0
  117. mapproxy/service/templates/demo/static/ol.css +345 -0
  118. mapproxy/service/templates/demo/static/ol.js +4 -0
  119. mapproxy/service/templates/demo/static/proj4.min.js +1 -0
  120. mapproxy/service/templates/demo/static/proj4defs.js +1 -0
  121. mapproxy/service/templates/demo/static/site.css +137 -0
  122. mapproxy/service/templates/demo/static/theme/default/framedCloud.css +0 -0
  123. mapproxy/service/templates/demo/static/theme/default/google.css +17 -0
  124. mapproxy/service/templates/demo/static/theme/default/ie6-style.css +10 -0
  125. mapproxy/service/templates/demo/static/theme/default/style.css +482 -0
  126. mapproxy/service/templates/demo/static.html +34 -0
  127. mapproxy/service/templates/demo/tms_demo.html +117 -0
  128. mapproxy/service/templates/demo/wms_demo.html +144 -0
  129. mapproxy/service/templates/demo/wmts_demo.html +118 -0
  130. mapproxy/service/templates/tms_capabilities.xml +13 -0
  131. mapproxy/service/templates/tms_exception.xml +4 -0
  132. mapproxy/service/templates/tms_root_resource.xml +7 -0
  133. mapproxy/service/templates/tms_tilemap_capabilities.xml +14 -0
  134. mapproxy/service/templates/wms100capabilities.xml +112 -0
  135. mapproxy/service/templates/wms100exception.xml +4 -0
  136. mapproxy/service/templates/wms110capabilities.xml +152 -0
  137. mapproxy/service/templates/wms110exception.xml +5 -0
  138. mapproxy/service/templates/wms111capabilities.xml +183 -0
  139. mapproxy/service/templates/wms111exception.xml +5 -0
  140. mapproxy/service/templates/wms130capabilities.xml +326 -0
  141. mapproxy/service/templates/wms130exception.xml +8 -0
  142. mapproxy/service/templates/wmts100capabilities.xml +155 -0
  143. mapproxy/service/templates/wmts100exception.xml +9 -0
  144. mapproxy/service/tile.py +540 -0
  145. mapproxy/service/wms.py +868 -0
  146. mapproxy/service/wmts.py +387 -0
  147. mapproxy/source/__init__.py +83 -0
  148. mapproxy/source/arcgis.py +39 -0
  149. mapproxy/source/error.py +40 -0
  150. mapproxy/source/mapnik.py +262 -0
  151. mapproxy/source/tile.py +97 -0
  152. mapproxy/source/wms.py +273 -0
  153. mapproxy/srs.py +734 -0
  154. mapproxy/template.py +54 -0
  155. mapproxy/test/__init__.py +0 -0
  156. mapproxy/test/conftest.py +8 -0
  157. mapproxy/test/helper.py +255 -0
  158. mapproxy/test/http.py +511 -0
  159. mapproxy/test/image.py +219 -0
  160. mapproxy/test/mocker.py +2291 -0
  161. mapproxy/test/schemas/inspire/common/1.0/common.xsd +1461 -0
  162. mapproxy/test/schemas/inspire/common/1.0/enums/enum_bul.xsd +108 -0
  163. mapproxy/test/schemas/inspire/common/1.0/enums/enum_cze.xsd +108 -0
  164. mapproxy/test/schemas/inspire/common/1.0/enums/enum_dan.xsd +108 -0
  165. mapproxy/test/schemas/inspire/common/1.0/enums/enum_dut.xsd +108 -0
  166. mapproxy/test/schemas/inspire/common/1.0/enums/enum_eng.xsd +155 -0
  167. mapproxy/test/schemas/inspire/common/1.0/enums/enum_est.xsd +108 -0
  168. mapproxy/test/schemas/inspire/common/1.0/enums/enum_fin.xsd +108 -0
  169. mapproxy/test/schemas/inspire/common/1.0/enums/enum_fre.xsd +108 -0
  170. mapproxy/test/schemas/inspire/common/1.0/enums/enum_ger.xsd +108 -0
  171. mapproxy/test/schemas/inspire/common/1.0/enums/enum_gle.xsd +109 -0
  172. mapproxy/test/schemas/inspire/common/1.0/enums/enum_gre.xsd +108 -0
  173. mapproxy/test/schemas/inspire/common/1.0/enums/enum_hun.xsd +108 -0
  174. mapproxy/test/schemas/inspire/common/1.0/enums/enum_ita.xsd +108 -0
  175. mapproxy/test/schemas/inspire/common/1.0/enums/enum_lav.xsd +108 -0
  176. mapproxy/test/schemas/inspire/common/1.0/enums/enum_lit.xsd +108 -0
  177. mapproxy/test/schemas/inspire/common/1.0/enums/enum_mlt.xsd +108 -0
  178. mapproxy/test/schemas/inspire/common/1.0/enums/enum_pol.xsd +108 -0
  179. mapproxy/test/schemas/inspire/common/1.0/enums/enum_por.xsd +108 -0
  180. mapproxy/test/schemas/inspire/common/1.0/enums/enum_rum.xsd +108 -0
  181. mapproxy/test/schemas/inspire/common/1.0/enums/enum_slo.xsd +108 -0
  182. mapproxy/test/schemas/inspire/common/1.0/enums/enum_slv.xsd +108 -0
  183. mapproxy/test/schemas/inspire/common/1.0/enums/enum_spa.xsd +108 -0
  184. mapproxy/test/schemas/inspire/common/1.0/enums/enum_swe.xsd +108 -0
  185. mapproxy/test/schemas/inspire/common/1.0/network.xsd +521 -0
  186. mapproxy/test/schemas/inspire/inspire_vs/1.0/inspire_vs.xsd +19 -0
  187. mapproxy/test/schemas/kml/2.2.0/ReadMe.txt +14 -0
  188. mapproxy/test/schemas/kml/2.2.0/atom-author-link.xsd +66 -0
  189. mapproxy/test/schemas/kml/2.2.0/ogckml22.xsd +1646 -0
  190. mapproxy/test/schemas/kml/2.2.0/xAL.xsd +1680 -0
  191. mapproxy/test/schemas/ows/1.1.0/ReadMe.txt +87 -0
  192. mapproxy/test/schemas/ows/1.1.0/ows19115subset.xsd +235 -0
  193. mapproxy/test/schemas/ows/1.1.0/owsAll.xsd +23 -0
  194. mapproxy/test/schemas/ows/1.1.0/owsCommon.xsd +157 -0
  195. mapproxy/test/schemas/ows/1.1.0/owsContents.xsd +86 -0
  196. mapproxy/test/schemas/ows/1.1.0/owsDataIdentification.xsd +127 -0
  197. mapproxy/test/schemas/ows/1.1.0/owsDomainType.xsd +279 -0
  198. mapproxy/test/schemas/ows/1.1.0/owsExceptionReport.xsd +76 -0
  199. mapproxy/test/schemas/ows/1.1.0/owsGetCapabilities.xsd +112 -0
  200. mapproxy/test/schemas/ows/1.1.0/owsGetResourceByID.xsd +51 -0
  201. mapproxy/test/schemas/ows/1.1.0/owsInputOutputData.xsd +59 -0
  202. mapproxy/test/schemas/ows/1.1.0/owsManifest.xsd +125 -0
  203. mapproxy/test/schemas/ows/1.1.0/owsOperationsMetadata.xsd +140 -0
  204. mapproxy/test/schemas/ows/1.1.0/owsServiceIdentification.xsd +60 -0
  205. mapproxy/test/schemas/ows/1.1.0/owsServiceProvider.xsd +47 -0
  206. mapproxy/test/schemas/sld/1.1.0/sld_capabilities.xsd +27 -0
  207. mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.dtd +353 -0
  208. mapproxy/test/schemas/wms/1.0.0/capabilities_1_0_0.xml +188 -0
  209. mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.dtd +524 -0
  210. mapproxy/test/schemas/wms/1.0.7/capabilities_1_0_7.xml +260 -0
  211. mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.dtd +273 -0
  212. mapproxy/test/schemas/wms/1.1.0/capabilities_1_1_0.xml +303 -0
  213. mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.dtd +6 -0
  214. mapproxy/test/schemas/wms/1.1.0/exception_1_1_0.xml +33 -0
  215. mapproxy/test/schemas/wms/1.1.1/OGC-exception.xsd +68 -0
  216. mapproxy/test/schemas/wms/1.1.1/WMS_DescribeLayerResponse.dtd +22 -0
  217. mapproxy/test/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd +274 -0
  218. mapproxy/test/schemas/wms/1.1.1/WMS_exception_1_1_1.dtd +5 -0
  219. mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.dtd +276 -0
  220. mapproxy/test/schemas/wms/1.1.1/capabilities_1_1_1.xml +303 -0
  221. mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.dtd +6 -0
  222. mapproxy/test/schemas/wms/1.1.1/exception_1_1_1.xml +33 -0
  223. mapproxy/test/schemas/wms/1.3.0/ReadMe.txt +8 -0
  224. mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xml +277 -0
  225. mapproxy/test/schemas/wms/1.3.0/capabilities_1_3_0.xsd +611 -0
  226. mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xml +34 -0
  227. mapproxy/test/schemas/wms/1.3.0/exceptions_1_3_0.xsd +28 -0
  228. mapproxy/test/schemas/wmsc/1.1.1/OGC-exception.xsd +68 -0
  229. mapproxy/test/schemas/wmsc/1.1.1/WMS_DescribeLayerResponse.dtd +22 -0
  230. mapproxy/test/schemas/wmsc/1.1.1/WMS_MS_Capabilities.dtd +283 -0
  231. mapproxy/test/schemas/wmsc/1.1.1/WMS_exception_1_1_1.dtd +5 -0
  232. mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.dtd +276 -0
  233. mapproxy/test/schemas/wmsc/1.1.1/capabilities_1_1_1.xml +303 -0
  234. mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.dtd +6 -0
  235. mapproxy/test/schemas/wmsc/1.1.1/exception_1_1_1.xml +33 -0
  236. mapproxy/test/schemas/wmts/1.0/ReadMe.txt +32 -0
  237. mapproxy/test/schemas/wmts/1.0/wmts.xsd +28 -0
  238. mapproxy/test/schemas/wmts/1.0/wmtsAbstract.wsdl +151 -0
  239. mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_request.xsd +38 -0
  240. mapproxy/test/schemas/wmts/1.0/wmtsGetCapabilities_response.xsd +564 -0
  241. mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_request.xsd +57 -0
  242. mapproxy/test/schemas/wmts/1.0/wmtsGetFeatureInfo_response.xsd +72 -0
  243. mapproxy/test/schemas/wmts/1.0/wmtsGetTile_request.xsd +91 -0
  244. mapproxy/test/schemas/wmts/1.0/wmtsKVP.xsd +76 -0
  245. mapproxy/test/schemas/wmts/1.0/wmtsPayload_response.xsd +70 -0
  246. mapproxy/test/schemas/xlink/1.0.0/ReadMe.txt +6 -0
  247. mapproxy/test/schemas/xlink/1.0.0/xlinks.xsd +122 -0
  248. mapproxy/test/schemas/xml.xsd +287 -0
  249. mapproxy/test/system/__init__.py +98 -0
  250. mapproxy/test/system/fixture/arcgis.yaml +57 -0
  251. mapproxy/test/system/fixture/auth.yaml +70 -0
  252. mapproxy/test/system/fixture/cache.mbtiles +0 -0
  253. mapproxy/test/system/fixture/cache_azureblob.yaml +59 -0
  254. mapproxy/test/system/fixture/cache_band_merge.yaml +73 -0
  255. mapproxy/test/system/fixture/cache_bulk_meta_tiles.yaml +24 -0
  256. mapproxy/test/system/fixture/cache_coverage.yaml +84 -0
  257. mapproxy/test/system/fixture/cache_data/dop_cache_EPSG3857/00/000/000/000/000/000/000.png +0 -0
  258. mapproxy/test/system/fixture/cache_data/wms_cache_EPSG900913/01/000/000/000/000/000/001.jpeg +0 -0
  259. mapproxy/test/system/fixture/cache_data/wms_cache_transparent_EPSG900913/01/000/000/000/000/000/001.png +0 -0
  260. mapproxy/test/system/fixture/cache_geopackage.yaml +56 -0
  261. mapproxy/test/system/fixture/cache_grid_names.yaml +50 -0
  262. mapproxy/test/system/fixture/cache_mbtiles.yaml +28 -0
  263. mapproxy/test/system/fixture/cache_s3.yaml +58 -0
  264. mapproxy/test/system/fixture/cache_source.yaml +81 -0
  265. mapproxy/test/system/fixture/combined_sources.yaml +130 -0
  266. mapproxy/test/system/fixture/coverage.yaml +77 -0
  267. mapproxy/test/system/fixture/demo.yaml +135 -0
  268. mapproxy/test/system/fixture/dimension.yaml +59 -0
  269. mapproxy/test/system/fixture/disable_storage.yaml +25 -0
  270. mapproxy/test/system/fixture/empty_ogrdata.geojson +1 -0
  271. mapproxy/test/system/fixture/formats.yaml +72 -0
  272. mapproxy/test/system/fixture/inspire.yaml +101 -0
  273. mapproxy/test/system/fixture/inspire_full.yaml +124 -0
  274. mapproxy/test/system/fixture/kml_layer.yaml +66 -0
  275. mapproxy/test/system/fixture/layer.yaml +260 -0
  276. mapproxy/test/system/fixture/layergroups.yaml +57 -0
  277. mapproxy/test/system/fixture/layergroups_root.yaml +106 -0
  278. mapproxy/test/system/fixture/legendgraphic.yaml +93 -0
  279. mapproxy/test/system/fixture/mapnik_source.yaml +66 -0
  280. mapproxy/test/system/fixture/mapproxy_export.yaml +12 -0
  281. mapproxy/test/system/fixture/mapserver.yaml +23 -0
  282. mapproxy/test/system/fixture/minimal_cgi.py +16 -0
  283. mapproxy/test/system/fixture/mixed_mode.yaml +49 -0
  284. mapproxy/test/system/fixture/multi_cache_layers.yaml +111 -0
  285. mapproxy/test/system/fixture/multiapp1.yaml +20 -0
  286. mapproxy/test/system/fixture/multiapp2.yaml +19 -0
  287. mapproxy/test/system/fixture/renderd_client.yaml +55 -0
  288. mapproxy/test/system/fixture/scalehints.yaml +70 -0
  289. mapproxy/test/system/fixture/seed.yaml +94 -0
  290. mapproxy/test/system/fixture/seed_mapproxy.yaml +39 -0
  291. mapproxy/test/system/fixture/seed_old.yaml +12 -0
  292. mapproxy/test/system/fixture/seed_timeouts.yaml +12 -0
  293. mapproxy/test/system/fixture/seed_timeouts_mapproxy.yaml +27 -0
  294. mapproxy/test/system/fixture/seedonly.yaml +51 -0
  295. mapproxy/test/system/fixture/sld.yaml +35 -0
  296. mapproxy/test/system/fixture/source_errors.yaml +84 -0
  297. mapproxy/test/system/fixture/source_errors_raise.yaml +82 -0
  298. mapproxy/test/system/fixture/tileservice_origin.yaml +26 -0
  299. mapproxy/test/system/fixture/tileservice_refresh.yaml +59 -0
  300. mapproxy/test/system/fixture/tilesource_minmax_res.yaml +22 -0
  301. mapproxy/test/system/fixture/util-conf-base-grids.yaml +5 -0
  302. mapproxy/test/system/fixture/util-conf-overwrite.yaml +13 -0
  303. mapproxy/test/system/fixture/util-conf-wms-111-cap.xml +90 -0
  304. mapproxy/test/system/fixture/util_grids.yaml +29 -0
  305. mapproxy/test/system/fixture/util_wms_capabilities111.xml +130 -0
  306. mapproxy/test/system/fixture/util_wms_capabilities130.xml +100 -0
  307. mapproxy/test/system/fixture/util_wms_capabilities_service_exception.xml +5 -0
  308. mapproxy/test/system/fixture/watermark.yaml +50 -0
  309. mapproxy/test/system/fixture/wms_srs_extent.yaml +39 -0
  310. mapproxy/test/system/fixture/wms_versions.yaml +38 -0
  311. mapproxy/test/system/fixture/wmts.yaml +134 -0
  312. mapproxy/test/system/fixture/wmts_dimensions.yaml +57 -0
  313. mapproxy/test/system/fixture/xslt_featureinfo.yaml +54 -0
  314. mapproxy/test/system/fixture/xslt_featureinfo_input.yaml +51 -0
  315. mapproxy/test/system/test_arcgis.py +156 -0
  316. mapproxy/test/system/test_auth.py +1133 -0
  317. mapproxy/test/system/test_behind_proxy.py +75 -0
  318. mapproxy/test/system/test_bulk_meta_tiles.py +106 -0
  319. mapproxy/test/system/test_cache_azureblob.py +127 -0
  320. mapproxy/test/system/test_cache_band_merge.py +103 -0
  321. mapproxy/test/system/test_cache_coverage.py +168 -0
  322. mapproxy/test/system/test_cache_geopackage.py +144 -0
  323. mapproxy/test/system/test_cache_grid_names.py +89 -0
  324. mapproxy/test/system/test_cache_mbtiles.py +85 -0
  325. mapproxy/test/system/test_cache_s3.py +115 -0
  326. mapproxy/test/system/test_cache_source.py +146 -0
  327. mapproxy/test/system/test_combined_sources.py +335 -0
  328. mapproxy/test/system/test_coverage.py +140 -0
  329. mapproxy/test/system/test_decorate_img.py +214 -0
  330. mapproxy/test/system/test_demo.py +56 -0
  331. mapproxy/test/system/test_demo_with_extra_service.py +57 -0
  332. mapproxy/test/system/test_dimensions.py +279 -0
  333. mapproxy/test/system/test_disable_storage.py +42 -0
  334. mapproxy/test/system/test_formats.py +219 -0
  335. mapproxy/test/system/test_inspire_vs.py +173 -0
  336. mapproxy/test/system/test_kml.py +264 -0
  337. mapproxy/test/system/test_layergroups.py +160 -0
  338. mapproxy/test/system/test_legendgraphic.py +308 -0
  339. mapproxy/test/system/test_mapnik.py +162 -0
  340. mapproxy/test/system/test_mapserver.py +83 -0
  341. mapproxy/test/system/test_mixed_mode_format.py +201 -0
  342. mapproxy/test/system/test_multi_cache_layers.py +169 -0
  343. mapproxy/test/system/test_multiapp.py +92 -0
  344. mapproxy/test/system/test_refresh.py +206 -0
  345. mapproxy/test/system/test_renderd_client.py +304 -0
  346. mapproxy/test/system/test_response_headers.py +54 -0
  347. mapproxy/test/system/test_scalehints.py +140 -0
  348. mapproxy/test/system/test_seed.py +425 -0
  349. mapproxy/test/system/test_seed_only.py +93 -0
  350. mapproxy/test/system/test_sld.py +120 -0
  351. mapproxy/test/system/test_source_errors.py +377 -0
  352. mapproxy/test/system/test_tilesource_minmax_res.py +54 -0
  353. mapproxy/test/system/test_tms.py +277 -0
  354. mapproxy/test/system/test_tms_origin.py +46 -0
  355. mapproxy/test/system/test_util_conf.py +434 -0
  356. mapproxy/test/system/test_util_export.py +210 -0
  357. mapproxy/test/system/test_util_grids.py +88 -0
  358. mapproxy/test/system/test_util_wms_capabilities.py +182 -0
  359. mapproxy/test/system/test_watermark.py +91 -0
  360. mapproxy/test/system/test_wms.py +1616 -0
  361. mapproxy/test/system/test_wms_srs_extent.py +165 -0
  362. mapproxy/test/system/test_wms_version.py +85 -0
  363. mapproxy/test/system/test_wmsc.py +116 -0
  364. mapproxy/test/system/test_wmts.py +334 -0
  365. mapproxy/test/system/test_wmts_dimensions.py +206 -0
  366. mapproxy/test/system/test_wmts_restful.py +198 -0
  367. mapproxy/test/system/test_xslt_featureinfo.py +423 -0
  368. mapproxy/test/test_http_helper.py +217 -0
  369. mapproxy/test/unit/__init__.py +0 -0
  370. mapproxy/test/unit/epsg +2 -0
  371. mapproxy/test/unit/polygons/polygons.dbf +0 -0
  372. mapproxy/test/unit/polygons/polygons.shp +0 -0
  373. mapproxy/test/unit/polygons/polygons.shx +0 -0
  374. mapproxy/test/unit/test_async.py +242 -0
  375. mapproxy/test/unit/test_auth.py +430 -0
  376. mapproxy/test/unit/test_cache.py +1356 -0
  377. mapproxy/test/unit/test_cache_azureblob.py +97 -0
  378. mapproxy/test/unit/test_cache_compact.py +324 -0
  379. mapproxy/test/unit/test_cache_couchdb.py +118 -0
  380. mapproxy/test/unit/test_cache_geopackage.py +256 -0
  381. mapproxy/test/unit/test_cache_redis.py +123 -0
  382. mapproxy/test/unit/test_cache_riak.py +80 -0
  383. mapproxy/test/unit/test_cache_s3.py +93 -0
  384. mapproxy/test/unit/test_cache_tile.py +477 -0
  385. mapproxy/test/unit/test_client.py +488 -0
  386. mapproxy/test/unit/test_client_arcgis.py +74 -0
  387. mapproxy/test/unit/test_client_cgi.py +140 -0
  388. mapproxy/test/unit/test_collections.py +116 -0
  389. mapproxy/test/unit/test_concat_legends.py +37 -0
  390. mapproxy/test/unit/test_conf_loader.py +1267 -0
  391. mapproxy/test/unit/test_conf_validator.py +427 -0
  392. mapproxy/test/unit/test_config.py +118 -0
  393. mapproxy/test/unit/test_decorate_img.py +185 -0
  394. mapproxy/test/unit/test_exceptions.py +270 -0
  395. mapproxy/test/unit/test_featureinfo.py +313 -0
  396. mapproxy/test/unit/test_file_lock_load.py +49 -0
  397. mapproxy/test/unit/test_geom.py +512 -0
  398. mapproxy/test/unit/test_grid.py +1279 -0
  399. mapproxy/test/unit/test_image.py +1051 -0
  400. mapproxy/test/unit/test_image_mask.py +181 -0
  401. mapproxy/test/unit/test_image_messages.py +209 -0
  402. mapproxy/test/unit/test_image_options.py +160 -0
  403. mapproxy/test/unit/test_isodate.py +118 -0
  404. mapproxy/test/unit/test_multiapp.py +163 -0
  405. mapproxy/test/unit/test_ogr_reader.py +51 -0
  406. mapproxy/test/unit/test_request.py +745 -0
  407. mapproxy/test/unit/test_request_wmts.py +178 -0
  408. mapproxy/test/unit/test_response.py +78 -0
  409. mapproxy/test/unit/test_seed.py +365 -0
  410. mapproxy/test/unit/test_seed_cachelock.py +91 -0
  411. mapproxy/test/unit/test_srs.py +215 -0
  412. mapproxy/test/unit/test_tiled_source.py +122 -0
  413. mapproxy/test/unit/test_tilefilter.py +31 -0
  414. mapproxy/test/unit/test_times.py +25 -0
  415. mapproxy/test/unit/test_timeutils.py +50 -0
  416. mapproxy/test/unit/test_util_conf_utils.py +75 -0
  417. mapproxy/test/unit/test_utils.py +476 -0
  418. mapproxy/test/unit/test_wms_capabilities.py +44 -0
  419. mapproxy/test/unit/test_wms_layer.py +113 -0
  420. mapproxy/test/unit/test_yaml.py +68 -0
  421. mapproxy/tilefilter.py +61 -0
  422. mapproxy/util/__init__.py +0 -0
  423. mapproxy/util/async_.py +229 -0
  424. mapproxy/util/collections.py +134 -0
  425. mapproxy/util/coverage.py +337 -0
  426. mapproxy/util/ext/__init__.py +14 -0
  427. mapproxy/util/ext/dictspec/__init__.py +1 -0
  428. mapproxy/util/ext/dictspec/spec.py +131 -0
  429. mapproxy/util/ext/dictspec/test/__init__.py +0 -0
  430. mapproxy/util/ext/dictspec/test/test_validator.py +278 -0
  431. mapproxy/util/ext/dictspec/validator.py +194 -0
  432. mapproxy/util/ext/local.py +198 -0
  433. mapproxy/util/ext/lockfile.py +140 -0
  434. mapproxy/util/ext/odict.py +321 -0
  435. mapproxy/util/ext/serving.py +491 -0
  436. mapproxy/util/ext/tempita/__init__.py +1093 -0
  437. mapproxy/util/ext/tempita/_looper.py +163 -0
  438. mapproxy/util/ext/tempita/string_utils.py +24 -0
  439. mapproxy/util/ext/wmsparse/__init__.py +3 -0
  440. mapproxy/util/ext/wmsparse/duration.py +600 -0
  441. mapproxy/util/ext/wmsparse/parse.py +307 -0
  442. mapproxy/util/ext/wmsparse/test/__init__.py +0 -0
  443. mapproxy/util/ext/wmsparse/test/test_parse.py +111 -0
  444. mapproxy/util/ext/wmsparse/test/test_util.py +23 -0
  445. mapproxy/util/ext/wmsparse/test/wms-example-111.xml +90 -0
  446. mapproxy/util/ext/wmsparse/test/wms-example-130.xml +120 -0
  447. mapproxy/util/ext/wmsparse/test/wms-large-111.xml +2114 -0
  448. mapproxy/util/ext/wmsparse/test/wms_nasa_cap.xml +386 -0
  449. mapproxy/util/ext/wmsparse/util.py +189 -0
  450. mapproxy/util/fs.py +164 -0
  451. mapproxy/util/geom.py +307 -0
  452. mapproxy/util/lib.py +117 -0
  453. mapproxy/util/lock.py +171 -0
  454. mapproxy/util/ogr.py +247 -0
  455. mapproxy/util/py.py +75 -0
  456. mapproxy/util/times.py +78 -0
  457. mapproxy/util/yaml.py +58 -0
  458. mapproxy/version.py +33 -0
  459. mapproxy/wsgiapp.py +167 -0
@@ -0,0 +1,1051 @@
1
+ # -:- encoding: utf8 -:-
2
+ # This file is part of the MapProxy project.
3
+ # Copyright (C) 2010 Omniscale <http://omniscale.de>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+
18
+ import os
19
+
20
+ from io import BytesIO
21
+
22
+ import pytest
23
+
24
+ from mapproxy.compat.image import Image, ImageDraw, PIL_VERSION_TUPLE
25
+ from mapproxy.image import (
26
+ BlankImageSource,
27
+ GeoReference,
28
+ ImageSource,
29
+ ReadBufWrapper,
30
+ SubImageSource,
31
+ TIFF_GEOKEYDIRECTORYTAG,
32
+ TIFF_MODELPIXELSCALETAG,
33
+ TIFF_MODELTIEPOINTTAG,
34
+ _make_transparent as make_transparent,
35
+ img_has_transparency,
36
+ is_single_color_image,
37
+ peek_image_format,
38
+ quantize,
39
+ )
40
+ from mapproxy.image.merge import merge_images, BandMerger
41
+ from mapproxy.image.opts import ImageOptions
42
+ from mapproxy.image.tile import TileMerger, TileSplitter
43
+ from mapproxy.image.transform import ImageTransformer, transform_meshes
44
+ from mapproxy.srs import SRS
45
+ from mapproxy.test.image import (
46
+ is_png,
47
+ is_jpeg,
48
+ is_tiff,
49
+ create_tmp_image_file,
50
+ check_format,
51
+ create_debug_img,
52
+ create_image,
53
+ assert_img_colors_eq,
54
+ )
55
+
56
+
57
+ PNG_FORMAT = ImageOptions(format="image/png")
58
+ JPEG_FORMAT = ImageOptions(format="image/jpeg")
59
+ TIFF_FORMAT = ImageOptions(format="image/tiff")
60
+
61
+
62
+ class TestImageSource(object):
63
+
64
+ def setup_method(self):
65
+ self.tmp_filename = create_tmp_image_file((100, 100))
66
+
67
+ def teardown_method(self):
68
+ os.remove(self.tmp_filename)
69
+
70
+ def test_from_filename(self):
71
+ ir = ImageSource(self.tmp_filename, PNG_FORMAT)
72
+ assert is_png(ir.as_buffer())
73
+ assert ir.as_image().size == (100, 100)
74
+
75
+ def test_from_file(self):
76
+ with open(self.tmp_filename, "rb") as tmp_file:
77
+ ir = ImageSource(tmp_file, "png")
78
+ assert ir.as_buffer() == tmp_file
79
+ assert ir.as_image().size == (100, 100)
80
+
81
+ def test_from_image(self):
82
+ img = Image.new("RGBA", (100, 100))
83
+ ir = ImageSource(img, (100, 100), PNG_FORMAT)
84
+ assert ir.as_image() == img
85
+ assert is_png(ir.as_buffer())
86
+
87
+ def test_from_non_seekable_file(self):
88
+ with open(self.tmp_filename, "rb") as tmp_file:
89
+ data = tmp_file.read()
90
+
91
+ class FileLikeDummy(object):
92
+ # "file" without seek, like urlopen response
93
+ def read(self):
94
+ return data
95
+
96
+ ir = ImageSource(FileLikeDummy(), "png")
97
+ assert ir.as_buffer(seekable=True).read() == data
98
+ assert ir.as_image().size == (100, 100)
99
+ assert ir.as_buffer().read() == data
100
+
101
+ def test_output_formats(self):
102
+ img = Image.new("RGB", (100, 100))
103
+ for format in ["png", "gif", "tiff", "jpeg", "GeoTIFF", "bmp"]:
104
+ ir = ImageSource(img, (100, 100), image_opts=ImageOptions(format=format))
105
+ check_format(ir.as_buffer(), format)
106
+
107
+ def test_converted_output(self):
108
+ ir = ImageSource(self.tmp_filename, (100, 100), PNG_FORMAT)
109
+ assert is_png(ir.as_buffer())
110
+ assert is_jpeg(ir.as_buffer(JPEG_FORMAT))
111
+ assert is_jpeg(ir.as_buffer())
112
+ assert is_tiff(ir.as_buffer(TIFF_FORMAT))
113
+ assert is_tiff(ir.as_buffer())
114
+
115
+ @pytest.mark.skipif(PIL_VERSION_TUPLE < (6, 1, 0), reason="Pillow 6.1.0 required GeoTIFF")
116
+ def test_tiff_compression(self):
117
+ def encoded_size(encoding_options):
118
+ ir = ImageSource(create_debug_img((100, 100)), PNG_FORMAT)
119
+ buf = ir.as_buffer(ImageOptions(format="tiff", encoding_options=encoding_options))
120
+ return len(buf.read())
121
+
122
+ orig = encoded_size({})
123
+ q90 = encoded_size({'tiff_compression': 'jpeg', 'jpeg_quality': 90})
124
+ q75 = encoded_size({'tiff_compression': 'jpeg', 'jpeg_quality': 75})
125
+ qdf = encoded_size({'tiff_compression': 'jpeg'})
126
+ q50 = encoded_size({'tiff_compression': 'jpeg', 'jpeg_quality': 50})
127
+ lzw = encoded_size({'tiff_compression': 'tiff_lzw'})
128
+
129
+ # print(orig, q90, q75, qdf, q50, lzw)
130
+ assert orig > q90
131
+ assert q90 > q75
132
+ assert q75 == qdf
133
+ assert qdf > q50
134
+ assert q50 > lzw
135
+
136
+ @pytest.mark.xfail(
137
+ PIL_VERSION_TUPLE >= (9, 0, 0),
138
+ reason="The palette colors order has been changed in Pillow 9.0.0"
139
+ )
140
+ def test_output_formats_greyscale_png(self):
141
+ img = Image.new("L", (100, 100))
142
+ ir = ImageSource(img, image_opts=PNG_FORMAT)
143
+ img = Image.open(
144
+ ir.as_buffer(ImageOptions(colors=256, transparent=True, format="image/png"))
145
+ )
146
+ assert img.mode == "P"
147
+ assert img.getpixel((0, 0)) == 255
148
+
149
+ def test_output_formats_greyscale_alpha_png(self):
150
+ img = Image.new("LA", (100, 100))
151
+ ir = ImageSource(img, image_opts=PNG_FORMAT)
152
+ img = Image.open(
153
+ ir.as_buffer(ImageOptions(colors=256, transparent=True, format="image/png"))
154
+ )
155
+ assert img.mode == "LA"
156
+ assert img.getpixel((0, 0)) == (0, 0)
157
+
158
+ @pytest.mark.xfail(
159
+ PIL_VERSION_TUPLE >= (9, 0, 0),
160
+ reason="The palette colors order has been changed in Pillow 9.0.0"
161
+ )
162
+ def test_output_formats_png8(self):
163
+ img = Image.new("RGBA", (100, 100))
164
+ ir = ImageSource(img, image_opts=PNG_FORMAT)
165
+ img = Image.open(
166
+ ir.as_buffer(ImageOptions(colors=256, transparent=True, format="image/png"))
167
+ )
168
+ assert img.mode == "P"
169
+ assert img.getpixel((0, 0)) == 255
170
+
171
+ def test_output_formats_png24(self):
172
+ img = Image.new("RGBA", (100, 100))
173
+ image_opts = PNG_FORMAT.copy()
174
+ image_opts.colors = 0 # TODO image_opts
175
+ ir = ImageSource(img, image_opts=image_opts)
176
+ img = Image.open(ir.as_buffer())
177
+ assert img.mode == "RGBA"
178
+ assert img.getpixel((0, 0)) == (0, 0, 0, 0)
179
+
180
+ def test_save_with_unsupported_transparency(self):
181
+ # check if encoding of non-RGB image with tuple as transparency
182
+ # works. workaround for Pillow #2633
183
+ img = Image.new("P", (100, 100))
184
+ img.info["transparency"] = (0, 0, 0)
185
+ image_opts = PNG_FORMAT.copy()
186
+
187
+ ir = ImageSource(img, image_opts=image_opts)
188
+ img = Image.open(ir.as_buffer())
189
+ assert img.mode == "P"
190
+
191
+
192
+ class TestSubImageSource(object):
193
+
194
+ def test_full(self):
195
+ sub_img = create_image((100, 100), color=[100, 120, 130, 140])
196
+ img = SubImageSource(
197
+ sub_img, size=(100, 100), offset=(0, 0), image_opts=ImageOptions()
198
+ ).as_image()
199
+ assert img.getcolors() == [(100 * 100, (100, 120, 130, 140))]
200
+
201
+ def test_larger(self):
202
+ sub_img = create_image((150, 150), color=[100, 120, 130, 140])
203
+ img = SubImageSource(
204
+ sub_img, size=(100, 100), offset=(0, 0), image_opts=ImageOptions()
205
+ ).as_image()
206
+ assert img.getcolors() == [(100 * 100, (100, 120, 130, 140))]
207
+
208
+ def test_negative_offset(self):
209
+ sub_img = create_image((150, 150), color=[100, 120, 130, 140])
210
+ img = SubImageSource(
211
+ sub_img, size=(100, 100), offset=(-50, 0), image_opts=ImageOptions()
212
+ ).as_image()
213
+ assert img.getcolors() == [(100 * 100, (100, 120, 130, 140))]
214
+
215
+ def test_overlap_right(self):
216
+ sub_img = create_image((50, 50), color=[100, 120, 130, 140])
217
+ img = SubImageSource(
218
+ sub_img,
219
+ size=(100, 100),
220
+ offset=(75, 25),
221
+ image_opts=ImageOptions(transparent=True),
222
+ ).as_image()
223
+ assert sorted(img.getcolors()) == [
224
+ (25 * 50, (100, 120, 130, 140)),
225
+ (100 * 100 - 25 * 50, (255, 255, 255, 0)),
226
+ ]
227
+
228
+ def test_outside(self):
229
+ sub_img = create_image((50, 50), color=[100, 120, 130, 140])
230
+ img = SubImageSource(
231
+ sub_img,
232
+ size=(100, 100),
233
+ offset=(200, 0),
234
+ image_opts=ImageOptions(transparent=True),
235
+ ).as_image()
236
+ assert img.getcolors() == [(100 * 100, (255, 255, 255, 0))]
237
+
238
+
239
+ class ROnly(object):
240
+
241
+ def __init__(self):
242
+ self.data = [b"Hello World!"]
243
+
244
+ def read(self):
245
+ if self.data:
246
+ return self.data.pop()
247
+ return b""
248
+
249
+ def __iter__(self):
250
+ it = iter(self.data)
251
+ self.data = []
252
+ return it
253
+
254
+
255
+ class TestReadBufWrapper(object):
256
+
257
+ def setup_method(self):
258
+ rbuf = ROnly()
259
+ self.rbuf_wrapper = ReadBufWrapper(rbuf)
260
+
261
+ def test_read(self):
262
+ assert self.rbuf_wrapper.read() == b"Hello World!"
263
+ self.rbuf_wrapper.seek(0)
264
+ assert self.rbuf_wrapper.read() == b""
265
+
266
+ def test_seek_read(self):
267
+ self.rbuf_wrapper.seek(0)
268
+ assert self.rbuf_wrapper.read() == b"Hello World!"
269
+ self.rbuf_wrapper.seek(0)
270
+ assert self.rbuf_wrapper.read() == b"Hello World!"
271
+
272
+ def test_iter(self):
273
+ data = list(self.rbuf_wrapper)
274
+ assert data == [b"Hello World!"]
275
+ self.rbuf_wrapper.seek(0)
276
+ data = list(self.rbuf_wrapper)
277
+ assert data == []
278
+
279
+ def test_seek_iter(self):
280
+ self.rbuf_wrapper.seek(0)
281
+ data = list(self.rbuf_wrapper)
282
+ assert data == [b"Hello World!"]
283
+ self.rbuf_wrapper.seek(0)
284
+ data = list(self.rbuf_wrapper)
285
+ assert data == [b"Hello World!"]
286
+
287
+ def test_hasattr(self):
288
+ assert hasattr(self.rbuf_wrapper, "seek")
289
+ assert hasattr(self.rbuf_wrapper, "readline")
290
+
291
+
292
+ class TestMergeAll(object):
293
+
294
+ def setup_method(self):
295
+ self.cleanup_tiles = []
296
+
297
+ def test_full_merge(self):
298
+ self.cleanup_tiles = [create_tmp_image_file((100, 100)) for _ in range(9)]
299
+ self.tiles = [ImageSource(tile) for tile in self.cleanup_tiles]
300
+ m = TileMerger(tile_grid=(3, 3), tile_size=(100, 100))
301
+ img_opts = ImageOptions()
302
+ result = m.merge(self.tiles, img_opts)
303
+ img = result.as_image()
304
+ assert img.size == (300, 300)
305
+
306
+ def test_one(self):
307
+ self.cleanup_tiles = [create_tmp_image_file((100, 100))]
308
+ self.tiles = [ImageSource(self.cleanup_tiles[0])]
309
+ m = TileMerger(tile_grid=(1, 1), tile_size=(100, 100))
310
+ img_opts = ImageOptions(transparent=True)
311
+ result = m.merge(self.tiles, img_opts)
312
+ img = result.as_image()
313
+ assert img.size == (100, 100)
314
+ assert img.mode == "RGBA"
315
+
316
+ def test_missing_tiles(self):
317
+ self.cleanup_tiles = [create_tmp_image_file((100, 100))]
318
+ self.tiles = [ImageSource(self.cleanup_tiles[0])]
319
+ self.tiles.extend([None] * 8)
320
+ m = TileMerger(tile_grid=(3, 3), tile_size=(100, 100))
321
+ img_opts = ImageOptions()
322
+ result = m.merge(self.tiles, img_opts)
323
+ img = result.as_image()
324
+ assert img.size == (300, 300)
325
+ assert img.getcolors() == [(80000, (255, 255, 255)), (10000, (0, 0, 0))]
326
+
327
+ def test_invalid_tile(self):
328
+ self.cleanup_tiles = [create_tmp_image_file((100, 100)) for _ in range(9)]
329
+ self.tiles = [ImageSource(tile) for tile in self.cleanup_tiles]
330
+ invalid_tile = self.tiles[0].source
331
+ with open(invalid_tile, "wb") as tmp:
332
+ tmp.write(b"invalid")
333
+ m = TileMerger(tile_grid=(3, 3), tile_size=(100, 100))
334
+ img_opts = ImageOptions(bgcolor=(200, 0, 50))
335
+ result = m.merge(self.tiles, img_opts)
336
+ img = result.as_image()
337
+ assert img.size == (300, 300)
338
+ assert img.getcolors() == [(10000, (200, 0, 50)), (80000, (0, 0, 0))]
339
+ assert not os.path.isfile(invalid_tile)
340
+
341
+ def test_none_merge(self):
342
+ tiles = [None]
343
+ m = TileMerger(tile_grid=(1, 1), tile_size=(100, 100))
344
+ img_opts = ImageOptions(mode="RGBA", bgcolor=(200, 100, 30, 40))
345
+ result = m.merge(tiles, img_opts)
346
+ img = result.as_image()
347
+ assert img.size == (100, 100)
348
+ assert img.getcolors() == [(100 * 100, (200, 100, 30, 40))]
349
+
350
+ def teardown_method(self):
351
+ for tile_fname in self.cleanup_tiles:
352
+ if tile_fname and os.path.isfile(tile_fname):
353
+ os.remove(tile_fname)
354
+
355
+
356
+ class TestGetCrop(object):
357
+
358
+ def setup_method(self):
359
+ self.tmp_file = create_tmp_image_file((100, 100), two_colored=True)
360
+ self.img = ImageSource(
361
+ self.tmp_file, image_opts=ImageOptions(format="image/png"), size=(100, 100)
362
+ )
363
+
364
+ def teardown_method(self):
365
+ if os.path.exists(self.tmp_file):
366
+ os.remove(self.tmp_file)
367
+
368
+ def test_perfect_match(self):
369
+ bbox = (-10, -5, 30, 35)
370
+ transformer = ImageTransformer(SRS(4326), SRS(4326))
371
+ result = transformer.transform(
372
+ self.img, bbox, (100, 100), bbox, image_opts=None
373
+ )
374
+ assert self.img == result
375
+
376
+ def test_simple_resize_nearest(self):
377
+ bbox = (-10, -5, 30, 35)
378
+ transformer = ImageTransformer(SRS(4326), SRS(4326))
379
+ result = transformer.transform(
380
+ self.img,
381
+ bbox,
382
+ (200, 200),
383
+ bbox,
384
+ image_opts=ImageOptions(resampling="nearest"),
385
+ )
386
+ img = result.as_image()
387
+
388
+ assert img.size == (200, 200)
389
+ assert len(img.getcolors()) == 2
390
+ img.close()
391
+
392
+ def test_simple_resize_bilinear(self):
393
+ bbox = (-10, -5, 30, 35)
394
+ transformer = ImageTransformer(SRS(4326), SRS(4326))
395
+ result = transformer.transform(
396
+ self.img,
397
+ bbox,
398
+ (200, 200),
399
+ bbox,
400
+ image_opts=ImageOptions(resampling="bilinear"),
401
+ )
402
+ img = result.as_image()
403
+
404
+ assert img.size == (200, 200)
405
+ # some shades of grey with bilinear
406
+ assert len(img.getcolors()) >= 4
407
+ img.close()
408
+
409
+
410
+ class TestLayerMerge(object):
411
+
412
+ def test_opacity_merge(self):
413
+ img1 = ImageSource(Image.new("RGB", (10, 10), (255, 0, 255)))
414
+ img2 = ImageSource(
415
+ Image.new("RGB", (10, 10), (0, 255, 255)),
416
+ image_opts=ImageOptions(opacity=0.5),
417
+ )
418
+
419
+ result = merge_images([img1, img2], ImageOptions(transparent=False))
420
+ img = result.as_image()
421
+ assert img.getpixel((0, 0)) == (127, 127, 255)
422
+
423
+ def test_opacity_merge_mixed_modes(self):
424
+ img1 = ImageSource(Image.new("RGBA", (10, 10), (255, 0, 255, 255)))
425
+ img2 = ImageSource(
426
+ Image.new("RGB", (10, 10), (0, 255, 255)).convert("P"),
427
+ image_opts=ImageOptions(opacity=0.5),
428
+ )
429
+
430
+ result = merge_images([img1, img2], ImageOptions(transparent=True))
431
+ img = result.as_image()
432
+ assert_img_colors_eq(img, [(10 * 10, (127, 127, 255, 255))])
433
+
434
+ def test_merge_L(self):
435
+ img1 = ImageSource(Image.new("RGBA", (10, 10), (255, 0, 255, 255)))
436
+ img2 = ImageSource(Image.new("L", (10, 10), 100))
437
+
438
+ # img2 overlays img1
439
+ result = merge_images([img1, img2], ImageOptions(transparent=True))
440
+ img = result.as_image()
441
+ assert_img_colors_eq(img, [(10 * 10, (100, 100, 100, 255))])
442
+
443
+ @pytest.mark.skipif(
444
+ not hasattr(Image, "FASTOCTREE"), reason="PIL has no FASTOCTREE"
445
+ )
446
+ def test_paletted_merge(self):
447
+ # generate RGBA images with a transparent rectangle in the lower right
448
+ img1 = ImageSource(Image.new("RGBA", (50, 50), (0, 255, 0, 255))).as_image()
449
+ draw = ImageDraw.Draw(img1)
450
+ draw.rectangle((25, 25, 49, 49), fill=(0, 0, 0, 0))
451
+ paletted_img = quantize(img1, alpha=True)
452
+ assert img_has_transparency(paletted_img)
453
+ assert paletted_img.mode == "P"
454
+
455
+ rgba_img = Image.new("RGBA", (50, 50), (255, 0, 0, 255))
456
+ draw = ImageDraw.Draw(rgba_img)
457
+ draw.rectangle((25, 25, 49, 49), fill=(0, 0, 0, 0))
458
+
459
+ img1 = ImageSource(paletted_img)
460
+ img2 = ImageSource(rgba_img)
461
+
462
+ # generate base image and merge the others above
463
+ img3 = ImageSource(Image.new("RGBA", (50, 50), (0, 0, 255, 255)))
464
+ result = merge_images([img3, img1, img2], ImageOptions(transparent=True))
465
+ img = result.as_image()
466
+
467
+ assert img.mode == "RGBA"
468
+ assert img.getpixel((49, 49)) == (0, 0, 255, 255)
469
+ assert img.getpixel((0, 0)) == (255, 0, 0, 255)
470
+
471
+ def test_solid_merge(self):
472
+ img1 = ImageSource(Image.new("RGB", (10, 10), (255, 0, 255)))
473
+ img2 = ImageSource(Image.new("RGB", (10, 10), (0, 255, 255)))
474
+
475
+ result = merge_images([img1, img2], ImageOptions(transparent=False))
476
+ img = result.as_image()
477
+ assert img.getpixel((0, 0)) == (0, 255, 255)
478
+
479
+ def test_merge_rgb_with_transp(self):
480
+ img1 = ImageSource(Image.new("RGB", (10, 10), (255, 0, 255)))
481
+ raw = Image.new("RGB", (10, 10), (0, 255, 255))
482
+ raw.info = {"transparency": (0, 255, 255)} # make full transparent
483
+ img2 = ImageSource(raw)
484
+
485
+ result = merge_images([img1, img2], ImageOptions(transparent=False))
486
+ img = result.as_image()
487
+ assert img.getpixel((0, 0)) == (255, 0, 255)
488
+
489
+
490
+ @pytest.mark.skipif(
491
+ not hasattr(Image, "alpha_composite"), reason="PIL has no alpha_composite"
492
+ )
493
+ class TestLayerCompositeMerge(object):
494
+
495
+ def test_composite_merge(self):
496
+ # http://stackoverflow.com/questions/3374878
497
+
498
+ img1 = Image.new("RGBA", size=(100, 100), color=(255, 0, 0, 255))
499
+ draw = ImageDraw.Draw(img1)
500
+ draw.rectangle((33, 0, 66, 100), fill=(255, 0, 0, 128))
501
+ draw.rectangle((67, 0, 100, 100), fill=(255, 0, 0, 0))
502
+ img1 = ImageSource(img1)
503
+ img2 = Image.new("RGBA", size=(100, 100), color=(0, 255, 0, 255))
504
+ draw = ImageDraw.Draw(img2)
505
+ draw.rectangle((0, 33, 100, 66), fill=(0, 255, 0, 128))
506
+ draw.rectangle((0, 67, 100, 100), fill=(0, 255, 0, 0))
507
+ img2 = ImageSource(img2)
508
+
509
+ result = merge_images([img2, img1], ImageOptions(transparent=True))
510
+ img = result.as_image()
511
+ assert img.mode == "RGBA"
512
+ assert_img_colors_eq(
513
+ img,
514
+ [
515
+ (1089, (0, 255, 0, 255)),
516
+ (1089, (255, 255, 255, 0)),
517
+ (1122, (0, 255, 0, 128)),
518
+ (1122, (128, 126, 0, 255)),
519
+ (1122, (255, 0, 0, 128)),
520
+ (1156, (170, 84, 0, 191)),
521
+ (3300, (255, 0, 0, 255)),
522
+ ],
523
+ )
524
+
525
+ def test_composite_merge_opacity(self):
526
+ bg = Image.new("RGBA", size=(100, 100), color=(255, 0, 255, 255))
527
+ bg = ImageSource(bg)
528
+ fg = Image.new("RGBA", size=(100, 100), color=(0, 0, 0, 0))
529
+ draw = ImageDraw.Draw(fg)
530
+ draw.rectangle((10, 10, 89, 89), fill=(0, 255, 255, 255))
531
+ fg = ImageSource(fg, image_opts=ImageOptions(opacity=0.5))
532
+
533
+ result = merge_images([bg, fg], ImageOptions(transparent=True))
534
+ img = result.as_image()
535
+ assert img.mode == "RGBA"
536
+ assert_img_colors_eq(
537
+ img, [(3600, (255, 0, 255, 255)), (6400, (128, 127, 255, 255))]
538
+ )
539
+
540
+
541
+ class TestTransform(object):
542
+
543
+ def setup_method(self):
544
+ self.src_img = ImageSource(create_debug_img((200, 200), transparent=False))
545
+ self.src_srs = SRS(31467)
546
+ self.dst_size = (100, 150)
547
+ self.dst_srs = SRS(4326)
548
+ self.dst_bbox = (0.2, 45.1, 8.3, 53.2)
549
+ self.src_bbox = self.dst_srs.transform_bbox_to(self.src_srs, self.dst_bbox)
550
+
551
+ def test_transform(self):
552
+ transformer = ImageTransformer(self.src_srs, self.dst_srs)
553
+ result = transformer.transform(
554
+ self.src_img,
555
+ self.src_bbox,
556
+ self.dst_size,
557
+ self.dst_bbox,
558
+ image_opts=ImageOptions(resampling="nearest"),
559
+ )
560
+ assert isinstance(result, ImageSource)
561
+ assert result.as_image() != self.src_img.as_image()
562
+ assert result.size == (100, 150)
563
+
564
+ def _test_compare_max_px_err(self):
565
+ """
566
+ Create transformations with different div values.
567
+ """
568
+ for err in [0.2, 0.5, 1, 2, 4, 6, 8, 12, 16]:
569
+ transformer = ImageTransformer(self.src_srs, self.dst_srs, max_px_err=err)
570
+ result = transformer.transform(
571
+ self.src_img,
572
+ self.src_bbox,
573
+ self.dst_size,
574
+ self.dst_bbox,
575
+ image_opts=ImageOptions(resampling="nearest"),
576
+ )
577
+ result.as_image().save("/tmp/transform-%03d.png" % (err * 10,))
578
+
579
+
580
+ def assert_geotiff_tags(img, expected_origin, expected_pixel_res, srs, projected):
581
+ tags = img.tag_v2
582
+ print(dict(tags))
583
+ print(dict(tags.tagtype))
584
+ assert tags[TIFF_MODELTIEPOINTTAG] == (
585
+ 0.0, 0.0, 0.0, expected_origin[0], expected_origin[1], 0.0,
586
+ )
587
+ assert tags[TIFF_MODELPIXELSCALETAG] == pytest.approx((
588
+ expected_pixel_res[0], expected_pixel_res[1], 0.0,
589
+ ))
590
+ assert len(tags[TIFF_GEOKEYDIRECTORYTAG]) == 4*4
591
+ assert tags[TIFF_GEOKEYDIRECTORYTAG][0*4+3] == 3
592
+ assert tags[TIFF_GEOKEYDIRECTORYTAG][1*4+3] == (1 if projected else 2)
593
+ assert tags[TIFF_GEOKEYDIRECTORYTAG][3*4+3] == srs
594
+
595
+
596
+ @pytest.mark.skipif(PIL_VERSION_TUPLE < (6, 1, 0), reason="Pillow 6.1.0 required GeoTIFF")
597
+ @pytest.mark.parametrize("compression", ['jpeg', 'raw', 'tiff_lzw'])
598
+ class TestGeoTIFF(object):
599
+
600
+ @pytest.mark.parametrize(
601
+ "srs,bbox,size,expected_pixel_res,expected_origin,projected",
602
+ [
603
+ (4326, (-180, -90, 180, 90), (360, 180), (1.0, 1.0), (-180, 90), False),
604
+ (4326, (-180, -90, 180, 90), (360, 360), (1.0, 0.5), (-180, 90), False),
605
+ (3857, (10000, 20000, 11000, 22000), (500, 1000), (2.0, 2.0), (10000, 22000), True),
606
+ (25832, (442691.10009850014, 5889716.375224128, 447502.95988220774, 5894528.235007785),
607
+ (256, 256), (18.796327, 18.796327), (442691.10009850014, 5894528.235007785), True),
608
+ ],
609
+ )
610
+ def test_geotiff_tags(
611
+ self, tmpdir, srs, bbox, size,
612
+ expected_pixel_res, expected_origin, projected,
613
+ compression,
614
+ ):
615
+ img = ImageSource(create_debug_img(size), georef=GeoReference(bbox=bbox, srs=SRS(srs)))
616
+
617
+ img_opts = ImageOptions(format='tiff', encoding_options={'tiff_compression': compression})
618
+ img2 = ImageSource(img.as_buffer(img_opts)).as_image()
619
+
620
+ assert_geotiff_tags(img2, expected_origin, expected_pixel_res, srs, projected)
621
+
622
+
623
+ class TestMesh(object):
624
+
625
+ def test_mesh_utm(self):
626
+ meshes = transform_meshes(
627
+ src_size=(1335, 1531),
628
+ src_bbox=(3.65, 39.84, 17.00, 55.15),
629
+ src_srs=SRS(4326),
630
+ dst_size=(853, 1683),
631
+ dst_bbox=(158512, 4428236, 1012321, 6111268),
632
+ dst_srs=SRS(25832),
633
+ )
634
+ assert len(meshes) == 40
635
+
636
+ def test_mesh_none(self):
637
+ meshes = transform_meshes(
638
+ src_size=(1000, 1500),
639
+ src_bbox=(0, 0, 10, 15),
640
+ src_srs=SRS(4326),
641
+ dst_size=(1000, 1500),
642
+ dst_bbox=(0, 0, 10, 15),
643
+ dst_srs=SRS(4326),
644
+ )
645
+
646
+ assert meshes == [
647
+ ((0, 0, 1000, 1500), [0.0, 0.0, 0.0, 1500.0, 1000.0, 1500.0, 1000.0, 0.0])
648
+ ]
649
+ assert len(meshes) == 1
650
+
651
+ def test_mesh(self):
652
+ # low map scale -> more meshes
653
+ # print(SRS(4326).transform_bbox_to(SRS(3857), (5, 50, 10, 55)))
654
+ meshes = transform_meshes(
655
+ src_size=(1000, 2000),
656
+ src_bbox=(556597, 6446275, 1113194, 7361866),
657
+ src_srs=SRS(3857),
658
+ dst_size=(1000, 1000),
659
+ dst_bbox=(5, 50, 10, 55),
660
+ dst_srs=SRS(4326),
661
+ )
662
+ assert len(meshes) == 16
663
+
664
+ # large map scale -> one meshes
665
+ # print(SRS(4326).transform_bbox_to(SRS(3857), (5, 50, 5.1, 50.1)))
666
+ meshes = transform_meshes(
667
+ src_size=(1000, 2000),
668
+ src_bbox=(
669
+ 556597.4539663672,
670
+ 6446275.841017158,
671
+ 567729.4030456939,
672
+ 6463612.124257667,
673
+ ),
674
+ src_srs=SRS(3857),
675
+ dst_size=(1000, 1000),
676
+ dst_bbox=(5, 50, 5.1, 50.1),
677
+ dst_srs=SRS(4326),
678
+ )
679
+ assert len(meshes) == 1
680
+
681
+ # quad stretches whole image plus 1 pixel
682
+ assert meshes[0][0] == (0, 0, 1000, 1000)
683
+ for e, a in zip(
684
+ meshes[0][1], [0.0, 0.0, 0.0, 2000.0, 1000.0, 2000.0, 1000.0, 0.0]
685
+ ):
686
+ assert e == pytest.approx(a, abs=1e-9)
687
+
688
+
689
+ class TestSingleColorImage(object):
690
+
691
+ def test_one_point(self):
692
+ img = Image.new("RGB", (100, 100), color="#ff0000")
693
+ draw = ImageDraw.Draw(img)
694
+ draw.point((99, 99))
695
+ del draw
696
+
697
+ assert not is_single_color_image(img)
698
+
699
+ def test_solid(self):
700
+ img = Image.new("RGB", (100, 100), color="#ff0102")
701
+ assert is_single_color_image(img) == (255, 1, 2)
702
+
703
+ def test_solid_w_alpha(self):
704
+ img = Image.new("RGBA", (100, 100), color="#ff0102")
705
+ assert is_single_color_image(img) == (255, 1, 2, 255)
706
+
707
+ def test_solid_paletted_image(self):
708
+ img = Image.new("P", (100, 100), color=20)
709
+ palette = []
710
+ for i in range(256):
711
+ palette.extend((i, i // 2, i % 3))
712
+ img.putpalette(palette)
713
+ assert is_single_color_image(img) == (20, 10, 2)
714
+
715
+
716
+ class TestMakeTransparent(object):
717
+
718
+ def _make_test_image(self):
719
+ img = Image.new("RGB", (50, 50), (130, 140, 120))
720
+ draw = ImageDraw.Draw(img)
721
+ draw.rectangle((10, 10, 39, 39), fill=(130, 150, 120))
722
+ return img
723
+
724
+ def _make_transp_test_image(self):
725
+ img = Image.new("RGBA", (50, 50), (130, 140, 120, 100))
726
+ draw = ImageDraw.Draw(img)
727
+ draw.rectangle((10, 10, 39, 39), fill=(130, 150, 120, 120))
728
+ return img
729
+
730
+ def test_result(self):
731
+ img = self._make_test_image()
732
+ img = make_transparent(img, (130, 150, 120), tolerance=5)
733
+ assert img.mode == "RGBA"
734
+ assert img.size == (50, 50)
735
+ colors = img.getcolors()
736
+ assert colors == [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 0))]
737
+
738
+ def test_with_color_fuzz(self):
739
+ img = self._make_test_image()
740
+ img = make_transparent(img, (128, 154, 121), tolerance=5)
741
+ assert img.mode == "RGBA"
742
+ assert img.size == (50, 50)
743
+ colors = img.getcolors()
744
+ assert colors == [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 0))]
745
+
746
+ def test_no_match(self):
747
+ img = self._make_test_image()
748
+ img = make_transparent(img, (130, 160, 120), tolerance=5)
749
+ assert img.mode == "RGBA"
750
+ assert img.size == (50, 50)
751
+ colors = img.getcolors()
752
+ assert colors == [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 255))]
753
+
754
+ def test_from_paletted(self):
755
+ img = self._make_test_image().quantize(256)
756
+ img = make_transparent(img, (130, 150, 120), tolerance=5)
757
+ assert img.mode == "RGBA"
758
+ assert img.size == (50, 50)
759
+ colors = img.getcolors()
760
+ assert colors == [(1600, (130, 140, 120, 255)), (900, (130, 150, 120, 0))]
761
+
762
+ def test_from_transparent(self):
763
+ img = self._make_transp_test_image()
764
+ draw = ImageDraw.Draw(img)
765
+ draw.rectangle((0, 0, 4, 4), fill=(130, 100, 120, 0))
766
+ draw.rectangle((5, 5, 9, 9), fill=(130, 150, 120, 255))
767
+ img = make_transparent(img, (130, 150, 120, 120), tolerance=5)
768
+ assert img.mode == "RGBA"
769
+ assert img.size == (50, 50)
770
+ colors = sorted(img.getcolors(), reverse=True)
771
+
772
+ assert colors == [
773
+ (1550, (130, 140, 120, 100)),
774
+ (900, (130, 150, 120, 0)),
775
+ (25, (130, 150, 120, 255)),
776
+ (25, (130, 100, 120, 0)),
777
+ ]
778
+
779
+
780
+ class TestTileSplitter(object):
781
+
782
+ def test_background_larger_crop(self):
783
+ img = ImageSource(Image.new("RGB", (356, 266), (130, 140, 120)))
784
+ img_opts = ImageOptions("RGB")
785
+ splitter = TileSplitter(img, img_opts)
786
+
787
+ tile = splitter.get_tile((0, 0), (256, 256))
788
+
789
+ assert tile.size == (256, 256)
790
+ colors = tile.as_image().getcolors()
791
+ assert colors == [(256 * 256, (130, 140, 120))]
792
+
793
+ tile = splitter.get_tile((256, 256), (256, 256))
794
+
795
+ assert tile.size == (256, 256)
796
+ colors = tile.as_image().getcolors()
797
+ assert sorted(colors) == [
798
+ (10 * 100, (130, 140, 120)),
799
+ (256 * 256 - 10 * 100, (255, 255, 255)),
800
+ ]
801
+
802
+ def test_background_larger_crop_with_transparent(self):
803
+ img = ImageSource(Image.new("RGBA", (356, 266), (130, 140, 120, 255)))
804
+ img_opts = ImageOptions("RGBA", transparent=True)
805
+ splitter = TileSplitter(img, img_opts)
806
+
807
+ tile = splitter.get_tile((0, 0), (256, 256))
808
+
809
+ assert tile.size == (256, 256)
810
+ colors = tile.as_image().getcolors()
811
+ assert colors == [(256 * 256, (130, 140, 120, 255))]
812
+
813
+ tile = splitter.get_tile((256, 256), (256, 256))
814
+
815
+ assert tile.size == (256, 256)
816
+ colors = tile.as_image().getcolors()
817
+ assert sorted(colors) == [
818
+ (10 * 100, (130, 140, 120, 255)),
819
+ (256 * 256 - 10 * 100, (255, 255, 255, 0)),
820
+ ]
821
+
822
+
823
+ @pytest.mark.skipif(not hasattr(Image, "FASTOCTREE"), reason="PIL has no FASTOCTREE")
824
+ class TestHasTransparency(object):
825
+
826
+ def test_rgb(self):
827
+ img = Image.new("RGB", (10, 10))
828
+ assert not img_has_transparency(img)
829
+
830
+ img = quantize(img, alpha=False)
831
+ assert not img_has_transparency(img)
832
+
833
+ def test_rbga(self):
834
+ img = Image.new("RGBA", (10, 10), (100, 200, 50, 255))
835
+ img.paste((255, 50, 50, 0), (3, 3, 7, 7))
836
+ assert img_has_transparency(img)
837
+
838
+ img = quantize(img, alpha=True)
839
+ assert img_has_transparency(img)
840
+
841
+
842
+ class TestPeekImageFormat(object):
843
+
844
+ @pytest.mark.parametrize(
845
+ "format,expected_format",
846
+ [
847
+ ["png", "png"],
848
+ ["tiff", "tiff"],
849
+ ["gif", "gif"],
850
+ ["jpeg", "jpeg"],
851
+ ["bmp", None],
852
+ ],
853
+ )
854
+ def test_peek_format(self, format, expected_format):
855
+ buf = BytesIO()
856
+ Image.new("RGB", (100, 100)).save(buf, format)
857
+ assert peek_image_format(buf) == expected_format
858
+
859
+
860
+ class TestBandMerge(object):
861
+
862
+ def setup_method(self):
863
+ self.img0 = ImageSource(Image.new("RGB", (10, 10), (0, 10, 20)))
864
+ self.img1 = ImageSource(Image.new("RGB", (10, 10), (100, 110, 120)))
865
+ self.img2 = ImageSource(Image.new("RGB", (10, 10), (200, 210, 220)))
866
+ self.img3 = ImageSource(Image.new("RGB", (10, 10), (0, 255, 0)))
867
+ self.blank = BlankImageSource(size=(10, 10), image_opts=ImageOptions())
868
+
869
+ def test_merge_noops(self):
870
+ """
871
+ Check that black image is returned for no ops.
872
+ """
873
+ merger = BandMerger(mode="RGB")
874
+
875
+ img_opts = ImageOptions("RGB")
876
+ result = merger.merge([self.img0], img_opts)
877
+ img = result.as_image()
878
+ assert img.size == (10, 10)
879
+ assert img.getpixel((0, 0)) == (0, 0, 0)
880
+
881
+ def test_merge_missing_source(self):
882
+ """
883
+ Check that empty source list or source list with missing images
884
+ returns BlankImageSource.
885
+ """
886
+ merger = BandMerger(mode="RGB")
887
+ merger.add_ops(dst_band=0, src_img=0, src_band=0)
888
+ merger.add_ops(dst_band=1, src_img=1, src_band=0)
889
+ merger.add_ops(dst_band=2, src_img=2, src_band=0)
890
+
891
+ img_opts = ImageOptions("RGBA", transparent=True)
892
+ result = merger.merge([], img_opts, size=(10, 10))
893
+ img = result.as_image()
894
+
895
+ assert img.size == (10, 10)
896
+ assert img.getpixel((0, 0)) == (255, 255, 255, 0)
897
+
898
+ result = merger.merge([self.img0, self.img1], img_opts, size=(10, 10))
899
+ img = result.as_image()
900
+
901
+ assert img.size == (10, 10)
902
+ assert img.getpixel((0, 0)) == (255, 255, 255, 0)
903
+
904
+ def test_rgb_merge(self):
905
+ """
906
+ Check merge of RGB bands
907
+ """
908
+ merger = BandMerger(mode="RGB")
909
+
910
+ merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5)
911
+ merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5)
912
+ merger.add_ops(dst_band=0, src_img=2, src_band=1)
913
+ merger.add_ops(dst_band=2, src_img=1, src_band=2)
914
+
915
+ img_opts = ImageOptions("RGB")
916
+ result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts)
917
+ img = result.as_image()
918
+
919
+ assert img.getpixel((0, 0)) == (210, 127, 120)
920
+
921
+ def test_rgb_merge_missing(self):
922
+ """
923
+ Check missing band is set to 0
924
+ """
925
+ merger = BandMerger(mode="RGB")
926
+
927
+ merger.add_ops(dst_band=0, src_img=2, src_band=1)
928
+ merger.add_ops(dst_band=2, src_img=1, src_band=2)
929
+
930
+ img_opts = ImageOptions("RGB")
931
+ result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts)
932
+ img = result.as_image()
933
+
934
+ assert img.getpixel((0, 0)) == (210, 0, 120)
935
+
936
+ def test_rgba_merge(self):
937
+ """
938
+ Check merge of RGBA bands
939
+ """
940
+ merger = BandMerger(mode="RGBA")
941
+
942
+ merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5)
943
+ merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5)
944
+ merger.add_ops(dst_band=0, src_img=2, src_band=1)
945
+ merger.add_ops(dst_band=2, src_img=1, src_band=2)
946
+ merger.add_ops(dst_band=3, src_img=1, src_band=1)
947
+
948
+ img_opts = ImageOptions("RGBA")
949
+ result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts)
950
+ img = result.as_image()
951
+
952
+ assert img.getpixel((0, 0)) == (210, 127, 120, 110)
953
+
954
+ def test_rgba_merge_missing_a(self):
955
+ """
956
+ Check that missing alpha band defaults to opaque
957
+ """
958
+ merger = BandMerger(mode="RGBA")
959
+
960
+ merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5)
961
+ merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5)
962
+ merger.add_ops(dst_band=0, src_img=2, src_band=1)
963
+ merger.add_ops(dst_band=2, src_img=1, src_band=2)
964
+
965
+ img_opts = ImageOptions("RGBA")
966
+ result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts)
967
+ img = result.as_image()
968
+
969
+ assert img.getpixel((0, 0)) == (210, 127, 120, 255)
970
+
971
+ def test_l_merge(self):
972
+ """
973
+ Check merge bands to grayscale image
974
+ """
975
+ merger = BandMerger(mode="L")
976
+
977
+ merger.add_ops(dst_band=0, src_img=0, src_band=2, factor=0.2)
978
+ merger.add_ops(dst_band=0, src_img=2, src_band=1, factor=0.3)
979
+ merger.add_ops(dst_band=0, src_img=3, src_band=1, factor=0.5)
980
+
981
+ img_opts = ImageOptions("L")
982
+ result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts)
983
+ img = result.as_image()
984
+
985
+ assert img.getpixel((0, 0)) == int(20 * 0.2) + int(210 * 0.3) + int(255 * 0.5)
986
+
987
+ def test_p_merge(self):
988
+ """
989
+ Check merge bands to paletted image
990
+ """
991
+ merger = BandMerger(mode="RGB")
992
+
993
+ merger.add_ops(dst_band=1, src_img=0, src_band=0, factor=0.5)
994
+ merger.add_ops(dst_band=1, src_img=3, src_band=1, factor=0.5)
995
+ merger.add_ops(dst_band=0, src_img=2, src_band=1)
996
+ merger.add_ops(dst_band=2, src_img=1, src_band=2)
997
+
998
+ img_opts = ImageOptions(
999
+ "P", format="image/png", encoding_options={"quantizer": "mediancut"}
1000
+ )
1001
+ result = merger.merge([self.img0, self.img1, self.img2, self.img3], img_opts)
1002
+
1003
+ # need to encode to get conversion to P
1004
+ img = Image.open(result.as_buffer())
1005
+
1006
+ assert img.mode == "P"
1007
+ img = img.convert("RGB")
1008
+ assert img.getpixel((0, 0)) == (210, 127, 120)
1009
+
1010
+ def test_from_p_merge(self):
1011
+ """
1012
+ Check merge bands from paletted image
1013
+ """
1014
+ merger = BandMerger(mode="RGB")
1015
+
1016
+ merger.add_ops(dst_band=0, src_img=0, src_band=2)
1017
+ merger.add_ops(dst_band=1, src_img=0, src_band=1)
1018
+ merger.add_ops(dst_band=2, src_img=0, src_band=0)
1019
+
1020
+ img = Image.new("RGB", (10, 10), (0, 100, 200)).quantize(256)
1021
+ assert img.mode == "P"
1022
+ # src img is P but we can still access RGB bands
1023
+ src_img = ImageSource(img)
1024
+
1025
+ img_opts = ImageOptions("RGB")
1026
+ result = merger.merge([src_img], img_opts)
1027
+
1028
+ img = result.as_image()
1029
+ assert img.mode == "RGB"
1030
+ assert img.getpixel((0, 0)) == (200, 100, 0)
1031
+
1032
+ def test_from_mixed_merge(self):
1033
+ """
1034
+ Check merge RGBA bands from image without alpha (mixed)
1035
+ """
1036
+ merger = BandMerger(mode="RGBA")
1037
+
1038
+ merger.add_ops(dst_band=0, src_img=0, src_band=2)
1039
+ merger.add_ops(dst_band=1, src_img=0, src_band=1)
1040
+ merger.add_ops(dst_band=2, src_img=0, src_band=0)
1041
+ merger.add_ops(dst_band=3, src_img=0, src_band=3)
1042
+
1043
+ img = Image.new("RGB", (10, 10), (0, 100, 200))
1044
+ src_img = ImageSource(img)
1045
+
1046
+ img_opts = ImageOptions("RGBA")
1047
+ result = merger.merge([src_img], img_opts)
1048
+
1049
+ img = result.as_image()
1050
+ assert img.mode == "RGBA"
1051
+ assert img.getpixel((0, 0)) == (200, 100, 0, 255)