├── host.json
├── assets
├── angry.png
├── happy.png
├── neutral.png
└── surprised.png
├── local.settings.json
├── azuredeploy.parameters.json
├── client
├── package.json
├── index.html
└── public
│ ├── css
│ └── main.css
│ └── js
│ └── codercards.js
├── CardGenerator
├── function.json
├── run.csx
└── image-lib.csx
├── CoderCards.sln
├── LICENSE
├── CoderCards.funproj
├── README.md
├── azuredeploy.json
└── .gitignore
/host.json:
--------------------------------------------------------------------------------
1 | { }
--------------------------------------------------------------------------------
/assets/angry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/options/CoderCards/master/assets/angry.png
--------------------------------------------------------------------------------
/assets/happy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/options/CoderCards/master/assets/happy.png
--------------------------------------------------------------------------------
/assets/neutral.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/options/CoderCards/master/assets/neutral.png
--------------------------------------------------------------------------------
/assets/surprised.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/options/CoderCards/master/assets/surprised.png
--------------------------------------------------------------------------------
/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "AzureWebJobsStorage": "",
5 | "AzureWebJobsDashboard": "",
6 | "input-container": "local-card-input",
7 | "output-container": "local-card-output",
8 | "ROOT": ".",
9 | "SITE_PATH": "."
10 | },
11 | "ConnectionStrings": {}
12 | }
--------------------------------------------------------------------------------
/azuredeploy.parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "siteName": {
6 | "value": "cf-cardGenerator"
7 | },
8 | "emotionAPIKey": {
9 | "value": ""
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "run": "live-server",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "dropzone": "^4.3.0"
14 | },
15 | "devDependencies": {
16 | "live-server":"1.2.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/CardGenerator/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "name": "image",
5 | "type": "blobTrigger",
6 | "direction": "in",
7 | "path": "%input-container%/{filename}.jpg",
8 | "connection": "AzureWebJobsDashboard"
9 | },
10 | {
11 | "type": "blob",
12 | "name": "outputBlob",
13 | "path": "%output-container%/{filename}.jpg",
14 | "connection": "AzureWebJobsDashboard",
15 | "direction": "out"
16 | }
17 | ],
18 | "disabled": false
19 | }
--------------------------------------------------------------------------------
/CoderCards.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.25420.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{33AD0EE8-D99B-4E14-8CD9-3F56ABEF97A2}") = "CoderCards", "CoderCards.funproj", "{1F94C537-F560-4249-BF17-D34674FBE175}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {1F94C537-F560-4249-BF17-D34674FBE175}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {1F94C537-F560-4249-BF17-D34674FBE175}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {1F94C537-F560-4249-BF17-D34674FBE175}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {1F94C537-F560-4249-BF17-D34674FBE175}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Donna Malayeri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CoderCards.funproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 14.0
5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
6 |
7 |
8 |
9 |
10 |
11 | 2.0
12 | 1f94c537-f560-4249-bf17-d34674fbe175
13 | {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
14 | Properties
15 | CoderCards
16 | CoderCards
17 | v4.5.2
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | title
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
1. Upload a picture of yourself
30 |
43 |
2. Your coder profile will appear below
44 |
45 |
46 |
47 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/client/public/css/main.css:
--------------------------------------------------------------------------------
1 | html {
2 | min-height: 100%;
3 | }
4 |
5 | body {
6 | min-height: 100%;
7 | margin-left: 50px;
8 | margin-right: 50px;
9 | font-family: Segoe UI,Frutiger,Frutiger Linotype,Dejavu Sans,Helvetica Neue,Arial,sans-serif;
10 | }
11 |
12 | .content {
13 | grid-area: content;
14 | }
15 |
16 | .header {
17 | grid-area: header;
18 | }
19 |
20 | .footer {
21 | grid-area: footer;
22 | }
23 |
24 | h1 {
25 | margin: 0;
26 | }
27 |
28 | .wrapper {
29 | height: 100%;
30 | display: grid;
31 | grid-gap: 10px;
32 | grid-template-rows: auto auto 150px;
33 | grid-template-columns: auto;
34 | grid-template-areas:
35 | "header"
36 | "content"
37 | "footer";
38 | background-color: #fff;
39 | color: #444;
40 | }
41 |
42 |
43 | .box {
44 | font-size: 150%;
45 | }
46 |
47 | .header,
48 | .footer {
49 | }
50 |
51 | #imageUploader {
52 | border-radius: 5px;
53 | border-style: dashed;
54 | border-color: black;
55 | border-width: 1px;
56 | min-height: 100px;
57 | }
58 |
59 | .dropzone { border: 2px dashed #0087F7; border-radius: 5px; background: white; }
60 | .dropzone .dz-message { font-weight: 400; }
61 |
62 | #images {
63 | display: flex;
64 | justify-content: center;
65 | min-height: 300px;
66 | flex-wrap: wrap;
67 | }
68 |
69 | .card {
70 | margin: 5px;
71 | height: 300px;
72 | width: 216px;
73 | padding: 2px;
74 | border: 2px solid black;
75 | border-radius: 5px;
76 | display: flex;
77 | align-items: center;
78 | justify-content: center;
79 | }
80 |
81 | .loading {
82 | -webkit-animation: loadingAnimation 1s 20 ease;
83 | -moz-animation: loadingAnimation 1s 20 ease;
84 | -o-animation: loadingAnimation 1s 20 ease;
85 | }
86 |
87 | @-webkit-keyframes loadingAnimation {
88 | from { -webkit-transform: rotate(4deg) scale(1) skew(1deg) translate(10px); }
89 | to { -webkit-transform: rotate(360deg) scale(0.795) skew(1deg) translate(0px); }
90 | }
91 | @-moz-keyframes loadingAnimation {
92 | from { -moz-transform: rotate(4deg) scale(1) skew(1deg) translate(10px); }
93 | to { -moz-transform: rotate(360deg) scale(0.795) skew(1deg) translate(0px); }
94 | }
95 | @-o-keyframes loadingAnimation {
96 | from { -o-transform: rotate(4deg) scale(1) skew(1deg) translate(10px); }
97 | to { -o-transform: rotate(360deg) scale(0.795) skew(1deg) translate(0px); }
98 | }
99 |
100 | .pulse {
101 | -webkit-animation: pulseAnimation 1s infinite ease;
102 | -moz-animation: pulseAnimation 1s infinite ease;
103 | -o-animation: pulseAnimation 1s infinite ease;
104 | }
105 | @-webkit-keyframes pulseAnimation {
106 | from { -webkit-transform: rotate(0deg) scale(1) skew(0deg) translate(0px, 10px); }
107 | to { -webkit-transform: rotate(0deg) scale(1.5) skew(0deg) translate(0px, -10px); }
108 | }
109 | @-moz-keyframes pulseAnimation {
110 | from { -moz-transform: rotate(0deg) scale(1) skew(0deg) translate(0px, 10px); }
111 | to { -moz-transform: rotate(0deg) scale(1.5) skew(0deg) translate(0px, -10px); }
112 | }
113 | @-o-keyframes pulseAnimation {
114 | from { -o-transform: rotate(0deg) scale(1) skew(0deg) translate(0px, 10px); }
115 | to { -o-transform: rotate(0deg) scale(1.5) skew(0deg) translate(0px, -10px); }
116 | }
117 |
118 |
--------------------------------------------------------------------------------
/client/public/js/codercards.js:
--------------------------------------------------------------------------------
1 | console.log("loading");
2 | // After DOM is loaded set up Dropzone/etc.
3 | function autorun() {
4 | console.log("Dropzone configured");
5 |
6 | var myDropzone = new Dropzone("form#dropzone", {
7 | autoQueue: false,
8 | url: (files) => {
9 | return `https://chrande-codercards.azurewebsites.net/upload/${files[0].name}`;
10 | },
11 | headers: {
12 | "x-ms-blob-type": "BlockBlob",
13 | "Content-Type": "image/jpeg"
14 | },
15 | clickable: true,
16 | method: "PUT",
17 | init: function () {
18 | this.on("success", function (file, response) {
19 | console.log(file.name);
20 | });
21 | this.on("addedfile", function (file) {
22 | uploadFile(file);
23 | const that = this;
24 | setTimeout(() => {
25 | that.removeFile(file);
26 | }, 1000);
27 | enqueueCard(file.name);
28 | });
29 | }
30 |
31 | });
32 | }
33 | if (window.addEventListener) window.addEventListener("load", autorun, false);
34 | else if (window.attachEvent) window.attachEvent("onload", autorun);
35 | else window.onload = autorun;
36 |
37 | function uploadFiles() {
38 | const files = document.getElementById("fileToUpload").files;
39 | for (const file of files) {
40 | uploadFile(file);
41 | }
42 | }
43 |
44 | function enqueueCard(name) {
45 | const images = document.getElementById("images");
46 | const imageDiv = document.createElement("div");
47 | imageDiv.classList.add("card");
48 | const loading = document.createElement("span");
49 | loading.classList.add("loading");
50 | loading.classList.add("fa");
51 | loading.classList.add("fa-refresh");
52 | imageDiv.appendChild(loading);
53 | images.appendChild(imageDiv);
54 | imageDiv.id = `image-${name}`;
55 | let interval = setInterval(() => {
56 | console.log("Looking for file");
57 | getImage(name, (done) => {
58 | if (done) {
59 | console.log("Clearing interval");
60 | clearInterval(interval);
61 | }
62 | });
63 | }, 10000);
64 | }
65 |
66 | function uploadFile(file) {
67 | const xhr = new XMLHttpRequest();
68 | xhr.open("PUT", `https://chrande-codercards.azurewebsites.net/upload/${file.name}`);
69 | xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob');
70 | xhr.setRequestHeader('Content-Type', 'image/jpeg');
71 | xhr.send(file);
72 | }
73 |
74 | function getImage(name, cb) {
75 | const xhr = new XMLHttpRequest();
76 | const imageUrl = `https://chrande-codercards.azurewebsites.net/card/${name}`;
77 | xhr.open("GET", imageUrl);
78 | xhr.responseType = "blob";
79 | xhr.onload = (e) => {
80 | if (xhr.status != 200) {
81 | console.log("Image does not exist yet");
82 | return cb(false);
83 | }
84 | let images = document.getElementById(`image-${name}`);
85 | images.innerHTML = '';
86 | let image = document.createElement('img');
87 | image.src = imageUrl;
88 | image.setAttribute("height", "300px");
89 | images.appendChild(image);
90 | console.log("Image created");
91 | return cb(true);
92 | }
93 | var results = xhr.send();
94 | }
--------------------------------------------------------------------------------
/CardGenerator/run.csx:
--------------------------------------------------------------------------------
1 | #load "image-lib.csx"
2 |
3 | using System.Net;
4 | using System.Net.Http;
5 | using System.Net.Http.Headers;
6 | using Newtonsoft.Json;
7 | using System.IO;
8 | using System.Drawing;
9 | using System.Drawing.Imaging;
10 |
11 | private const string EMOTION_API_URI = "https://westus.api.cognitive.microsoft.com/emotion/v1.0/recognize";
12 | private const string EMOTION_API_KEY_NAME = "EmotionAPIKey";
13 | private const string ASSETS_FOLDER = "assets";
14 |
15 | public static async Task Run(byte[] image, string filename, Stream outputBlob, TraceWriter log)
16 | {
17 | string result = await CallEmotionAPI(image);
18 | log.Info(result);
19 |
20 | if (String.IsNullOrEmpty(result)) {
21 | log.Error("No result from Emotion API");
22 | return;
23 | }
24 |
25 | var imageData = JsonConvert.DeserializeObject(result);
26 |
27 | // hardcoded version if not calling the emotion APIs
28 | // var imageData = new Face[] {
29 | // new Face() {
30 | // FaceRectangle = new FaceRectangle(),
31 | // Scores = new Scores() { Happiness = 1.0 }
32 | // }
33 | // };
34 |
35 | if (imageData.Length == 0) {
36 | log.Error("No face detected in image");
37 | return;
38 | }
39 |
40 | double score = 0;
41 | var faceData = imageData[0]; // assume exactly one face
42 | var card = GetCardImageAndScores(faceData.Scores, out score);
43 |
44 | var personInfo = GetNameAndTitle(filename); // extract name and title from filename
45 | MergeCardImage(card, image, personInfo, score);
46 |
47 | SaveAsJpeg(card, outputBlob);
48 | }
49 |
50 | public static Tuple GetNameAndTitle(string filename)
51 | {
52 | string[] words = filename.Split('-');
53 |
54 | return words.Length > 1 ? Tuple.Create(words[0], words[1]) : Tuple.Create("", "");
55 | }
56 |
57 | static Image GetCardImageAndScores(Scores scores, out double score)
58 | {
59 | NormalizeScores(scores);
60 |
61 | var cardBack = "neutral.png";
62 | score = scores.Neutral;
63 | const int angerBoost = 2, happyBoost = 4;
64 |
65 | if (scores.Surprise > 10) {
66 | cardBack = "surprised.png";
67 | score = scores.Surprise;
68 | }
69 | else if (scores.Anger > 10) {
70 | cardBack = "angry.png";
71 | score = scores.Anger * angerBoost;
72 | }
73 | else if (scores.Happiness > 50) {
74 | cardBack = "happy.png";
75 | score = scores.Happiness * happyBoost;
76 | }
77 |
78 | return Image.FromFile(GetFullImagePath(cardBack));
79 | }
80 |
81 | static async Task CallEmotionAPI(byte[] image)
82 | {
83 | var client = new HttpClient();
84 |
85 | var content = new StreamContent(new MemoryStream(image));
86 | var key = Environment.GetEnvironmentVariable(EMOTION_API_KEY_NAME);
87 |
88 | client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", key);
89 | content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
90 | var httpResponse = await client.PostAsync(EMOTION_API_URI, content);
91 |
92 | if (httpResponse.StatusCode == HttpStatusCode.OK) {
93 | return await httpResponse.Content.ReadAsStringAsync();
94 | }
95 |
96 | return null;
97 | }
98 |
99 | static string GetFullImagePath(string filename)
100 | {
101 | var path = Path.Combine(
102 | Environment.GetEnvironmentVariable("ROOT"),
103 | Environment.GetEnvironmentVariable("SITE_PATH"),
104 | ASSETS_FOLDER,
105 | filename);
106 |
107 | return Path.GetFullPath(path);
108 | }
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CoderCards
2 |
3 | 
4 |
5 | ## About the demo
6 |
7 | * The function is triggered when a new .jpg file appears in the container `card-input`.
8 |
9 | * Based on an input image, one of 4 card templates is chosen based on emotion
10 |
11 | * The **filename** of the input image is used to draw the name and title on the card.
12 |
13 | * **Filename must be in the form `Name of person-Title of person.jpg`**
14 |
15 | * A score based on the predominant emotion (e.g., anger, happiness) is drawn on the top
16 |
17 | * The card is written to the output blob container `card-output`
18 |
19 | ## Running the demo
20 |
21 | ### Demo Setup
22 |
23 | 1. Fork the repo into your own GitHub
24 |
25 | 2. Ensure that you've authorized at least one Azure Web App on your subscription to connect to your GitHub account. To learn more, see [Continuous Deployment to Azure App Service](https://azure.microsoft.com/en-us/documentation/articles/app-service-continuous-deployment/).
26 |
27 | 3. Click the Deploy to Azure button above.
28 |
29 | * Enter a sitename **with no dashes**.
30 |
31 | * A storage account will be created with the same site name (and Storage does not allow dashes in account names).
32 |
33 | * Enter the API key from the Cognitive Services Emotion API (https://www.microsoft.com/cognitive-services/en-us/emotion-api)
34 |
35 | 4. Open the Function App you just deployed. Go to Function App settings -> Configure Continuous Integration. In the command bar, select **Disconnect**.
36 |
37 | 5. Close and reopen the Function App. Verify that you can edit code in CardGenerator -> Develop.
38 |
39 | 6. In [Azure Storage Explorer](http://storageexplorer.com/), navigate to the storage account with the same name as your Function App.
40 |
41 | * Create the blob container `card-input`
42 |
43 | * Output from the function will be in `card-output`, but you don't need to create this container explicitly.
44 |
45 | ### Running the demo
46 |
47 | 1. Choose images that are **square** with a filename in the form `Name of person-Title of person.jpg`. The filename is parsed to produce text on the card.
48 |
49 | 2. Drop images into the `card-input` container. Once the function runs, you'll see generated cards in `card-output`.
50 |
51 | ### Notes
52 |
53 | * The demo uses System.Drawing, which is NOT recommended for production apps. To learn more, see [Why you should not use System\.Drawing from ASP\.NET applications](http://www.asprangers.com/post/2012/03/23/Why-you-should-not-use-SystemDrawing-from-ASPNET-applications.aspx).
54 |
55 | * Happy faces get a multiplier of 4, angry gets a multiplier of 2. I encourage you to tweak for maximum comedic effect!
56 |
57 | ### Talking points about Azure Functions
58 |
59 | * The code is triggered off a new blob in a container. We automatically get a binding for both the byte array and the blob name
60 |
61 | * The blob name is used to generate the text on the image
62 |
63 | * The input binding is just a byte array, which makes it easy to manipulate with memory streams (no need to create new ones)
64 |
65 | * Other binding types for C# are Stream, CloudBlockBlob, etc, which is very flexible.
66 |
67 | * The output binding is just a stream that you just write to
68 |
69 | * This is a very declarative model, details of binding are in a separate json file that can be edited manually or through the Integrate UX
70 |
71 | * In general, functions will scale based on the amount of events in input. A real system would probably use something like Azure Queue or Service Bus triggers, in order to track messages more closely.
72 |
--------------------------------------------------------------------------------
/azuredeploy.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "siteName": {
6 | "type": "string"
7 | },
8 | "location": {
9 | "type": "string",
10 | "allowedValues": [
11 | "Brazil South",
12 | "East US",
13 | "East US 2",
14 | "Central US",
15 | "North Central US",
16 | "South Central US",
17 | "West US",
18 | "West US 2"
19 | ],
20 | "defaultValue": "[resourceGroup().location]"
21 | },
22 | "emotionAPIKey": {
23 | "type": "string"
24 | },
25 | "repoUrl": {
26 | "type": "string"
27 | },
28 | "branch": {
29 | "type": "string",
30 | "defaultValue": "master",
31 | "allowedValues": [
32 | "master",
33 | "demo-start"
34 | ]
35 | }
36 | },
37 | "variables": {
38 | "appServicePlanName": "[parameters('siteName')]",
39 | "storageAccountName": "[toLower(parameters('siteName'))]",
40 | "storageAccountType": "Standard_LRS",
41 | "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
42 | "storageLocation": "[parameters('location')]"
43 | },
44 | "resources": [
45 | {
46 | "type": "Microsoft.Storage/storageAccounts",
47 | "name": "[variables('storageAccountName')]",
48 | "apiVersion": "2015-05-01-preview",
49 | "location": "[variables('storageLocation')]",
50 | "properties": {
51 | "accountType": "[variables('storageAccountType')]"
52 | }
53 | },
54 | {
55 | "apiVersion": "2015-08-01",
56 | "name": "[variables('appServicePlanName')]",
57 | "type": "Microsoft.Web/serverfarms",
58 | "location": "[parameters('location')]",
59 | "sku": {
60 | "name": "B1",
61 | "tier": "Basic"
62 | },
63 | "properties": {}
64 | },
65 | {
66 | "apiVersion": "2015-08-01",
67 | "name": "[parameters('siteName')]",
68 | "type": "Microsoft.Web/sites",
69 | "kind": "functionapp",
70 | "location": "[parameters('location')]",
71 | "dependsOn": [
72 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
73 | "[resourceId('Microsoft.Web/serverfarms', parameters('siteName'))]"
74 | ],
75 | "properties": {
76 | "serverFarmId": "[variables('appServicePlanName')]",
77 | "alwaysOn": true
78 | },
79 | "resources": [
80 | {
81 | "apiVersion": "2015-08-01",
82 | "name": "appsettings",
83 | "type": "config",
84 | "dependsOn": [
85 | "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]",
86 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
87 | ],
88 | "properties": {
89 | "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]",
90 | "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]",
91 | "FUNCTIONS_EXTENSION_VERSION": "~0.5",
92 | "EmotionAPIKey": "[parameters('emotionAPIKey')]"
93 | }
94 | },
95 | {
96 | "apiVersion": "2015-04-01",
97 | "name": "web",
98 | "type": "sourcecontrols",
99 | "dependsOn": [
100 | "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]",
101 | "[concat('Microsoft.Web/Sites/', parameters('siteName'), '/config/appsettings')]"
102 | ],
103 | "properties": {
104 | "repoUrl": "[parameters('repoUrl')]",
105 | "branch": "[parameters('branch')]",
106 | "IsManualIntegration": true
107 | }
108 | }
109 | ]
110 | }
111 | ]
112 | }
113 |
--------------------------------------------------------------------------------
/CardGenerator/image-lib.csx:
--------------------------------------------------------------------------------
1 | #r "Newtonsoft.Json"
2 | #r "System.Drawing"
3 |
4 | using System;
5 | using System.IO;
6 | using System.Drawing;
7 | using System.Drawing.Imaging;
8 |
9 | #region POCO definitions
10 | public class Face
11 | {
12 | public FaceRectangle FaceRectangle { get; set; }
13 | public Scores Scores { get; set; }
14 | }
15 |
16 | public class FaceRectangle
17 | {
18 | public int Left { get; set; }
19 | public int Top { get; set; }
20 | public int Width { get; set; }
21 | public int Height { get; set; }
22 | }
23 |
24 | public class Scores
25 | {
26 | public double Anger { get; set; }
27 | public double Contempt { get; set; }
28 | public double Disgust { get; set; }
29 | public double Fear { get; set; }
30 | public double Happiness { get; set; }
31 | public double Neutral { get; set; }
32 | public double Sadness { get; set; }
33 | public double Surprise { get; set; }
34 | }
35 | #endregion
36 |
37 | #region Pixel locations
38 | const int TopLeftFaceX = 85;
39 | const int TopLeftFaceY = 187;
40 | const int FaceRect = 648;
41 | const int NameTextX = 56;
42 | const int NameTextY = 60;
43 | const int TitleTextX = 108;
44 | const int NameWidth = 430;
45 | const int ScoreX = 654;
46 | const int ScoreY = 70;
47 | const int ScoreWidth = 117;
48 | #endregion
49 |
50 | #region Font info
51 | const string FontFamilyName = "Microsoft Sans Serif";
52 | const int NameFontSize = 38;
53 | const int TitleFontSize = 30;
54 | const short ScoreFontSize = 55;
55 | #endregion
56 |
57 | // This code uses System.Drawing to merge images and render text on the image
58 | // System.Drawing SHOULD NOT be used in a production application
59 | // It is not supported in server scenarios and is used here as a demo only!
60 | public static Image MergeCardImage(Image card, byte[] imageBytes, Tuple personInfo, double score)
61 | {
62 | using (MemoryStream faceImageStream = new MemoryStream(imageBytes))
63 | {
64 | using (Image faceImage = Image.FromStream(faceImageStream, true))
65 | {
66 | using (Graphics g = Graphics.FromImage(card))
67 | {
68 | g.DrawImage(faceImage, TopLeftFaceX, TopLeftFaceY, FaceRect, FaceRect);
69 | RenderText(g, NameFontSize, NameTextX, NameTextY, NameWidth, personInfo.Item1);
70 | RenderText(g, TitleFontSize, NameTextX + 4, TitleTextX, NameWidth, personInfo.Item2); // second line seems to need some left padding
71 |
72 | RenderScore(g, ScoreX, ScoreY, ScoreWidth, score.ToString());
73 | }
74 |
75 | return card;
76 | }
77 | }
78 | }
79 |
80 | public static void RenderScore(Graphics graphics, int xPos, int yPos, int width, string score)
81 | {
82 | var brush = new SolidBrush(Color.Black);
83 | var font = CreateFont(ScoreFontSize);
84 | SizeF size = graphics.MeasureString(score, font);
85 |
86 | graphics.DrawString(score, font, brush, width - size.Width + xPos, yPos);
87 | }
88 |
89 | private static Font CreateFont(int fontSize)
90 | {
91 | return new Font(FontFamilyName, fontSize, FontStyle.Bold, GraphicsUnit.Pixel);
92 | }
93 |
94 | public static void RenderText(Graphics graphics, int fontSize, int xPos, int yPos, int width, string text)
95 | {
96 | var brush = new SolidBrush(Color.Black);
97 | var font = CreateFont(fontSize);
98 | SizeF size;
99 |
100 | do
101 | {
102 | font = CreateFont(fontSize--);
103 | size = graphics.MeasureString(text, font);
104 | }
105 | while (size.Width > width);
106 |
107 | graphics.DrawString(text, font, brush, xPos, yPos);
108 | }
109 |
110 | // save with higher quality than the default, to avoid jpeg artifacts on the text and numbers
111 | public static void SaveAsJpeg(Image image, Stream outputStream)
112 | {
113 | var jpgEncoder = GetEncoder(ImageFormat.Jpeg);
114 | var qualityEncoder = System.Drawing.Imaging.Encoder.Quality;
115 | var encoderParams = new EncoderParameters(1);
116 | encoderParams.Param[0] = new EncoderParameter(qualityEncoder, 90L);
117 |
118 | image.Save(outputStream, jpgEncoder, encoderParams);
119 | }
120 |
121 | static ImageCodecInfo GetEncoder(ImageFormat format)
122 | {
123 | ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
124 | foreach (ImageCodecInfo codec in codecs)
125 | {
126 | if (codec.FormatID == format.Guid)
127 | {
128 | return codec;
129 | }
130 | }
131 | return null;
132 | }
133 |
134 | public static double RoundScore(double score) => Math.Round(score * 100);
135 |
136 | public static void NormalizeScores(Scores scores)
137 | {
138 | scores.Anger = RoundScore(scores.Anger);
139 | scores.Happiness = RoundScore(scores.Happiness);
140 | scores.Neutral = RoundScore(scores.Neutral);
141 | scores.Sadness = RoundScore(scores.Sadness);
142 | scores.Surprise = RoundScore(scores.Surprise);
143 | }
144 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/visualstudiocode,windows,osx,linux,visualstudio,csharp
3 |
4 | ### VisualStudioCode ###
5 | .vscode/*
6 | !.vscode/settings.json
7 | !.vscode/tasks.json
8 | !.vscode/launch.json
9 |
10 |
11 | ### Windows ###
12 | # Windows image file caches
13 | Thumbs.db
14 | ehthumbs.db
15 |
16 | # Folder config file
17 | Desktop.ini
18 |
19 | # Recycle Bin used on file shares
20 | $RECYCLE.BIN/
21 |
22 | # Windows Installer files
23 | *.cab
24 | *.msi
25 | *.msm
26 | *.msp
27 |
28 | # Windows shortcuts
29 | *.lnk
30 |
31 |
32 | ### OSX ###
33 | *.DS_Store
34 | .AppleDouble
35 | .LSOverride
36 |
37 | # Icon must end with two \r
38 | Icon
39 |
40 |
41 | # Thumbnails
42 | ._*
43 |
44 | # Files that might appear in the root of a volume
45 | .DocumentRevisions-V100
46 | .fseventsd
47 | .Spotlight-V100
48 | .TemporaryItems
49 | .Trashes
50 | .VolumeIcon.icns
51 | .com.apple.timemachine.donotpresent
52 |
53 | # Directories potentially created on remote AFP share
54 | .AppleDB
55 | .AppleDesktop
56 | Network Trash Folder
57 | Temporary Items
58 | .apdisk
59 |
60 |
61 | ### Linux ###
62 | *~
63 |
64 | # temporary files which can be created if a process still has a handle open of a deleted file
65 | .fuse_hidden*
66 |
67 | # KDE directory preferences
68 | .directory
69 |
70 | # Linux trash folder which might appear on any partition or disk
71 | .Trash-*
72 |
73 |
74 | ### Csharp ###
75 | ## Ignore Visual Studio temporary files, build results, and
76 | ## files generated by popular Visual Studio add-ons.
77 |
78 | # User-specific files
79 | *.suo
80 | *.user
81 | *.userosscache
82 | *.sln.docstates
83 |
84 | # User-specific files (MonoDevelop/Xamarin Studio)
85 | *.userprefs
86 |
87 | # Build results
88 | [Dd]ebug/
89 | [Dd]ebugPublic/
90 | [Rr]elease/
91 | [Rr]eleases/
92 | x64/
93 | x86/
94 | bld/
95 | [Bb]in/
96 | [Oo]bj/
97 | [Ll]og/
98 |
99 | # Visual Studio 2015 cache/options directory
100 | .vs/
101 | # Uncomment if you have tasks that create the project's static files in wwwroot
102 | #wwwroot/
103 |
104 | # MSTest test Results
105 | [Tt]est[Rr]esult*/
106 | [Bb]uild[Ll]og.*
107 |
108 | # NUNIT
109 | *.VisualState.xml
110 | TestResult.xml
111 |
112 | # Build Results of an ATL Project
113 | [Dd]ebugPS/
114 | [Rr]eleasePS/
115 | dlldata.c
116 |
117 | # DNX
118 | project.lock.json
119 | project.fragment.lock.json
120 | artifacts/
121 |
122 | *_i.c
123 | *_p.c
124 | *_i.h
125 | *.ilk
126 | *.meta
127 | *.obj
128 | *.pch
129 | *.pdb
130 | *.pgc
131 | *.pgd
132 | *.rsp
133 | *.sbr
134 | *.tlb
135 | *.tli
136 | *.tlh
137 | *.tmp
138 | *.tmp_proj
139 | *.log
140 | *.vspscc
141 | *.vssscc
142 | .builds
143 | *.pidb
144 | *.svclog
145 | *.scc
146 |
147 | # Chutzpah Test files
148 | _Chutzpah*
149 |
150 | # Visual C++ cache files
151 | ipch/
152 | *.aps
153 | *.ncb
154 | *.opendb
155 | *.opensdf
156 | *.sdf
157 | *.cachefile
158 | *.VC.db
159 | *.VC.VC.opendb
160 |
161 | # Visual Studio profiler
162 | *.psess
163 | *.vsp
164 | *.vspx
165 | *.sap
166 |
167 | # TFS 2012 Local Workspace
168 | $tf/
169 |
170 | # Guidance Automation Toolkit
171 | *.gpState
172 |
173 | # ReSharper is a .NET coding add-in
174 | _ReSharper*/
175 | *.[Rr]e[Ss]harper
176 | *.DotSettings.user
177 |
178 | # JustCode is a .NET coding add-in
179 | .JustCode
180 |
181 | # TeamCity is a build add-in
182 | _TeamCity*
183 |
184 | # DotCover is a Code Coverage Tool
185 | *.dotCover
186 |
187 | # Visual Studio code coverage results
188 | *.coverage
189 | *.coveragexml
190 |
191 | # NCrunch
192 | _NCrunch_*
193 | .*crunch*.local.xml
194 | nCrunchTemp_*
195 |
196 | # MightyMoose
197 | *.mm.*
198 | AutoTest.Net/
199 |
200 | # Web workbench (sass)
201 | .sass-cache/
202 |
203 | # Installshield output folder
204 | [Ee]xpress/
205 |
206 | # DocProject is a documentation generator add-in
207 | DocProject/buildhelp/
208 | DocProject/Help/*.HxT
209 | DocProject/Help/*.HxC
210 | DocProject/Help/*.hhc
211 | DocProject/Help/*.hhk
212 | DocProject/Help/*.hhp
213 | DocProject/Help/Html2
214 | DocProject/Help/html
215 |
216 | # Click-Once directory
217 | publish/
218 |
219 | # Publish Web Output
220 | *.[Pp]ublish.xml
221 | *.azurePubxml
222 | # TODO: Comment the next line if you want to checkin your web deploy settings
223 | # but database connection strings (with potential passwords) will be unencrypted
224 | *.pubxml
225 | *.publishproj
226 |
227 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
228 | # checkin your Azure Web App publish settings, but sensitive information contained
229 | # in these scripts will be unencrypted
230 | PublishScripts/
231 |
232 | # NuGet Packages
233 | *.nupkg
234 | # The packages folder can be ignored because of Package Restore
235 | **/packages/*
236 | # except build/, which is used as an MSBuild target.
237 | !**/packages/build/
238 | # Uncomment if necessary however generally it will be regenerated when needed
239 | #!**/packages/repositories.config
240 | # NuGet v3's project.json files produces more ignoreable files
241 | *.nuget.props
242 | *.nuget.targets
243 |
244 | # Microsoft Azure Build Output
245 | csx/
246 | *.build.csdef
247 |
248 | # Microsoft Azure Emulator
249 | ecf/
250 | rcf/
251 |
252 | # Windows Store app package directories and files
253 | AppPackages/
254 | BundleArtifacts/
255 | Package.StoreAssociation.xml
256 | _pkginfo.txt
257 |
258 | # Visual Studio cache files
259 | # files ending in .cache can be ignored
260 | *.[Cc]ache
261 | # but keep track of directories ending in .cache
262 | !*.[Cc]ache/
263 |
264 | # Others
265 | ClientBin/
266 | ~$*
267 | *~
268 | *.dbmdl
269 | *.dbproj.schemaview
270 | *.jfm
271 | *.pfx
272 | *.publishsettings
273 | node_modules/
274 | orleans.codegen.cs
275 |
276 | # Since there are multiple workflows, uncomment next line to ignore bower_components
277 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
278 | #bower_components/
279 |
280 | # RIA/Silverlight projects
281 | Generated_Code/
282 |
283 | # Backup & report files from converting an old project file
284 | # to a newer Visual Studio version. Backup files are not needed,
285 | # because we have git ;-)
286 | _UpgradeReport_Files/
287 | Backup*/
288 | UpgradeLog*.XML
289 | UpgradeLog*.htm
290 |
291 | # SQL Server files
292 | *.mdf
293 | *.ldf
294 |
295 | # Business Intelligence projects
296 | *.rdl.data
297 | *.bim.layout
298 | *.bim_*.settings
299 |
300 | # Microsoft Fakes
301 | FakesAssemblies/
302 |
303 | # GhostDoc plugin setting file
304 | *.GhostDoc.xml
305 |
306 | # Node.js Tools for Visual Studio
307 | .ntvs_analysis.dat
308 |
309 | # Visual Studio 6 build log
310 | *.plg
311 |
312 | # Visual Studio 6 workspace options file
313 | *.opt
314 |
315 | # Visual Studio LightSwitch build output
316 | **/*.HTMLClient/GeneratedArtifacts
317 | **/*.DesktopClient/GeneratedArtifacts
318 | **/*.DesktopClient/ModelManifest.xml
319 | **/*.Server/GeneratedArtifacts
320 | **/*.Server/ModelManifest.xml
321 | _Pvt_Extensions
322 |
323 | # Paket dependency manager
324 | .paket/paket.exe
325 | paket-files/
326 |
327 | # FAKE - F# Make
328 | .fake/
329 |
330 | # JetBrains Rider
331 | .idea/
332 | *.sln.iml
333 |
334 | # CodeRush
335 | .cr/
336 |
337 | # Python Tools for Visual Studio (PTVS)
338 | __pycache__/
339 | *.pyc
340 |
341 |
342 | ### VisualStudio ###
343 | ## Ignore Visual Studio temporary files, build results, and
344 | ## files generated by popular Visual Studio add-ons.
345 |
346 | # User-specific files
347 | *.suo
348 | *.user
349 | *.userosscache
350 | *.sln.docstates
351 |
352 | # User-specific files (MonoDevelop/Xamarin Studio)
353 | *.userprefs
354 |
355 | # Build results
356 | [Dd]ebug/
357 | [Dd]ebugPublic/
358 | [Rr]elease/
359 | [Rr]eleases/
360 | x64/
361 | x86/
362 | bld/
363 | [Bb]in/
364 | [Oo]bj/
365 | [Ll]og/
366 |
367 | # Visual Studio 2015 cache/options directory
368 | .vs/
369 | # Uncomment if you have tasks that create the project's static files in wwwroot
370 | #wwwroot/
371 |
372 | # MSTest test Results
373 | [Tt]est[Rr]esult*/
374 | [Bb]uild[Ll]og.*
375 |
376 | # NUNIT
377 | *.VisualState.xml
378 | TestResult.xml
379 |
380 | # Build Results of an ATL Project
381 | [Dd]ebugPS/
382 | [Rr]eleasePS/
383 | dlldata.c
384 |
385 | # DNX
386 | project.lock.json
387 | project.fragment.lock.json
388 | artifacts/
389 |
390 | *_i.c
391 | *_p.c
392 | *_i.h
393 | *.ilk
394 | *.meta
395 | *.obj
396 | *.pch
397 | *.pdb
398 | *.pgc
399 | *.pgd
400 | *.rsp
401 | *.sbr
402 | *.tlb
403 | *.tli
404 | *.tlh
405 | *.tmp
406 | *.tmp_proj
407 | *.log
408 | *.vspscc
409 | *.vssscc
410 | .builds
411 | *.pidb
412 | *.svclog
413 | *.scc
414 |
415 | # Chutzpah Test files
416 | _Chutzpah*
417 |
418 | # Visual C++ cache files
419 | ipch/
420 | *.aps
421 | *.ncb
422 | *.opendb
423 | *.opensdf
424 | *.sdf
425 | *.cachefile
426 | *.VC.db
427 | *.VC.VC.opendb
428 |
429 | # Visual Studio profiler
430 | *.psess
431 | *.vsp
432 | *.vspx
433 | *.sap
434 |
435 | # TFS 2012 Local Workspace
436 | $tf/
437 |
438 | # Guidance Automation Toolkit
439 | *.gpState
440 |
441 | # ReSharper is a .NET coding add-in
442 | _ReSharper*/
443 | *.[Rr]e[Ss]harper
444 | *.DotSettings.user
445 |
446 | # JustCode is a .NET coding add-in
447 | .JustCode
448 |
449 | # TeamCity is a build add-in
450 | _TeamCity*
451 |
452 | # DotCover is a Code Coverage Tool
453 | *.dotCover
454 |
455 | # Visual Studio code coverage results
456 | *.coverage
457 | *.coveragexml
458 |
459 | # NCrunch
460 | _NCrunch_*
461 | .*crunch*.local.xml
462 | nCrunchTemp_*
463 |
464 | # MightyMoose
465 | *.mm.*
466 | AutoTest.Net/
467 |
468 | # Web workbench (sass)
469 | .sass-cache/
470 |
471 | # Installshield output folder
472 | [Ee]xpress/
473 |
474 | # DocProject is a documentation generator add-in
475 | DocProject/buildhelp/
476 | DocProject/Help/*.HxT
477 | DocProject/Help/*.HxC
478 | DocProject/Help/*.hhc
479 | DocProject/Help/*.hhk
480 | DocProject/Help/*.hhp
481 | DocProject/Help/Html2
482 | DocProject/Help/html
483 |
484 | # Click-Once directory
485 | publish/
486 |
487 | # Publish Web Output
488 | *.[Pp]ublish.xml
489 | *.azurePubxml
490 | # TODO: Comment the next line if you want to checkin your web deploy settings
491 | # but database connection strings (with potential passwords) will be unencrypted
492 | *.pubxml
493 | *.publishproj
494 |
495 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
496 | # checkin your Azure Web App publish settings, but sensitive information contained
497 | # in these scripts will be unencrypted
498 | PublishScripts/
499 |
500 | # NuGet Packages
501 | *.nupkg
502 | # The packages folder can be ignored because of Package Restore
503 | **/packages/*
504 | # except build/, which is used as an MSBuild target.
505 | !**/packages/build/
506 | # Uncomment if necessary however generally it will be regenerated when needed
507 | #!**/packages/repositories.config
508 | # NuGet v3's project.json files produces more ignoreable files
509 | *.nuget.props
510 | *.nuget.targets
511 |
512 | # Microsoft Azure Build Output
513 | csx/
514 | *.build.csdef
515 |
516 | # Microsoft Azure Emulator
517 | ecf/
518 | rcf/
519 |
520 | # Windows Store app package directories and files
521 | AppPackages/
522 | BundleArtifacts/
523 | Package.StoreAssociation.xml
524 | _pkginfo.txt
525 |
526 | # Visual Studio cache files
527 | # files ending in .cache can be ignored
528 | *.[Cc]ache
529 | # but keep track of directories ending in .cache
530 | !*.[Cc]ache/
531 |
532 | # Others
533 | ClientBin/
534 | ~$*
535 | *~
536 | *.dbmdl
537 | *.dbproj.schemaview
538 | *.jfm
539 | *.pfx
540 | *.publishsettings
541 | node_modules/
542 | orleans.codegen.cs
543 |
544 | # Since there are multiple workflows, uncomment next line to ignore bower_components
545 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
546 | #bower_components/
547 |
548 | # RIA/Silverlight projects
549 | Generated_Code/
550 |
551 | # Backup & report files from converting an old project file
552 | # to a newer Visual Studio version. Backup files are not needed,
553 | # because we have git ;-)
554 | _UpgradeReport_Files/
555 | Backup*/
556 | UpgradeLog*.XML
557 | UpgradeLog*.htm
558 |
559 | # SQL Server files
560 | *.mdf
561 | *.ldf
562 |
563 | # Business Intelligence projects
564 | *.rdl.data
565 | *.bim.layout
566 | *.bim_*.settings
567 |
568 | # Microsoft Fakes
569 | FakesAssemblies/
570 |
571 | # GhostDoc plugin setting file
572 | *.GhostDoc.xml
573 |
574 | # Node.js Tools for Visual Studio
575 | .ntvs_analysis.dat
576 |
577 | # Visual Studio 6 build log
578 | *.plg
579 |
580 | # Visual Studio 6 workspace options file
581 | *.opt
582 |
583 | # Visual Studio LightSwitch build output
584 | **/*.HTMLClient/GeneratedArtifacts
585 | **/*.DesktopClient/GeneratedArtifacts
586 | **/*.DesktopClient/ModelManifest.xml
587 | **/*.Server/GeneratedArtifacts
588 | **/*.Server/ModelManifest.xml
589 | _Pvt_Extensions
590 |
591 | # Paket dependency manager
592 | .paket/paket.exe
593 | paket-files/
594 |
595 | # FAKE - F# Make
596 | .fake/
597 |
598 | # JetBrains Rider
599 | .idea/
600 | *.sln.iml
601 |
602 | # CodeRush
603 | .cr/
604 |
605 | # Python Tools for Visual Studio (PTVS)
606 | __pycache__/
607 | *.pyc
608 |
609 | ### VisualStudio Patch ###
610 | build/
611 |
612 | ### ARM ###
613 | azuredeploy.parameters.json
--------------------------------------------------------------------------------