0

I have image with some area (rectangle) for text. Text can be with any length, one word or long string (that must be wrapped). I need to:

  1. Calculate font size (by string, font and rectangle)
  2. Draw text in this rectangle (with wrapping!)

The main requirement: get maximum of fontSize, the text should fill ALL rectangle.

What I done. I found nice 3rd party in NuGet: ImageProcessor. But, ImageFactory.Watermark gets only start point, not rectangle.

OK, I've implemented own solution:

  1. Try to draw text in rectangle with very big font size (use MeasureText, not real render).
  2. For example, my rectangle has height = 100, but MeasureString returns 200. Great, change fontSize from 50 to 25.
  3. With new fontSize every char became smaller, not only height, and width too! This is why I replace newFontSize = oldFontSize * (measuredHeight / requiredHeight) to newFontSize = oldFontSize * Math.Sqrt(measuredHeight / requiredHeight)
  4. Looks better. But I still have problems.

Prob 1: I use Graphics.MeasureString from GDI+ in WPF. This is not thread-safe, I have to use locks.

Prob 2: MeasureString returning wrong height, as if text has big margin. For example, I have 3 rectangles close to each other:

  • RECTANGLE1
  • RECTANGLE2
  • RECTANGLE3

After render I see VERY big spaces between them.

I will happy get 3rd party with what I need! If no, to fix my code will be also great. Thanks!

Code:

    private static void RenderText(string text, RectangleF rectangle, 
        string fontFamily, int maximumFontSize, Color textColor, Graphics graphics)
    {
        var font = new Font(fontFamily, maximumFontSize, FontStyle.Regular);
        var stringFormat = new StringFormat
        {
            //   LineAlignment = StringAlignment.Center,
            FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.FitBlackBox,
            Trimming = StringTrimming.None
        };

        var previewSize = graphics.MeasureString(text, font,
            new SizeF(rectangle.Width, rectangle.Height), stringFormat);

        if (previewSize.Height > rectangle.Height)
        {
            var scale = Math.Sqrt(rectangle.Height / previewSize.Height);
            font = new Font(fontFamily, (float) (maximumFontSize * scale), FontStyle.Regular);
        }

        graphics.DrawString(text, font, new SolidBrush(textColor), rectangle, stringFormat);
    }

Additional note:

What I'm trying to get with this is to have both text-wrapping and font-scaling at the same time. Please see this sketch as an example.

quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
TimeCoder
  • 175
  • 1
  • 8
  • 1
    Woo.. why down to GDI and MeasureText and manual render? Could you explain shortly, why can't you just use a standard base TextBlock (with given font) in a Border (with colors, acting as a rectangle) and render that to an image (and then use that image as the watermark)? If you have problems using custom fonts -> ie: http://stackoverflow.com/questions/3765647/using-a-custom-font-in-wpf – quetzalcoatl Apr 18 '17 at 18:49
  • Because this is renderer, this app gets some basic image with 2 rectangles (where should be text), 2 strings, and new size of image. I have to resize image, draw text over image, and save this image to jpg. User don't see this image with text in UI. – TimeCoder Apr 18 '17 at 19:12
  • Moreover! Even if all of this happening in UI: how I can use TextBlock? Text can be with any length, I must change FontSize. WPF-control can make wrapping text, but how TextBlock can be filled with text (var length) for fixed height of control? – TimeCoder Apr 18 '17 at 19:19
  • 1
    If you look into `Viewbox` you mind find a suitable answer. A `Viewbox` will allow the contents to fill to it's sizes (hint stick a `TextBlock` inside) – Bijington Apr 18 '17 at 19:33

2 Answers2

1

"Because (..) User don't see this image with text in UI"

It doesn't really matter. Window/Screen is just a surface. You can use WPF components to print to printer or to render to a bitmap. You don't need to see them on UI.

See this answer to see how to set an off-screen component to a given size and then render it to a bitmap.

"Even if all of this happening in UI: (..) how TextBlock can be filled with text (var length) for fixed height of control"

If I read your request correctly, you are not doing a wrapping-to-height, but rather, scaling-the-font-so-it-fits.

In that case, just like Bijington said in the comments, just use ViewBox component. Please see the code below - I set the ViewBox to scale both up or down according to the area available. Play with the window, see how the "text" "scales".

Actually, it's not the text itself - all textboxes have the same style. It's the area that's this text is drawn upon that is being scaled. Think of it like smart "zoom" applied to the contents of a ViewBox.

<Window x:Class="stack43479959.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:stack43479959"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <UniformGrid Rows="3" Columns="3">
        <Grid>
            <Border HorizontalAlignment="Center" VerticalAlignment="Center"
                    Padding="5"
                    BorderThickness="1" BorderBrush="Red">
                <Viewbox StretchDirection="Both" Stretch="Uniform">
                    <TextBlock TextWrapping="WrapWithOverflow">
Lorem ipsum dolor sit amet, consectetur adipiscing elit,<LineBreak/>
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.<LineBreak/>
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.<LineBreak/>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.<LineBreak/>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
                    </TextBlock>
                </Viewbox>
            </Border>
        </Grid>

        <Grid>
            <Border HorizontalAlignment="Center" VerticalAlignment="Center"
                    Padding="5"
                    BorderThickness="1" BorderBrush="Red">
                <Viewbox StretchDirection="Both" Stretch="Uniform">
                    <TextBlock TextWrapping="WrapWithOverflow">
Lorem ipsum dolor sit amet,<LineBreak/>
sed do eiusmod tempor
                    </TextBlock>
                </Viewbox>
            </Border>
        </Grid>

        <Grid>
            <Border HorizontalAlignment="Center" VerticalAlignment="Center"
                    Padding="5"
                    BorderThickness="1" BorderBrush="Red">
                <Viewbox StretchDirection="Both" Stretch="Uniform">
                    <TextBlock TextWrapping="WrapWithOverflow">
Lorem ipsum<LineBreak/>
dolor sit amet
                    </TextBlock>
                </Viewbox>
            </Border>
        </Grid>

        <Grid>
            <Border HorizontalAlignment="Center" VerticalAlignment="Center"
                    Padding="5"
                    BorderThickness="1" BorderBrush="Red">
                <Viewbox StretchDirection="Both" Stretch="Uniform">
                    <TextBlock TextWrapping="WrapWithOverflow">
Lorem ipsum<LineBreak/>
dolor sit amet
                    </TextBlock>
                </Viewbox>
            </Border>
        </Grid>

        <Grid>
            <Border HorizontalAlignment="Center" VerticalAlignment="Center"
                    Padding="5"
                    BorderThickness="1" BorderBrush="Red">
                <Viewbox StretchDirection="Both" Stretch="Uniform">
                    <TextBlock TextWrapping="WrapWithOverflow">
L<LineBreak/>
O
                    </TextBlock>
                </Viewbox>
            </Border>
        </Grid>

        <Grid>
            <Border HorizontalAlignment="Center" VerticalAlignment="Center"
                    Padding="5"
                    BorderThickness="1" BorderBrush="Red">
                <Viewbox StretchDirection="Both" Stretch="Uniform">
                    <TextBlock TextWrapping="WrapWithOverflow">
L
                    </TextBlock>
                </Viewbox>
            </Border>
        </Grid>
    </UniformGrid>
</Window>
Community
  • 1
  • 1
quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
  • Thank you for the efforts! The main issue is: in my case text rectangle has two responsabilities at the same time: text wrapping and change font-size for ideal text fitting.Please take a look example based on your code : https://gyazo.com/8bdf63cf2f1a8b9a4bdaa3690b9560ff – TimeCoder Apr 19 '17 at 06:20
  • @TimeCoder: Hm.. now that will be harder with standard text-wrapping.. (1) IIRC, it does not care about height - it always uses width, and (2) it does not notify you if it has 'overflown' or not. – quetzalcoatl Apr 19 '17 at 07:23
  • @TimeCoder: However, you still can use a mix of what I proposed and of what you have right now: run a loop that will try out various fontsizes and will try rendering and checking if it overflow. You can use 'off-screen control' to do the wrapping and rendering. Set the off-screen textblock to some desired Width and FontSize, give it infinite vertical space (dont set it at all) and then render it. Then, inspect the TextBlock.ActualHeight to see how much space it took to write the text. If it's over the limit, change fontsize, retry -> 100% WPF, no manual MeasureText, no manual wrapping. – quetzalcoatl Apr 19 '17 at 07:25
0

thanks to all for the help. I completed this task, works very cool! I can't share all code (commercial project), but some key moments (where I wasted a lot of time).

Using WPF for render - good idea. Just create grid + image + textblocks (not in UI, in memory). Example of textBlock:

        var titleTextBlock = new TextBlock
        {
            FontSize = layout.TitleFontSize,
            FontFamily = new FontFamily(layout.TitleFontFamily),
            LineStackingStrategy = LineStackingStrategy.BlockLineHeight,

            LineHeight = layout.TitleFontSize,
            FontWeight = FontWeights.Normal,
            Foreground = new SolidColorBrush(Color.FromRgb(18, 75, 14)),
            VerticalAlignment = VerticalAlignment.Top,
            HorizontalAlignment = HorizontalAlignment.Left,
               Width = layout.TitleRectangle.Width,
            Margin = new Thickness(layout.TitleRectangle.Left,
                layout.TitleRectangle.Top, 0, 0),
            TextWrapping = TextWrapping.WrapWithOverflow
        };
        Grid.SetRow(titleTextBlock, 0);
        Grid.SetColumn(titleTextBlock, 0);
        grid.Children.Add(titleTextBlock);

Incremental fitting:

    private static void FitToRectangle(TextBlock textBlock, RectangleF rectangle, bool fitToHeight)
    {
        while (true)
        {
            if (fitToHeight)
            {
                textBlock.Measure(new Size(double.PositiveInfinity, rectangle.Height));
            }
            else
            {
                textBlock.Measure(new Size(rectangle.Width, double.PositiveInfinity));
            }

            var size = textBlock.DesiredSize;
            var rect = new Rect(size);
            textBlock.Arrange(rect);

            if ((fitToHeight && textBlock.ActualHeight > rectangle.Height) ||
                (!fitToHeight && textBlock.ActualWidth > rectangle.Width))
            {
                textBlock.FontSize -= 5;
                textBlock.LineHeight -= 5;
            }
            else
            {
                if (fitToHeight)
                {
                    FitToRectangle(textBlock, rectangle, false);
                }
                break;
            }
        }
  1. Fit to width is also important! If text has veryVerylongWordWithoutSeparators, "Wrap" just cut it in the middle and wrap! "WrapWithOverflow" - and width of long word will be greater then limit. This is why we need final width-fitting after height-fitting.
  2. I need a lot of parallel threads for render. You can create Grid only inside STA thread. If you pass SyncContext of UI thread into Task.StartNew - this will work, but UI thread will freeze during render. This what helped:

    public static Task StartSTATask(Action<object> func, object parameter)
    {
        var tcs = new TaskCompletionSource<object>();
        var thread = new Thread(param =>
        {
            try
            {
                func(param);
                tcs.SetResult(null);
            }
            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });
    
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start(parameter);
        return tcs.Task;
    }
    
  3. Don't forget measure and arrange grid also, after textBox fitting.

  4. And finally, code of render "off-screen" controls to file:

            var drawingVisual = new DrawingVisual();
    
            using (var ctx = drawingVisual.RenderOpen())
            {
                var visualBrush = new VisualBrush
                {
                    AutoLayoutContent = true,
                    Visual = grid
                };
    
                ctx.DrawRectangle(visualBrush, null, 
                    new Rect(0, 0, layout.Size.Width, layout.Size.Height));
            }
    
            var bmp = new RenderTargetBitmap((int) layout.Size.Width, (int) layout.Size.Height, 
                96, 96, PixelFormats.Pbgra32);
            bmp.Render(drawingVisual);
    
            var encoder = new JpegBitmapEncoder();
    
            encoder.Frames.Add(BitmapFrame.Create(bmp));
            using (Stream stm = File.Create(filename))
            {
                encoder.Save(stm);
                lock (this.sync)
                {
                    this.rendered++;
                    UpdatePercentage();
                }
            }
    
TimeCoder
  • 175
  • 1
  • 8