[Git][debian-gis-team/trollimage][master] 4 commits: New upstream version 1.14.0
Antonio Valentino
gitlab at salsa.debian.org
Wed Sep 23 07:59:59 BST 2020
Antonio Valentino pushed to branch master at Debian GIS Project / trollimage
Commits:
cfed734a by Antonio Valentino at 2020-09-23T06:51:34+00:00
New upstream version 1.14.0
- - - - -
d495c902 by Antonio Valentino at 2020-09-23T06:51:42+00:00
Update upstream source from tag 'upstream/1.14.0'
Update to upstream version '1.14.0'
with Debian dir be90267fdfa423397a18bac852b033c7550901dc
- - - - -
01ab2950 by Antonio Valentino at 2020-09-23T06:52:22+00:00
New upstream release
- - - - -
7eface4a by Antonio Valentino at 2020-09-23T06:55:35+00:00
Set distribution to unstable
- - - - -
8 changed files:
- CHANGELOG.md
- debian/changelog
- setup.py
- trollimage/colormap.py
- trollimage/tests/test_colormap.py
- trollimage/tests/test_image.py
- trollimage/version.py
- trollimage/xrimage.py
Changes:
=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,43 @@
+## Version 1.14.0 (2020/09/18)
+
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 71](https://github.com/pytroll/trollimage/pull/71) - Fix tiff tag writing if start_time is None
+
+#### Features added
+
+* [PR 70](https://github.com/pytroll/trollimage/pull/70) - Implement colorize for dask arrays
+
+In this release 2 pull requests were closed.
+
+
+## Version 1.13.0 (2020/06/08)
+
+### Issues Closed
+
+* [Issue 65](https://github.com/pytroll/trollimage/issues/65) - Writing to file.tiff raises KeyError ([PR 69](https://github.com/pytroll/trollimage/pull/69))
+* [Issue 61](https://github.com/pytroll/trollimage/issues/61) - Add rasterio overview resampling argument ([PR 67](https://github.com/pytroll/trollimage/pull/67))
+
+In this release 2 issues were closed.
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 68](https://github.com/pytroll/trollimage/pull/68) - Add workaround for broken aggdraw.Font usage in satpy/pycoast
+
+#### Features added
+
+* [PR 69](https://github.com/pytroll/trollimage/pull/69) - Add .tiff as recognized geotiff extension ([65](https://github.com/pytroll/trollimage/issues/65))
+* [PR 67](https://github.com/pytroll/trollimage/pull/67) - Add option for geotiff overview resampling and auto-levels ([61](https://github.com/pytroll/trollimage/issues/61))
+* [PR 66](https://github.com/pytroll/trollimage/pull/66) - Add more helpful error message when saving JPEG with alpha band
+
+In this release 4 pull requests were closed.
+
+
## Version 1.12.0 (2020/03/02)
### Pull Requests Merged
=====================================
debian/changelog
=====================================
@@ -1,3 +1,9 @@
+trollimage (1.14.0-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Antonio Valentino <antonio.valentino at tiscali.it> Wed, 23 Sep 2020 06:55:13 +0000
+
trollimage (1.13.0-1) unstable; urgency=medium
* New upstream release.
=====================================
setup.py
=====================================
@@ -21,9 +21,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-"""Setup for trollimage.
-"""
+"""Setup for trollimage."""
from setuptools import setup
import versioneer
@@ -54,4 +52,5 @@ setup(name="trollimage",
'geotiff': ['rasterio'],
'xarray': ['xarray', 'dask[array]'],
},
+ tests_require=['xarray', 'dask[array]'],
)
=====================================
trollimage/colormap.py
=====================================
@@ -20,55 +20,136 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""A simple colormap module.
-"""
+"""A simple colormap module."""
import numpy as np
from trollimage.colorspaces import rgb2hcl, hcl2rgb
def colorize(arr, colors, values):
- """Colorize a monochromatic array *arr*, based *colors* given for
- *values*. Interpolation is used. *values* must be in ascending order.
+ """Colorize a monochromatic array *arr*, based *colors* given for *values*.
+
+ Interpolation is used. *values* must be in ascending order.
+
+ Args:
+ arr (numpy array, numpy masked array, dask array)
+ data to be colorized.
+ colors (numpy array):
+ the colors to use (R, G, B)
+ values (numpy array):
+ the values corresponding to the colors in the array
"""
- hcolors = np.array([rgb2hcl(*i[:3]) for i in colors])
- # unwrap colormap in hcl space
- hcolors[:, 0] = np.rad2deg(np.unwrap(np.deg2rad(np.array(hcolors)[:, 0])))
+ if can_be_block_mapped(arr):
+ return _colorize_dask(arr, colors, values)
+ else:
+ return _colorize(arr, colors, values)
+
+
+def can_be_block_mapped(data):
+ """Check if the array can be processed in chunks."""
+ return hasattr(data, 'map_blocks')
+
+
+def _colorize_dask(dask_array, colors, values):
+ """Colorize a dask array.
+
+ The channels are stacked on the first dimension.
+ """
+ return dask_array.map_blocks(_colorize, colors, values, dtype=colors.dtype, new_axis=0,
+ chunks=[colors.shape[1]] + list(dask_array.chunks))
+
+
+def _colorize(arr, colors, values):
+ """Colorize the array."""
+ channels = _interpolate_rgb_colors(arr, colors, values)
+ alpha = _interpolate_alpha(arr, colors, values)
+ channels.extend(alpha)
+ channels = _mask_channels(channels, arr)
+ return np.stack(channels, axis=0)
+
+
+def _interpolate_rgb_colors(arr, colors, values):
+ hcl_colors = _convert_rgb_list_to_hcl(colors)
channels = [np.interp(arr,
np.array(values),
- np.array(hcolors)[:, i])
+ np.array(hcl_colors)[:, i])
for i in range(3)]
-
channels = list(hcl2rgb(*channels))
+ return channels
+
+
+def _convert_rgb_list_to_hcl(colors):
+ hcl_colors = np.array([rgb2hcl(*i[:3]) for i in colors])
+ _unwrap_colors_in_hcl_space(hcl_colors)
+ return hcl_colors
+
+
+def _unwrap_colors_in_hcl_space(hcl_colors):
+ hcl_colors[:, 0] = np.rad2deg(np.unwrap(np.deg2rad(np.array(hcl_colors)[:, 0])))
- rest = [np.interp(arr,
- np.array(values),
- np.array(colors)[:, i + 3])
- for i in range(np.array(colors).shape[1] - 3)]
- channels.extend(rest)
+def _interpolate_alpha(arr, colors, values):
+ alpha = [np.interp(arr,
+ np.array(values),
+ np.array(colors)[:, i + 3])
+ for i in range(np.array(colors).shape[1] - 3)]
+ return alpha
+
+def _mask_channels(channels, arr):
+ """Mask the channels if arr is a masked array."""
+ try:
+ channels = [_mask_array(channel, arr) for channel in channels]
+ except AttributeError:
+ pass
+ return channels
+
+
+def _mask_array(new_array, arr):
+ """Mask new_array with the mask from array."""
try:
- return [np.ma.array(channel, mask=arr.mask) for channel in channels]
+ return np.ma.array(new_array, mask=arr.mask)
except AttributeError:
- return channels
+ return new_array
def palettize(arr, colors, values):
- """From start *values* apply *colors* to *data*.
+ """Apply *colors* to *data* from start *values*.
+
+ Args:
+ arr (numpy array, numpy masked array, dask array):
+ data to be palettized.
+ colors (numpy array):
+ the colors to use (R, G, B)
+ values (numpy array):
+ the values corresponding to the colors in the array
"""
+ if can_be_block_mapped(arr):
+ return _palettize_dask(arr, colors, values), tuple(colors)
+ else:
+ return _palettize(arr, values), tuple(colors)
+
+
+def _palettize_dask(darr, colors, values):
+ """Apply a palette to a dask array."""
+ return darr.map_blocks(_palettize, values, dtype=colors.dtype)
+
+
+def _palettize(arr, values):
+ """Apply palette to array."""
+ new_arr = _digitize_array(arr, values)
+ reshaped_array = new_arr.reshape(arr.shape)
+ return _mask_array(reshaped_array, arr)
+
+
+def _digitize_array(arr, values):
new_arr = np.digitize(arr.ravel(),
np.concatenate((values,
[max(np.nanmax(arr),
values.max()) + 1])))
new_arr -= 1
new_arr = new_arr.clip(min=0, max=len(values) - 1)
- try:
- new_arr = np.ma.array(new_arr.reshape(arr.shape), mask=arr.mask)
- except AttributeError:
- new_arr = new_arr.reshape(arr.shape)
-
- return new_arr, tuple(colors)
+ return new_arr
class Colormap(object):
@@ -89,6 +170,7 @@ class Colormap(object):
"""
def __init__(self, *tuples, **kwargs):
+ """Set up the instance."""
if 'colors' in kwargs and 'values' in kwargs:
values = kwargs['values']
colors = kwargs['colors']
@@ -99,31 +181,26 @@ class Colormap(object):
self.colors = np.array(colors)
def colorize(self, data):
- """Colorize a monochromatic array *data*, based on the current colormap.
- """
- return colorize(data,
- self.colors,
- self.values)
+ """Colorize a monochromatic array *data*, based on the current colormap."""
+ return colorize(data, self.colors, self.values)
def palettize(self, data):
- """Palettize a monochromatic array *data* based on the current colormap.
- """
+ """Palettize a monochromatic array *data* based on the current colormap."""
return palettize(data, self.colors, self.values)
def __add__(self, other):
+ """Append colormap together."""
new = Colormap()
new.values = np.concatenate((self.values, other.values))
new.colors = np.concatenate((self.colors, other.colors))
return new
def reverse(self):
- """Reverse the current colormap in place.
- """
+ """Reverse the current colormap in place."""
self.colors = np.flipud(self.colors)
def set_range(self, min_val, max_val):
- """Set the range of the colormap to [*min_val*, *max_val*]
- """
+ """Set the range of the colormap to [*min_val*, *max_val*]."""
if min_val > max_val:
max_val, min_val = min_val, max_val
self.values = (((self.values * 1.0 - self.values.min()) /
@@ -131,8 +208,7 @@ class Colormap(object):
* (max_val - min_val) + min_val)
def to_rio(self):
- """Converts the colormap to a rasterio colormap.
- """
+ """Convert the colormap to a rasterio colormap."""
self.colors = (((self.colors * 1.0 - self.colors.min()) /
(self.colors.max() - self.colors.min())) * 255)
return dict(zip(self.values, tuple(map(tuple, self.colors))))
@@ -447,8 +523,7 @@ qualitative_colormaps = [set1, set2, set3,
def colorbar(height, length, colormap):
- """Return the channels of a colorbar.
- """
+ """Return the channels of a colorbar."""
cbar = np.tile(np.arange(length) * 1.0 / (length - 1), (height, 1))
cbar = (cbar * (colormap.values.max() - colormap.values.min())
+ colormap.values.min())
@@ -457,8 +532,7 @@ def colorbar(height, length, colormap):
def palettebar(height, length, colormap):
- """Return the channels of a palettebar.
- """
+ """Return the channels of a palettebar."""
cbar = np.tile(np.arange(length) * 1.0 / (length - 1), (height, 1))
cbar = (cbar * (colormap.values.max() + 1 - colormap.values.min())
+ colormap.values.min())
=====================================
trollimage/tests/test_colormap.py
=====================================
@@ -27,37 +27,70 @@ import numpy as np
class TestColormapClass(unittest.TestCase):
- """Test case for the colormap object.
- """
+ """Test case for the colormap object."""
- def test_colorize(self):
- """Test colorize
- """
- cm_ = colormap.Colormap((1, (1.0, 1.0, 0.0)),
- (2, (0.0, 1.0, 1.0)),
- (3, (1, 1, 1)),
- (4, (0, 0, 0)))
+ def setUp(self):
+ """Set up the test case."""
+ self.colormap = colormap.Colormap((1, (1.0, 1.0, 0.0)),
+ (2, (0.0, 1.0, 1.0)),
+ (3, (1, 1, 1)),
+ (4, (0, 0, 0)))
+ def test_colorize_no_interpolation(self):
+ """Test colorize."""
data = np.array([1, 2, 3, 4])
- channels = cm_.colorize(data)
+ channels = self.colormap.colorize(data)
for i in range(3):
self.assertTrue(np.allclose(channels[i],
- cm_.colors[:, i],
+ self.colormap.colors[:, i],
atol=0.001))
- def test_palettize(self):
- """Test palettize
- """
- cm_ = colormap.Colormap((1, (1.0, 1.0, 0.0)),
- (2, (0.0, 1.0, 1.0)),
- (3, (1, 1, 1)),
- (4, (0, 0, 0)))
+ def test_colorize_with_interpolation(self):
+ """Test colorize."""
+ data = np.array([1.5, 2.5, 3.5, 4])
+
+ expected_channels = [np.array([0.22178232, 0.61069262, 0.50011605, 0.]),
+ np.array([1.08365532, 0.94644083, 0.50000605, 0.]),
+ np.array([0.49104964, 1.20509947, 0.49989589, 0.])]
+ channels = self.colormap.colorize(data)
+ for i in range(3):
+ self.assertTrue(np.allclose(channels[i],
+ expected_channels[i],
+ atol=0.001))
+
+ def test_colorize_dask_with_interpolation(self):
+ """Test colorize dask arrays."""
+ import dask.array as da
+ data = da.from_array(np.array([[1.5, 2.5, 3.5, 4],
+ [1.5, 2.5, 3.5, 4],
+ [1.5, 2.5, 3.5, 4]]), chunks=2)
+
+ expected_channels = [np.array([[0.22178232, 0.61069262, 0.50011605, 0.],
+ [0.22178232, 0.61069262, 0.50011605, 0.],
+ [0.22178232, 0.61069262, 0.50011605, 0.]]),
+ np.array([[1.08365532, 0.94644083, 0.50000605, 0.],
+ [1.08365532, 0.94644083, 0.50000605, 0.],
+ [1.08365532, 0.94644083, 0.50000605, 0.]]),
+ np.array([[0.49104964, 1.20509947, 0.49989589, 0.],
+ [0.49104964, 1.20509947, 0.49989589, 0.],
+ [0.49104964, 1.20509947, 0.49989589, 0.]])]
+
+ channels = self.colormap.colorize(data)
+ for i, expected_channel in enumerate(expected_channels):
+ current_channel = channels[i, :, :]
+ assert isinstance(current_channel, da.Array)
+ self.assertTrue(np.allclose(current_channel.compute(),
+ expected_channel,
+ atol=0.001))
+
+ def test_palettize(self):
+ """Test palettize."""
data = np.array([1, 2, 3, 4])
- channels, colors = cm_.palettize(data)
- self.assertTrue(np.allclose(colors, cm_.colors))
+ channels, colors = self.colormap.palettize(data)
+ self.assertTrue(np.allclose(colors, self.colormap.colors))
self.assertTrue(all(channels == [0, 1, 2, 3]))
cm_ = colormap.Colormap((0, (0.0, 0.0, 0.0)),
@@ -78,9 +111,21 @@ class TestColormapClass(unittest.TestCase):
self.assertTrue(np.allclose(colors, cm_.colors))
self.assertTrue(all(channels == [0, 0, 1, 2, 3, 3]))
+ def test_palettize_dask(self):
+ """Test palettize on a dask array."""
+ import dask.array as da
+ data = da.from_array(np.array([[1, 2, 3, 4],
+ [1, 2, 3, 4],
+ [1, 2, 3, 4]]), chunks=2)
+ channels, colors = self.colormap.palettize(data)
+ assert isinstance(channels, da.Array)
+ self.assertTrue(np.allclose(colors, self.colormap.colors))
+ self.assertTrue(np.allclose(channels.compute(), [[0, 1, 2, 3],
+ [0, 1, 2, 3],
+ [0, 1, 2, 3]]))
+
def test_set_range(self):
- """Test set_range
- """
+ """Test set_range."""
cm_ = colormap.Colormap((1, (1.0, 1.0, 0.0)),
(2, (0.0, 1.0, 1.0)),
(3, (1, 1, 1)),
@@ -91,8 +136,7 @@ class TestColormapClass(unittest.TestCase):
self.assertTrue(cm_.values[-1] == 8)
def test_invert_set_range(self):
- """Test inverted set_range
- """
+ """Test inverted set_range."""
cm_ = colormap.Colormap((1, (1.0, 1.0, 0.0)),
(2, (0.0, 1.0, 1.0)),
(3, (1, 1, 1)),
@@ -103,8 +147,7 @@ class TestColormapClass(unittest.TestCase):
self.assertTrue(cm_.values[-1] == 8)
def test_reverse(self):
- """Test reverse
- """
+ """Test reverse."""
cm_ = colormap.Colormap((1, (1.0, 1.0, 0.0)),
(2, (0.0, 1.0, 1.0)),
(3, (1, 1, 1)),
@@ -114,8 +157,7 @@ class TestColormapClass(unittest.TestCase):
self.assertTrue(np.allclose(np.flipud(colors), cm_.colors))
def test_add(self):
- """Test adding colormaps
- """
+ """Test adding colormaps."""
cm_ = colormap.Colormap((1, (1.0, 1.0, 0.0)),
(2, (0.0, 1.0, 1.0)),
(3, (1, 1, 1)),
@@ -132,8 +174,7 @@ class TestColormapClass(unittest.TestCase):
self.assertTrue(np.allclose(cm3.values, cm_.values))
def test_colorbar(self):
- """Test colorbar
- """
+ """Test colorbar."""
cm_ = colormap.Colormap((1, (1, 1, 0)),
(2, (0, 1, 1)),
(3, (1, 1, 1)),
@@ -146,8 +187,7 @@ class TestColormapClass(unittest.TestCase):
atol=0.001))
def test_palettebar(self):
- """Test colorbar
- """
+ """Test colorbar."""
cm_ = colormap.Colormap((1, (1, 1, 0)),
(2, (0, 1, 1)),
(3, (1, 1, 1)),
@@ -159,8 +199,7 @@ class TestColormapClass(unittest.TestCase):
self.assertTrue(np.allclose(palette, cm_.colors))
def test_to_rio(self):
- """Test conversion to rasterio colormap
- """
+ """Test conversion to rasterio colormap."""
cm_ = colormap.Colormap((1, (1, 1, 0)),
(2, (0, 1, 1)),
(3, (1, 1, 1)),
=====================================
trollimage/tests/test_image.py
=====================================
@@ -1002,6 +1002,25 @@ class TestXRImage(unittest.TestCase):
np.testing.assert_allclose(file_data[3][not_null], 255) # completely opaque
np.testing.assert_allclose(file_data[3][~not_null], 0) # completely transparent
+ @unittest.skipIf(sys.platform.startswith('win'), "'NamedTemporaryFile' not supported on Windows")
+ def test_save_geotiff_datetime(self):
+ """Test saving geotiffs when start_time is in the attributes."""
+ import xarray as xr
+ import datetime as dt
+
+ data = xr.DataArray(np.arange(75).reshape(5, 5, 3), dims=[
+ 'y', 'x', 'bands'], coords={'bands': ['R', 'G', 'B']})
+
+ # "None" as start_time in the attributes
+ data.attrs['start_time'] = None
+ tags = _get_tags_after_writing_to_geotiff(data)
+ assert "TIFFTAG_DATETIME" not in tags
+
+ # Valid datetime
+ data.attrs['start_time'] = dt.datetime.utcnow()
+ tags = _get_tags_after_writing_to_geotiff(data)
+ assert "TIFFTAG_DATETIME" in tags
+
@unittest.skipIf(sys.platform.startswith('win'), "'NamedTemporaryFile' not supported on Windows")
def test_save_geotiff_int(self):
"""Test saving geotiffs when input data is int."""
@@ -2034,3 +2053,14 @@ class TestXRImage(unittest.TestCase):
# make it happen
res.data.data.compute()
pil_img.convert.assert_called_with('RGB')
+
+
+def _get_tags_after_writing_to_geotiff(data):
+ from trollimage import xrimage
+ import rasterio as rio
+
+ img = xrimage.XRImage(data)
+ with NamedTemporaryFile(suffix='.tif') as tmp:
+ img.save(tmp.name)
+ with rio.open(tmp.name) as f:
+ return f.tags()
=====================================
trollimage/version.py
=====================================
@@ -23,9 +23,9 @@ def get_keywords():
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
- git_refnames = " (HEAD -> master, tag: v1.13.0)"
- git_full = "1f63856486d83faccc1aafea2bea4585a5ab1530"
- git_date = "2020-06-03 10:04:09 -0500"
+ git_refnames = " (HEAD -> master, tag: v1.14.0)"
+ git_full = "301c1a9dc5721bda9547d0acec112902a5800484"
+ git_date = "2020-09-18 11:34:04 +0200"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
=====================================
trollimage/xrimage.py
=====================================
@@ -35,14 +35,14 @@ chunks can be saved in parallel.
import logging
import os
import threading
+from contextlib import suppress
-import numpy as np
-from PIL import Image as PILImage
-import xarray as xr
import dask
import dask.array as da
+import numpy as np
+import xarray as xr
+from PIL import Image as PILImage
from dask.delayed import delayed
-
from trollimage.image import check_image_format
try:
@@ -113,10 +113,8 @@ class RIOFile(object):
def __del__(self):
"""Delete the instance."""
- try:
+ with suppress(IOError, OSError):
self.close()
- except (IOError, OSError):
- pass
@property
def colorinterp(self):
@@ -531,8 +529,8 @@ class XRImage(object):
except KeyError:
logger.info("Couldn't create geotransform")
- if "start_time" in data.attrs:
- stime = data.attrs['start_time']
+ stime = data.attrs.get("start_time")
+ if stime:
stime_str = stime.strftime("%Y:%m:%d %H:%M:%S")
tags.setdefault('TIFFTAG_DATETIME', stime_str)
elif driver == 'JPEG' and 'A' in mode:
@@ -1301,13 +1299,6 @@ class XRImage(object):
self.channels[i].mask = np.logical_and(selfmask,
img.channels[i].mask)
- @staticmethod
- def _colorize(l_data, colormap):
- # 'l_data' is (1, rows, cols)
- # 'channels' will be a list of 3 (RGB) or 4 (RGBA) arrays
- channels = colormap.colorize(l_data)
- return np.concatenate(channels, axis=0)
-
def colorize(self, colormap):
"""Colorize the current image using `colormap`.
@@ -1324,10 +1315,9 @@ class XRImage(object):
else:
alpha = None
- l_data = self.data.sel(bands=['L'])
- new_data = l_data.data.map_blocks(self._colorize, colormap,
- chunks=(colormap.colors.shape[1],) + l_data.data.chunks[1:],
- dtype=np.float64)
+ l_data = self.data.sel(bands='L')
+
+ new_data = colormap.colorize(l_data.data)
if colormap.colors.shape[1] == 4:
mode = "RGBA"
@@ -1344,12 +1334,6 @@ class XRImage(object):
dims = self.data.dims
self.data = xr.DataArray(new_data, coords=coords, attrs=attrs, dims=dims)
- @staticmethod
- def _palettize(data, colormap):
- """Operate in a dask-friendly manner."""
- # returns data and palette, only need data
- return colormap.palettize(data)[0]
-
def palettize(self, colormap):
"""Palettize the current image using `colormap`.
@@ -1362,8 +1346,7 @@ class XRImage(object):
raise ValueError("Image should be grayscale to colorize")
l_data = self.data.sel(bands=['L'])
- new_data = l_data.data.map_blocks(self._palettize, colormap, dtype=l_data.dtype)
- self.palette = tuple(colormap.colors)
+ new_data, self.palette = colormap.palettize(l_data.data)
if self.mode == "L":
mode = "P"
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/compare/ad601a0a54c93503c8470bc172b80291aa9f950f...7eface4a0144f0b9cdcf6de05681ad75e6f2d6bd
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/compare/ad601a0a54c93503c8470bc172b80291aa9f950f...7eface4a0144f0b9cdcf6de05681ad75e6f2d6bd
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20200923/2247dd25/attachment-0001.html>
More information about the Pkg-grass-devel
mailing list