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

New feature: FPDF.mirror() #536

Closed
Lucas-C opened this issue Sep 13, 2022 · 15 comments
Closed

New feature: FPDF.mirror() #536

Lucas-C opened this issue Sep 13, 2022 · 15 comments

Comments

@Lucas-C
Copy link
Member

Lucas-C commented Sep 13, 2022

Original issue title: New feature: text transformations - skew, diagonal text, mirror

The idea comes from one of @digidigital recipes: https://github.com/digidigital/Extensions-and-Scripts-for-pyFPDF-fpdf2/tree/main/skew_shear_rotate_text#skew-shear-and-rotate-text (cf. #274)

This extension allows to print rotated and sheared (i.e. distorted like in italic) text.

In terms of design / API, a potential user-interface could be to give access to the underlying Tm PDF operator through text_skew & text_shear parameters that could be passed to local_context(), so that this feature could affect calls to .write(), .cell() or .text():

from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.set_font('helvetica', size=12)
with pdf.skew(angle=-45):
    with pdf.text_axis(angle=45):
       pdf.cell(txt="hello world")
pdf.output("skew_shear.pdf")

Of course, new unit tests should be added, as well as a bit of user documentation
You are also free to suggest another API/implementation/user-interface


By implementing this feature you, as a benevolent FLOSS developper, will provide access to the large community of fpdf2 users to a useful functionality.
As a contributor you will get review feedbacks from the maintainer & other contributors, and learn about the lifecycle & structure of a Python library on the way.
You will also be added to the contributors list & map.

This issue can count as part of hacktoberfest

@gmischler
Copy link
Collaborator

gmischler commented Sep 14, 2022

I'm not sure if we should overload local_context() with this, the result might not turn out to be particularly readable. And if a user wants to apply several transformations, their sequence would be ambiguous.

We already have .rotation() as its own context manager:

with pdf.rotation(angle=90, x=x, y=y):
    pdf.something()

A separate .shearing() could work quite similarly:

with pdf.shearing(angle=30):
    pdf.something()

Is the difference betwen shearing and skewing just the direction? Maybe calling them something like shearing_x() and shearing_y() might be less ambiguous then?

Oh, and once we're at it, how about mirror_x() and mirror_y()? They may need an x or y value respectively to define the mirror axis.

All of those are of course able to be nested, and their application is not limited to text at all.

@Lucas-C
Copy link
Member Author

Lucas-C commented Sep 14, 2022

And if a user wants to apply several transformations, their sequence would be ambiguous.

Oh you mean that in this case:

with pdf.local_context(text_shear=45, text_skew=-45):
    pdf.cell(txt="hello world")

...the order of the "skew" & "shear" operations is ambiguous?
Well spotted, I hadn't thought of that issue.

I agree with the idea of separate context-managers then.

Is the difference betwen shearing and skewing just the direction?

No, I made a distinction between them in my mind, but now I realize the terms were wrong...
We should use clearer, more explicit terms instead. I'm going to edit the issue title & description.

This feature covers two kind of text transformations:

  • write text along a diagonal axis (instead of horizontally)
  • skew/shear = the text font is bent, like in italics.
    Other effects could also be performed: Bend and Skew Text Examples

Oh, and once we're at it, how about mirror_x() and mirror_y()? They may need an x or y value respectively to define the mirror axis.

Good idea!
Maybe a single context-manager? with pdf.mirror(x=..., y=...): ...

@Lucas-C Lucas-C changed the title New feature: skew & shear text New feature: text transformations - skew, diagonal text, mirror Sep 14, 2022
@gmischler
Copy link
Collaborator

  • write text along a diagonal axis (instead of horizontally)

That sounds like it should already be covered by rotation... But you may well mean what I'd call vertical skew.

skew/shear = the text font is bent, like in italics

To me, like apparently to you, "skew" and "shear" are near synonyms.
I'm not sure if "bent" is the right word here though, despite the example using it. To me bending is something that happens around a curve.
Any native speakers around? 😉

If we start with an orthogonal font: image

According to Gimp, this is "shear x": image

and this is "shear y": image

What that example link calls "bent" rather seems to be a perspective distortion, which can also be seen as a non-uniform shear/skew.
Gimp does that with the "unified transform tool": image
Doing it as a custom method would require at least a central axis and some "skew by length" factor. Not sure what the most convenient API for it would be though.

Maybe a single context-manager? with pdf.mirror(x=..., y=...): ...

Umm... Were those operations commutative again? (I think so, in which case it would probably work)

Anyway, whoever wants to implement any of it has a choice of both which methods they want to start with and what to call them.

@erap129
Copy link

erap129 commented Dec 7, 2022

Hi, I'm new to fpdf and to the inner workings of the PDF format in general, but I dove a bit into this particular use case and it seems interesting, I am willing to take this. Just a question regarding a possible implementation - From what I understand a Tm operator needs to be inside the text block (inside the BT...ET block) am I right on this? Anyway, if that's the case, then I think what needs to be done is to add new values to the fpdf object, such as self.skew or something. Assuming it's a boolean value (it's not, but let's pretend it is) it will look like this:

with self.local_context():
            self.skew = True
            yield

inside of a skew function that is decorated with @contextmanager and @check_page just like rotate.
Then, when adding using cell, text or write we will need to explicitly check if skew was set and inject the appropriate Tm into the text block. Is this a good approach?
Thanks! Greate package, looking forward to contributing here.
Elad

P.S - I work full-time so I'm not sure everything will be immediate, but I'll do my best!

@Lucas-C
Copy link
Member Author

Lucas-C commented Dec 8, 2022

Hi, I'm new to fpdf and to the inner workings of the PDF format in general, but I dove a bit into this particular use case and it seems interesting, I am willing to take this.

Great, thank you for your interest @erap129! 😊

From what I understand a Tm operator needs to be inside the text block (inside the BT...ET block) am I right on this?

Yes

I think what needs to be done is to add new values to the fpdf object, such as self.skew or something

OK. This property would then be taken in consideration by which methods? All text-rendering ones?

I think an approach that should work and may the be best in term of code consistency would be to use PaintedPath.transform. A PaintedPath is created when calling FPDF.new_path(): https://github.com/PyFPDF/fpdf2/blob/2.6.0/fpdf/fpdf.py#L1078

P.S - I work full-time so I'm not sure everything will be immediate, but I'll do my best!

No worry, contribution are always best-effort, volunteer work: you do as much as you want/can, no pressure 😉

@Lucas-C
Copy link
Member Author

Lucas-C commented Dec 8, 2022

I think an approach that should work and may the be best in term of code consistency would be to use PaintedPath.transform. A PaintedPath is created when calling FPDF.new_path(): https://github.com/PyFPDF/fpdf2/blob/2.6.0/fpdf/fpdf.py#L1078

On second thought, this is silly.
We want to skew text, not paths/shapes 🤦‍♂️

The approach you suggest is fine with me @erap129.
You will have to add a if self.skew: ... in FPDF.text() & FPDF._render_styled_text_line().
Maybe you have other suggestions @gmischler?

@erap129
Copy link

erap129 commented Dec 10, 2022

OK after digging in a bit I found that the Tm operator defines an affine transformation, which allows for shear-x and shear-y (quite easily) but not for this:
image.
(https://www.graphicsmill.com/docs/gm/affine-and-projective-transformations.htm#:~:text=A%20projective%20transformation%20shows%20how,both%20these%20classes%20of%20transformations.)
Any ideas on how to pull off the latter?

@Lucas-C
Copy link
Member Author

Lucas-C commented Dec 10, 2022

@erap129: based on the page you linked, this effect () should be doable with an affine tranformation:

@gmischler
Copy link
Collaborator

gmischler commented Dec 12, 2022

@erap129: based on the page you linked, this effect () should be doable with an affine tranformation:

The text distinguishes between affine and projective transformations, both of which are linear transformations. The PDF specs use neither of those terms though, but only talk about matrices and transformations without any special qualification. This seems to imply the more general linear transformation type, which would allow for perspective distortion. I guess it's a matter of experimentation about what exactly the various readers can handle.

Apart from that, I'd be more interested in how this functionality can be made accessible to users without a higher math education. The most flexible way would be to let the caller specify the full matrix, but that is also the least convenient approach for manipulating text (more suitable for transforming graphics in general).
For text, we probably want an API that guides the users in an easy to understand way to acheive specific effects, with methods/parameters that are limited to one type of effect each. Another consideration would be whether we want to distort (parts of) a single line, or multi-line blocks of text. Can the latter be handled by Tm as well, or would we need to use the global cm operator for that?

Note that I'm not saying that you need to implement every possible variation now. In fact, I'd recommend to just start eg. with the text skew operations only. But whatever you start with should be part of a larger concept encompassing the rest. Maybe we should check out other software packages and see what approaches they have taken to present similar functionality.

@gmischler
Copy link
Collaborator

Anyway, if that's the case, then I think what needs to be done is to add new values to the fpdf object, such as self.skew or something. Assuming it's a boolean value

You may have found that already, but in general any such new parameters should be implemented in graphics_state.GraphicsStateMixin(), and stored in its state stack. (Many of the related API methods are currently in FPDF(), but may eventually migrate to the mixin as well). From the state stack they are automatically copied into each text fragment, and _render_styled_text_line() can then use the information in the fragment to apply the necessary PDF operators (eventually the fragments may get smarter and handle part of that themselves).

In light of my above reply though, this approach has a disadvantage in this special case, in that it works on a per-fragment basis. With HTML/Markdown styling or through write(), a fragment can be anything from a line, to a single word, down to a single character. So if we want to apply transformations to more than just a fragment at a time, we probably need to store a reference base point as well. This may be less of a problem with (line based) horizontal skew, but vertical skew and perspective distortion should use a common pivot point or weird things will happen.

inside of a skew function that is decorated with @contextmanager and @check_page just like rotate. Then, when adding using cell, text or write we will need to explicitly check if skew was set and inject the appropriate Tm into the text block. Is this a good approach?

I'm not sure we need a seperate context manager for that. It should probably be enough to use local_context(). But then, I may be overlooking some special use case...

rotate() needs its own context because it operates on arbitrary elements with an arbitrary pivot point. With text transformations, I think we should stay within the constraints of the line based positioning (at least for now).

@erap129 erap129 mentioned this issue Dec 14, 2022
5 tasks
@erap129 erap129 mentioned this issue Dec 31, 2022
5 tasks
@Lucas-C
Copy link
Member Author

Lucas-C commented Jan 2, 2023

@erap129 is working on a very promising implemention for .skew() in #656

I think a .mirror(x=..., y=...) context-manager could be another useful method to add to fpdf2
Another PR implementing it would be welcome!

@Lucas-C Lucas-C changed the title New feature: text transformations - skew, diagonal text, mirror New feature: FPDF.mirror() Jan 14, 2023
@Lucas-C Lucas-C removed the font label Jan 14, 2023
@sebastiantia
Copy link

sebastiantia commented May 5, 2023

@erap129 is working on a very promising implemention for .skew() in #656

I think a .mirror(x=..., y=...) context-manager could be another useful method to add to fpdf2 Another PR implementing it would be welcome!

I'm interested in taking upon the mirror implementation :) @Lucas-C with point reflection, would users have to specify the type of reflection e.g. vertically/horizontally/digaonally? Or would an input argument representing a mirror line be more appropriate? If so, I think two points would be the most flexible approach of representing a line.

@gmischler
Copy link
Collaborator

would users have to specify the type of reflection e.g. vertically/horizontally/digaonally? Or would an input argument representing a mirror line be more appropriate?

An interesting and important question!

A mirror line can be specified in two ways:

  • a point and an angle (with special cases horizontal/vertical)
  • two points

While the two are mathematically equivalent, I suspect that most people will first think of an angle when asked to specify the direction of a line. Only in an interactive application does it end up easier to pick two points than to specify an angle, but that is not really our use case.
It seems that in the PDF spec, positive angles are typically specified in counter clockwise direction starting with positive X as zero. An alternative approach would be to go with an anology to a compass, with zero at the top. This metaphor also allows to specify certain angles as "N", "W", "NW", etc. as an Angle enum (or maybe Direction?).

You could also offer both options next to each other:

with pdf.mirroring(
    origin: Sequence[float, float],
    angle: Union[Angle, float] = None,
    pt2 : Sequence[float, float] = None
    ):
        ....

where exactly one of angle and pt2 must be not None.

@Lucas-C
Copy link
Member Author

Lucas-C commented May 5, 2023

I'm interested in taking upon the mirror implementation :)

Great! That's very good news @sebastiantia 😊 Go for it!

While the two are mathematically equivalent, I suspect that most people will first think of an angle when asked to specify the direction of a line.

I fully agree.
I think we could start by implementing the origin + angle arguments.

A basic usage example could then be:

with pdf.mirroring(origin=(pdf.epw/2, 0), angle="SOUTH"):
    pdf.circle(pdf.epw/4, pdf.epw/2, 50)

@Lucas-C Lucas-C mentioned this issue May 8, 2023
5 tasks
@Lucas-C
Copy link
Member Author

Lucas-C commented May 8, 2023

The mirror function has been implemented by @sebastiantia in PR #783

Closing this issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants