Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Figure.savefig: Support generating GeoTIFF file (with extension '.tiff') #2698

Merged
merged 26 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
41767a2
Figure.savefig: Support generating GeoTIFF file (with extension '.tiff')
seisman Sep 22, 2023
cd9918f
Test the .tiff extension
seisman Sep 22, 2023
f2348cc
GeoTIFF doesn't need the -T option
seisman Sep 22, 2023
921912d
Add a new test to check if .tiff is a GeoTIFF file
seisman Sep 22, 2023
a83862f
Revert the changes in test_figure_savefig_exists
seisman Sep 22, 2023
51c8dbe
Simplify the test
seisman Sep 22, 2023
cf771cc
Fix a linting issue
seisman Sep 22, 2023
3303ba4
Fix typos
seisman Sep 24, 2023
4fd2f50
Apply suggestions from code review
seisman Sep 24, 2023
9e77ef3
Merge branch 'main' into geotiff
seisman Sep 29, 2023
d5e6617
Merge branch 'main' into geotiff
seisman Oct 4, 2023
f1f41bf
Merge remote-tracking branch 'origin/geotiff' into geotiff
seisman Oct 4, 2023
252514b
Delete the .pgw if exists
seisman Oct 4, 2023
c4d480e
Merge branch 'main' into geotiff
seisman Oct 4, 2023
2137682
Merge branch 'main' into geotiff
seisman Oct 5, 2023
d57d8e0
Merge branch 'main' into geotiff
seisman Oct 7, 2023
44bfcfe
Improve the comment about removing .pgw file
seisman Oct 9, 2023
e7c2b36
Check bounds in the test
seisman Oct 9, 2023
9127756
Merge branch 'main' into geotiff
seisman Oct 9, 2023
26837e8
Fix typos and linting issues
seisman Oct 9, 2023
d5ececc
Merge branch 'main' into geotiff
seisman Oct 12, 2023
05ef422
Merge branch 'main' into geotiff
seisman Oct 18, 2023
71611df
Fix the bounds and shape for geotiff files
seisman Oct 18, 2023
916b4a8
Merge branch 'main' into geotiff
seisman Oct 18, 2023
6a1caa3
Check Affine transformation
seisman Oct 24, 2023
da9ca29
Merge branch 'main' into geotiff
seisman Oct 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions pygmt/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,19 @@ def savefig(
"""
Save the figure to a file.

This method implements a matplotlib-like interface for
:meth:`pygmt.Figure.psconvert`.
Supported file formats and their extensions:

Supported formats: PNG (``.png``), JPEG (``.jpg`` or ``.jpeg``),
PDF (``.pdf``), BMP (``.bmp``), TIFF (``.tif``), EPS (``.eps``), and
KML (``.kml``). The KML output generates a companion PNG file.
- PNG (``.png``)
- JPEG (``.jpg`` or ``.jpeg``)
- PDF (``.pdf``)
- BMP (``.bmp``)
- TIFF (``.tif`` or `.tiff`)
seisman marked this conversation as resolved.
Show resolved Hide resolved
- EPS (``.eps``)
- KML (``.kml``)

For TIFF format, ``.tiff`` generates a GeoTIFF file with embedded
georeferencing information and a companion world file. For KML format,
a companion PNG file is also generated.

You can pass in any keyword arguments that
:meth:`pygmt.Figure.psconvert` accepts.
Expand All @@ -279,8 +286,8 @@ def savefig(
If ``True``, will crop the figure canvas (page) to the plot area.
anti_alias: bool
If ``True``, will use anti-aliasing when creating raster images
(PNG, JPG, TIFF). More specifically, it passes arguments ``t2``
and ``g2`` to the ``anti_aliasing`` parameter of
(BMP, PNG, JPEG and TIFF). More specifically, it passes arguments
``t2`` and ``g2`` to the ``anti_aliasing`` parameter of
:meth:`pygmt.Figure.psconvert`. Ignored if creating vector
graphics.
seisman marked this conversation as resolved.
Show resolved Hide resolved
show: bool
Expand All @@ -301,15 +308,20 @@ def savefig(
"bmp": "b",
"eps": "e",
"tif": "t",
"tiff": None, # GeoTIFF doesn't need the -T option
"kml": "g",
}

fname = Path(fname)
prefix, suffix = fname.with_suffix("").as_posix(), fname.suffix
ext = suffix[1:].lower() # Remove the . and normalize to lowercase
# alias jpeg to jpg
if ext == "jpeg":

if ext == "jpeg": # Alias jpeg to jpg
ext = "jpg"
elif ext == "tiff": # GeoTIFF
kwargs["W"] = "+g"
elif ext == "kml": # KML
kwargs["W"] = "+k"

if ext not in fmts:
if ext == "ps":
Expand All @@ -328,8 +340,6 @@ def savefig(
if anti_alias:
kwargs["Qt"] = 2
kwargs["Qg"] = 2
if ext == "kml":
kwargs["W"] = "+k"

self.psconvert(prefix=prefix, fmt=fmt, crop=crop, **kwargs)

Expand Down
40 changes: 40 additions & 0 deletions pygmt/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,46 @@ def test_figure_savefig_exists():
fname.unlink()


def test_figure_savefig_geotiff():
"""
Make sure .tif generates a normal TIFF file and .tiff generates a GeoTIFF
file.
"""
fig = Figure()
fig.basemap(region=[0, 10, 0, 10], projection="M10c", frame=True)

# save as GeoTIFF
seisman marked this conversation as resolved.
Show resolved Hide resolved
geofname = Path("test_figure_savefig_geotiff.tiff")
fig.savefig(geofname)
assert geofname.exists()
assert geofname.with_suffix(".pgw").exists() # The companion world file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange, why is a pgw file generated instead of a tfw file? See https://en.wikipedia.org/wiki/World_file#Filename_extension. A .tif or .tiff file should be linked to .tfw no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #2658 (comment) for some tests.

Here, we call gmt psconvert -A -P -W+g without specifying the -T option.

Looking at the psconvert source code (https://github.com/GenericMappingTools/gmt/blob/master/src/psconvert.c#L1008-L1010),

	if (!Ctrl->T.active) {	/* Set default output device if none is specified */
		Ctrl->T.device = (Ctrl->W.warp) ? GS_DEV_PNG : GS_DEV_JPG;	/* Lossless PNG if we are making a geotiff in the end */
	}

The default format is PNG is -W+g is used. I think GMT fist converts PS to lossless PNG, then write the world file, and calls gdal_translate to combine the PNG and world file into a GeoTIFF file. The extension of the world file is determined from the extension of the raster file (in this case PNG), thus we have a pgw file. This may be an upstream feature or bug.

Actually, since we already have a GeoTIFF file, the world_file is no longer needed. Maybe we should remove it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, thanks for the explanation. You're right that we wouldn't need the pgw file or any world file anyway since the GeoTIFF should already have the metadata embedded inside.

Maybe we should remove it?

Maybe best to add the code to remove the pgw file upstream in GMT when a GeoTIFF is created, or at least rename pgw to tfw? On the PyGMT side, we could match the upstream implementation with GMT <= 6.4.0.

Copy link
Member Author

@seisman seisman Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe best to add the code to remove the pgw file upstream in GMT when a GeoTIFF is created, or at least rename pgw to tfw?

Removing pgw makes more sense to me. I'll open an issue report and see what the GMT team thinks about it.

Edit: upstream issue report at GenericMappingTools/gmt#7844

On the PyGMT side, we could match the upstream implementation with GMT <= 6.4.0.

Do you mean we should keep the pgw file? I'm thinking about adding a new parameter worldfile=True to Figure.savefig() which generates a companion world file.

Copy link
Member

@weiji14 weiji14 Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the PyGMT side, we could match the upstream implementation with GMT <= 6.4.0.

Do you mean we should keep the pgw file? I'm thinking about adding a new parameter worldfile=True to Figure.savefig() which generates a companion world file.

I mean we should match whatever GMT does with the pgw world file later in GMT 6.5.0 (e.g. delete the pgw file), but open up that issue first. The worldfile=True parameter sounds nice actually, this would be similar to GDAL's TFW=YES/NO option when creating GeoTIFFs - https://gdal.org/drivers/raster/gtiff.html#creation-options

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that GenericMappingTools/gmt#7865 was merged, and the upcoming GMT 6.5.0 should delete the pgw world file if a GeoTIFF output is requested.


# Save as TIFF
fname = Path("test_figure_savefig_geotiff.tif")
seisman marked this conversation as resolved.
Show resolved Hide resolved
fig.savefig(fname)
assert fname.exists()

# Check is a TIFF is georeferenced or not
seisman marked this conversation as resolved.
Show resolved Hide resolved
try:
# pylint: disable=import-outside-toplevel
import rioxarray
from rasterio.errors import NotGeoreferencedWarning
seisman marked this conversation as resolved.
Show resolved Hide resolved

# GeoTIFF
with rioxarray.open_rasterio(geofname) as xds:
assert xds.rio.crs is not None
seisman marked this conversation as resolved.
Show resolved Hide resolved
# TIFF
with pytest.warns(expected_warning=NotGeoreferencedWarning) as record:
with rioxarray.open_rasterio(fname) as xds:
assert xds.rio.crs is None
seisman marked this conversation as resolved.
Show resolved Hide resolved
assert len(record) == 1
except ImportError:
pass
geofname.unlink()
geofname.with_suffix(".pgw").unlink()
fname.unlink()


def test_figure_savefig_directory_nonexists():
"""
Make sure that Figure.savefig() raises a FileNotFoundError when the parent
Expand Down