diff --git a/doc/api/index.rst b/doc/api/index.rst index 7828a225652..a8defc86296 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -31,6 +31,7 @@ Plotting map elements Figure.inset Figure.legend Figure.logo + Figure.paragraph Figure.solar Figure.text Figure.timestamp diff --git a/pygmt/figure.py b/pygmt/figure.py index 474cd91179b..b6e5fc0f5d4 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -445,6 +445,7 @@ def _repr_html_(self) -> str: legend, logo, meca, + paragraph, plot, plot3d, psconvert, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..6b163a31792 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -38,6 +38,7 @@ from pygmt.src.makecpt import makecpt from pygmt.src.meca import meca from pygmt.src.nearneighbor import nearneighbor +from pygmt.src.paragraph import paragraph from pygmt.src.plot import plot from pygmt.src.plot3d import plot3d from pygmt.src.project import project diff --git a/pygmt/src/paragraph.py b/pygmt/src/paragraph.py new file mode 100644 index 00000000000..76f653d1992 --- /dev/null +++ b/pygmt/src/paragraph.py @@ -0,0 +1,92 @@ +""" +paragraph - Typeset one or multiple paragraphs. +""" + +import io +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.alias import Alias, AliasSystem +from pygmt.clib import Session +from pygmt.exceptions import GMTValueError +from pygmt.helpers import ( + _check_encoding, + build_arg_list, + is_nonstr_iter, + non_ascii_to_octal, +) + + +def paragraph( + self, + x: float | str, + y: float | str, + text: str | Sequence[str], + parwidth: float | str, + linespacing: float | str, + font: float | str | None = None, + angle: float | None = None, + justify: AnchorCode | None = None, + alignment: Literal["left", "center", "right", "justified"] = "left", +): + """ + Typeset one or multiple paragraphs. + + Parameters + ---------- + x/y + The x, y coordinates of the paragraph. + text + The paragraph text to typeset. If a sequence of strings is provided, each string + is treated as a separate paragraph. + parwidth + The width of the paragraph. + linespacing + The spacing between lines. + font + The font of the text. + angle + The angle of the text. + justify + The justification of the block of text, relative to the given x, y position. + alignment + The alignment of the text. Valid values are ``"left"``, ``"center"``, + ``"right"``, and ``"justified"``. + """ + self._activate_figure() + + _valid_alignments = {"left", "center", "right", "justified"} + if alignment not in _valid_alignments: + raise GMTValueError( + alignment, + description="value for parameter 'alignment'", + choices=_valid_alignments, + ) + + aliasdict = AliasSystem( + F=[ + Alias(font, name="font", prefix="+f"), + Alias(angle, name="angle", prefix="+a"), + Alias(justify, name="justify", prefix="+j"), + ] + ).merge({"M": True}) + + confdict = {} + # Prepare the text string that will be passed to an io.StringIO object. + # Multiple paragraphs are separated by a blank line "\n\n". + _textstr: str = "\n\n".join(text) if is_nonstr_iter(text) else str(text) + # Check the encoding of the text string and convert it to octal if necessary. + if (encoding := _check_encoding(_textstr)) != "ascii": + _textstr = non_ascii_to_octal(_textstr, encoding=encoding) + confdict["PS_CHAR_ENCODING"] = encoding + + with Session() as lib: + with io.StringIO() as buffer: # Prepare the StringIO input. + buffer.write(f"> {x} {y} {linespacing} {parwidth} {alignment[0]}\n") + buffer.write(_textstr) + with lib.virtualfile_in(data=buffer) as vfile: + lib.call_module( + "text", + args=build_arg_list(aliasdict, infile=vfile, confdict=confdict), + ) diff --git a/pygmt/tests/baseline/test_paragraph.png.dvc b/pygmt/tests/baseline/test_paragraph.png.dvc new file mode 100644 index 00000000000..82906933e1d --- /dev/null +++ b/pygmt/tests/baseline/test_paragraph.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: c5b1df47e811475defb0db79e49cab3d + size: 27632 + hash: md5 + path: test_paragraph.png diff --git a/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc new file mode 100644 index 00000000000..a131677880d --- /dev/null +++ b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 0df1eb71a781f0b8cc7c48be860dd321 + size: 29109 + hash: md5 + path: test_paragraph_multiple_paragraphs_blankline.png diff --git a/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc new file mode 100644 index 00000000000..879799cc5db --- /dev/null +++ b/pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 167d4be24bca4e287b2056ecbfbb629a + size: 29076 + hash: md5 + path: test_paragraph_multiple_paragraphs_list.png diff --git a/pygmt/tests/test_paragraph.py b/pygmt/tests/test_paragraph.py new file mode 100644 index 00000000000..2193dc1384a --- /dev/null +++ b/pygmt/tests/test_paragraph.py @@ -0,0 +1,67 @@ +""" +Tests for Figure.paragraph. +""" + +import pytest +from pygmt import Figure + + +@pytest.mark.mpl_image_compare +def test_paragraph(): + """ + Test typesetting a single paragraph. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.paragraph( + x=4, + y=4, + text="This is a long paragraph. " * 10, + parwidth="5c", + linespacing="12p", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_paragraph_multiple_paragraphs_list(): + """ + Test typesetting a single paragraph. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.paragraph( + x=4, + y=4, + text=[ + "This is the first paragraph. " * 5, + "This is the second paragraph. " * 5, + ], + parwidth="5c", + linespacing="12p", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_paragraph_multiple_paragraphs_blankline(): + """ + Test typesetting a single paragraph. + """ + text = """ +This is the first paragraph. +This is the first paragraph. +This is the first paragraph. +This is the first paragraph. +This is the first paragraph. + +This is the second paragraph. +This is the second paragraph. +This is the second paragraph. +This is the second paragraph. +This is the second paragraph. +""" + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.paragraph(x=4, y=4, text=text, parwidth="5c", linespacing="12p") + return fig