py-pdf / fpdf2

Simple PDF generation for Python
https://py-pdf.github.io/fpdf2/
GNU Lesser General Public License v3.0
1.13k stars 253 forks source link

Rotate text for table header #1140

Open Tomnl opened 8 months ago

Tomnl commented 8 months ago

Please explain your intent I would like to rotate text for a table header. In particular, have the heading text rotated so that it vertical in the table.

As far as I can tell this not currently possible.

Describe the solution you'd like When using the pdf.table functionality as described in the docs, it would be great if there was a way to rotate the text in the cells (particulary for the table header).

Wishful thinking... but perhaps defined something like this

with pdf.table() as table:
    headings = table.row()
    headings.cell("Long heading name 1", rotate=90)
    headings.cell("Long heading name 2", rotate=90)
    headings.cell("Age")
    headings.cell("City")

I have looked into using the pdf.rotation approach described here but it does not seem compatible with pdf.table

e.g. the following row.cell will not be changed by the pdf.rotation (and would be difficult to know the x and y coordinates to use anyway)

with pdf.table() as table:
    row = table.row()
    with pdf.rotation(angle=90, x=10, y=300):
        row.cell(text='Long heading name 1') 

Additional context See below for example format (not produced by fpdf2) to clarify the format I am referring to.

Untitled ref

Lucas-C commented 8 months ago

Hi @Tomnl

Thank you for reaching out and suggesting this feature.

I think it could be a handy addition. Would you like to work on adding this feature to fpdf2 yourself?

Regarding how this feature would operate, I'm not sure that pdf.rotation() would be the best choice. Maybe an optional rotate=<number> parameter passed to Row.cell() or Table.row() would be better.

In order to build the final table-related feature, a first "building-block" would be to be able to render some text block (with FPDF.write(), FPDF.text(), FPDF.cell() or FPDF.multi_cell()) at a given (X, Y) top-left corner position, with a given rotation applied to text. We will especially need that in FPDF.multi_cell(), so that when .multi_cell(..., output=MethodReturnValue.PAGE_BREAK | MethodReturnValue.HEIGHT) is called in Table._render_table_cell(), we can retrieve the cell height.

Also, we actually have too few unit tests regarding the rotation of text content, i.e. combining FPDF.rotation() with FPDF.write(), FPDF.text(), FPDF.cell() & FPDF.multi_cell(). Any addition of tests regarding those cases would be welcome!

Tomnl commented 8 months ago

Thanks for the quick reply @Lucas-C.

Maybe an optional rotate= parameter passed to Row.cell() or Table.row() would be better.

I agree that seems to be the logical place to add it

In order to build the final table-related feature, a first "building-block" would be to be able to render some text block (with FPDF.write(), FPDF.text(), FPDF.cell() or FPDF.multi_cell()) at a given (X, Y) top-left corner position, with a given rotation applied to text. We will especially need that in FPDF.multi_cell(), so that when .multi_cell(..., output=MethodReturnValue.PAGE_BREAK | MethodReturnValue.HEIGHT) is called in Table._render_table_cell(), we can retrieve the cell height.

So am I correct in thinking that within the .multi_cell() (and more generally FPDF.write(), FPDF.text(), FPDF.cell()) there should be an option to rotate the text and output the correct height? Would that still be using FPDF.rotation() or a new function?

Also, we actually have too few unit tests regarding the rotation of text content, i.e. combining FPDF.rotation() with FPDF.write(), FPDF.text(), FPDF.cell() & FPDF.multi_cell().

Not sure how much help I can bring. But maybe I can contribute a unit test first to help me understand the code base a bit better and then see what I could contribute!

Lucas-C commented 8 months ago

So am I correct in thinking that within the .multi_cell() (and more generally FPDF.write(), FPDF.text(), FPDF.cell()) there should be an option to rotate the text and output the correct height?

Yes, I think that could be handy. I'd be curious to know @gmischler opinion on that.

Would that still be using FPDF.rotation() or a new function?

Yes, that would be the simplest way to implement it, and I do not see any issue with using this approach.

Not sure how much help I can bring. But maybe I can contribute a unit test first to help me understand the code base a bit better and then see what I could contribute!

That would be very welcome! πŸ™‚ You can start by reading this page: https://py-pdf.github.io/fpdf2/Development.html

gmischler commented 7 months ago

There's quite a few challenges here.

First, there's the general question of supporting vertically arranged text, which may or may not get handled by the same code. There are at least two methods to do this. One group of scripts (eg. chinese and japanese) keep the orientation of the individual glyphs, and just stack them on top of each other. In those cases (not sure if there are exceptions) both writing directions are possible. grafik Another group (eg. Mongolian) are actually written horizontally, and the software then needs to rotate the result 90Β° clockwise. Displaying the text in horizontal orientation is typographically wrong, but common in mixed language text. grafik

The other general question is, if we want to support other rotation angles than 90 degrees. Spreadsheet software usually allows rotating the text in a cell within a wide gamut.

grafik LibreOffice supports the full 360Β°, but rotates wrapped text as a block, lifting some lines above the baseline.

grafik Excel can only support +/- 90Β° of rotation, but it staggers wrapped line of text to follow the baseline. (It also sometimes gets confused about which border lines to draw, but that's besides the point here.)

And the third general question: Where do we want to support the rotation of text. The original suggestion here was in table headers, which implies table cells in general. And since table cells are soon going to be reimplemented as text regions, it's probably a good idea to start there. And once we're looking at that, should whole regions support vertical and/or rotated text, or does it make more sense to add this functionality to individual paragraphs?

Besides the general questions, there's also quite a few details to consider.

line wrapping angled text

Width and height determination:

Now obvously, this isn't a catalog of what sould get implemented right now. I just tried to collect as many of the poential features we might want to consider. The goal is, if you implement something, try to do so in a way that doesn't stand in the way of other features later. I'm perfectly fine if you only want to consider horizontal and vertical text within orthogonal shapes for now. text regions will offer tools to fill text into other types of shape, which may lend a hand in allowing it to be at any arbitrary angle.

I'm sure there are other aspects that I can't think of right now...

Lucas-C commented 7 months ago

Now obvously, this isn't a catalog of what sould get implemented right now. I just tried to collect as many of the poential features we might want to consider. The goal is, if you implement something, try to do so in a way that doesn't stand in the way of other features later. I'm perfectly fine if you only want to consider horizontal and vertical text within orthogonal shapes for now. text regions will offer tools to fill text into other types of shape, which may lend a hand in allowing it to be at any arbitrary angle.

I fully agree: we can start with a basic (limited) implementation for now, but keep in mind how it could be generalized later on 😊

Lucas-C commented 6 months ago

Hi @Tomnl

We have given lengthy answers, I hope they did not discourage you to contribute a PR to fpdf2 πŸ˜…

Are you still willing to work on this feature? If not, or if you don't have time for this anymore, that's totally OK πŸ™‚

Lucas-C commented 5 months ago

Closing this issue for now. Please add a comment if you want this to be reopened πŸ™‚

NickFabry commented 4 months ago

Hi @Lucas-C and @gmischler ; I stumbled across this issue when I was trying to do exactly what @Tomnl was suggesting - rotating table headers vertically. I deal very often with data where the column/field names are quite long, but the data itself is quite short, sometimes only a single character (e.g. a boolean.) Being able to effectively narrow a column to the maximum data width rather than the field name would be very helpful in making tables that fit on a single page of width.

I would be very interested in this feature, and I saw your suggestions to @Tomnl about possibly how to implement it and submit a PR. I'd like to give it a go. However... I haven't contributed much to projects before, and I'm quite unfamiliar with this project in particular. I work a lot with pandas and sqlite and matplotlib - dealing with generating PDFs is new for me, so I might be a bit slow (well, very slow) and need a bit of handholding with how to submit a PR that's appropriate and useful.

I think it would be best to narrow the scope to start - maybe restrict the rotation angle to Β± 90ΒΊ, and rotate the entire block of text, not each individual character. But, for the future, let's take in two parameters: angle, rot_style. Angle is a float (default 0.0), rot_style a string. rot_style would (eventually) accept either 'all' (the default) or 'by_char' (as some Asian scripts are supposed to be rendered.) Probably these parameters would be passed as arguments to Row.cell() or Table.row()

Let me know if you would still be interested in adding this, if I was able to do some of the work.

Lucas-C commented 3 months ago

Sorry for the delay in answering you @NickFabry 😒 Comments on closed issues are very often missed... I'm reopening this issue now.

I would be very interested in this feature, and I saw your suggestions to @Tomnl about possibly how to implement it and submit a PR. I'd like to give it a go.

That's great! Thanks for offering your help!

However... I haven't contributed much to projects before, and I'm quite unfamiliar with this project in particular. I work a lot with pandas and sqlite and matplotlib - dealing with generating PDFs is new for me, so I might be a bit slow (well, very slow) and need a bit of handholding with how to submit a PR that's appropriate and useful.

That's perfectly fine for us in the maintainers team, as long as you are not in a urge to get feedbacks / you PR merged, we are very happy to welcome casual FLOSS contributors, and to help them as much as we can!

I think it would be best to narrow the scope to start - maybe restrict the rotation angle to Β± 90ΒΊ, and rotate the entire block of text, not each individual character. But, for the future, let's take in two parameters: angle, rot_style. Angle is a float (default 0.0), rot_style a string. rot_style would (eventually) accept either 'all' (the default) or 'by_char' (as some Asian scripts are supposed to be rendered.) Probably these parameters would be passed as arguments to Row.cell() or Table.row()

It's good to discuss the API / interface beforehand. I'm not really sure what would be the best there... Given that @gmischler already provided a lentghy answer, it may be interesting to get some insights from him 😊 However I agree that we could (and probably should) aim to implement this feature progressively.

Let me know if you would still be interested in adding this, if I was able to do some of the work.

Yes! πŸ‘

NickFabry commented 2 months ago

Hi @Lucas-C - sorry for the long delay getting back. This is a 'keep alive' reply - I'm hoping I can make a go of this in mid-October; I have some known time free then. In the meanwhile - thanks for reopening this idea!

sanketjainindian commented 1 month ago

I would like to contribute to this specific issue and project as well, kindly let me know if i could be any help

Lucas-C commented 1 month ago

Hi @NickFabry

It's mid-october, so I was wondering if you were still planning to tackle this issue? πŸ™‚

NickFabry commented 1 month ago

...yes. But it won't be mid-October! I got a ton of work this week. So... another keep-alive post. I'll need a couple of free days to sit down, think about the API seriously and learn where and how to poke things. If it becomes completely infeasible for me to do, I'll reach out explicitly and let you know so you're not waiting forever. Thanks for your patience!