QuestPDF / QuestPDF

QuestPDF is a modern open-source .NET library for PDF document generation. Offering comprehensive layout engine powered by concise and discoverable C# Fluent API. Easily generate PDF reports, invoices, exports, etc.
https://www.questpdf.com
Other
11.97k stars 631 forks source link

Paragraph Styles #513

Open flew2bits opened 1 year ago

flew2bits commented 1 year ago

I want to start this off by saying I'm really enjoying using this library, and since I discovered it late last week, I've shared it with a few of my colleagues and already deployed an application using it.

The problem that I'm running into has to do with the layout of paragraphs.

The library currently supports a text style ambient context. Text styles consist of font family, weight, width, italics/oblique/etc., size, leading, tracking, kerning, character scaling (width and height), baseline shift, slant, and things like all-caps, small-caps, superscript, subscript, underline, and strikethrough. This ambient context is transported through all of the containers and can be updated at any point in the hierarchy.

What's missing is a paragraph style ambient context. Paragraph styles include text alignment (left, right, centered, justified with last line left, justified with last line centered, justified with with last line right, and justify all lines), left indent, right indent, first line left indent, last line right indent (I'm not sure how this would be used), spacing before, spacing after, spacing between, drop caps, and so on.

I would envision an ambient paragraph style working much the same as the ambient text style. For example:

page.DefaultParagraphStyle(style => 
    style
        .AlignJustifiedWithLastLeft()
        .Spacing(.25f, Units.inch)
        .Indent(0.5f, Units.Inch)
        .IndentFirstLine(-0.5f, Units.Inch);

This would apply justified text with a .5" indent for all line except the first, and .25" spacing between paragraphs.

By default, a carriage return in any text span would start a new paragraph block, using the settings in the current paragraph style context. Within a TextDescriptor or TextSpanDescriptor overriding the ambient paragraph context would force a new paragraph, effectively forcing a carriage return before and after the text span or block.

Some of these things are possible within the existing framework, but you have to fight to get some of them to work well. For example, if I want to have multiple paragraphs of each of left, centered, and right aligned text within a block all with paragraph spacing, I have to create a column with the chosen spacing, and then either put all of the text I want per block in a TextSpan and align them separately or use a TextDescriptor to align multiple paragraphs, again remembering to set ParagraphSpacing (which of course creates a column with the given spacing).

A full paragraph can be indented (positive or negative) by adding a left or right padding to the container. Similarly, before and after spacing can be handled with vertical padding. However, there is no way to indent just the first line of a paragraph. Nor is there a way to implement drop caps. (These last two things might be possible with a DynamicComponent -- I'd have to investigate that further.)

Another feature that could possibly be implemented on top of the paragraph context would be tab stops. In issue #512, I used a dynamic component to implement a string of periods between left and right aligned text on a line. A paragraph style could define these tab stops for a block of text and the layout engine could use "\t" to determine alignment, position, and leader characters between.

flew2bits commented 1 year ago

I forgot to say, I've started looking at the code, and while I don't fully understand how everything with the layout engine works, I feel that could probably start implementing some of this on my own within a few days of study. I've forked the project and plan to start working on them.

I know that there's work ongoing to add justified text, but I think this would prove to be a better overall solution.

flew2bits commented 1 year ago

I just remembered one other thing that paragraph styles can address, and that is widow/orphan control.

girlpunk commented 1 year ago

What you're suggesting has some interesting connotations, particularly if QuestPDF were to (optionally) decouple text layout from text content (or even further if desired). For example, a layout could be defined with three columns with an image at the bottom of one, and a single text/paragraph object then rendered into all three columns, flowing into the next as needed.

flew2bits commented 1 year ago

This was actually the next step in what I was planning on looking into, specifically template pages such as a three-column layout and text "flows". I've put some thought into how it would work, but it's tricky since some of the layout part of it can possibly necessitate human intervention. However, the concept of flows already kind of exists in the library since a "Page" can actually be split across multiple physical pages in the document. A page would instead populate a flow on a template.

flew2bits commented 1 year ago

I've got a start on this. It's ugly, but it currently works. I think it could benefit from some reworking of how text is laid out, but I'd have to research more.

I've discovered what the difficulty is with justified text. The PDF standard supports word spacing through the use of the Tw directive in a text block. However, SkiaSharp (and skia) have no support for word spacing. So the only way to get justified text is to manually position each word on a line. It shouldn't be too difficult since it appears it's already positioning each character independently as it is, so it should just be a matter of calculating the "width" of a space per line of text: Sw = (Lw - sum(Ww)) / (Wc - 1) where Sw is space width, Lw is line width, Ww is word width, and Wc is word count.

I know that Marcin was working on the justified text, but I may go ahead and see if I can implement it.

Here's my current branch with the updates I've made so far: https://github.com/flew2bits/QuestPDF/tree/paragraph-styles

LeaFrock commented 5 months ago

Any progress? Now I don't know how to achieve first line indentation of paragraph.

flew2bits commented 5 months ago

Unfortunately not at this time. I've been too busy with work to do much on this for some time now.

MarcinZiabek commented 5 months ago

Paragraph first line indentation is available from the 2024.3.X release. I am currently writing documentation for this feature.

LeaFrock commented 5 months ago

Paragraph first line indentation is available from the 2024.3.X release. I am currently writing documentation for this feature.

I've found the related PR, but the feature has not been released?

MarcinZiabek commented 5 months ago

Yes, it has already been merged and deployed. Please find documentation here: https://www.questpdf.com/api-reference/text.html#first-line-indentation

luis-fss commented 5 months ago

Hi @MarcinZiabek, I hope you are well. The code below does not compile. I even tried to decompile QuesPDF to see if I can find the "ParagraphFirstLineIndentation" method, but it seems that the dll in the nuget package is not updated with the repository. Could you check, please?

.Text(Placeholders.Paragraphs())
.ParagraphFirstLineIndentation(20);
MarcinZiabek commented 5 months ago

@luis-fss Indeed! I apologize for any confusion 😢 This feature is intended to be included in the next major release. As the stability and quality of the 2024.3.X release are my top priorities, this feature has been postponed slightly. It has been in the code for so long that I mistakenly thought it was already available.

I would like to publish (hopefully) the last 2024.3.X quality release, and then focus on the next major release and publish it very soon.

luis-fss commented 5 months ago

@MarcinZiabek No problem my friend, your work here is already extraordinary and I can only thank you. This feature is very interesting, but not essential, especially since I already managed to implement a work around. Thanks again and have a nice Sunday.