Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Add Text to Image - Sample Application

4.87/5 (26 votes)
6 Mar 2016CPOL6 min read 38.2K  
Small application that allows text to be added to an image for annotation or to create greeting cards using ASP.NET MVC and jQuery

Image 1

Introduction

Web-site is a one page ASP.NET MVC application (C#). Entity Framework is used to save data into MS SQL database. CRUD operations are performed using Web API controllers. All operations on client side are executed on JavaScript with JQuery.

To generate images from text a wonderful library Outline Text from Shao Voon Wong is used.

SVG does the main job for manipulating images.

Site has responsive design. I did not rely on a MVC framework built-in functionality which is based on type of browser. Elements are arranged on the screen depending on its width.

Web-site contains only one page and this page is divided on two areas. When user opens site the first area is displayed: 

Image 2

This area is used to upload source image or select a sample from the Sample Gallery. So user has three options:

  • drag and drop image to the place with arrows;
  • select an image from the local drive;
  • select a sample from the Sample Gallery;

After image is uploaded or sample was selected, first area hides and second is shown:

Image 3

Uploaded image is located on the right side of the screen. On the left side there is a panel for adding text and change its settings: font size, color, rotation, etc.

Method that generates an image from the text has a lot of options: font type, color, outline color and thickness, shadow color and thickness, etc. Putting controls for these options on the page may discourage users. To reduce their quantity Text Template Gallery was created. Each Text Template includes all these attributes and with its selection user can only change main color and font size.

Clipart that can be added from the Clipart Gallery will make an image more attractive. Approach for generating clipart image is the same as for text. Clipart is one character of special clipart font. 

Resulting image can be saved by user to the local file system by clicking Save button.

Running the code

To run the code:

  • Open the solution in Visual Studio 2013.
  • Rebuild all.
  • Set AddTextToImage.WebUI as a startup project.
  • Run the application.

Using the code

The code is contained in one solution, which is a Visual Studio 2013 solution, and consists of five projects:

Image 4

AddTextToImage.Data

Image 5

Data Access Layer - contains repository and DbContextFactory interfaces and implementations.  For data access Entity Framework Code First approach is being used.

The Db class which inherits DbContext has a DbSet<Entity> for each entity as required by EF Code First.

The DbContextFactory class is used to construct and get the DbContext.

Repository<T> is a generic repository which does all the basic Data Access operations.

AddTextToImage.Domain 

Image 6

Project AddTextToImage.Domain contains POCO entities for the application. Diagram bellow shows these classes:

Image 7

Class Entity is the base class for all POCO classes. Model and ModelItem represent source image and images generated from the added text.  The purpose of Sample and SampleItem classes is to display Sample Gallery. Classes ClipartGallery, ClipartTemplate and TextGallery, TextTemplate show Clipart Gallery and Text Template Gallery correspondingly. ClipartGallery and TextGallery, ClipartTemplate and TextTemplate have the same structure. However, I've preferred to use separate classes to have individual tables in the database.

Abstract class TemplateBase is a base class for ClipartTemplate and TextTemplate. It appeared to serve as a parameter for a constructor of the class OutlineTextProcessor. During the creation of OutlineTextProcessor class its constructor takes ClipartTemplate or TextTemplate as a parameter. Class FontInfo contains file names of fonts which are located on the file system.

AddTextToImage.ImageGenarator

Image 8

It has only one class OutlineTextProcessor which generates images from the text using TextDesignerCSLibrary.dll library. (Link to an article about image generation process and this library is motioned above).

AddTextToImage.UnitTests

Project contains one class with several unit tests for controllers. For this purpose I used Visual Studio Unit Testing Framework and Moq library.

AddTextToImage.WebUI

Image 9

As I mentioned above, site has one page, so, there is one regular controller named HomeController in the project. All other controllers are Web API and they perform CRUD operations or return generated images.

To make life easier I used jQuery for DOM manipulation. Also I used Dialog widget from jQuery UI to display Text Template Gallery and Clipart Gallery. These two libraries are loaded directly from CDN network.

All JavaScript code is in a single file: app.js. List of the base JavaScript objects, which is used in the application, are in the table:

JavaScript objects Description
textAsImage Top object, contains all objects bellow.
errorMessage Shows error message when server returns an error on AJAX request.
model Saves properties of source image. Contains array of text images (modelItems).
modelItem Saves all properties to generate image from the text.
textSelector Represents Text Template Gallery.
clipartSelector Represents Clipart Gallery.
sampleSelector  Represents Sample Gallery.
fileUpload Uploads a source image to the server.

Bellow I describe main operations that JavaScript does:

Adding source image

Once the page is loaded, it contains empty container (SVG element) in a hidden area:

HTML
<svg id="canvas" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" baseProfile="full" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 0 0">
</svg>

When user selects a source image, it saves into database via AJAX request:

JavaScript
// Upload source image to the server.
function uploadFile(files) {

    var data = new FormData();

    // Add the uploaded image content to the form data collection.
    if (files.length > 0) {

        disableControls();
        $("#file-placeholder").attr("src", basePath + "Content/Images/image-loading.gif");

        data.append("UploadedImage", files[0]);

        // AJAX request to upload source image to the server.
        $.ajax({
            type: "POST",
            url: basePath + "api/Model/UploadFile/",
            contentType: false,
            processData: false,
            data: data,

            success: function (data) {

                $("#select-image").remove();
                $("#image-worker").show();

                model.addModel(data.Id, data.ImageWidth, data.ImageHeight);
            },

            error: function (xhr, textStatus, errorThrown) {
                errorMessage.show(errorThrown);
            }
        });
    }
}

If saving operation completed successfully, the first area of the page is deleted and the second area is shown.

Function addModel adds source image to SVG container:

JavaScript
// Add source image.
function addModel(modelId, modelWidth, modelHeight) {

    // Save id, width and height of the source image.
    id = modelId;
    width = modelWidth;
    height = modelHeight;

    // Create SVG <image> element for source image.
    var svgSourceImg = document.createElementNS("http://www.w3.org/2000/svg", "image");
    svgSourceImg.setAttribute("id", "model" + id);
    svgSourceImg.setAttribute("height", "100%");
    svgSourceImg.setAttribute("width", "100%");
    svgSourceImg.setAttributeNS("http://www.w3.org/1999/xlink", "href", basePath + "api/Model/Image/" + id + "/");
    svgSourceImg.setAttribute("x", "0");
    svgSourceImg.setAttribute("y", "0");
    canvas.setAttribute("style", "margin-left: auto; margin-right: auto; max-width: " + modelWidth + "px;");
    canvas.setAttribute("viewBox", "0 0 " + modelWidth + " " + modelHeight);

    // Append image to SVG container
    canvas.appendChild(svgSourceImg);

    if (detectIE()) {

        var imgHeigth = modelHeight;

        if (modelWidth - $("#image-main").width() > 0) {

            imgHeigth = modelHeight * $("#image-main").width() / modelWidth;
        }

        $("#image-main").css("height", imgHeigth + "px");
    }

    // Set URL for downloading the resulting image.
    $("#form-save-result").attr("action", basePath + "api/Image/Result/" + id);

    // Set size for Delete image and thickness for bounding rectangle depending on the width of the source image.
    setDelImageSize();
}

As a result SVG container has <image> element:

HTML
<svg style="margin-left: auto; margin-right: auto; max-width: 1632px;" id="canvas" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" baseProfile="full" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1632 1224">
    <image y="0" x="0" xlink:href="/api/Model/Image/1424/" width="100%" height="100%" id="model1424"></image>
</svg>

Adding text

When a user entered text and pressed "Add Text" button new object modelItem is created and it also stores in the database. Saving information in the database is required for the subsequent generation of the output image:

JavaScript
$("#btn-add-text").on("click", function () {

    if ($("#sample-text").val().length > 0) {

        // Create new text image.
        var modelItem = new ModelItem();
        modelItem.id = 0;
        modelItem.modelId = id;
        modelItem.itemType = 0;
        modelItem.text = $("#sample-text").val();
        modelItem.templateId = textSelector.getSelectedItemId();
        modelItem.fontSize = $("#font-size").val();
        modelItem.fontColor = $("#font-color").spectrum("get").toHexString();
        modelItem.rotation = $("#rotation").val();

        $.ajax({
            url: basePath + "api/Model/AddModelItem/",
            type: "PUT",
            dataType: "json",
            data: modelItem.getData(),

            success: function (modelItemId) {

                modelItem.id = modelItemId;

                addModelItem(modelItem);
            },

            error: function (xhr, textStatus, errorThrown) {
                errorMessage.show(errorThrown);
            }
        });
    }
});

If saving operation completed successfully, function addModelItem adds HTML code to show generated image: 

// Add text image.
function addModelItem(modelItem) {

    // Create SVG <g> element.
    var svgGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
    svgGroup.setAttribute("id", "img-group" + modelItem.id);

    // Create SVG <rect> element: bounding rectangle for the image
    var svgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    svgRect.setAttribute("id", "rect" + modelItem.id);
    svgRect.setAttribute("x", modelItem.positionLeft);
    svgRect.setAttribute("y", modelItem.positionTop);
    svgRect.setAttribute("height", "0");
    svgRect.setAttribute("width", "0");
    svgRect.setAttribute("stroke", "red");
    svgRect.setAttribute("stroke-width", rectangleThickness);
    svgRect.setAttribute("stroke-dasharray", "5");
    svgRect.setAttribute("fill-opacity", "0.4");
    svgRect.setAttribute("fill", "none");
    svgRect.style.display = "none";

    svgGroup.appendChild(svgRect);

    // Create SVG <image> element for image generated from the text.
    var svgTextImg = document.createElementNS("http://www.w3.org/2000/svg", "image");
    svgTextImg.setAttribute("id", "img" + modelItem.id);
    svgTextImg.setAttribute("height", "0");
    svgTextImg.setAttribute("width", "0");
    svgTextImg.setAttributeNS("http://www.w3.org/1999/xlink", "href", basePath + "api/Image/ModelItem/" + modelItem.id + "/" + modelItem.getUrl());
    svgTextImg.setAttribute("x", modelItem.positionLeft);
    svgTextImg.setAttribute("y", modelItem.positionTop);
    svgTextImg.style.cursor = "move";

    svgGroup.appendChild(svgTextImg);

    // Create SVG <image> element for Delete image.
    var svgDelImg = document.createElementNS("http://www.w3.org/2000/svg", "image");
    svgDelImg.setAttribute("id", "del" + modelItem.id);
    svgDelImg.setAttributeNS("http://www.w3.org/1999/xlink", "href", basePath + "Content/Images/delete.png");
    svgDelImg.setAttribute("x", modelItem.positionLeft);
    svgDelImg.setAttribute("y", modelItem.positionTop - 16);
    svgDelImg.style.display = "none";
    svgDelImg.style.cursor = "pointer";
    svgDelImg.setAttribute("height", delImageSize);
    svgDelImg.setAttribute("width", delImageSize);

    svgGroup.appendChild(svgDelImg);

    // Add the whole group: bounding rectangle, image generated from the text and Delete image to canvas.
    canvas.appendChild(svgGroup);

    // Create hidden additional image. When it's loaded its width and height are set to svgRect and svgTextImg elements.
    var $hiddenImg = $("<img>", {
        id: "hidden-img" + modelItem.id,
        src: basePath + "api/Image/ModelItem/" + modelItem.id + "/" + modelItem.getUrl()
    });

    // Add created image to hidden area.
    $("#hidden-images").append($hiddenImg);

    // Attach events.
    $($hiddenImg).on("load", onLoadImage);
    $(svgDelImg).on("click", onClickDelete);
    $(svgTextImg).on("mousedown", onMoveStart);
    $(svgTextImg).on("mouseup", onMoveEnd);
    $(svgTextImg).on("click", onClickImage);
    $(svgTextImg).on("touchstart", onMoveStart);
    $(svgTextImg).on("touchend", onMoveEnd);

    // Add item to an array.
    modelItems.push(modelItem);

    // Select added item: bounding rectangle and Delete image are visible.
    selectItem(modelItem.id);
}

After adding the text we get the following HTML output:

Image 10

In this example we have SVG container with source image: id=model1424. To show generated image four elements are used:

<g id=img-group1666> - groups rect and two image elements;
<rect id=rect1666> - bounding rectangle. It shows that image is selected;
<image  id=img1666> - image generated from the text;
<image  id=del1666> - serves as a delete button;

Image movement

The click-and-drag functionality is split into next events:  mousedown, mousemove, mouseupm, mouseout or touchstart, touchmove, touchend for touch screen devices. The first is the click, which is triggered when the left mouse button is pressed down while the cursor is over an image or when a touch point is placed on the image:

JavaScript
// mousedown and touchstart event handlers.
function onMoveStart(e) {

    // Needed for Firefox to allow dragging correctly
    e.preventDefault();

    if (e.type === "touchstart") {

        // Attach touchmove event handler
        $(e.target).on("touchmove", onMove);

        // Save the initial touch coordinates 
        mouseStart = getPoint(e.originalEvent.touches[0]);

    }
    else {

        // Attach mousemove and mouseout event handlers
        $(e.target).on("mousemove", onMove).on("mouseout", onMoveEnd);

        // Save the initial mouse coordinates
        mouseStart = getPoint(e);
    }

    // Save top and left position of the image.
    elementStart = {
        x: e.target["x"].animVal.value,
        y: e.target["y"].animVal.value
    };

    // Show bounding rectangle and Delete image. Set values for controls in Control Panel for selected modelItem.
    selectItem(e.target.id.substring(3));
}

The function that deals with moving the image:

JavaScript
// mousemove and touchmove event handlers.
function onMove(e) {

    // Get digital part of the image id.
    var id = e.target.id.substring(3);

    // Get current mouse or touch coordinates.
    var svgPoint = (e.type === "mousemove") ? getPoint(e) : getPoint(e.originalEvent.touches[0]);

    svgPoint.x = svgPoint.x - mouseStart.x;
    svgPoint.y = svgPoint.y - mouseStart.y;

    var m = e.target.getTransformToElement(canvas).inverse();

    m.e = m.f = 0;
    svgPoint = svgPoint.matrixTransform(m);

    // Set new position for image, bounding rectangle and Delete image.
    $("#img" + id).attr({
        "x": elementStart.x + svgPoint.x,
        "y": elementStart.y + svgPoint.y
    });
    $("#rect" + id).attr({
        "x": elementStart.x + svgPoint.x,
        "y": elementStart.y + svgPoint.y
    });
    $("#del" + id).attr({
        "x": elementStart.x + svgPoint.x + parseInt($("#img" + id).attr("width")),
        "y": elementStart.y + svgPoint.y - delImageSize
    });

    if (selectedItem != null) {

        selectedItem.positionLeft = Math.round(elementStart.x + svgPoint.x);
        selectedItem.positionTop = Math.round(elementStart.y + svgPoint.y);
    }
}

Events mouseup and mouseout or touchend are used to detect when the user stops moving the image:

JavaScript
// mouseup, mouseout and touchend event handlers.
function onMoveEnd(e) {

    if (e.type === "touchend") {

        // Detach touchmove event handler
        $(e.target).off("touchmove", onMove);
    }
    else {

        // Detach mousemove and mouseout event handlers
        $(e.target).off("mousemove", onMove).off("mouseout", onMoveEnd);
    }

    if (selectedItem != null) {
        selectedItem.updateDatabase();
    }
}

Acknowledgments

Thanks to Shao Voon Wong for his articles: Outline Text and Outline Text Part 2. His library does the main job in this application.

Thanks to Rod Stephens for his article Rotate images by an arbitrary angle in C#, which I used in my app.

Thanks to Brian Grinstead for his The No Hassle JavaScript Colorpicker.

History

  • 06.03.2016 - Initial version released. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)