Meme Generator

This tutorial will walk you through the process of developing a class for the generation of Memes.

The UI

  1. Create a new ASP.NET web forms project called Meme. You may want to choose a non-empty one, as this provides some easy styling courtesy of bootstrap, but it is not a requirement
  2. Create a new web form named default.aspx
  3. Add two text boxes, each with labels, a file upload control and a button to a web form. Set the text of the labels as shown in the example below HTML page with two text boxes, a file input and a button. Text boxes are labelled "Top line of meme" and "Bottom line of meme" The example above uses the bootstrap styling, but it is not a requirement for this tutorial to work.
  4. Beneath that add an Image control, and set its ID to MemeImage

The class

We'll come back to the UI later, but next we will create a class whose responsibility it will be to make Memes.

  1. Add a new class to the project and name it MemeGenerator
  2. We will be working with the code in the Drawing namespace in ASP.NET, so add the following using statements at the top of the file:
    using System.Drawing;
    using System.Drawing.Drawing2D;
  3. The class will need a source image, so add a private field for this as follows
     private Image sourceImage;
    Note that an image in the Drawing namespace differs from the one in System.Web.UI (which you would might reference from inside a web form's class, and which reprents a HTML img element). We will make use of both in this project!
  4. Add a constructor which will take an Image as a parameter, and use it to populate the private field you just added as follows:
    public MemeGenerator(Image image)
    {
        this.sourceImage = image;
    }
    A shortcut to creating a constructor in Visual Studio is to type 'ctor' and then press the tab key twice.
  5. Next we will write code to generate a meme given two lines of text. Add the following method stub:
    public Image Generate(string topLine, string bottomLine)
    {
    
    }
  6. Next add the following code, which will create a Graphics object using a copy of the image. We use a copy because the Graphics object draws onto the image, and should we wish to generate a new Meme with the same image, we want the original unaltered. The final three lines of code ensure that the finial image quality will be good:
    Image memeImage = sourceImage.Clone() as Image;
    Graphics textGraphics = Graphics.FromImage(memeImage);
    textGraphics.CompositingQuality = CompositingQuality.HighQuality;
    textGraphics.SmoothingMode = SmoothingMode.HighQuality;
    textGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
  7. As the input text length and size will vary based on user input, and font choice we need to ensure that both the lines of text will fit on the screen. Add the following code to set an initial font:
    float fontSize = 10.0f; //default starting font size - will be adjusted to fit image
    Font font = new Font(FontFamily.GenericSansSerif, fontSize, FontStyle.Bold);
  8. We now need to estimate the size of the text as is, so we can then scale the size of our final rendered text to fit the image. Add the following method to enable us to estimate the size of a string rendered as an image:
    private RectangleF estimateSizeOfString(string text, Font font)
    {
        GraphicsPath path = new GraphicsPath();
        path.AddString(text, font.FontFamily, (int)font.Style, font.Size, new Point(0, 0), new StringFormat());
        return path.GetBounds();
    }
  9. Next, return to the Generate method, and add the following lines of code which will find the higher of the estimated sizes of each string:
    RectangleF topSize = estimateSizeOfString(topLine, font);
    RectangleF bottomSize = estimateSizeOfString(bottomLine, font);
    float textWidthAtTenEm = Math.Max(topSize.Width, bottomSize.Width);
    
    (You may wonder why we didnt just measure the longest string. Whilst this would work in most cases, for most font faces, the size of the individual characters varies (consider the width of a lowercase 'i' compared to an uppercase 'W'). Because of this, the longest string won't always be the longest line of text graphically)
  10. Next we will scale the font size so it is 98% of the width of the image, and reinstantiate the font object with the new size. (We have to reinstatiate it, as an instance of Font doesn't provide a mutator for the font size)
    float scale = memeImage.Width / textWidthAtTenEm;
    fontSize *= scale * 0.98f;
    font = new Font(font.FontFamily, fontSize, font.Style);

Creating a Graphics Path

  1. The next task is to write a method which will provide us with a graphical representation of the text. For the bottom line of text, it will need to be offset from the top of the image, but this will not be needed for the top line. Add the following method stub which includes a parameter with a default value (this means when we call the method, specifying the topOffset value is optional, and if we don't it will default to 0)
    private GraphicsPath textPathWithVerticalOffSet(string text, Font font, int topOffset = 0)
    {
            
    }
  2. Next add the following code to the method which will determine that the String will be aligned to the centre, that it is to be rendered at the centre of the image, and offset vertically as required. The penultimate line of code adds the String to the path (note that it is not actually drawn at this stage).
    StringFormat centreFormat = new StringFormat();
    centreFormat.Alignment = StringAlignment.Center;
    Point renderPoint = new Point(sourceImage.Width / 2, topOffset);
    GraphicsPath path = new GraphicsPath();
    path.AddString(text, font.FontFamily, (int)font.Style, font.Size, renderPoint, centreFormat);
    return path;

Drawing the text on the image

  1. Return to the Generate method and add code to create a pen to draw the strings with, as follows:
    Pen thickPen = new Pen(Brushes.Black, 3.0f);
  2. Next add the following code which will get the paths for both lines (using the method we just created). Note how the topOffset parameter is only used when getting the path for the bottom line of text. The outline of the text is drawn in black and it is filled with white, so it stands out on any image:
    GraphicsPath topPath = textPathWithVerticalOffSet(topLine, font);
    textGraphics.DrawPath(thickPen, topPath);
    textGraphics.FillPath(Brushes.White, topPath);
    float bottomOffset = sourceImage.Height - (fontSize * 1.2f);
    GraphicsPath bottomPath = textPathWithVerticalOffSet(bottomLine, font, (int)bottomOffset);
    textGraphics.DrawPath(thickPen, bottomPath);
    textGraphics.FillPath(Brushes.White, bottomPath);
  3. Finally we can dispose of the Graphics object (remember that the Graphics object draws on the image), and then we return the modified image:
    textGraphics.Dispose();
    return memeImage;

Connecting it up to the UI

When the user adds the text to the fields, chooses an image and pressed the button, a few things need to happen:

  1. The file has to be converted from its bytes to a stream and then to an Image (specifically a System.Drawing.Image)
  2. The memegenerator needs to be instantiated with the image, and then the generate method called to get the meme
  3. The meme needs to be saved to disk in jpg format (in this case named using a Guid so users do not overwrite each others memes)
  4. An Image control (i.e. a System.Web.Ui.WebControls.Image) has various properties set, so that it points to the image file, is set to the correct dimensions, and has alternate text (based on the text the user entered)
All that remains is to add the final code.

  1. Add a button handler for the button on the web form, if you have not done so already, and configure it as follows (you may need to change the names of the various controls, depending on how they are named in the .asx page):
    protected void SubmitButton_Click(object sender, EventArgs e)
    {
        Byte[] file = ImageUploader.FileBytes;
        MemoryStream str = new MemoryStream(file);
        System.Drawing.Image image = System.Drawing.Image.FromStream(str);
    
        MemeGenerator memeGen = new MemeGenerator(image);
        System.Drawing.Image memeImage = memeGen.Generate(txtTopLine.Text,txtBottomLine.Text);
    
        String guid = Guid.NewGuid().ToString();
        memeImage.Save(Server.MapPath("~/Memes/"+guid+".jpg"),System.Drawing.Imaging.ImageFormat.Jpeg);
    
        MemeImage.ImageUrl = "memes/"+guid+".jpg";
        MemeImage.AlternateText = txtTopLine.Text + ": " + txtBottomLine.Text;
        MemeImage.Attributes.Add("width", memeImage.Width.ToString());
        MemeImage.Attributes.Add("height", memeImage.Height.ToString());
    }
    
  2. Test the application and ensure it works

You can Try out a completed version of the meme generator here

Further tasks