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

Text justification #188

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Conversation

bennetbo
Copy link
Contributor

This PR implements text justification.
Should close #159 #25.

Showcase of the result rendered with the added test JustifiedText:

Justify
.

@MarcinZiabek
Copy link
Member

I was thinking about similar approach. However, there is one significant problem. Rendering multiple text spans in a single line. In such case, we need to proportionally distribute available space across all spans available in the line. Therefore, the justification flag does not belong to the TextStyle class, rather to the Text element. It is just another use case along with aligning to the left/center/right.

.Text(text => 
{
    text.Justify();

    // content
    text.Span("This is example text. ").Bold();
    text.Span(" Different text in the same line.").FontColor(Colors.Red.Medium);
    // content
});

@bennetbo
Copy link
Contributor Author

However, there is one significant problem. Rendering multiple text spans in a single line

Yes, you are 100% correct with the approach i implemented, its only possible to render a single chunk of text with one format (not mixed font weights/ bold etc.).
Your approach is the better one, however im not sure how to implement this. I will spend some time investigating and see if i can manage to implement this correctly.

@bennetbo
Copy link
Contributor Author

I gave it another try and actually got it to work.
Here is the result:
image

Code:

.Text(text => 
{    
    text.AlignJustify();
    
    // content
    text.Span("This is example text. ").Bold();
    text.Span(Placeholders.LoremIpsum());
    text.Span(" Different text in the same line.").FontColor(Colors.Red.Medium);
    text.Line("Last line should not be justified.");
});

However i did not manage to implement this without significant changes in the TextSpanDescriptor code.
My idea was to move the calculation of the justification to TextBlock and handle everything there. However while debugging i realized that there is more then one Textblock.
So i basically removed the IList<TextBlock> and appended all runs into a single TextBlock. Im afraid that without this change i cannot get this to work.

@MarcinZiabek
Can you please explain why there is a list of TextBlocks where each textblock only contains a single ITextBlockItem, rather then a single TextBlock with multiple ITextBlockItems ? Im afraid i cant wrap my head around it 😅

@MarcinZiabek
Copy link
Member

Can you please explain why there is a list of TextBlocks where each textblock only contains a single ITextBlockItem, rather then a single TextBlock with multiple ITextBlockItems ? Im afraid i cant wrap my head around it 😅

As far as I remember: I am trying to reuse existing components as much as I can. Back then, I decided that the TextBlock element will be responsible of rendering only one paragraph. This way, by using the Column element I can introduce proper paragraph spacing.

  • If you use only one text style, you get only one ITextBlockItem inside a TextBlock.
  • However, if you use many different font styles, or other text-related content such us hyperlinks, page numbers or injected elements, a TextBlock contains multiple items.

I am not sure if this is the easiest approach. It was just my idea back then that has worked 😁

@bennetbo
Copy link
Contributor Author

Thanks for the explanation 👍 After youre explanation i realized that i was trying to split the text at the wrong time.
I was able to remove my workaround with a single textblock.
My last commit tries to add support for proper justification. However im afraid there are still so many corner cases and its more of a showcase/concept rather then an actual implementation we can use in the library.

Here is the result now (Corner cases are marked with red squares):
image

Please tell me what you think of this approach. You mentioned you already thought about how to implement this, do you have any other ideas? Maybe im following the wrong concept.
I certainly thought implementing this would be a lot easier, but i got a little ahead of myself, sorry.

@MarcinZiabek
Copy link
Member

MarcinZiabek commented Apr 24, 2022

Here is the result now (Corner cases are marked with red squares):

I think that mentioned corner cases are already handled in the existing implementation:

  • when the span is the first one within the line, and the line is not the start of the text, ignore leading spaces,
  • when the span is the last one within the line, and the line is not the end of the text, ignore spaces at the end,
  • when the span is empty, don't draw anything.

This should be applied in both the measuring and drawing process. The concept is simple but introduces even more corner cases, e.g. with float rounding when measuring text.

Please tell me what you think of this approach. You mentioned you already thought about how to implement this, do you have any other ideas? Maybe im following the wrong concept.

In my opinion, the current text implementation is quite difficult to introduce the concept of text justification, especially in a performant way.

In the nearest future, I would like to introduce the text shaping capability. It changes text into a set of font glyphs (e.g. in the Arabic language, multiple characters can become one glyph). As a result of this operation, each glyph gets its position and size. This could be a great opportunity to divide glyphs by whitespaces and just add proper spaces wherever needed.

I certainly thought implementing this would be a lot easier, but i got a little ahead of myself, sorry.

I am very impressed that you have been able to go so far! Something to be proud of!

To be honest, text-rendering capabilities are something that still makes me question the usefulness of QuestPDF. Text justification, font fallback, and text shaping are only the beginning. We also have:

  • custom line breaking rules (e.g. Chinese does not use whitespaces, rather a special set of rules for characters),
  • text jufitication for non-whitespace breaking languagues, e.g. Chinese,
  • widows and orphans: if we want to avoid situations when broken text takes only one or two lines on the next page. In such a case, we want to break text prematurely, so to have at least 3 lines of text on the next page. This makes the text look nicer.

Each of the mentioned features is manageable. Combining them all makes the domain difficult 😁

@bennetbo
Copy link
Contributor Author

bennetbo commented Apr 26, 2022

when the span is the first one within the line, and the line is not the start of the text, ignore leading spaces,

That is correct, however:

when the span is the last one within the line, and the line is not the end of the text, ignore spaces at the end,

Sorry if i am missing something, but i didnt see any handling of this in the current code.
In fact i believe this would be quite hard.
At the time you measure the span you dont know if this will be the last one in the line.
So you would need to measure the span with whitespace the first time, the second time trim the whitespace and measure again.

As a result of this operation, each glyph gets its position and size. This could be a great opportunity to divide glyphs by whitespaces and just add proper spaces wherever needed.

That sounds great, justificiation should be a lot easier to implement then 👍.

custom line breaking rules (e.g. Chinese does not use whitespaces, rather a special set of rules for characters),

That sounds complex 😅 I looked into a document of how text justification is handled in asian languages and must say it was quite overwhelming.

@MarcinZiabek
Copy link
Member

I would like to once again thank you for your commitment and help! I will do my best to analyse your changes and contact whenever something is not clear. I am afraid however, that this needs to wait a while. A lot is going on in my personal life, very positive yet time-consuming events. I hope that you understand 😁

@bennetbo
Copy link
Contributor Author

bennetbo commented May 8, 2022

No worries, personal life should always be the priority 😊.
If the text rendering mechanism is reworked in the future, like you mentioned earilier, I dont think this feature is worth merging anyway (but hopefully useful as an inspiration for the new implementation).

@MarcinZiabek
Copy link
Member

Depending on priorities, I will either rework the text rendering algorithm first or investigate your implementation and include it in one of the future releases 😁 Text rendering is crucial for library stability and therefore I tend to be extra careful.

@alhancock
Copy link

I'd welcome an update on this. My requirements really need text justification. Thanks.

@brunotourinho
Copy link

Looking forward to this update! Awesome job guys!

@brunotourinho
Copy link

Hey guys, any updates on this?

@girlpunk
Copy link

in #405, Marcin mentioned they are hoping to include this in the January update

@blogcraft
Copy link

Is there any way of justify text?

@xiaoguaishoubaobao
Copy link

I also need to address the full-Justify Text.
This full-Justify Text in the PDF paper is almost the default alignment.
Is there a good way to solve this problem now?

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

Successfully merging this pull request may close these issues.

Justify Text
7 participants