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

Mirror function #783

Merged
merged 6 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ in order to get warned about deprecated features used in your code.
This can also be enabled programmatically with `warnings.simplefilter('default', DeprecationWarning)`.

## [2.7.5] - Not released yet
### Added
- [`FPDF.mirror()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.mirror) - New method: [documentation page](https://pyfpdf.github.io/fpdf2/Transformations.html) - Contributed by @sebastiantia
sebastiantia marked this conversation as resolved.
Show resolved Hide resolved

## [2.7.4] - 2023-04-28
### Added
Expand Down
36 changes: 35 additions & 1 deletion docs/Transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,38 @@ pdf.set_fill_color(r=230, g=30, b=180)
with pdf.skew(ax=-45, ay=0, x=100, y=170):
pdf.circle(x=100, y=170, r=10, style="FD")
```
![](slanted_circle.png)
![](slanted_circle.png)

## Mirror ##

The `mirror` context-manager applies a mirror transformation to all objects inserted in its indented block over a given mirror line by specifying starting co-ordinate and angle.

```python
x = 100
y = 100
pdf.text(x, y, txt="mirror this text")
with pdf.mirror("EAST", (x, y)):
pdf.set_text_color(r=255, g=128, b=0)
pdf.text(x, y, txt="mirror this text")
```
![](horizontal_mirror.png)
sebastiantia marked this conversation as resolved.
Show resolved Hide resolved

```python
pdf.text(x, y, txt="mirror this text")
with pdf.mirror("NORTH", (x, y)):
pdf.set_text_color(r=255, g=128, b=0)
pdf.text(x, y, txt="mirror this text")
```
![](vertical_mirror.png)

```python
prev_x = pdf.x
prev_y = pdf.y
sebastiantia marked this conversation as resolved.
Show resolved Hide resolved
pdf.multi_cell(w=50, txt=LOREM_IPSUM)
with pdf.mirror("NORTHEAST", (pdf.x, pdf.y)):
# Reset cursor to mirror original multi-cell
pdf.x = prev_x
pdf.y = prev_y
sebastiantia marked this conversation as resolved.
Show resolved Hide resolved
pdf.multi_cell(w=50, txt=LOREM_IPSUM, fill=True)
```
![](diagonal_mirror.png)
Binary file added docs/diagonal_mirror.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/horizontal_mirror.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/vertical_mirror.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions fpdf/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,18 @@ class YPos(CoerciveEnum):
"bottom page margin (end of printable area)"


class Angle(CoerciveIntEnum):
"Direction values used for mirror transformations specifying the angle of mirror line"
NORTH = 90
EAST = 0
SOUTH = 270
WEST = 180
NORTHEAST = 45
SOUTHEAST = 315
SOUTHWEST = 225
NORTHWEST = 135


class PageLayout(CoerciveEnum):
"Specify the page layout shall be used when the document is opened"

Expand Down
41 changes: 39 additions & 2 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Image:
from .enums import (
AccessPermission,
Align,
Angle,
AnnotationFlag,
AnnotationName,
CharVPos,
Expand Down Expand Up @@ -2499,7 +2500,7 @@ def rotate(self, angle, x=None, y=None):
@contextmanager
def rotation(self, angle, x=None, y=None):
"""
This method allows to perform a rotation around a given center.
Method to perform a rotation around a given center.
It must be used as a context-manager using `with`:

with rotation(angle=90, x=x, y=y):
Expand Down Expand Up @@ -2540,7 +2541,7 @@ def rotation(self, angle, x=None, y=None):
@contextmanager
def skew(self, ax=0, ay=0, x=None, y=None):
"""
This method allows to perform a skew transformation originating from a given center.
Method to perform a skew transformation originating from a given center.
It must be used as a context-manager using `with`:

with skew(ax=15, ay=15, x=x, y=y):
Expand Down Expand Up @@ -2570,6 +2571,42 @@ def skew(self, ax=0, ay=0, x=None, y=None):
)
yield

@check_page
@contextmanager
def mirror(self, angle, origin):
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
"""
Method to perform a reflection transformation over a given mirror line.
It must be used as a context-manager using `with`:

with mirror(angle="SOUTH", origin=(15,15)):
pdf.something()

The mirror transformation affects all elements which are rendered inside the indented
context (with the exception of clickable areas).

Args:
angle: (fpdf.enums.Angle): the direction of the mirror line
origin (Sequence(float, float)): a point on the mirror line
"""
angle = Angle.coerce(angle)
x, y = origin

try:
theta = Angle.coerce(angle).value
except ValueError:
theta = angle

a = math.cos(math.radians(theta * 2))
b = math.sin(math.radians(theta * 2))
cx, cy = x * self.k, (self.h - y) * self.k

with self.local_context():
self._out(
sebastiantia marked this conversation as resolved.
Show resolved Hide resolved
f"{a:.5f} {b:.5f} {b:.5f} {a*-1:.5f} {cx:.2f} {cy:.2f} cm "
f"1 0 0 1 -{cx:.2f} -{cy:.2f} cm"
)
yield

@check_page
@contextmanager
def local_context(
Expand Down
Binary file added help.pdf
Binary file not shown.
Binary file added test/mirror.pdf
Binary file not shown.
Binary file added test/mirror_cell.pdf
Binary file not shown.
Binary file added test/mirror_multi_cell.pdf
Binary file not shown.
Binary file added test/mirror_text.pdf
Binary file not shown.
178 changes: 178 additions & 0 deletions test/test_mirror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import math
sebastiantia marked this conversation as resolved.
Show resolved Hide resolved
from pathlib import Path

from fpdf import FPDF
from fpdf.enums import Angle
from test.conftest import assert_pdf_equal, LOREM_IPSUM

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


def draw_mirror_line(pdf, angle, origin=(None, None)):
"""
A helper method which converts a given angle and origin to two co-ordinates to
then draw a line.
Used to help visualize & test mirror transformations.

Args:
pdf (fpdf.FPDF): pdf to modify
angle: (fpdf.enums.Angle): the direction of the mirror line
origin (Sequence[float, float]): a point on the mirror line
"""

angle = Angle.coerce(angle)

x, y = origin
if x is None:
x = pdf.x
if y is None:
y = pdf.y

theta = angle.value

cos_theta, sin_theta = (
math.cos(math.radians(theta)),
math.sin(math.radians(theta)) * -1,
)

x1 = x - (150 * cos_theta)
y1 = y - (150 * sin_theta)
x2 = x + (150 * cos_theta)
y2 = y + (150 * sin_theta)
pdf.line(x1=x1, y1=y1, x2=x2, y2=y2)


def test_mirror(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.set_line_width(0.5)
pdf.set_draw_color(r=255, g=128, b=0)

img_filepath = HERE / "image/png_images/66ac49ef3f48ac9482049e1ab57a53e9.png"

pdf.image(img_filepath, x=100, y=100)

pdf.image(img_filepath, x=100, y=100)
with pdf.mirror("WEST", (pdf.epw / 2, pdf.eph / 2.5)):
draw_mirror_line(pdf, "WEST", (pdf.epw / 2, pdf.eph / 2.5))
pdf.image(img_filepath, x=100, y=100)

with pdf.mirror("SOUTH", (pdf.epw / 2.5, pdf.eph / 2)):
pdf.set_draw_color(r=128, g=0, b=0)
draw_mirror_line(pdf, "SOUTH", (pdf.epw / 2.5, pdf.eph / 2))
pdf.image(img_filepath, x=100, y=100)

with pdf.mirror("SOUTHWEST", (pdf.epw / 1.5, pdf.eph / 1.5)):
pdf.set_draw_color(r=0, g=0, b=128)
draw_mirror_line(pdf, "SOUTHWEST", (pdf.epw / 1.5, pdf.eph / 1.5))
pdf.image(img_filepath, x=100, y=100)

with pdf.mirror("SOUTHEAST", (pdf.epw / 3, pdf.eph / 2.5)):
pdf.set_draw_color(r=0, g=128, b=0)
draw_mirror_line(pdf, "SOUTHEAST", (pdf.epw / 3, pdf.eph / 2.5))
pdf.image(img_filepath, x=100, y=100)

assert_pdf_equal(pdf, HERE / "mirror.pdf", tmp_path)
sebastiantia marked this conversation as resolved.
Show resolved Hide resolved


def test_mirror_text(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.set_font("helvetica", size=12)

pdf.text(pdf.epw / 2, pdf.epw / 2, txt="mirror this text")

with pdf.mirror("EAST", (pdf.epw / 2, pdf.eph / 2.5)):
pdf.text(pdf.epw / 2, pdf.eph / 2, txt="mirrored text E/W")

with pdf.mirror("NORTH", (pdf.epw / 2.5, pdf.eph / 2)):
pdf.text(pdf.epw / 2, pdf.eph / 2, txt="mirrored text N/S")

with pdf.mirror("NORTHWEST", (pdf.epw / 1.5, pdf.eph / 1.5)):
pdf.text(pdf.epw / 2, pdf.eph / 2, txt="mirrored text NW/SE")

with pdf.mirror("NORTHEAST", (pdf.epw / 2.5, pdf.eph / 2.5)):
pdf.text(pdf.epw / 2, pdf.eph / 2, txt="mirrored text NE/SW")

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


def test_mirror_cell(tmp_path):
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
pdf = FPDF()
pdf.add_page()
pdf.set_font("helvetica", size=12)
pdf.set_fill_color(255, 255, 0)

pdf.cell(txt="cell to be mirrored repeatedly")
pdf.x = pdf.l_margin
with pdf.mirror("NORTH", (pdf.epw / 2, 0)):
draw_mirror_line(pdf, "NORTH", (pdf.epw / 2, 0))
pdf.cell(txt="cell mirrored", fill=True)
pdf.cell(txt="cell mirrored", fill=True)
pdf.cell(txt="cell mirrored", fill=True)
pdf.ln(40)

pdf.cell(txt="cell text 1")
pdf.x = pdf.l_margin
with pdf.mirror("EAST", (pdf.epw / 2, pdf.eph / 4)):
draw_mirror_line(pdf, "EAST", (pdf.epw / 2, pdf.eph / 4))
pdf.cell(txt="cell text 1", fill=True)
pdf.ln(40)

pdf.cell(txt="cell text 2")
pdf.x = pdf.l_margin
with pdf.mirror("SOUTHWEST", (pdf.epw / 2, 0)):
draw_mirror_line(pdf, "SOUTHWEST", (pdf.epw / 2, 0))
pdf.cell(txt="cell text 2", fill=True)
pdf.ln(40)

pdf.cell(txt="cell text 3")
pdf.x = pdf.l_margin
with pdf.mirror("NORTHEAST", (pdf.epw / 2, pdf.eph / 4)):
draw_mirror_line(pdf, "NORTHEAST", (pdf.epw / 2, pdf.eph / 4))
pdf.cell(txt="cell text 3", fill=True)
pdf.ln(40)

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


def test_mirror_multi_cell(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.set_font("helvetica", size=12)
pdf.set_fill_color(255, 255, 0)

pdf.multi_cell(w=50, txt=LOREM_IPSUM[:200])

pdf.x = pdf.l_margin
pdf.y = pdf.t_margin
with pdf.mirror("NORTHEAST", (pdf.epw / 2, pdf.eph / 4)):
draw_mirror_line(pdf, "NORTHEAST", (pdf.epw / 2, pdf.eph / 4))
pdf.multi_cell(w=50, txt=LOREM_IPSUM[:200], fill=True)
pdf.ln(20)

prev_y = pdf.y
pdf.multi_cell(w=100, txt=LOREM_IPSUM[:200])
pdf.x = pdf.l_margin
pdf.y = prev_y

with pdf.mirror("EAST", (0, pdf.eph / 2)):
draw_mirror_line(pdf, "EAST", (0, pdf.eph / 2))
pdf.multi_cell(w=100, txt=LOREM_IPSUM[:200], fill=True)
pdf.ln(150)

prev_y = pdf.y
pdf.multi_cell(w=120, txt=LOREM_IPSUM[:120])
pdf.x = pdf.l_margin
pdf.y = prev_y

with pdf.mirror("SOUTH", (pdf.epw / 2, pdf.eph)):
draw_mirror_line(pdf, "SOUTH", (pdf.epw / 2, pdf.eph))
pdf.multi_cell(w=120, txt=LOREM_IPSUM[:120], fill=True)

pdf.output(HERE / "mirror_multi_cell.pdf")
assert_pdf_equal(
pdf,
HERE / "mirror_multi_cell.pdf",
tmp_path,
)