Flow words around drop caps in C#


[drop caps]

The example Flow blocks around obstacles for document layout in C# implements a document layout algorithm that flows blocks around obstacles. Moving from there to flowing words around obstacles is relatively simple. The following sections describe the main places where I modified the previous algorithm to provide document layout for words instead of blocks.

Block

The following code shows the new example’s Block class.

public class Block
{
    public RectangleF Bounds;
    public float TopHeight;
    public float BottomHeight
    {
        get { return Bounds.Height - TopHeight; }
    }

    public Block()
    {
    }
    public Block(RectangleF bounds, float top_height)
    {
        Bounds = bounds;
        TopHeight = top_height;
    }

    // Draw the block.
    public virtual void Draw(Graphics gr, Brush fg_brush,
        Brush bg_brush, Pen pen)
    {
        if (Bounds.X < 0) return;

        gr.FillRectangle(bg_brush, Bounds);
        gr.DrawRectangle(pen, Bounds);
    }

    // Return true if the block intersects the rectangle.
    public bool IntersectsWith(RectangleF rect)
    {
        return Bounds.IntersectsWith(rect);
    }

    // Return true if the block intersects the other block.
    public bool IntersectsWith(Block other)
    {
        return Bounds.IntersectsWith(other.Bounds);
    }
}

The main differences are the addition of a parameterless constructor and the two versions of the IntersectsWith method. Those methods return true if block’s bounds intersect with a rectangle or with another Block object.

Also note that the Draw method is now virtual. That allows the TextBlock class to override it so it can draw text instead of just a box.

The TextBlock Class

The previous example uses the Block class to represent a rectangle that should be flowed around obstacles. To flow words, this example derives a new TextBlock class from the Block class. The following code shows the new class.

public class TextBlock : Block
{
    public Font Font;
    public String Text;

    public TextBlock(string text, Font font, Graphics gr)
    {
        Font = font;
        Text = text;

        SizeF size = gr.MeasureString(Text, font);
        Bounds = new RectangleF(new PointF(), size);

        FontInfo font_info = new FontInfo(gr, font);
        TopHeight = font_info.AscentPixels;
    }

    // Draw the text block.
    public override void Draw(Graphics gr, Brush fg_brush,
        Brush bg_brush, Pen pen)
    {
        if (Bounds.X < 0) return;

        // Draw the box (optional). 
        base.Draw(gr, fg_brush, bg_brush, pen);

        // Draw the text.
        using (StringFormat sf = new StringFormat())
        {
            sf.Alignment = StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;
            gr.DrawString(Text, Font, fg_brush, Bounds, sf);
        }
    }

    public override string ToString()
    {
        return Text;
    }
}

This class inherits from the Block class. It provides two new fields: Font and Text. You can probably guess that those are the font that the object should use to draw its text and the text that it should draw.

The constructor takes as parameters the block’s text, its font, and a Graphics object that it can use to measure the text. The code saves the font and text, and then calls the Graphics object’s MeasureString method to measure the text as drawn with the given font. It uses the text’s size to define the block’s bounds.

Next, the program must figure out where the text’s baseline should be. Recall that the previous example’s document layout method aligned blocks on their baselines. For the blocks, that was unnecessary, but for text, that’s important so different words line up properly.

The distance between the top of a piece of text and its baseline is called its ascent. Unfortunately, neither the Font nor Graphics objects give you the ascent directly. Fortunately, you can figure it out. For an explanation of how to do that and to see a picture showing where a piece of text’s ascent is, see my earlier post Get font metrics in C#. That example defines a FontInfo class that gives the font’s ascent. The TextBlock class simply creates a FontInfo object for the font and saves its AscentPixels property in its TopHeight field.

The final piece of the TextBlock class is its Draw method, which overrides the same method defined by the Block class. The new method first calls the Draw class’s version of the method to draw a box where the text will be positioned as shown in the picture at the top of the post. In the final program, you can comment out that call so the program only draws the text as shown in the following picture.


[document layout]

The Draw method then draws the object’s text inside its bounds. Because the constructor made the bounds fit the text exactly, the text fills the bounds completely so the text is centered both vertically and horizontally within the bounds.

PictureBlock

I wanted the final program to be able to make text flow around pictures in fixed positions. The program uses the following PictureBlock class to represent and display those pictures.

public class PictureBlock : Block
{
    public Image Picture;

    public PictureBlock(Image picture, float x, float y)
    {
        Picture = picture;
        Bounds = new RectangleF(x, y,
            picture.Width, picture.Height);
    }

    // Draw the block.
    public override void Draw(Graphics gr, Brush fg_brush,
        Brush bg_brush, Pen pen)
    {
        if (Bounds.X < 0) return;

        // Draw the box (optional). 
        base.Draw(gr, fg_brush, bg_brush, pen);

        // Draw the image.
        gr.DrawImage(Picture, Bounds.Location);
    }
}

Like TextBlock the PictureBlock class inherits from Block. This class’s constructor takes as a parameter the picture that it should draw and the X and Y coordinates where it should be drawn. It saves the picture and uses the coordinates and the picture’s dimensions to set the object’s bounds.

The Draw method calls the base class’s Draw method. It then uses the Graphics object’s DrawImage method to draw the picture.

Form Load

The following code shows the variables that the program uses to hold the text and obstacles that it draws.

// Obstacles.
private List<Block> Obstacles;

// Blocks to flow around obstacles.
private List<List<Block>> ParagraphBlocks;

// The initials.
private List<Block> Initials;

// The text to draw.
private string[] Paragraphs =
{
    "Lorem ipsum dolor sit amet, consectetur...",
    "Etiam at nulla accumsan, fringilla erat...",
    "..."
};

// Extra space between paragraphs.
private const float ParagraphSpacing = 20;

The Obstacles list holds blocks that the text should flow around. These will be PictureBlock objects at fixed locations.

The ParagraphBlocks variable holds lists of blocks. Each list contains the words that make up a paragraph. (The words are Lorem Ipsum words. For more information on this interesting test string, see the site Lorem Ipsum.)

The first letter of the first word in each paragraph is removed and placed in the Initials list. Those values are the letters used for the drop caps. For example, Initials[2] is holds the first letter of paragraph number two.

Notice that the Obstacles, ParagraphBlocks, and Initials lists all contain Block objects. The TextBlock and PictureBlock classes are derived from Block, so TextBlock and PictureBlock objects are kinds of block. That means the program can use those objects as Block objects and can place them inside those lists.

The Paragraphs array holds all of the text. Finally, the value ParagraphSpacing indicates the amount of extra space to add between paragraphs.

The program initializes the data in the following Form_Load event handler.

// Define some blocks.
private void Form1_Load(object sender, EventArgs e)
{
    // Create a PictureBlock at a fixed position.
    PictureBlock picture_block = new PictureBlock(
        Properties.Resources.essential_algorithms_2e, 250, 70);

    // Add the PictureBlock as an obstacle.
    Obstacles = new List<Block>();
    Obstacles.Add(picture_block);

    // Build the lists of paragraphs and initials.
    ParagraphBlocks = new List<List<Block>>();
    using (Graphics gr = CreateGraphics())
    {
        // Give the initials the same size.
        RectangleF initial_bounds = new RectangleF(0, 0, 64, 64);

        // Break each paragraph into words.
        Initials = new List<Block>();
        Font initial_font =
            new Font("Times New Roman", 40, FontStyle.Bold);
        for (int i = 0; i < Paragraphs.Length; i++)
        {
            // Remove the first lettter of the first word.
            Initials.Add(new TextBlock(Paragraphs[i].Substring(0, 1),
                initial_font, gr));
            Paragraphs[i] = Paragraphs[i].Substring(1);
            Initials[i].Bounds = initial_bounds;

            // Break the rest of the paragraph into blocks.
            List<Block> new_blocks = new List<Block>();
            foreach (string word in Paragraphs[i].Split(' '))
            {
                // Make the word's block.
                new_blocks.Add(new TextBlock(word, RandomFont(), gr));
            }
            ParagraphBlocks.Add(new_blocks);
        }

        // Perform the initial flow.
        FlowParagraphBlocks(gr, picWriting.ClientRectangle,
            Obstacles, new SizeF(64, 64), ParagraphBlocks,
            ParagraphSpacing);
    }
}

This method first creates a PictureBlock at a specific position and adds that object to the Obstacles list.

Next, the code makes the ParagraphBlocks list and creates a Graphics object that it can use to measure text. It also creates a 64×64 pixel RectangleF to represent the size of the initials. It uses a single RetangleF so all of the drop caps will have the same size.

The code then loops through the paragraphs in the Paragraphs array. It removes the first letter from the first word in each paragraph and adds it to the Initials list. It then breaks the paragraph into words and places a TextBlock representing each word in the ParagraphBlocks list.

Notice that the code calls the RandomFont method to pick a different font for each word. That method gives each word a random font name, size, and style (bold, italic, regular, strikeout, or underline. If you comment out the compile-time symbol RANDOM_FONTS defined at the top of the file, then the RandomFont method returns a specific font instead of a random one. Download the example to see how it works.

The Load event handler finishes by calling the FlowParagraphBlocks method to flow the drop caps and text around the fixed obstacles.

FlowParagraphBlocks

The following code shows the FlowParagraphBlocks method that does the most interesting work.

// Flow blocks around obstacles.
private void FlowParagraphBlocks(Graphics gr,
    RectangleF bounds,
    List<Block> fixed_obstacles, SizeF min_initial_size, 
    List<List<Block>> paragraph_blocks,
    float paragraph_spacing)
{
    // Place objects to be hidden here.
    PointF hidden_point = new PointF(-1, -1);

    // Make a list to hold fixed obstacles and initials.
    List<Block> obstacles = new List<Block>(fixed_obstacles);

    // Start at the top.
    float y = bounds.Y;

    // Repeat until we place all blocks or run out of room.
    for (int paragraph_num = 0;
        paragraph_num < paragraph_blocks.Count;
        paragraph_num++)
    {
        // Position this paragraph's blocks.
        List<Block> this_paragraphs_blocks =
            paragraph_blocks[paragraph_num];

        // If this paragraph's initial won't fit,
        // move y down so we stop positioning blocks.
        if (y + Initials[paragraph_num].Bounds.Height > bounds.Bottom)
            y = bounds.Bottom + 1;

        // If we have run out of room, place everything at (-1, -1).
        if (y > bounds.Bottom)
        {
            // Position the initial and other blocks at (-1, -1).
            Initials[paragraph_num].Bounds.Location = hidden_point;

            // Position the text blocks.
            for (int i = 0; i < this_paragraphs_blocks.Count; i++)
                this_paragraphs_blocks[i].Bounds.Location = hidden_point;

            // Go on to the next paragraph.
            continue;
        }

        // Position the initial.
        Initials[paragraph_num].Bounds.Location =
            new PointF(bounds.Left, y);
        obstacles.Add(Initials[paragraph_num]);

        // Position the remaining text.
        int first_block = 0;
        while (first_block < this_paragraphs_blocks.Count)
        {
            // Position a row of blocks.
            int num_positioned = PositionOneRow(
                bounds, obstacles, this_paragraphs_blocks,
                ref first_block, ref y);

            // See if any fit.
            if (num_positioned == 0)
            {
                // None fit. Move down.
                MoveDown(bounds, obstacles,
                    this_paragraphs_blocks[first_block], ref y);
            }

            // See if we have run out of room.
            if (y > bounds.Bottom)
            {
                // Position the remaining blocks at (-1, -1).
                for (int i = first_block;
                    i < this_paragraphs_blocks.Count; i++)
                {
                    this_paragraphs_blocks[i].Bounds.Location =
                        hidden_point;
                }

                // Stop positioning the blocks in this
                // paragraph and go on to the next paragraph.
                break;
            }
        }

        // Don't start the next paragraph before the
        // bottom of the current paragraph's initial.
        if ((paragraph_num < paragraph_blocks.Count - 1) &&
            (y < bounds.Bottom))
        {
            if (y < Initials[paragraph_num].Bounds.Bottom)
                y = Initials[paragraph_num].Bounds.Bottom;
        }

        // Add some extra space between paragraphs.
        y += paragraph_spacing;
    }
}

The method makes a new obstacles list and copies the fixed obstacles (the PictureBlock) into it. It sets variable y equal to the top of the bounds where it is allowed to draw.

The code then loops through the paragraphs. It first checks whether the current paragraph’s initial will fit within the allowed bounds. If the initial won’t fit, the code moves y down beyond the bottom of the drawing area so the method won’t try to draw any more text.

Next, if y is below the bottom of the drawing area, the code loops through all of the paragraph’s words and positions them all at (-1, -1) to indicate that they did not fit.

If the initial does fit, then the code sets its position. That places the drop caps at the left edge of the drawing area. (You could modify that if you like. For example, you may want the drop caps indented.)

The code then adds the initial to the obstacles list so the paragraph’s text blocks can flow around it on subsequent lines.

Next, the code loops through the paragraph’s TextBlock objects. It calls the PositionOneRow method as in the previous example. If no word will fit on the current line, the code calls the MoveDown method to move down a bit, again as in the previous example.

After it has drawn text on the current line or moved down, the code checks the y position to see if we have fallen off of the drawing area. If we have left the drawing area, the code loops through the paragraph’s remaining words and places them at (-1, -1).

Before it continues looping through the paragraphs, the program checks that the y coordinate is at least beyond the bottom of the current paragraph’s initial. It then adds the extra spacing between paragraphs and continues looping through the paragraphs.

Download the example and take a look at the code for extra details about the PositionOneRow and MoveDown methods.

Paint

When the form loads, the program calls FlowParagraphBlocks to arrange the words. The program also calls that method when the form resizes.

When the form paints, it simply draws all of the blocks in their assigned positions. The following code shows how the Paint event handler does that.

// Draw the blocks.
private void picWriting_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // Draw obstacles.
    foreach (Block obstacle in Obstacles)
    {
        obstacle.Draw(e.Graphics, Brushes.Red, Brushes.Pink, Pens.Red);
    }

    // Draw initials.
    using (Brush bg_brush =
        new TextureBrush(Properties.Resources.Butterflies))
    {
        foreach (TextBlock initial in Initials)
        {
            initial.Draw(e.Graphics, Brushes.Black, bg_brush, Pens.Black);
        }
    }

    // Draw flowed blocks.
    foreach (List<Block> this_paragraphs_blocks in ParagraphBlocks)
    {
        foreach (Block block in this_paragraphs_blocks)
        {
#if DRAW_BOXES
            block.Draw(e.Graphics, Brushes.Black,
                Brushes.Transparent, Pens.Red);
#else
            block.Draw(e.Graphics, Brushes.Black,
                Brushes.Transparent, Pens.Transparent);
#endif
        }
    }
}

[Essential Algorithms, 2e]

This event handler sets the Graphics object’s SmoothingMode. It then loops through the fixed obstacles and draws them. In this example, that draws the single picture of my book Essential Algorithms, Second Edition.

The code then makes a TextureBrush that draws copies of an image. (In this example, it uses a colorful image of some butterflies tiled in the style of M. C. Escher.) The code then loops through the initials’ TextBlocks and calls their Draw methods to draw the drop caps. The parameters that the code uses makes that method fill the drop caps with the butterfly image, outline the rectangle in black, and draw the text in black.

After drawing the drop caps, the code loops through the blocks in the ParagraphBlocks lists and calls each block’s Draw method. The code outlines each block if the DRAW_BOXES symbol is defined.

That’s all the program needs to do to draw the picture, drop caps, and the other text. Flowing the text around the drop caps is fairly complicated, but after the text is positioned, drawing it is simple.

Conclusion

That’s the end of this example. I can’t say it was exactly simple, but the pieces are manageable if you take them slowly and one at a time.

Of course, all of this is just a taste of what a true document layout system must do. A program such as Microsoft Word or LaTeX must be able to flow text around obstacles such as pictures. Those pictures could hold drawings or text used for drop caps. Both pictures and inline text could include text with various colors and fonts, borders, fill colors, equations, tables, and a whole host of other features.

The problem is certainly daunting. Treating every object as a single Block base class helps. That lets you treat every object as something that occupies a rectangle and then you can shuffle the objects around. Exactly how you do that depends on your layout strategy.

As usual, download the example to see additional details and to experiment with the example.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in algorithms, fonts, graphics, strings and tagged , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.