diff --git a/examples/gallery/images/image.py b/examples/gallery/images/image.py index 49f0fe757fa..5aecbbfc465 100644 --- a/examples/gallery/images/image.py +++ b/examples/gallery/images/image.py @@ -6,23 +6,25 @@ many formats (e.g., png, jpg, eps, pdf) on a figure. We must specify the filename via the ``imagefile`` parameter or simply use the filename as the first argument. You can also use a full URL pointing to your desired image. The ``position`` parameter allows -us to set a reference point on the map for the image. +us to place the image at a specific location on the plot. """ # %% from pathlib import Path import pygmt +from pygmt.params import Position fig = pygmt.Figure() fig.basemap(region=[0, 2, 0, 2], projection="X10c", frame=True) -# Place and center ("+jCM") the image "needle.jpg" provided by GMT to the position -# ("+g") 1/1 on the current plot, scale it to a width of 8 centimeters ("+w") and draw -# a rectangular border around it +# Place the center of the image "needle.jpg" provided by GMT to the position (1, 1) on +# the current plot, scale it to a width of 8 centimeters and draw a rectangular border +# around it. fig.image( imagefile="https://oceania.generic-mapping-tools.org/cache/needle.jpg", - position="g1/1+w8c+jCM", + position=Position((1, 1), cstype="mapcoords", anchor="MC"), + width="8c", box=True, ) diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 1909fa73d6f..4ed36fb243f 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -5,18 +5,24 @@ from collections.abc import Sequence from typing import Literal -from pygmt._typing import PathLike +from pygmt._typing import AnchorCode, PathLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Box +from pygmt.params import Box, Position +from pygmt.src._common import _parse_position @fmt_docstring -@use_alias(D="position", G="bitcolor") +@use_alias(G="bitcolor") def image( # noqa: PLR0913 self, imagefile: PathLike, + position: Position | Sequence[float | str] | AnchorCode | None = None, + width: float | str | None = None, + height: float | str | None = None, + dpi: float | str | None = None, + replicate: int | Sequence[int] | None = None, box: Box | bool = False, monochrome: bool = False, invert: bool = False, @@ -33,10 +39,10 @@ def image( # noqa: PLR0913 r""" Plot raster or EPS images. - Reads Encapsulated PostScript (EPS) or raster image files and plots them. The - image can be scaled arbitrarily, and 1-bit raster images can be: + Reads an Encapsulated PostScript file or a raster image file and plot it on a map. + The image can be scaled arbitrarily, and 1-bit raster images can be: - - inverted, i.e., black pixels (on) becomes white (off) and vice versa. + - inverted, i.e., black pixels (on) become white (off) and vice versa. - colorized, by assigning different foreground and background colors. - made transparent where either the back- or foreground is painted. @@ -50,6 +56,7 @@ def image( # noqa: PLR0913 $aliases - B = frame + - D = position, **+w**: width/height, **+r**: dpi, **+n**: replicate - F = box - I = invert - J = projection @@ -66,11 +73,34 @@ def image( # noqa: PLR0913 An Encapsulated PostScript (EPS) file or a raster image file. An EPS file must contain an appropriate BoundingBox. A raster file can have a depth of 1, 8, 24, or 32 bits and is read via GDAL. - position : str - [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ **+r**\ *dpi*\ - **+w**\ [**-**]\ *width*\ [/*height*]\ [**+j**\ *justify*]\ - [**+n**\ *nx*\ [/*ny*]]\ [**+o**\ *dx*\ [/*dy*]]. - Set reference point on the map for the image. + position + Position of the GMT logo on the plot. It can be specified in multiple ways: + + - A :class:`pygmt.params.Position` object to fully control the reference point, + anchor point, and offset. + - A sequence of two values representing the x- and y-coordinates in plot + coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``. + - A :doc:`2-character justification code ` for a + position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot. + + If not specified, defaults to the Bottom Left corner of the plot (position + ``(0, 0)`` with anchor ``"BL"``). + width + height + Width (and height) of the image in plot coordinates (inches, cm, etc.). If + ``height`` (or ``width``) is set to 0, then the original aspect ratio of the + image is maintained. If ``width`` (or ``height``) is negative, the absolute + value is used to interpolate image to the device resolution using the PostScript + image operator. If neither dimensions nor ``dpi`` are set then revert to the + default dpi [:gmt-term:`GMT_GRAPHICS_DPU`]. + dpi + Set the dpi of the image in dots per inch, or append **c** to indicate this is + dots per cm. + replicate + *nx* or (*nx*, *ny*). + Replicate the (scaled) image *nx* times in the horizontal direction, and *ny* + times in the vertical direction. If a single integer *nx* is given, *ny* = *nx*. + [Default is (1, 1)]. box Draw a background box behind the image. If set to ``True``, a simple rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box appearance, @@ -104,7 +134,24 @@ def image( # noqa: PLR0913 """ self._activate_figure() + position = _parse_position( + position, + kwdict={"width": width, "height": height, "dpi": dpi, "replicate": replicate}, + default=Position((0, 0), cstype="plotcoords"), # Default to (0,0) in plotcoords + ) + + # width is required when only height is given. + if width is None and height is not None: + width = 0 + aliasdict = AliasSystem( + D=[ + Alias(position, name="position"), + Alias(width, name="width", prefix="+w"), # +wwidth/height + Alias(height, name="height", prefix="/"), + Alias(replicate, name="replicate", prefix="+n", sep="/", size=2), + Alias(dpi, name="dpi", prefix="+r"), + ], F=Alias(box, name="box"), M=Alias(monochrome, name="monochrome"), I=Alias(invert, name="invert"), diff --git a/pygmt/tests/baseline/test_image.png.dvc b/pygmt/tests/baseline/test_image.png.dvc index 8e36bf6a7a0..04679b1ad18 100644 --- a/pygmt/tests/baseline/test_image.png.dvc +++ b/pygmt/tests/baseline/test_image.png.dvc @@ -1,4 +1,5 @@ outs: -- md5: d7d0d71a44a232d5907dbd44f7a08f18 - size: 30811 +- md5: 3bafd31eb0374ec175c1283c95ab0530 + size: 24065 path: test_image.png + hash: md5 diff --git a/pygmt/tests/baseline/test_image_complete.png.dvc b/pygmt/tests/baseline/test_image_complete.png.dvc new file mode 100644 index 00000000000..f37108dfe06 --- /dev/null +++ b/pygmt/tests/baseline/test_image_complete.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: b2984015b085a78cda9c90fbd500b97e + size: 64696 + hash: md5 + path: test_image_complete.png diff --git a/pygmt/tests/baseline/test_image_height_no_width.png.dvc b/pygmt/tests/baseline/test_image_height_no_width.png.dvc new file mode 100644 index 00000000000..fb8f54365b2 --- /dev/null +++ b/pygmt/tests/baseline/test_image_height_no_width.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 510cf7715a4059cf65cb0917aabacc8d + size: 34733 + hash: md5 + path: test_image_height_no_width.png diff --git a/pygmt/tests/test_image.py b/pygmt/tests/test_image.py index e69a8d71938..9c8a2061495 100644 --- a/pygmt/tests/test_image.py +++ b/pygmt/tests/test_image.py @@ -4,7 +4,8 @@ import pytest from pygmt import Figure -from pygmt.params import Box +from pygmt.exceptions import GMTInvalidInput +from pygmt.params import Box, Position @pytest.mark.mpl_image_compare @@ -13,5 +14,63 @@ def test_image(): Place images on map. """ fig = Figure() - fig.image(imagefile="@circuit.png", position="x0/0+w2c", box=Box(pen="thin,blue")) + fig.image(imagefile="@circuit.png") return fig + + +@pytest.mark.mpl_image_compare +def test_image_complete(): + """ + Test all parameters of image. + """ + fig = Figure() + fig.image( + imagefile="@circuit.png", + position=Position((0, 0)), + width="4c", + height="0", + replicate=(2, 1), + dpi=300, + box=Box(pen="thin,blue"), + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_image_height_no_width(): + """ + Test all parameters of image. + """ + fig = Figure() + fig.image(imagefile="@circuit.png", height=2) + return fig + + +@pytest.mark.mpl_image_compare(filename="test_image_complete.png") +def test_image_position_deprecated_syntax(): + """ + Test that passing the deprecated GMT CLI syntax string to 'position' works. + """ + fig = Figure() + fig.image( + imagefile="@circuit.png", + position="x0/0+w4c/0c+n2/1+r300", + box=Box(pen="thin,blue"), + ) + return fig + + +def test_image_position_mixed_syntax(): + """ + Test that an error is raised when 'position' is given as a raw GMT CLI string + and conflicts with other parameters. + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.image(imagefile="@circuit.png", position="x0/0", width="4c") + with pytest.raises(GMTInvalidInput): + fig.image(imagefile="@circuit.png", position="x0/0", height="3c") + with pytest.raises(GMTInvalidInput): + fig.image(imagefile="@circuit.png", position="x0/0", dpi="300") + with pytest.raises(GMTInvalidInput): + fig.image(imagefile="@circuit.png", position="x0/0", replicate=(2, 1))