Skip to content

Commit

Permalink
Add FPDF.bezier() method exposing PaintedPath.curve_to (py-pdf#1174)
Browse files Browse the repository at this point in the history
* Began implementation of bezier method.

* Focus on cubic Bezier curves for now, added to doc page on shapes, and added one test.

* Image for shapes doc page

* Remove code paste comment from writing Bezier test

* FPDF.bezier(): Remove unused variable, add extra space following

* Mention new Cubic bezier method in CHANGELOG.md

* Make Bezier curves as open paths by default

* Quadratic or cubic curves with bezier(), Draw Bezier curves as open paths by default

* Error handling in FPDF.bezier() and adding closed paths to tests

* Accept style in .bezier(), convert it to PathPaintRule

* Fix style errors reported by pylint

* Fix accidentally calling boolean attribute as if it were function

* Show styles functionality in Bezier curve testing

* Test that line width and dash pattern are updated from FPDF object; Remove generate=True from Bezier tests; clean up point drawing function for bezier tests.

* Update template PDFs for Bezier test.

* Remove debug_stream param from fdpf.bezier(), Ran black

* Do not mention removed parameter in docstring for fpdf.bezier()

* Fix merge conflict in and mention Bezier curves in CHANGELOG.md
  • Loading branch information
awmc000 committed May 30, 2024
1 parent 4a70f4b commit f0bd468
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 2 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
### Added
* [`Templates`](https://py-pdf.github.io/fpdf2/fpdf/Templates.html) can now be also defined in JSON files.
* support to optionally set `wrapmode` in templates (default `"WORD"` can optionally be set to `"CHAR"` to support wrapping on characters for scripts like Chinese or Japanese) - _cf._ [#1159](https://github.com/py-pdf/fpdf2/issues/1159)

* support for quadratic and cubic Bézier curves with [`FPDF.bezier()`](https://py-pdf.github.io/fpdf2/fpdf/Shapes.html#fpdf.fpdf.FPDF.bezier)
### Fixed
* [`fpdf.drawing.DeviceCMYK`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceCMYK) objects can now be passed to [`FPDF.set_draw_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_draw_color), [`FPDF.set_fill_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_fill_color) and [`FPDF.set_text_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_text_color) without raising a `ValueError`: [documentation](https://py-pdf.github.io/fpdf2/Text.html#text-formatting).
* individual `/Resources` directories are now properly created for each document page. This change ensures better compliance with the PDF specification but results in a slight increase in the size of PDF documents. You can still use the old behavior by setting `FPDF().single_resources_object = True`.
Expand Down Expand Up @@ -666,4 +666,4 @@ prevented strings passed first to the text-rendering methods to be displayed.
### Modified
* turned `accept_page_break` into a property
* unit tests now use the standard `unittest` lib
* massive code cleanup using `flake8`
* massive code cleanup using `flake8`
13 changes: 13 additions & 0 deletions docs/Shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,19 @@ pdf.output("solid_arc.pdf")
```
![](solid_arc.png)

## Bezier Curve ##
Using [`bezier()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.bezier) to create a cubic Bezier curve:
```python
from fpdf import FPDF
pdf = FPDF()
pdf.add_page()
pdf.set_fill_color(r=255, g=0, b=255)
pdf.bezier([(20, 80), (40, 20), (60, 80)])
pdf.output("bezier.pdf")
```

![](bezier.png)

## Regular Polygon ##

Using [`regular_polygon()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.regular_polygon):
Expand Down
Binary file added docs/bezier.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,74 @@ def solid_arc(
style,
)

def bezier(self, point_list, closed=False, style=None):
"""
Outputs a quadratic or cubic Bézier curve, defined by three or four coordinates.
Args:
point_list (list of tuples): List of Abscissa and Ordinate of
segments that should be drawn. Should be
three or four tuples. The first and last
points are the start and end point. The
middle point(s) are the control point(s).
closed (bool): True to draw the curve as a closed path, False (default)
for it to be drawn as an open path.
style (fpdf.enums.RenderStyle, str): Optional style of rendering. Allowed values are:
* `D` or None: draw border. This is the default value.
* `F`: fill
* `DF` or `FD`: draw and fill
"""
points = len(point_list)
if points not in (3, 4):
raise ValueError(
"point_list should contain 3 tuples for a quadratic curve"
"or 4 tuples for a cubic curve."
)

if style is None:
style = RenderStyle.DF
else:
style = RenderStyle.coerce(style)

# QuadraticBezierCurve and BezierCurve make use of `initial_point` when instantiated.
# If we want to define all 3 (quad.) or 4 (cubic) points, we can set `initial_point`
# to be the first point given in `point_list` by creating a separate dummy path at that pos.
with self.drawing_context() as ctxt:
p1 = point_list[0]
x1, y1 = p1[0], p1[1]

dummy_path = PaintedPath(x1, y1)
ctxt.add_item(dummy_path)

p2 = point_list[1]
x2, y2 = p2[0], p2[1]

p3 = point_list[2]
x3, y3 = p3[0], p3[1]

if points == 4:
p4 = point_list[3]
x4, y4 = p4[0], p4[1]

path = PaintedPath(x1, y1)

# Translate enum style (RenderStyle) into rule (PathPaintRule)
rule = PathPaintRule.STROKE_FILL_NONZERO
if style.is_draw and not style.is_fill:
rule = PathPaintRule.STROKE
elif style.is_fill and not style.is_draw:
rule = PathPaintRule.FILL_NONZERO

path.style.paint_rule = rule
path.style.auto_close = closed

if points == 4:
path.curve_to(x2, y2, x3, y3, x4, y4)
elif points == 3:
path.curve_to(x2, y2, x2, y2, x3, y3)

ctxt.add_item(path)

def add_font(self, family=None, style="", fname=None, uni="DEPRECATED"):
"""
Imports a TrueType or OpenType font and makes it available
Expand Down
Binary file added test/shapes/bezier_curve_line_settings.pdf
Binary file not shown.
Binary file added test/shapes/cubic_bezier_curve.pdf
Binary file not shown.
Binary file added test/shapes/quadratic_bezier_curve.pdf
Binary file not shown.
86 changes: 86 additions & 0 deletions test/shapes/test_bezier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from pathlib import Path

import fpdf
from test.conftest import assert_pdf_equal


HERE = Path(__file__).resolve().parent


def draw_points(pdf, point_lists):
for pl in point_lists:
for p in pl:
pdf.circle(x=p[0], y=p[1], r=1, style="FD")


def test_quadratic_beziers(tmp_path):
pdf = fpdf.FPDF(unit="mm")
pdf.add_page()

pl1 = [(20, 40), (40, 20), (60, 40)]
pl2 = [(20, 80), (50, 100), (60, 80)]
pl3 = [(20, 120), (40, 110), (60, 120)]
pl4 = [(20, 170), (40, 160), (60, 170)]
pl5 = [[20, 230], (40, 280), (60, 250)]

pdf.set_fill_color(r=255, g=0, b=0)
pdf.bezier(pl1)
pdf.set_fill_color(r=0, g=255, b=0)
pdf.bezier(pl2)
pdf.set_fill_color(r=0, g=0, b=255)
pdf.bezier(pl3, closed=True)
pdf.bezier(pl4, style="F")
pdf.bezier(pl5, style="D")

draw_points(pdf, [pl1, pl2, pl3, pl4, pl5])

assert_pdf_equal(pdf, HERE / "quadratic_bezier_curve.pdf", tmp_path)


def test_cubic_beziers(tmp_path):
pdf = fpdf.FPDF(unit="mm")
pdf.add_page()

pl1 = [(120, 40), (140, 30), (160, 49), (180, 50)]
pl2 = [(120, 80), (150, 100), (160, 80), (180, 80)]
pl3 = [(120, 120), (140, 130), (160, 140), (180, 120)]
pl4 = [(20, 20), (40, 10), (60, 20)]
pl5 = [[20, 80], (40, 90), (60, 80)]

pdf.set_fill_color(r=255, g=0, b=0)
pdf.bezier(pl1)
pdf.set_fill_color(r=0, g=255, b=0)
pdf.bezier(pl2)
pdf.set_fill_color(r=0, g=0, b=255)
pdf.bezier(pl3, closed=True)
pdf.bezier(pl4, style="F")
pdf.bezier(pl5, style="D")

draw_points(pdf, [pl1, pl2, pl3, pl4, pl5])

assert_pdf_equal(pdf, HERE / "cubic_bezier_curve.pdf", tmp_path)


def test_bezier_line_settings(tmp_path):
pdf = fpdf.FPDF(unit="mm")
pdf.add_page()

pl1 = [(120, 40), (140, 30), (160, 49), (180, 50)]
pl2 = [(20, 80), (50, 100), (60, 80)]

pdf.set_fill_color(r=255, g=0, b=0)
pdf.set_dash_pattern(dash=2, gap=3)
pdf.bezier(pl1)

pdf.set_fill_color(r=0, g=255, b=0)
pdf.set_dash_pattern(dash=4, gap=6)
pdf.set_line_width(2)
pdf.bezier(pl2)

# Reset for drawing points
pdf.set_line_width(0.2)
pdf.set_dash_pattern(0, 0, 0)

draw_points(pdf, [pl1, pl2])

assert_pdf_equal(pdf, HERE / "bezier_curve_line_settings.pdf", tmp_path)

0 comments on commit f0bd468

Please sign in to comment.