Skip to content

Commit a61a67b

Browse files
committed
Support miter imits
When using Page insert_text() / insert_textbox() with render_mode != 0, using proper values for `miter_limit` can control the generation of "spikes".
1 parent 8c75ca6 commit a61a67b

File tree

6 files changed

+83
-6
lines changed

6 files changed

+83
-6
lines changed

docs/images/spikes-no.png

3.25 KB
Loading

docs/images/spikes-yes.png

3.29 KB
Loading

docs/page.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,12 +723,13 @@ In a nutshell, this is what you can do with PyMuPDF:
723723
pair: morph; insert_text
724724
pair: overlay; insert_text
725725
pair: render_mode; insert_text
726+
pair: miter_limit; insert_text
726727
pair: rotate; insert_text
727728
pair: stroke_opacity; insert_text
728729
pair: fill_opacity; insert_text
729730
pair: oc; insert_text
730731

731-
.. 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)
732+
.. 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)
732733

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

@@ -751,12 +752,13 @@ In a nutshell, this is what you can do with PyMuPDF:
751752
pair: morph; insert_textbox
752753
pair: overlay; insert_textbox
753754
pair: render_mode; insert_textbox
755+
pair: miter_limit; insert_textbox
754756
pair: rotate; insert_textbox
755757
pair: stroke_opacity; insert_textbox
756758
pair: fill_opacity; insert_textbox
757759
pair: oc; insert_textbox
758760

759-
.. 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)
761+
.. 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)
760762

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

docs/shape.rst

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,13 @@ Several draw methods can be executed in a row and each one of them will contribu
286286
pair: lineheight; insert_text
287287
pair: morph; insert_text
288288
pair: render_mode; insert_text
289+
pair: miter_limit; insert_text
289290
pair: rotate; insert_text
290291
pair: stroke_opacity; insert_text
291292
pair: fill_opacity; insert_text
292293
pair: oc; insert_text
293294

294-
.. 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)
295+
.. 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)
295296

296297
Insert text lines starting at ``point``.
297298

@@ -328,10 +329,11 @@ Several draw methods can be executed in a row and each one of them will contribu
328329
pair: lineheight; insert_textbox
329330
pair: morph; insert_textbox
330331
pair: render_mode; insert_textbox
332+
pair: miter_limit; insert_textbox
331333
pair: rotate; insert_textbox
332334
pair: oc; insert_textbox
333335

334-
.. 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)
336+
.. 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)
335337

336338
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.
337339

@@ -591,7 +593,7 @@ Common Parameters
591593

592594
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.
593595

594-
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.
596+
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.
595597

596598
----
597599

@@ -616,6 +618,28 @@ Common Parameters
616618

617619
----
618620

621+
**miter_limit** (*float*)
622+
623+
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").
624+
625+
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`.
626+
627+
For instance, when joins meet at 90°, then the miter length is ``sqrt(2) * line-width``, so the miter quotient is ``sqrt(2)``.
628+
629+
If ``miter_limit`` is exceeded, then all joins with a larger qotient will appear as beveled ("butt" appearance).
630+
631+
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.
632+
633+
Example text showing spikes (``miter_limit=None``):
634+
635+
.. image:: images/spikes-yes.*
636+
637+
Example text suppressing spikes (``miter_limit=1``):
638+
639+
.. image:: images/spikes-no.*
640+
641+
----
642+
619643
**overlay** (*bool*)
620644

621645
Causes the item to appear in foreground (default) or background.

src/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1932,6 +1932,7 @@ def insert_textbox(
19321932
align: int = 0,
19331933
rotate: int = 0,
19341934
render_mode: int = 0,
1935+
miter_limit: float = 1,
19351936
border_width: float = 0.05,
19361937
morph: OptSeq = None,
19371938
overlay: bool = True,
@@ -1973,6 +1974,7 @@ def insert_textbox(
19731974
fill=fill,
19741975
expandtabs=expandtabs,
19751976
render_mode=render_mode,
1977+
miter_limit=miter_limit,
19761978
border_width=border_width,
19771979
align=align,
19781980
rotate=rotate,
@@ -2000,6 +2002,7 @@ def insert_text(
20002002
color: OptSeq = None,
20012003
fill: OptSeq = None,
20022004
border_width: float = 0.05,
2005+
miter_limit: float = 1,
20032006
render_mode: int = 0,
20042007
rotate: int = 0,
20052008
morph: OptSeq = None,
@@ -2023,6 +2026,7 @@ def insert_text(
20232026
fill=fill,
20242027
border_width=border_width,
20252028
render_mode=render_mode,
2029+
miter_limit=miter_limit,
20262030
rotate=rotate,
20272031
morph=morph,
20282032
stroke_opacity=stroke_opacity,
@@ -3774,6 +3778,7 @@ def insert_text(
37743778
fill: OptSeq = None,
37753779
render_mode: int = 0,
37763780
border_width: float = 0.05,
3781+
miter_limit: float = 1,
37773782
rotate: int = 0,
37783783
morph: OptSeq = None,
37793784
stroke_opacity: float = 1,
@@ -3910,7 +3915,8 @@ def insert_text(
39103915
if render_mode > 0:
39113916
nres += "%i Tr " % render_mode
39123917
nres += _format_g(border_width * fontsize) + " w "
3913-
3918+
if miter_limit is not None:
3919+
nres += _format_g(miter_limit) + " M "
39143920
if color is not None:
39153921
nres += color_str
39163922
if fill is not None:
@@ -3961,6 +3967,7 @@ def insert_textbox(
39613967
fill: OptSeq = None,
39623968
expandtabs: int = 1,
39633969
border_width: float = 0.05,
3970+
miter_limit: float = 1,
39643971
align: int = 0,
39653972
render_mode: int = 0,
39663973
rotate: int = 0,
@@ -4251,6 +4258,8 @@ def pixlen(x):
42514258
if render_mode > 0:
42524259
nres += "%i Tr " % render_mode
42534260
nres += _format_g(border_width * fontsize) + " w "
4261+
if miter_limit is not None:
4262+
nres += _format_g(miter_limit) + " M "
42544263

42554264
if align == 3:
42564265
nres += _format_g(spacing) + " Tw "

tests/test_spikes.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pymupdf
2+
import pathlib
3+
import os
4+
5+
6+
def test_spikes():
7+
"""Check suppression of text spikes caused by long miters."""
8+
root = os.path.abspath(f"{__file__}/../..")
9+
spikes_yes = pathlib.Path(f"{root}/docs/images/spikes-yes.png")
10+
spikes_no = pathlib.Path(f"{root}/docs/images/spikes-no.png")
11+
doc = pymupdf.open()
12+
text = "NATO MEMBERS" # some text provoking spikes ("N", "M")
13+
point = (10, 35) # insert point
14+
15+
# make text provoking spikes
16+
page = doc.new_page(width=200, height=50) # small page
17+
page.insert_text(
18+
point,
19+
text,
20+
fontsize=20,
21+
render_mode=1, # stroke text only
22+
border_width=0.3, # causes thick border lines
23+
miter_limit=None, # do not care about miter spikes
24+
)
25+
# write same text in white over the previous for better demo purpose
26+
page.insert_text(point, text, fontsize=20, color=(1, 1, 1))
27+
pix1 = page.get_pixmap()
28+
assert pix1.tobytes() == spikes_yes.read_bytes()
29+
30+
# make text suppressing spikes
31+
page = doc.new_page(width=200, height=50)
32+
page.insert_text(
33+
point,
34+
text,
35+
fontsize=20,
36+
render_mode=1,
37+
border_width=0.3,
38+
miter_limit=1, # suppress each and every miter spike
39+
)
40+
page.insert_text(point, text, fontsize=20, color=(1, 1, 1))
41+
pix2 = page.get_pixmap()
42+
assert pix2.tobytes() == spikes_no.read_bytes()

0 commit comments

Comments
 (0)