[Git][debian-gis-team/trollimage][upstream] New upstream version 1.14.0

Antonio Valentino gitlab at salsa.debian.org
Wed Sep 23 08:00:07 BST 2020



Antonio Valentino pushed to branch upstream at Debian GIS Project / trollimage


Commits:
cfed734a by Antonio Valentino at 2020-09-23T06:51:34+00:00
New upstream version 1.14.0
- - - - -


7 changed files:

- CHANGELOG.md
- 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


=====================================
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/-/commit/cfed734ab8469b04a580d51a38be5c0e075b4da3

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/trollimage/-/commit/cfed734ab8469b04a580d51a38be5c0e075b4da3
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/adf52f95/attachment-0001.html>


More information about the Pkg-grass-devel mailing list