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:
function uploadFile(files) {
var data = new FormData();
if (files.length > 0) {
disableControls();
$("#file-placeholder").attr("src", basePath + "Content/Images/image-loading.gif");
data.append("UploadedImage", files[0]);
$.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:
function addModel(modelId, modelWidth, modelHeight) {
id = modelId;
width = modelWidth;
height = modelHeight;
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);
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");
}
$("#form-save-result").attr("action", basePath + "api/Image/Result/" + id);
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) {
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:
function addModelItem(modelItem) {
var svgGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
svgGroup.setAttribute("id", "img-group" + modelItem.id);
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);
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);
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);
canvas.appendChild(svgGroup);
var $hiddenImg = $("<img>", {
id: "hidden-img" + modelItem.id,
src: basePath + "api/Image/ModelItem/" + modelItem.id + "/" + modelItem.getUrl()
});
$("#hidden-images").append($hiddenImg);
$($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);
modelItems.push(modelItem);
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:
function onMoveStart(e) {
e.preventDefault();
if (e.type === "touchstart") {
$(e.target).on("touchmove", onMove);
mouseStart = getPoint(e.originalEvent.touches[0]);
}
else {
$(e.target).on("mousemove", onMove).on("mouseout", onMoveEnd);
mouseStart = getPoint(e);
}
elementStart = {
x: e.target["x"].animVal.value,
y: e.target["y"].animVal.value
};
selectItem(e.target.id.substring(3));
}
The function that deals with moving the image:
function onMove(e) {
var id = e.target.id.substring(3);
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);
$("#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:
function onMoveEnd(e) {
if (e.type === "touchend") {
$(e.target).off("touchmove", onMove);
}
else {
$(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.