Add Text to Image - Sample Application






4.87/5 (25 votes)
Small application that allows text to be added to an image for annotation or to create greeting cards using ASP.NET MVC and jQuery
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:
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:
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:
AddTextToImage.Data
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
Project AddTextToImage.Domain contains POCO entities for the application. Diagram bellow shows these classes:
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
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
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:
<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:
// 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:
// 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:
<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:
$("#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:
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:
// 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:
// 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:
// 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.