Skip to content

Add AutoFontSize feature to TextBlock for automatic font size adjustment - #997

Open
thijmenjk wants to merge 2 commits into
QuestPDF:mainfrom
thijmenjk:auto-font-size
Open

thijmenjk wants to merge 2 commits into
QuestPDF:mainfrom
thijmenjk:auto-font-size

Conversation

@thijmenjk

Copy link
Copy Markdown

Hello QuestPDF team,

First of all, thank you for developing such an amazing library! We've been using QuestPDF in our projects, and it has been great for our document generation needs.

We have a use case where we need text to dynamically adjust its font size to fit within a given container. Additionally, we require control over the maximum number of lines the text can span and the exact font sizes that can be used. Because of this we implemented some changes that we would like to share with this PR.

Why Not ScaleToFit?

Initially, we tried using the ScaleToFit feature, but we encountered some limitations:

  • No Control Over Maximum Lines: ScaleToFit doesn't allow us to specify the maximum number of lines the text can occupy, which is required for our use case.
  • Issues with Smaller Containers: In smaller containers, ScaleToFit sometimes doesn't work as expected, leading to text overflow or inadequate scaling (see example below).

To overcome these challenges, we've implemented an AutoFontSize feature for the TextBlock element.

Feature Highlights:

  • Automatic Font Size Adjustment: The text block automatically selects the largest font size from a provided range that allows the text to fit within the available space.
  • Maximum Line Control: You can specify the maximum number of lines the text is allowed to span, ensuring consistent layout.

Usage Examples:

We've added two extension methods to the TextBlockDescriptor and ITextDescriptor interfaces:

  1. Using a Font Size Range:

    textBlock.AutoFontSize(minSize: 8, maxSize: 20, stepSize: 0.5f, maxLines: 2);
  2. Using Specific Font Sizes:

    textBlock.AutoFontSize([8f, 10f, 12f, 14f], maxLines: 1);

Examples:

In the attached example, we compare the ScaleToFit and AutoFontSize features under various container sizes and font families. You'll notice that AutoFontSize provides better control over the text layout, especially in small containers and when limiting the number of lines.

example.pdf

Code Details

using CheckNet.Labelizer.Engine.Extensions;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;

List<(float, float)> sizes = [
    (50, 40),
    (40, 50),
    (100, 20),
    (20, 100),
    (60, 60)
];
List<string> fonts = [
    "Arial", // Sans-Serif
    "Times New Roman", // Serif
    "Consolas", // Monospace
    "Impact" // Fancy
];

void ComposeScaleToFitExample(IContainer container, string text, string fontName, float width, float height)
{
    container.Column(col =>
    {
        col.Item()
            .Width(width)
            .Height(height)
            .DebugArea()
            .VerticalAlign(VerticalAlignment.Bottom)
            .Row(row =>
                row.AutoItem()
                    .Width(width)
                    .ScaleToFit()
                    .DebugArea(color: Colors.Blue.Lighten1)
                    .Text(text)
                    .FontFamily(fontName)
                    .LineHeight(.8f)
                    .FontSize(Math.Max(width, height))
                    .ExtraBold()
            );
    });
}

void ComposeAutoFontSizeExample(IContainer container, string text, string fontName, float width, float height, int? maxLines = 1)
{
    container.Column(col =>
    {
        col.Item()
            .Width(width)
            .Height(height)
            .DebugArea()
            .VerticalAlign(VerticalAlignment.Bottom)
            .Row(row =>
                row.AutoItem()
                    .MaxWidth(width)
                    .MaxHeight(height)
                    .DebugArea(color: Colors.Blue.Lighten1)
                    .Text(text)
                    .FontFamily(fontName)
                    .LineHeight(.8f)
                    .AutoFontSize(.5f, Math.Max(width, height), .5f, maxLines)
                    .ExtraBold()
            );
    });
}

var doc = Document.Create(doc =>
{
    doc.Page(page =>
    {
        page.Margin(20);
        page.MinSize(new PageSize(1, 1));
        page.MaxSize(new PageSize(Size.Max.Width, Size.Max.Height));
        page.Content()
            .Column(col =>
            {
                col.Item().Row(row =>
                {
                    row.Spacing(5);
                    foreach (var font in fonts)
                    {
                        row.AutoItem().Column(col =>
                        {
                            col.Spacing(5);
                            col.Item().Text($"Font: {font}").FontSize(16).FontColor(Colors.Black);
                            foreach (var (width, height) in sizes)
                            {
                                col.Item().Text($"Red rectangle: width={width}, height={height}").FontSize(12).FontColor(Colors.Grey.Darken1);
                                col.Item()
                                    .BorderBottom(.5f)
                                    .Row(row =>
                                    {
                                        row.Spacing(5);

                                        row.ConstantItem(100)
                                            .Text("ScaleToFit")
                                            .FontColor(Colors.Grey.Lighten1)
                                            .FontSize(10);
                                        ComposeScaleToFitExample(row.AutoItem(), "One Size", font, width, height);
                                        ComposeScaleToFitExample(row.AutoItem(), "XXXL", font, width, height);
                                        ComposeScaleToFitExample(row.AutoItem(), "XL", font, width, height);
                                        ComposeScaleToFitExample(row.AutoItem(), "S", font, width, height);
                                    });

                                col.Item()
                                    .BorderBottom(.5f)
                                    .Row(row =>
                                    {
                                        row.Spacing(5);

                                        row.ConstantItem(100)
                                            .Text("AutoFontSize")
                                            .FontColor(Colors.Grey.Lighten1)
                                            .FontSize(10);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "One Size", font, width, height, maxLines: null);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "XXXL", font, width, height, maxLines: null);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "XL", font, width, height, maxLines: null);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "S", font, width, height, maxLines: null);
                                    });

                                col.Item()
                                    .BorderBottom(.5f)
                                    .Row(row =>
                                    {
                                        row.Spacing(5);

                                        row.ConstantItem(100)
                                            .Text("AutoFontSize (ML: 1)")
                                            .FontColor(Colors.Grey.Lighten1)
                                            .FontSize(10);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "One Size", font, width, height, maxLines: 1);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "XXXL", font, width, height, maxLines: 1);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "XL", font, width, height, maxLines: 1);
                                        ComposeAutoFontSizeExample(row.AutoItem(), "S", font, width, height, maxLines: 1);
                                    });

                                col.Item().Height(20);
                            }
                        });
                    }
                });

            });
    });
});


doc.ShowInPreviewer();

Challenges

For our use case, baseline alignment is important. Especially when varying fonts, sometimes the baseline becomes misaligned. We tried using the BaselineOffset to adjust for such shifts but did not manage to get a working prototype, since the text seems to be unresponsive to any changes to this offset. Finding a solution for solving the baseline alignment is still future work. A workaround is setting the LineHeight to different values per font. Currently, we use 0.8 in the example.

Conclusion

If there is a way to achieve our use case with the existing QuestPDF functionality, we would be excited to learn about it. We have explored the latest release but couldn't find a solution that meets all our requirements. We think this feature enhances the flexibility of text rendering in QuestPDF and could benefit other users with similar requirements.

Thank you for considering this pull request. We're happy to make any adjustments if needed. Also, if our use case can be done with existing QuestPDF functionality, that would be even better, but we did not find a way as of the latest release.

@thijmenjk
thijmenjk marked this pull request as draft September 13, 2024 13:05
@thijmenjk
thijmenjk marked this pull request as ready for review September 13, 2024 13:18
thijmenjk pushed a commit to thijmenjk/QuestPDF that referenced this pull request Sep 16, 2024
Mirror of QuestPDF#997

Related work items: #141420
@ctatton

ctatton commented Jan 14, 2025

Copy link
Copy Markdown

I have similar issues and think this would help a lot.

@jianliulin

Copy link
Copy Markdown

This is a very useful feature

@MarcinZiabek

Copy link
Copy Markdown
Member

Thank you for sharing this improvement and the thorough investigation 😄

As the first step, I would like to investigate the ScaleToFit API to ensure it works properly in all use cases. This effort is tracked here: #1220. As the next step, I will look over your pull request and check if it fits on the future roadmap.

Please feel free to share your perspective with me. It is a perfect time for brainstorming.

@yenn104

yenn104 commented May 3, 2025

Copy link
Copy Markdown

This is a very useful feature, and I really need it. Has it been integrated yet? Thanks!

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.

5 participants