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

Add path drawing API #196

Merged
merged 73 commits into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
0f1a0ae
Add path drawing API
torque Jul 27, 2021
02acba8
drawing: rewrite to support alpha blending and style inheriting
torque Aug 9, 2021
234b28f
Implement basic SVG converter
torque Aug 15, 2021
bd5487a
changelog: add notes for the new drawing and SVG APIs.
torque Aug 16, 2021
1463c11
Add basic unit tests for drawing API
torque Jul 29, 2021
13d9dc9
svg: support basic shapes and more transforms, units, and styles.
torque Aug 22, 2021
81e2cf4
docs: add some starter documentation
torque Aug 22, 2021
e2a86c8
test: add SVG conversion tests.
torque Aug 22, 2021
aa6a332
docs: spew docstrings everywhere.
torque Sep 4, 2021
acf7a96
drawing.PaintedPath: add property for setting the ClippingPath.
torque Sep 5, 2021
4e39363
drawing: default to copying path elements when adding them.
torque Sep 5, 2021
3107a8c
drawing: add RoundedRectangle and Ellipse classes for drawing.
torque Sep 5, 2021
692ee5d
test.svg: add path SVG parsing tests.
torque Sep 5, 2021
4ffea24
drawing: remove global graphics state registry.
torque Sep 9, 2021
7d57201
drawing: fix some bugs in the debug render path.
torque Sep 9, 2021
10b8337
tests: update rendered PDFs for drawing and SVG tests.
torque Sep 9, 2021
dda33bf
svg: pass encoding to open to appease pylint.
torque Sep 22, 2021
9e72cbf
test: disable pylint warnings that conflict with the way pytest works.
torque Sep 22, 2021
e90edca
test.svg: add tests for the remainder of path element parsing.
torque Sep 22, 2021
4631fea
test.drawing: move test parameters to a separate file.
torque Sep 30, 2021
5d22692
test.drawing: continue to move parameters and improve test coverage.
torque Oct 2, 2021
2f00150
drawing: minor docstring improvement
torque Oct 2, 2021
9e22cab
drawing: implement character escaping for Name and string literals.
torque Oct 3, 2021
15495b2
drawing: fix / and // operator overloading on Point.
torque Oct 3, 2021
d2b4728
test.drawing: continue to improve test coverage.
torque Oct 3, 2021
8b1a3a4
test.drawing: more tests.
torque Oct 9, 2021
f30e47f
test.drawing: path element rendering tests.
torque Oct 10, 2021
bb08f0e
drawing: fix some docstrings and improve naming consistency.
torque Oct 10, 2021
e8e2775
drawing: don't render DrawingContext if its members are all empty.
torque Oct 10, 2021
3452385
drawing: fix docstrings for render calls.
torque Oct 10, 2021
c2e0a1d
drawing: handle an edge case in paint style resolution.
torque Oct 10, 2021
98fd698
test.drawing: PaintedPath tests.
torque Oct 10, 2021
4a79b3a
test: complete basic test coverage for fpdf.drawing
torque Oct 18, 2021
6aee6bc
svg: make path parsing follow the spec a bit more closely.
torque Nov 1, 2021
28a2458
svg: make cross_references not global.
torque Nov 1, 2021
a3fbce0
test.svg: reorganize a bit and improve path parsing tests.
torque Nov 1, 2021
558d2b8
changelog: link to documentation for new APIs
torque Nov 26, 2021
7a79a1c
docs.drawing: minor improvements
torque Nov 26, 2021
d2bdb50
drawing: improve render_pdf_primitive
torque Nov 27, 2021
83a21e9
test.drawing: add test for inheriting document graphical styles
torque Nov 27, 2021
8920fa3
svg: upcase names of constants
torque Nov 27, 2021
e58ce9c
drawing: add some missing documentation.
torque Nov 27, 2021
a56fb2f
drawing, svg: un-negate some default-negated logic
torque Nov 27, 2021
4e2f5e1
readme: mention drawing and SVG conversion functionality
torque Nov 27, 2021
9d7505d
drawing: police GraphicsStyle attribute setting
torque Nov 27, 2021
42ba9d5
fpdf: preserve dash pattern numeric values
torque Nov 27, 2021
0a24cd5
fpdf: use a pre-unscaled local context for inheriting document styles
torque Nov 27, 2021
9f96280
tests: regenerate all drawing/svg test PDFs
torque Nov 27, 2021
e045892
test.svg: add ShapeBuilder tests
torque Nov 28, 2021
45e40c0
test.svg: add transform handling tests
torque Nov 29, 2021
82c20df
svg: fix rect separate rx ry handling
torque Nov 29, 2021
33a119e
svg: support rotation transform center point
torque Nov 29, 2021
c94b061
svg: fix some attribute conversions
torque Nov 29, 2021
38e2ad4
test.svg: add style attribute conversion tests
torque Nov 29, 2021
6056839
svg: fix min-x and min-y on viewbox
torque Nov 29, 2021
c26e263
test.svg: test implicit path directives
torque Nov 29, 2021
57a51f7
test.svg: add document view scaling tests
torque Nov 29, 2021
53ed7b3
test.svg: add visual viewbox test
torque Nov 29, 2021
b3fd6ce
svg: fix default stroke cap style
torque Nov 29, 2021
d1d0a23
test.svg: regenerate test pdfs
torque Nov 29, 2021
8f6d5e3
docs.SVG: slightly clarify percentage based SVG size behavior
torque Nov 29, 2021
e573c98
docs.drawing: add demo output images/PDFs
torque Nov 30, 2021
befb3d2
svg: address pylint remarks
torque Dec 1, 2021
80b35cd
drawing: use util.escape_parens for rendering strings.
torque Dec 1, 2021
53f66b6
drawing: decouple stroke_dash_pattern and stroke_dash_phase
torque Dec 12, 2021
8c45a5d
tests: ignore additional verapdf transparency-related rules
torque Dec 12, 2021
d49e881
fpdf.drawing: follow document transparency support
torque Dec 12, 2021
a8686b0
test: fix tests to match updated APIs
torque Dec 12, 2021
a07f8c9
svg: produce fewer curly braces on unknown namespaces
torque Dec 12, 2021
4cf2964
test.(drawing|svg): improve edge case test coverage
torque Dec 13, 2021
eeede56
test: regenerate PDFs
torque Dec 12, 2021
05aeebe
drawing: fix pylint nit
torque Dec 17, 2021
6b7f86f
test.svg: remove stray test PDF generation
torque Jan 8, 2022
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/),
and [PEP 440](https://www.python.org/dev/peps/pep-0440/).

## [2.5.0] - not released yet
- add [`fpdf.drawing`](https://pyfpdf.github.io/fpdf2/Drawing.html) API for composing paths from an arbitrary sequence of lines and curves.
- add [`fpdf.svg.convert_svg_to_drawing`](https://pyfpdf.github.io/fpdf2/SVG.html) function to support converting basic scalable vector graphics (SVG) images to PDF paths.

## [2.4.7] - not released yet
### Fixed
- `will_page_break()` & `accept_page_break` are not invoked anymore during a call to `multi_cell(split_only=True)`
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Features
* Internal/External Links
* PNG, GIF and JPG support (including transparency and alpha channel)
* Shape, Line Drawing
* Arbitrary path drawing and basic SVG import
* Generate [Code 39](https://fr.wikipedia.org/wiki/Code_39) & [Interleaved 2 of 5](https://en.wikipedia.org/wiki/Interleaved_2_of_5) barcodes
* Cell / multi-cell / plaintext writing, automatic page breaks
* Basic [conversion from HTML to PDF](https://pyfpdf.github.io/fpdf2/HTML.html)
Expand Down
200 changes: 200 additions & 0 deletions docs/Drawing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Drawing #

The `fpdf.drawing` module provides an API for composing paths out of an
arbitrary sequence of straight lines and curves. This allows fairly low-level
control over the graphics primitives that PDF provides, giving the user the
ability to draw pretty much any vector shape on the page.

The drawing API makes use of features (notably transparency and blending modes)
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
that were introduced in PDF 1.4. Therefore, use of the features of this module
will automatically set the output version to 1.4 (fpdf normally defaults to
version 1.3. Because the PDF 1.4 specification was originally published in
2001, this version should be compatible with all viewers currently in general
use).

## Getting Started

The easiest way to add a drawing to the document is via `fpdf.FPDF.new_path`.
This is a context manager that takes care of serializing the path to the
document once the context is exited.

Drawings follow the fpdf convention that the origin (that is, coordinate(0, 0)),
is at the top-left corner of the page. The numbers specified to the various
path commands are interpreted in the document units.

```python
import fpdf

pdf = fpdf.FPDF(unit='mm', format=(10, 10))
pdf.add_page()

with pdf.new_path() as path:
path.move_to(2, 2)
path.line_to(8, 8)
path.horizontal_line_relative(-6)
path.line_relative(6, -6)
path.close()

pdf.output("drawing-demo.pdf")
```
This example draws an hourglass shape centered on the page:

<p align="center"><img src="drawing/demo-1.webp"/></p>
<p align="center"><a href="drawing/demo-1.pdf">view as PDF</a></p>


## Adding Some Style

Drawings can be styled, changing how they look and blend with other drawings.
Styling can change the color, opacity, stroke shape, and other attributes of a
drawing.

Let's add some color to the above example:

```python
import fpdf

pdf = fpdf.FPDF(unit='mm', format=(10, 10))
pdf.add_page()

with pdf.new_path() as path:
path.style.fill_color = '#A070D0'
path.style.stroke_color = fpdf.drawing.gray8(210)
path.style.stroke_width = 1
path.style.stroke_opacity = 0.75
path.style.stroke_join_style = 'round'

path.move_to(2, 2)
path.line_to(8, 8)
path.horizontal_line_relative(-6)
path.line_relative(6, -6)
path.close()

pdf.output("drawing-demo.pdf")
```

If you make color choices like these, it's probably not a good idea to quit your
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
day job to become a graphic designer. Here's what the output should look like:

<p align="center"><img src="drawing/demo-2.webp"/></p>
<p align="center"><a href="drawing/demo-2.pdf">view as PDF</a></p>

## Transforms And You

Transforms provide the ability to manipulate the placement of points within a
path without having to do any pesky math yourself. Transforms are composable
using python's matrix multiplication operator (`@`), so, for example, a
transform that both rotates and scales an object can be create by matrix
multiplying a rotation transform with a scaling transform.

An important thing to note about transforms is that the result is order
dependent, which is to say that something like performing a rotation followed
by scaling will not, in the general case, result in the same output as
performing the same scaling followed by the same rotation.

Additionally, it's not generally possible to deconstruct a composed
transformation (representing an ordered sequence of translations, scaling,
rotations, shearing) back into the sequence of individual transformation
functions that produced it. That's okay, because this isn't important unless
you're trying to do something like animate transforms after they've been
composed, which you can't do in a PDF anyway.

All that said, let's take the example we've been working with for a spin (the
pun is intended, you see, because we're going to rotate the drawing).
Explaining the joke does make it better.
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved

An easy way to apply a transform to a path is through the `path.transform`
property.

```python
import fpdf

pdf = fpdf.FPDF(unit="mm", format=(10, 10))
pdf.add_page()

with pdf.new_path() as path:
path.style.fill_color = "#A070D0"
path.style.stroke_color = fpdf.drawing.gray8(210)
path.style.stroke_width = 1
path.style.stroke_opacity = 0.75
path.style.stroke_join_style = "round"
path.transform = fpdf.drawing.Transform.rotation_d(45).scale(0.707).about(5, 5)

path.move_to(2, 2)
path.line_to(8, 8)
path.horizontal_line_relative(-6)
path.line_relative(6, -6)

path.close()

pdf.output("drawing-demo.pdf")
```

<p align="center"><img src="drawing/demo-3.webp"/></p>
<p align="center"><a href="drawing/demo-3.pdf">view as PDF</a></p>

The transform in the above example rotates the path 45 degrees clockwise
and scales it by `1/sqrt(2)` around its center point. This transform could be
equivalently written as:

```python
import fpdf
T = fpdf.drawing.Transform

T.translation(-5, -5) @ T.rotation_d(45) @ T.scaling(0.707) @ T.translation(5, 5)
```

Because all transforms operate on points relative to the origin, if we had
rotated the path without first centering it on the origin, we would have
rotated it partway off of the page. Similarly, the size-reduction from the
scaling would have moved it closer to the origin. By bracketing the transforms
with the two translations, the placement of the drawing on the page is
preserved.

## Clipping Paths

The clipping path is used to define the region that the normal path is actually
painted. This can be used to create drawings that would otherwise be difficult
to produce.

```python
import fpdf

pdf = fpdf.FPDF(unit="mm", format=(10, 10))
pdf.add_page()

clipping_path = fpdf.drawing.ClippingPath()
clipping_path.rectangle(x=2.5, y=2.5, w=5, h=5, rx=1, ry=1)

with pdf.new_path() as path:
path.style.fill_color = "#A070D0"
path.style.stroke_color = fpdf.drawing.gray8(210)
path.style.stroke_width = 1
path.style.stroke_opacity = 0.75
path.style.stroke_join_style = "round"

path.clipping_path = clipping_path

path.move_to(2, 2)
path.line_to(8, 8)
path.horizontal_line_relative(-6)
path.line_relative(6, -6)

path.close()

pdf.output("drawing-demo.pdf")
```
<p align="center"><img src="drawing/demo-4.webp"/></p>
<p align="center"><a href="drawing/demo-4.pdf">view as PDF</a></p>

## Next Steps

The presented API style is designed to make it simple to produce shapes
declaratively in your Python scripts. However, paths can just as easily be
created programmatically by creating instances of the
`fpdf.drawing.PaintedPath` for paths and `fpdf.drawing.GraphicsContext` for
groups of paths.

Storing paths in intermediate objects allows reusing them and can open up more
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
advanced use-cases. The [`fpdf.svg`](SVG.html) SVG converter, for example, is
implemented using the `fpdf.drawing` interface.
93 changes: 93 additions & 0 deletions docs/SVG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Scalable Vector Graphics (SVG) #

`fpdf2` supports basic conversion of SVG paths into PDF paths, which can be
inserted into an existing PDF document or used as the contents of a new PDF
document.

Not all SVGs will convert correctly. Please see
[the list of unsupported features](#currently-unsupported-notable-svg-features)
for more information about what to look out for.

## A simple example ##

The following script will create a PDF that consists only of the graphics
contents of the provided SVG file:

```python
import fpdf

svg = fpdf.svg.SVGObject.from_file("my_file.svg")

pdf = fpdf.FPDF(unit="pt", format=(svg.width, svg.height))
pdf.add_page()
svg.draw_to_page(pdf)

pdf.output("my_file.pdf")
```

Because this takes the PDF document size from the source SVG, it does assume
that the width/height of the SVG are specified in absolute units rather than
relative ones (i.e. the top-level `<svg>` tag has something like `width="5cm"`
and not `width=50%`). In this case, if the values are percentages, they will be
interpreted as their literal numeric value (i.e. `100%` would be treated as
`100 pt`). The next example uses `transform_to_page_viewport`, which will scale
an SVG with a percentage based `width` to the pre-defined PDF page size.

The converted SVG object can be returned as an fpdf.drawing.GraphicsContext
collection of drawing directives for more control over how it is rendered:

```python
import fpdf

svg = fpdf.svg.SVGObject.from_file("my_file.svg")

pdf = FPDF(unit="in", format=(8.5, 11))
pdf.add_page()

# We pass align_viewbox=False because we want to perform positioning manually
# after the size transform has been computed.
width, height, paths = svg.transform_to_page_viewport(pdf, align_viewbox=False)
# note: transformation order is important! This centers the svg drawing at the
# origin, rotates it 90 degrees clockwise, and then repositions it to the
# middle of the output page.
paths.transform = paths.transform @ fpdf.drawing.Transform.translation(
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
-width / 2, -height / 2
).rotate_d(90).translate(pdf.w / 2, pdf.h / 2)

pdf.draw_path(paths)

pdf.output("my_file.pdf")
```

## Supported SVG Features ##

- groups
- paths
- basic shapes (rect, circle, ellipse, line, polyline, polygon)
- basic cross-references
- stroke & fill coloring and opacity
- basic stroke styling

## Currently Unsupported Notable SVG Features ##

Everything not listed as supported is unsupported, which is a lot. SVG is a
ridiculously complex format that has become increasingly complex as it absorbs
more of the entire browser rendering stack into its specification. However,
there are some pretty commonly used features that are unsupported that may
cause unexpected results (up to and including a normal-looking SVG rendering as
a completely blank PDF). It is very likely that off-the-shelf SVGs will not be
converted fully correctly without some preprocessing.

The biggest unsupported feature is probably:

- CSS styling of SVG elements

In addition to that:

- text/tspan/textPath
- symbols
- markers
- patterns
- gradients
- embedded images or other content (including nested SVGs)
- a lot of attributes
torque marked this conversation as resolved.
Show resolved Hide resolved
Binary file added docs/drawing/demo-1.pdf
Binary file not shown.
Binary file added docs/drawing/demo-1.webp
Binary file not shown.
Binary file added docs/drawing/demo-2.pdf
Binary file not shown.
Binary file added docs/drawing/demo-2.webp
Binary file not shown.
Binary file added docs/drawing/demo-3.pdf
Binary file not shown.
Binary file added docs/drawing/demo-3.webp
Binary file not shown.
Binary file added docs/drawing/demo-4.pdf
Binary file not shown.
Binary file added docs/drawing/demo-4.webp
Binary file not shown.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This repository is a fork of the library's [original port by Max Pat](http://www
* [Unicode](Unicode.md) (UTF-8) TrueType font subset embedding (Central European, Cyrillic, Greek, Baltic, Thai, Chinese, Japanese, Korean, Hindi and almost any other language in the world)
* PNG, GIF and JPG support (including transparency and alpha channel)
* Shape, Line Drawing
* Arbitrary path drawing and basic SVG import
torque marked this conversation as resolved.
Show resolved Hide resolved
* Generate [Code 39](https://fr.wikipedia.org/wiki/Code_39) & [Interleaved 2 of 5](https://en.wikipedia.org/wiki/Interleaved_2_of_5) barcodes
* Cell / multi-cell / plaintext writing, automatic page breaks
* Basic conversion from HTML to PDF
Expand Down
1 change: 1 addition & 0 deletions fpdf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from .html import HTMLMixin, HTML2FPDF
from .template import Template, FlexTemplate
from . import svg
from .deprecation import WarnOnDeprecatedModuleAttributes

FPDF_VERSION = _FPDF_VERSION
Expand Down
Loading