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

Support miter #4237

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file added docs/images/spikes-no.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/spikes-yes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions docs/page.rst
Original file line number Diff line number Diff line change
Expand Up @@ -723,12 +723,13 @@ In a nutshell, this is what you can do with PyMuPDF:
pair: morph; insert_text
pair: overlay; insert_text
pair: render_mode; insert_text
pair: miter_limit; insert_text
pair: rotate; insert_text
pair: stroke_opacity; insert_text
pair: fill_opacity; insert_text
pair: oc; insert_text

.. method:: insert_text(point, text, *, fontsize=11, fontname="helv", fontfile=None, idx=0, color=None, fill=None, render_mode=0, border_width=1, encoding=TEXT_ENCODING_LATIN, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, overlay=True, oc=0)
.. method:: insert_text(point, text, *, fontsize=11, fontname="helv", fontfile=None, idx=0, color=None, fill=None, render_mode=0, miter_limit=1, border_width=0.05, encoding=TEXT_ENCODING_LATIN, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, overlay=True, oc=0)

PDF only: Insert text lines starting at :data:`point_like` ``point``. See :meth:`Shape.insert_text`.

Expand All @@ -751,12 +752,13 @@ In a nutshell, this is what you can do with PyMuPDF:
pair: morph; insert_textbox
pair: overlay; insert_textbox
pair: render_mode; insert_textbox
pair: miter_limit; insert_textbox
pair: rotate; insert_textbox
pair: stroke_opacity; insert_textbox
pair: fill_opacity; insert_textbox
pair: oc; insert_textbox

.. method:: insert_textbox(rect, buffer, *, fontsize=11, fontname="helv", fontfile=None, idx=0, color=None, fill=None, render_mode=0, border_width=1, encoding=TEXT_ENCODING_LATIN, expandtabs=8, align=TEXT_ALIGN_LEFT, charwidths=None, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, oc=0, overlay=True)
.. method:: insert_textbox(rect, buffer, *, fontsize=11, fontname="helv", fontfile=None, idx=0, color=None, fill=None, render_mode=0, miter_limit=1, border_width=1, encoding=TEXT_ENCODING_LATIN, expandtabs=8, align=TEXT_ALIGN_LEFT, charwidths=None, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, oc=0, overlay=True)

PDF only: Insert text into the specified :data:`rect_like` *rect*. See :meth:`Shape.insert_textbox`.

Expand Down
30 changes: 27 additions & 3 deletions docs/shape.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,13 @@ Several draw methods can be executed in a row and each one of them will contribu
pair: lineheight; insert_text
pair: morph; insert_text
pair: render_mode; insert_text
pair: miter_limit; insert_text
pair: rotate; insert_text
pair: stroke_opacity; insert_text
pair: fill_opacity; insert_text
pair: oc; insert_text

.. method:: insert_text(point, text, *, fontsize=11, fontname="helv", fontfile=None, set_simple=False, encoding=TEXT_ENCODING_LATIN, color=None, lineheight=None, fill=None, render_mode=0, border_width=1, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, oc=0)
.. method:: insert_text(point, text, *, fontsize=11, fontname="helv", fontfile=None, set_simple=False, encoding=TEXT_ENCODING_LATIN, color=None, lineheight=None, fill=None, render_mode=0, miter_limit=1, border_width=1, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, oc=0)

Insert text lines starting at ``point``.

Expand Down Expand Up @@ -328,10 +329,11 @@ Several draw methods can be executed in a row and each one of them will contribu
pair: lineheight; insert_textbox
pair: morph; insert_textbox
pair: render_mode; insert_textbox
pair: miter_limit; insert_textbox
pair: rotate; insert_textbox
pair: oc; insert_textbox

.. method:: insert_textbox(rect, buffer, *, fontsize=11, fontname="helv", fontfile=None, set_simple=False, encoding=TEXT_ENCODING_LATIN, color=None, fill=None, render_mode=0, border_width=1, expandtabs=8, align=TEXT_ALIGN_LEFT, rotate=0, lineheight=None, morph=None, stroke_opacity=1, fill_opacity=1, oc=0)
.. method:: insert_textbox(rect, buffer, *, fontsize=11, fontname="helv", fontfile=None, set_simple=False, encoding=TEXT_ENCODING_LATIN, color=None, fill=None, render_mode=0, miter_limit=1, border_width=1, expandtabs=8, align=TEXT_ALIGN_LEFT, rotate=0, lineheight=None, morph=None, stroke_opacity=1, fill_opacity=1, oc=0)

PDF only: Insert text into the specified rectangle. The text will be split into lines and words and then filled into the available space, starting from one of the four rectangle corners, which depends on `rotate`. Line feeds and multiple space will be respected.

Expand Down Expand Up @@ -591,7 +593,7 @@ Common Parameters

Both values are floats in range [0, 1]. Negative values or values > 1 will ignored (in most cases). Both set the transparency such that a value 0.5 corresponds to 50% transparency, 0 means invisible and 1 means intransparent. For e.g. a rectangle the stroke opacity applies to its border and fill opacity to its interior.

For text insertions (:meth:`Shape.insert_text` and :meth:`Shape.insert_textbox`), use *fill_opacity* for the text. At first sight this seems surprising, but it becomes obvious when you look further down to *render_mode*: *fill_opacity* applies to the yellow and *stroke_opacity* applies to the blue color.
For text insertions (:meth:`Shape.insert_text` and :meth:`Shape.insert_textbox`), use *fill_opacity* for the text. At first sight this seems surprising, but it becomes obvious when you look further down to `render_mode`: `fill_opacity` applies to the yellow and `stroke_opacity` applies to the blue color.

----

Expand All @@ -616,6 +618,28 @@ Common Parameters

----

**miter_limit** (*float*)

A float specifying the maximum acceptable value of the quotient `miter-length / line-width` ("miter quotient"). Used in text output methods. This is only relevant for non-zero render mode values -- then, characters are written with border lines (i.e. "stroked").

If two lines stroking some character meet at a sharp (<= 90°) angle and the line width is large enough, then "spikes" may become visible -- causing an ugly appearance as shown below. For more background, see page 126 of the :ref:`AdobeManual`.

For instance, when joins meet at 90°, then the miter length is ``sqrt(2) * line-width``, so the miter quotient is ``sqrt(2)``.

If ``miter_limit`` is exceeded, then all joins with a larger qotient will appear as beveled ("butt" appearance).

The default value 1 (and any smaller value) will ensure that all joins are rendered as a butt. A value of ``None`` will use the PDF default value.

Example text showing spikes (``miter_limit=None``):

.. image:: images/spikes-yes.*

Example text suppressing spikes (``miter_limit=1``):

.. image:: images/spikes-no.*

----

**overlay** (*bool*)

Causes the item to appear in foreground (default) or background.
Expand Down
11 changes: 10 additions & 1 deletion src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1932,6 +1932,7 @@ def insert_textbox(
align: int = 0,
rotate: int = 0,
render_mode: int = 0,
miter_limit: float = 1,
border_width: float = 0.05,
morph: OptSeq = None,
overlay: bool = True,
Expand Down Expand Up @@ -1973,6 +1974,7 @@ def insert_textbox(
fill=fill,
expandtabs=expandtabs,
render_mode=render_mode,
miter_limit=miter_limit,
border_width=border_width,
align=align,
rotate=rotate,
Expand Down Expand Up @@ -2000,6 +2002,7 @@ def insert_text(
color: OptSeq = None,
fill: OptSeq = None,
border_width: float = 0.05,
miter_limit: float = 1,
render_mode: int = 0,
rotate: int = 0,
morph: OptSeq = None,
Expand All @@ -2023,6 +2026,7 @@ def insert_text(
fill=fill,
border_width=border_width,
render_mode=render_mode,
miter_limit=miter_limit,
rotate=rotate,
morph=morph,
stroke_opacity=stroke_opacity,
Expand Down Expand Up @@ -3774,6 +3778,7 @@ def insert_text(
fill: OptSeq = None,
render_mode: int = 0,
border_width: float = 0.05,
miter_limit: float = 1,
rotate: int = 0,
morph: OptSeq = None,
stroke_opacity: float = 1,
Expand Down Expand Up @@ -3910,7 +3915,8 @@ def insert_text(
if render_mode > 0:
nres += "%i Tr " % render_mode
nres += _format_g(border_width * fontsize) + " w "

if miter_limit is not None:
nres += _format_g(miter_limit) + " M "
if color is not None:
nres += color_str
if fill is not None:
Expand Down Expand Up @@ -3961,6 +3967,7 @@ def insert_textbox(
fill: OptSeq = None,
expandtabs: int = 1,
border_width: float = 0.05,
miter_limit: float = 1,
align: int = 0,
render_mode: int = 0,
rotate: int = 0,
Expand Down Expand Up @@ -4251,6 +4258,8 @@ def pixlen(x):
if render_mode > 0:
nres += "%i Tr " % render_mode
nres += _format_g(border_width * fontsize) + " w "
if miter_limit is not None:
nres += _format_g(miter_limit) + " M "

if align == 3:
nres += _format_g(spacing) + " Tw "
Expand Down
42 changes: 42 additions & 0 deletions tests/test_spikes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pymupdf
import pathlib
import os


def test_spikes():
"""Check suppression of text spikes caused by long miters."""
root = os.path.abspath(f"{__file__}/../..")
spikes_yes = pathlib.Path(f"{root}/docs/images/spikes-yes.png")
spikes_no = pathlib.Path(f"{root}/docs/images/spikes-no.png")
doc = pymupdf.open()
text = "NATO MEMBERS" # some text provoking spikes ("N", "M")
point = (10, 35) # insert point

# make text provoking spikes
page = doc.new_page(width=200, height=50) # small page
page.insert_text(
point,
text,
fontsize=20,
render_mode=1, # stroke text only
border_width=0.3, # causes thick border lines
miter_limit=None, # do not care about miter spikes
)
# write same text in white over the previous for better demo purpose
page.insert_text(point, text, fontsize=20, color=(1, 1, 1))
pix1 = page.get_pixmap()
assert pix1.tobytes() == spikes_yes.read_bytes()

# make text suppressing spikes
page = doc.new_page(width=200, height=50)
page.insert_text(
point,
text,
fontsize=20,
render_mode=1,
border_width=0.3,
miter_limit=1, # suppress each and every miter spike
)
page.insert_text(point, text, fontsize=20, color=(1, 1, 1))
pix2 = page.get_pixmap()
assert pix2.tobytes() == spikes_no.read_bytes()
Loading