From 618522a054e9954c29ca958ebb0e7e48e73adc3c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 24 Oct 2024 17:20:40 +0800 Subject: [PATCH] Figure.paragraph: Initial implementation focusing on input data --- pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/paragraph.py | 113 ++++++++++++++++++ pygmt/tests/baseline/test_paragraph.png.dvc | 5 + ...raph_multiple_paragraphs_blankline.png.dvc | 5 + ...paragraph_multiple_paragraphs_list.png.dvc | 5 + pygmt/tests/test_paragraph.py | 67 +++++++++++ 7 files changed, 197 insertions(+) create mode 100644 pygmt/src/paragraph.py create mode 100644 pygmt/tests/baseline/test_paragraph.png.dvc create mode 100644 pygmt/tests/baseline/test_paragraph_multiple_paragraphs_blankline.png.dvc create mode 100644 pygmt/tests/baseline/test_paragraph_multiple_paragraphs_list.png.dvc create mode 100644 pygmt/tests/test_paragraph.py diff --git a/pygmt/figure.py b/pygmt/figure.py index 374eb1d8fee..b332bbbb73c 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -424,6 +424,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..d591f2587a1 --- /dev/null +++ b/pygmt/src/paragraph.py @@ -0,0 +1,113 @@ +""" +paragraph - Typeset one or multiple paragraphs. +""" + +import io +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import ( + _check_encoding, + build_arg_list, + is_nonstr_iter, + non_ascii_to_octal, +) + + +def _parse_font_angle_justify( + font: float | str | None, angle: float | None, justify: AnchorCode | None +) -> str | None: + """ + Parse the font, angle, and justification arguments and return the string to be + appended to the module options. + + Examples + -------- + >>> _parse_font_angle_justify(None, None, None) + >>> _parse_font_angle_justify("10p", None, None) + '+f10p' + >>> _parse_font_angle_justify(None, 45, None) + '+a45' + >>> _parse_font_angle_justify(None, None, "CM") + '+jCM' + >>> _parse_font_angle_justify("10p,Helvetica-Bold", 45, "CM") + '+f10p,Helvetica-Bold+a45+jCM' + """ + args = ((font, "+f"), (angle, "+a"), (justify, "+j")) + if all(arg is None for arg, _ in args): + return None + return "".join(f"{flag}{arg}" for arg, flag in args if arg is not None) + + +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._preprocess() + + # Validate 'alignment' argument. + if alignment not in {"left", "center", "right", "justified"}: + msg = ( + "Invalid value for 'alignment': {alignment}. " + "Valid values are 'left', 'center', 'right', and 'justified'." + ) + raise GMTInvalidInput(msg) + + confdict = {} + # Prepare the keyword dictionary for the module options + kwdict = {"M": True, "F": _parse_font_angle_justify(font, angle, justify)} + + # Initialize a stringio object for storing the input data. + stringio = io.StringIO() + # The header line. + stringio.write(f"> {x} {y} {linespacing} {parwidth} {alignment[0]}\n") + # The text string to be written to the 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 + # Write the text string to the stringio object. + stringio.write(_textstr) + + with Session() as lib: + with lib.virtualfile_from_stringio(stringio) as vfile: + lib.call_module( + "text", args=build_arg_list(kwdict, 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