├── .deployment ├── .gitignore ├── AzureDeploy ├── AzureDeploy.deployproj ├── AzureDeploy.sln ├── Deploy-AzureResourceGroup.ps1 ├── Deployment.targets ├── azuredeploy.json └── azuredeploy.parameters.json ├── CoderCards.sln ├── CoderCards ├── CardGenerator.cs ├── CoderCards.csproj ├── ImageHelpers.cs ├── ImageHelpersXPlat.cs ├── Properties │ └── launchSettings.json ├── assets │ ├── angry.png │ ├── codercards.ai │ ├── happy.png │ ├── neutral.png │ └── surprised.png ├── host.json ├── local.settings.json └── proxies.json ├── CoderCardsClient ├── index.html └── package.json ├── LICENSE ├── README.md ├── function-bindings.png └── setup.py /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | project = CoderCards/CoderCards.csproj -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | /CoderCardsLibrary/appsettings.json 254 | /CoderCardsWebsite/appsettings.json 255 | /CoderCards/local.settings.json -------------------------------------------------------------------------------- /AzureDeploy/AzureDeploy.deployproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 8 | 9 | Release 10 | AnyCPU 11 | 12 | 13 | 14 | 973b77cd-1e0a-40c2-8215-b6c6e63a7125 15 | 16 | 17 | Deployment 18 | 1.0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | False 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /AzureDeploy/AzureDeploy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}") = "AzureDeploy", "AzureDeploy.deployproj", "{973B77CD-1E0A-40C2-8215-B6C6E63A7125}" 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 | {973B77CD-1E0A-40C2-8215-B6C6E63A7125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {973B77CD-1E0A-40C2-8215-B6C6E63A7125}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {973B77CD-1E0A-40C2-8215-B6C6E63A7125}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {973B77CD-1E0A-40C2-8215-B6C6E63A7125}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /AzureDeploy/Deploy-AzureResourceGroup.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 3.0 2 | #Requires -Module AzureRM.Resources 3 | #Requires -Module Azure.Storage 4 | 5 | Param( 6 | [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation, 7 | [string] $ResourceGroupName = 'CoderCardsV2', 8 | [switch] $UploadArtifacts, 9 | [string] $StorageAccountName, 10 | [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts', 11 | [string] $TemplateFile = 'azuredeploy.json', 12 | [string] $TemplateParametersFile = 'azuredeploy.parameters.json', 13 | [string] $ArtifactStagingDirectory = '.', 14 | [string] $DSCSourceFolder = 'DSC', 15 | [switch] $ValidateOnly 16 | ) 17 | 18 | try { 19 | [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ','_'), '3.0.0') 20 | } catch { } 21 | 22 | $ErrorActionPreference = 'Stop' 23 | Set-StrictMode -Version 3 24 | 25 | function Format-ValidationOutput { 26 | param ($ValidationOutput, [int] $Depth = 0) 27 | Set-StrictMode -Off 28 | return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) }) 29 | } 30 | 31 | $OptionalParameters = New-Object -TypeName Hashtable 32 | $TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile)) 33 | $TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile)) 34 | 35 | if ($UploadArtifacts) { 36 | # Convert relative paths to absolute paths if needed 37 | $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory)) 38 | $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder)) 39 | 40 | # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present 41 | $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json 42 | if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) { 43 | $JsonParameters = $JsonParameters.parameters 44 | } 45 | $ArtifactsLocationName = '_artifactsLocation' 46 | $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken' 47 | $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select -Expand $ArtifactsLocationName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore 48 | $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore 49 | 50 | # Create DSC configuration archive 51 | if (Test-Path $DSCSourceFolder) { 52 | $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName}) 53 | foreach ($DSCSourceFilePath in $DSCSourceFilePaths) { 54 | $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip' 55 | Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose 56 | } 57 | } 58 | 59 | # Create a storage account name if none was provided 60 | if ($StorageAccountName -eq '') { 61 | $StorageAccountName = 'stage' + ((Get-AzureRmContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19) 62 | } 63 | 64 | $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName}) 65 | 66 | # Create the storage account if it doesn't already exist 67 | if ($StorageAccount -eq $null) { 68 | $StorageResourceGroupName = 'ARM_Deploy_Staging' 69 | New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force 70 | $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation" 71 | } 72 | 73 | # Generate the value for artifacts location if it is not provided in the parameter file 74 | if ($OptionalParameters[$ArtifactsLocationName] -eq $null) { 75 | $OptionalParameters[$ArtifactsLocationName] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName 76 | } 77 | 78 | # Copy files from the local storage staging location to the storage account container 79 | New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1 80 | 81 | $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName} 82 | foreach ($SourcePath in $ArtifactFilePaths) { 83 | Set-AzureStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) ` 84 | -Container $StorageContainerName -Context $StorageAccount.Context -Force 85 | } 86 | 87 | # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file 88 | if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) { 89 | $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString -AsPlainText -Force ` 90 | (New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4)) 91 | } 92 | } 93 | 94 | # Create or update the resource group using the specified template file and template parameters file 95 | New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force 96 | 97 | if ($ValidateOnly) { 98 | $ErrorMessages = Format-ValidationOutput (Test-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName ` 99 | -TemplateFile $TemplateFile ` 100 | -TemplateParameterFile $TemplateParametersFile ` 101 | @OptionalParameters) 102 | if ($ErrorMessages) { 103 | Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.' 104 | } 105 | else { 106 | Write-Output '', 'Template is valid.' 107 | } 108 | } 109 | else { 110 | New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) ` 111 | -ResourceGroupName $ResourceGroupName ` 112 | -TemplateFile $TemplateFile ` 113 | -TemplateParameterFile $TemplateParametersFile ` 114 | @OptionalParameters ` 115 | -Force -Verbose ` 116 | -ErrorVariable ErrorMessages 117 | if ($ErrorMessages) { 118 | Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") }) 119 | } 120 | } -------------------------------------------------------------------------------- /AzureDeploy/Deployment.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | bin\$(Configuration)\ 7 | false 8 | true 9 | false 10 | None 11 | obj\ 12 | $(BaseIntermediateOutputPath)\ 13 | $(BaseIntermediateOutputPath)$(Configuration)\ 14 | $(IntermediateOutputPath)ProjectReferences 15 | $(ProjectReferencesOutputPath)\ 16 | true 17 | 18 | 19 | 20 | false 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Always 33 | 34 | 35 | Never 36 | 37 | 38 | false 39 | Build 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | _GetDeploymentProjectContent; 48 | _CalculateContentOutputRelativePaths; 49 | _GetReferencedProjectsOutput; 50 | _CalculateArtifactStagingDirectory; 51 | _CopyOutputToArtifactStagingDirectory; 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Configuration=$(Configuration);Platform=$(Platform) 69 | 70 | 71 | 75 | 76 | 77 | 78 | $([System.IO.Path]::GetFileNameWithoutExtension('%(ProjectReference.Identity)')) 79 | 80 | 81 | 82 | 83 | 84 | 85 | $(OutDir) 86 | $(OutputPath) 87 | $(ArtifactStagingDirectory)\ 88 | $(ArtifactStagingDirectory)staging\ 89 | $(Build_StagingDirectory) 90 | 91 | 92 | 93 | 94 | 96 | 97 | <_OriginalIdentity>%(DeploymentProjectContentOutput.Identity) 98 | <_RelativePath>$(_OriginalIdentity.Replace('$(MSBuildProjectDirectory)', '')) 99 | 100 | 101 | 102 | 103 | $(_RelativePath) 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | PrepareForRun 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /AzureDeploy/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "The name of the function app that you wish to create." 9 | } 10 | }, 11 | "storageAccountType": { 12 | "type": "string", 13 | "defaultValue": "Standard_LRS", 14 | "allowedValues": [ 15 | "Standard_LRS", 16 | "Standard_GRS", 17 | "Standard_ZRS", 18 | "Premium_LRS" 19 | ], 20 | "metadata": { 21 | "description": "Storage Account type" 22 | } 23 | }, 24 | "repoUrl": { 25 | "type": "string" 26 | } 27 | }, 28 | "variables": { 29 | "functionAppName": "[concat(parameters('appName'), substring(uniquestring(resourceGroup().id), 0, 6))]", 30 | "hostingPlanName": "[concat(parameters('appName'), substring(uniquestring(resourceGroup().id), 0, 6))]", 31 | "storageAccountName": "[concat(substring(uniquestring(resourceGroup().id), 0, 6), 'azfunctions')]", 32 | "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", 33 | "cognitiveAccountName": "[concat(parameters('appName'),'emotion')]" 34 | }, 35 | "resources": [ 36 | { 37 | "type": "Microsoft.CognitiveServices/accounts", 38 | "sku": { 39 | "name": "S0" 40 | }, 41 | "kind": "Emotion", 42 | "name": "[variables('cognitiveAccountName')]", 43 | "apiVersion": "2017-04-18", 44 | "location": "[resourceGroup().location]", 45 | "properties": {}, 46 | "dependsOn": [] 47 | }, 48 | { 49 | "type": "Microsoft.Storage/storageAccounts", 50 | "name": "[variables('storageAccountName')]", 51 | "apiVersion": "2015-06-15", 52 | "location": "[resourceGroup().location]", 53 | "properties": { 54 | "accountType": "[parameters('storageAccountType')]" 55 | } 56 | }, 57 | { 58 | "type": "Microsoft.Web/serverfarms", 59 | "apiVersion": "2015-04-01", 60 | "name": "[variables('hostingPlanName')]", 61 | "location": "[resourceGroup().location]", 62 | "properties": { 63 | "name": "[variables('hostingPlanName')]", 64 | "computeMode": "Dynamic", 65 | "sku": "Dynamic" 66 | } 67 | }, 68 | { 69 | "apiVersion": "2015-08-01", 70 | "type": "Microsoft.Web/sites", 71 | "name": "[variables('functionAppName')]", 72 | "location": "[resourceGroup().location]", 73 | "kind": "functionapp", 74 | "dependsOn": [ 75 | "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 76 | "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", 77 | "[resourceId('Microsoft.CognitiveServices/accounts/', variables('cognitiveAccountName'))]" 78 | ], 79 | "properties": { 80 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", 81 | "siteConfig": { 82 | "appSettings": [ 83 | { 84 | "name": "AzureWebJobsDashboard", 85 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" 86 | }, 87 | { 88 | "name": "AzureWebJobsStorage", 89 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" 90 | }, 91 | { 92 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 93 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]" 94 | }, 95 | { 96 | "name": "WEBSITE_CONTENTSHARE", 97 | "value": "[toLower(variables('functionAppName'))]" 98 | }, 99 | { 100 | "name": "FUNCTIONS_EXTENSION_VERSION", 101 | "value": "~1" 102 | }, 103 | { 104 | "name": "WEBSITE_NODE_DEFAULT_VERSION", 105 | "value": "6.5.0" 106 | }, 107 | { 108 | "name": "EmotionAPIKey", 109 | "value": "[listKeys(concat(resourceGroup().id,'/providers/Microsoft.CognitiveServices/accounts/', variables('cognitiveAccountName')),'2016-02-01-preview').key1]" 110 | }, 111 | { 112 | "name": "input-container", 113 | "value": "card-input" 114 | }, 115 | { 116 | "name": "output-container", 117 | "value": "card-output" 118 | }, 119 | { 120 | "name": "input-queue", 121 | "value": "queue-input" 122 | }, 123 | { 124 | "name": "SITE_PATH", 125 | "value": "site\\wwwroot" 126 | } 127 | ] 128 | } 129 | }, 130 | "resources": [ 131 | { 132 | "apiVersion": "2015-04-01", 133 | "name": "web", 134 | "type": "sourcecontrols", 135 | "dependsOn": [ 136 | "[resourceId('Microsoft.Web/Sites', variables('functionAppName'))]" 137 | ], 138 | "properties": { 139 | "repoUrl": "[parameters('repoUrl')]", 140 | "branch": "master", 141 | "IsManualIntegration": true 142 | } 143 | } 144 | ] 145 | } 146 | 147 | ] 148 | } 149 | -------------------------------------------------------------------------------- /AzureDeploy/azuredeploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appName": { 6 | "value": "donnam-codercards" 7 | }, 8 | "repoUrl": { 9 | "value": "https://github.com/lindydonna/CoderCardsV2" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /CoderCards.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26507.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoderCards", "CoderCards\CoderCards.csproj", "{528146A4-9809-4CE6-BE9C-EF09441C1EE8}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {528146A4-9809-4CE6-BE9C-EF09441C1EE8}.Debug|x86.ActiveCfg = Debug|x86 15 | {528146A4-9809-4CE6-BE9C-EF09441C1EE8}.Debug|x86.Build.0 = Debug|x86 16 | {528146A4-9809-4CE6-BE9C-EF09441C1EE8}.Release|x86.ActiveCfg = Release|x86 17 | {528146A4-9809-4CE6-BE9C-EF09441C1EE8}.Release|x86.Build.0 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /CoderCards/CardGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.IO; 4 | using System.Drawing; 5 | using Microsoft.Azure.WebJobs.Host; 6 | 7 | using static CoderCardsLibrary.ImageHelpersXPlat; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.ProjectOxford.Emotion; 10 | using Microsoft.ProjectOxford.Common.Contract; 11 | using Microsoft.Azure.WebJobs.Extensions.Http; 12 | 13 | namespace CoderCardsLibrary 14 | { 15 | [StorageAccount("AzureWebJobsStorage")] 16 | public class CardGenerator 17 | { 18 | [FunctionName("GenerateCard")] 19 | public static async Task GenerateCard( 20 | [QueueTrigger("%input-queue%")] CardInfoMessage cardInfo, 21 | [Blob("%input-container%/{BlobName}", FileAccess.Read)] byte[] image, 22 | [Blob("%output-container%/{BlobName}", FileAccess.Write)] Stream outputBlob, 23 | TraceWriter log, ExecutionContext context) 24 | { 25 | Emotion[] faceDataArray = await RecognizeEmotionAsync(image, log); 26 | 27 | if (faceDataArray == null) { 28 | log.Error("No result from Emotion API"); 29 | return; 30 | } 31 | 32 | if (faceDataArray.Length == 0) { 33 | log.Error("No face detected in image"); 34 | return; 35 | } 36 | 37 | var faceData = faceDataArray[0]; 38 | 39 | var testscores = new EmotionScores { Happiness = 1 }; 40 | string cardPath = GetCardImageAndScores(faceDataArray[0].Scores, out double score, context.FunctionDirectory); // assume exactly one face 41 | 42 | MergeCardImage(cardPath, image, outputBlob, cardInfo.PersonName, cardInfo.Title, score); 43 | 44 | //SaveAsJpeg(card, outputBlob); 45 | } 46 | 47 | [FunctionName("RequestImageProcessing")] 48 | [return: Queue("%input-queue%")] 49 | public static CardInfoMessage RequestImageProcessing([HttpTrigger(AuthorizationLevel.Anonymous, new string[] { "POST" })] CardInfoMessage input, TraceWriter log) 50 | { 51 | return input; 52 | } 53 | 54 | [FunctionName("Settings")] 55 | public static SettingsMessage Settings([HttpTrigger(AuthorizationLevel.Anonymous, new string[] { "GET" })] string input, TraceWriter log) 56 | { 57 | string stage = (Environment.GetEnvironmentVariable("STAGE") == null) ? "LOCAL" : Environment.GetEnvironmentVariable("STAGE"); 58 | return new SettingsMessage() { 59 | Stage = stage, 60 | SiteURL = Environment.GetEnvironmentVariable("SITEURL"), 61 | StorageURL = Environment.GetEnvironmentVariable("STORAGE_URL"), 62 | ContainerSAS = Environment.GetEnvironmentVariable("CONTAINER_SAS"), 63 | InputContainerName = Environment.GetEnvironmentVariable("input-container"), 64 | OutputContainerName = Environment.GetEnvironmentVariable("output-container") 65 | }; 66 | } 67 | 68 | static string GetCardImageAndScores(EmotionScores scores, out double score, string functionDirectory) 69 | { 70 | NormalizeScores(scores); 71 | 72 | var cardBack = "neutral.png"; 73 | score = scores.Neutral; 74 | const int angerBoost = 2, happyBoost = 4; 75 | 76 | if (scores.Surprise > 10) { 77 | cardBack = "surprised.png"; 78 | score = scores.Surprise; 79 | } 80 | else if (scores.Anger > 10) { 81 | cardBack = "angry.png"; 82 | score = scores.Anger * angerBoost; 83 | } 84 | else if (scores.Happiness > 50) { 85 | cardBack = "happy.png"; 86 | score = scores.Happiness * happyBoost; 87 | } 88 | 89 | var path = Path.Combine(functionDirectory, "..\\", AssetsFolderLocation, cardBack); 90 | return Path.GetFullPath(path); 91 | } 92 | 93 | #region Helpers 94 | 95 | private const string EmotionAPIKeyName = "EmotionAPIKey"; 96 | private const string AssetsFolderLocation = "assets"; 97 | 98 | public class CardInfoMessage 99 | { 100 | public string PersonName { get; set; } 101 | public string Title { get; set; } 102 | public string BlobName { get; set; } 103 | } 104 | 105 | static async Task RecognizeEmotionAsync(byte[] image, TraceWriter log) 106 | { 107 | try 108 | { 109 | var emotionServiceClient = new EmotionServiceClient(Environment.GetEnvironmentVariable(EmotionAPIKeyName)); 110 | 111 | using (MemoryStream faceImageStream = new MemoryStream(image)) 112 | { 113 | return await emotionServiceClient.RecognizeAsync(faceImageStream); 114 | } 115 | 116 | } 117 | catch (Exception e) 118 | { 119 | log.Error("Error processing image", e); 120 | return null; 121 | } 122 | 123 | } 124 | 125 | public class SettingsMessage 126 | { 127 | public string Stage { get; set; } 128 | public string SiteURL { get; set; } 129 | public string StorageURL { get; set; } 130 | public string ContainerSAS { get; set; } 131 | public string InputContainerName { get; set; } 132 | public string OutputContainerName { get; set; } 133 | } 134 | 135 | #endregion 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /CoderCards/CoderCards.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net461 5 | AnyCPU;x86 6 | win7-x86; 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | PreserveNewest 42 | 43 | 44 | PreserveNewest 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /CoderCards/ImageHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Drawing; 4 | using System.Drawing.Imaging; 5 | using Microsoft.ProjectOxford.Common.Contract; 6 | 7 | namespace CoderCardsLibrary 8 | { 9 | public class ImageHelpers 10 | { 11 | #region Pixel locations 12 | const int TopLeftFaceX = 85; 13 | const int TopLeftFaceY = 187; 14 | const int FaceRect = 648; 15 | const int NameTextX = 56; 16 | const int NameTextY = 60; 17 | const int TitleTextX = 108; 18 | const int NameWidth = 430; 19 | const int ScoreX = 654; 20 | const int ScoreY = 70; 21 | const int ScoreWidth = 117; 22 | #endregion 23 | 24 | #region Font info 25 | const string FontFamilyName = "Microsoft Sans Serif"; 26 | const int NameFontSize = 38; 27 | const int TitleFontSize = 30; 28 | const short ScoreFontSize = 55; 29 | #endregion 30 | 31 | // This code uses System.Drawing to merge images and render text on the image 32 | // System.Drawing SHOULD NOT be used in a production application 33 | // It is not supported in server scenarios and is used here as a demo only! 34 | public static void MergeCardImage(Image card, byte[] imageBytes, string personName, string personTitle, double score) 35 | { 36 | using (MemoryStream faceImageStream = new MemoryStream(imageBytes)) 37 | { 38 | using (Image faceImage = Image.FromStream(faceImageStream, true)) 39 | { 40 | using (Graphics g = Graphics.FromImage(card)) 41 | { 42 | g.DrawImage(faceImage, TopLeftFaceX, TopLeftFaceY, FaceRect, FaceRect); 43 | RenderText(g, NameFontSize, NameTextX, NameTextY, NameWidth, personName); 44 | RenderText(g, TitleFontSize, NameTextX + 4, TitleTextX, NameWidth, personTitle); // second line seems to need some left padding 45 | 46 | RenderScore(g, ScoreX, ScoreY, ScoreWidth, score.ToString()); 47 | } 48 | } 49 | } 50 | } 51 | 52 | public static void RenderScore(Graphics graphics, int xPos, int yPos, int width, string score) 53 | { 54 | var brush = new SolidBrush(Color.Black); 55 | var font = CreateFont(ScoreFontSize); 56 | SizeF size = graphics.MeasureString(score, font); 57 | 58 | graphics.DrawString(score, font, brush, width - size.Width + xPos, yPos); 59 | } 60 | 61 | private static Font CreateFont(int fontSize) 62 | { 63 | return new Font(FontFamilyName, fontSize, FontStyle.Bold, GraphicsUnit.Pixel); 64 | } 65 | 66 | public static void RenderText(Graphics graphics, int fontSize, int xPos, int yPos, int width, string text) 67 | { 68 | var brush = new SolidBrush(Color.Black); 69 | var font = CreateFont(fontSize); 70 | SizeF size; 71 | 72 | do 73 | { 74 | font = CreateFont(fontSize--); 75 | size = graphics.MeasureString(text, font); 76 | } 77 | while (size.Width > width); 78 | 79 | graphics.DrawString(text, font, brush, xPos, yPos); 80 | } 81 | 82 | // save with higher quality than the default, to avoid jpeg artifacts on the text and numbers 83 | public static void SaveAsJpeg(Image image, Stream outputStream) 84 | { 85 | var jpgEncoder = GetEncoder(ImageFormat.Jpeg); 86 | var qualityEncoder = System.Drawing.Imaging.Encoder.Quality; 87 | var encoderParams = new EncoderParameters(1); 88 | encoderParams.Param[0] = new EncoderParameter(qualityEncoder, 90L); 89 | 90 | image.Save(outputStream, jpgEncoder, encoderParams); 91 | } 92 | 93 | static ImageCodecInfo GetEncoder(ImageFormat format) 94 | { 95 | ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders(); 96 | foreach (ImageCodecInfo codec in codecs) 97 | { 98 | if (codec.FormatID == format.Guid) 99 | { 100 | return codec; 101 | } 102 | } 103 | return null; 104 | } 105 | 106 | public static float RoundScore(float score) => (float)Math.Round((decimal)(score * 100), 0); 107 | 108 | public static void NormalizeScores(EmotionScores scores) 109 | { 110 | scores.Anger = RoundScore(scores.Anger); 111 | scores.Happiness = RoundScore(scores.Happiness); 112 | scores.Neutral = RoundScore(scores.Neutral); 113 | scores.Sadness = RoundScore(scores.Sadness); 114 | scores.Surprise = RoundScore(scores.Surprise); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /CoderCards/ImageHelpersXPlat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using SkiaSharp; 7 | using Microsoft.ProjectOxford.Common.Contract; 8 | using System.IO; 9 | 10 | namespace CoderCardsLibrary 11 | { 12 | public class ImageHelpersXPlat 13 | { 14 | #region Pixel locations 15 | const int TopLeftFaceX = 85; 16 | const int TopLeftFaceY = 187; 17 | const int FaceWidth = 648; // Width = Height 18 | const int NameTextX = 56; 19 | const int NameTextY = 88; 20 | const int TitleTextY = 125; 21 | const int NameWidth = 430; 22 | const int ScoreX = 640; 23 | const int ScoreY = 110; 24 | const int ScoreWidth = 117; 25 | 26 | const int CardWidth = 819; 27 | const int CardHeight = 1150; 28 | #endregion 29 | 30 | #region Font info 31 | const string FontFamilyName = "Microsoft Sans Serif"; 32 | const int NameFontSize = 38; 33 | const int TitleFontSize = 30; 34 | const short ScoreFontSize = 55; 35 | #endregion 36 | 37 | // This code uses System.Drawing to merge images and render text on the image 38 | // System.Drawing SHOULD NOT be used in a production application 39 | // It is not supported in server scenarios and is used here as a demo only! 40 | public static void MergeCardImage(string cardPath, byte[] imageBytes, Stream outputStream, string personName, string personTitle, double score) 41 | { 42 | using (MemoryStream faceImageStream = new MemoryStream(imageBytes)) { 43 | using (var surface = SKSurface.Create(width: CardWidth, height: CardHeight, colorType: SKImageInfo.PlatformColorType, alphaType: SKAlphaType.Premul)) { 44 | SKCanvas canvas = surface.Canvas; 45 | 46 | canvas.DrawColor(SKColors.White); // clear the canvas / fill with white 47 | 48 | using (var fileStream = File.OpenRead(cardPath)) 49 | using (var stream = new SKManagedStream(fileStream)) // decode the bitmap from the stream 50 | using (var cardBack = SKBitmap.Decode(stream)) 51 | using (var face = SKBitmap.Decode(imageBytes)) 52 | using (var paint = new SKPaint()) { 53 | canvas.DrawBitmap(cardBack, SKRect.Create(0, 0, CardWidth, CardHeight), paint); 54 | canvas.DrawBitmap(face, SKRect.Create(TopLeftFaceX, TopLeftFaceY, FaceWidth, FaceWidth)); 55 | 56 | RenderText(canvas, NameFontSize, NameTextX, NameTextY, NameWidth, personName); 57 | RenderText(canvas, TitleFontSize, NameTextX, TitleTextY, NameWidth, personTitle); 58 | RenderScore(canvas, ScoreX, ScoreY, ScoreWidth, score.ToString()); 59 | 60 | canvas.Flush(); 61 | 62 | using (var jpgImage = surface.Snapshot().Encode(SKEncodedImageFormat.Jpeg, 80)) { 63 | jpgImage.SaveTo(outputStream); 64 | } 65 | } 66 | 67 | } 68 | 69 | } 70 | } 71 | 72 | 73 | public static void RenderScore(SKCanvas canvas, int xPos, int yPos, int width, string score) 74 | { 75 | var font = SKTypeface.FromFamilyName(FontFamilyName, SKTypefaceStyle.Bold); 76 | var brush = CreateBrush(font, ScoreFontSize); 77 | 78 | var textWidth = brush.MeasureText(score); 79 | 80 | canvas.DrawText(score, xPos + width - textWidth, yPos, brush); 81 | } 82 | 83 | 84 | public static void RenderText(SKCanvas canvas, int fontSize, int xPos, int yPos, int width, string text) 85 | { 86 | var font = SKTypeface.FromFamilyName(FontFamilyName, SKTypefaceStyle.Bold); 87 | 88 | SKPaint brush = null; 89 | float textWidth; 90 | 91 | do { 92 | brush = CreateBrush(font, fontSize); 93 | textWidth = brush.MeasureText(text); 94 | fontSize--; 95 | } 96 | while (textWidth > width); 97 | 98 | canvas.DrawText(text, xPos, yPos, brush); 99 | } 100 | 101 | static SKPaint CreateBrush(SKTypeface font, int fontSize) 102 | { 103 | return new SKPaint { 104 | Typeface = font, 105 | TextSize = fontSize, 106 | IsAntialias = true, 107 | Color = new SKColor(0, 0, 0) 108 | }; 109 | } 110 | 111 | public static float RoundScore(float score) => (float)Math.Round((decimal)(score * 100), 0); 112 | 113 | public static void NormalizeScores(EmotionScores scores) 114 | { 115 | scores.Anger = RoundScore(scores.Anger); 116 | scores.Happiness = RoundScore(scores.Happiness); 117 | scores.Neutral = RoundScore(scores.Neutral); 118 | scores.Sadness = RoundScore(scores.Sadness); 119 | scores.Surprise = RoundScore(scores.Surprise); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CoderCards/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CoderCards": { 4 | "commandName": "Project", 5 | "commandLineArgs": "host start --cors * --pause-on-error" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /CoderCards/assets/angry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-codercards/HEAD/CoderCards/assets/angry.png -------------------------------------------------------------------------------- /CoderCards/assets/codercards.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-codercards/HEAD/CoderCards/assets/codercards.ai -------------------------------------------------------------------------------- /CoderCards/assets/happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-codercards/HEAD/CoderCards/assets/happy.png -------------------------------------------------------------------------------- /CoderCards/assets/neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-codercards/HEAD/CoderCards/assets/neutral.png -------------------------------------------------------------------------------- /CoderCards/assets/surprised.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-codercards/HEAD/CoderCards/assets/surprised.png -------------------------------------------------------------------------------- /CoderCards/host.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /CoderCards/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsDashboard": "", 5 | "AzureWebJobsStorage": "", 6 | "WEBSITE_NODE_DEFAULT_VERSION": "6.5.0", 7 | "EmotionAPIKey": "", 8 | "input-container": "input-local", 9 | "output-container": "output-local", 10 | "input-queue": "local-queue", 11 | "SITEURL": "http://localhost:7071", 12 | "STORAGE_URL": "", 13 | "CONTAINER_SAS": "" 14 | }, 15 | "ConnectionStrings": {} 16 | } 17 | -------------------------------------------------------------------------------- /CoderCards/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": { 4 | "root": { 5 | "matchCondition": { 6 | "route": "/" 7 | }, 8 | "backendUri": "%STORAGE_URL%content/index.html" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /CoderCardsClient/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coder Cards 7 | 8 | 9 | 10 | 11 | 12 | 162 | 163 | 164 | 168 | 169 | 170 | 171 | 172 |
173 |
174 |

Coder Cards

175 |
176 |
177 |
178 |

1. Upload a picture of yourself

179 |
180 |
181 |
182 | Drop files here or click to upload.
183 |
184 |
185 |
186 |

2. Your coder profile will appear below

187 |
188 |
189 |
190 | 197 |
198 | 199 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /CoderCardsClient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "scripts": { 4 | "start": "live-server", 5 | "test": "echo \"Error: no test specified\" && exit 1" 6 | }, 7 | "devDependencies": { 8 | "live-server":"1.2.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | services: functions 3 | platforms: dotnet 4 | author: lindydonna 5 | --- 6 | 7 | # CoderCards - trading card generator 8 | 9 | CoderCards is a geek trading card generator. It uses Microsoft Cognitive Services to detect the predominant emotion in a face, which is used to choose a card back. 10 | 11 | The sample demonstrates the following features 12 | - C# attributes and Visual Studio 2017 tooling 13 | - Functions backing a SPA, hosted in Azure Storage 14 | - Azure Functions proxies to customize the site index.html 15 | 16 | There's also a C# script version of this sample: [CoderCards](https://github.com/lindydonna/codercards). 17 | 18 | [![Deploy to Azure](http://azuredeploy.net/deploybutton.svg)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Flindydonna%2FCoderCardsV2%2Fmaster%2FAzureDeploy%2Fazuredeploy.json) 19 | 20 | ## Prerequisites 21 | 22 | * To build the functions project, use [Visual Studio 15.3 Preview](https://www.visualstudio.com/vs/preview/) and the [Azure Functions Tooling VSIX](https://marketplace.visualstudio.com/items?itemName=AndrewBHall-MSFT.AzureFunctionToolsforVisualStudio2017). 23 | 24 | * To run the setup script, install the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli). 25 | 26 | ## About the sample 27 | 28 | * There are two functions defined in this project: 29 | * **RequestImageProcessing**. HTTP trigger that writes a queue message. The request payload must be in the following form: 30 | 31 | ```json 32 | { 33 | "PersonName": "Scott Guthrie", 34 | "Title": "Red Polo Connoisseur", 35 | "BlobName": "Scott Guthrie-Red Polo Connoisseur.jpg" 36 | } 37 | ``` 38 | 39 | * **GenerateCard**. Queue trigger that binds to the blob specified in the BlobName property of the queue payload. Based on the predominant emotion of the input image, it generates a card using one of 4 card templates. 40 | 41 | * The card is written to the output blob container specified by the app setting `output-container`. 42 | 43 | Here's a visualization of the bindings, using the [Azure Functions Bindings Visualizer](https://functions-visualizer.azurewebsites.net): 44 | 45 | ![Functions bindings](function-bindings.png) 46 | 47 | ## Setup 48 | 49 | ### Setup script 50 | 51 | Use the Python setup script [setup.py](setup.py). This uses the Azure CLI 2.0 to automate the storage account setup. Run the following commands: 52 | 53 | ``` 54 | az login 55 | python setup.py storage-account resource-group true 56 | ``` 57 | 58 | This will modify the file [local.settings.json](CoderCards/local.settings.json). The last argument controls whether to create containers prefixed with "local". 59 | 60 | Alternatively, you can run the script from the Azure Cloud Shell in the Azure Portal. Just run `python` and paste the script. The script prints out settings values that you can use to manually modify `local.settings.json`. 61 | 62 | ### Required App Settings 63 | 64 | | Key | Description | 65 | |----- | ------| 66 | | AzureWebJobsStorage | Storage account connection string | 67 | | EmotionAPIKey | Key for [Cognitive Services Emotion API](https://www.microsoft.com/cognitive-services/en-us/emotion-api) | 68 | | input-queue | Name of Storage queue for to trigger card generation. Use a value like "local-queue" locally and "input-queue" on Azure 69 | | input-container | Name of Storage container for input images. Use a value like "local-card-input" locally and "card-input" on Azure | 70 | | output-container | Name of Storage container for output images. Use a value like "local-card-output" locally and "card-output" on Azure | 71 | | SITEURL | Set to `http://localhost:7071` locally. Not required on Azure. | 72 | | STORAGE_URL | URL of storage account, in the form `https://accountname.blob.core.windows.net/` | 73 | | CONTAINER_SAS | SAS token for uploading to input-container. Include the "?" prefix. | 74 | 75 | If you want to set these values in Azure, you can set them in *local.settings.json* and use the Azure Functions Core Tools to publish to Azure. 76 | 77 | ``` 78 | python setup.py storage-account resource-group false 79 | func azure functionapp publish function-app-name --publish-app-settings 80 | ``` 81 | 82 | ## Local debugging in Visual Studio 83 | 84 | - If you're using Visual Studio 2017 Update 3 and the Azure Functions Tools VSIX, open the project [CoderCards.csproj](CoderCards/CoderCards.csproj). F5 will automatically launch the Azure Functions Core tools. 85 | 86 | - The project has a custom launchSettings.json that passes these arguments to the Functions Core Tools: `host start --cors * --pause-on-error`. 87 | 88 | ## Running the demo 89 | 90 | ### Running using the provided SPA webpage 91 | 92 | Make sure the functions host is running locally via Visual Studio or the Azure Functions Core Tools. 93 | 94 | In a command prompt, go to the `CoderCardsClient` directory. 95 | 96 | - Run `npm install` 97 | - Run `npm start`. This will launch a webpage at `http://127.0.0.1:8080/`. Navigate instead to `http://localhost:8080`. 98 | 99 | ### Running manually 100 | 1. Choose images that are **square** and upload to the `card-input` container. (Images that aren't square will be stretched.) 101 | 2. Send an HTTP request using Postman or CURL, specifying the path of the blob you just uploaded: 102 | 103 | ```json 104 | { 105 | "PersonName": "My Name", 106 | "Title": "My Title", 107 | "BlobName": "BlobFilename.jpg" 108 | } 109 | ``` 110 | 111 | ## Notes 112 | 113 | * The demo uses System.Drawing, which is NOT recommended for production apps. To learn more, see [5 Reasons You Should Stop Using System\.Drawing from ASP\.NET](http://photosauce.net/blog/post/5-reasons-you-should-stop-using-systemdrawing-from-aspnet). 114 | 115 | * Happy faces get a multiplier of 4, angry gets a multiplier of 2. I encourage you to tweak for maximum comedic effect! 116 | 117 | ## Talking points about Azure Functions 118 | 119 | * Creating an HTTP trigger that writes a queue message is just one line of code! 120 | 121 | * Using a queue message to trigger blob processing is preferable to a blob trigger, as it is easier to ensure transactional processing. Blob triggers can be [delayed for up to 10 minutes on the Consumption plan](https://docs.microsoft.com/en-us/azure/azure-functions/functions-scale#how-the-consumption-plan-works) 122 | 123 | * By binding to a POCO, you can use the payload of a trigger to configure an input binding. In this example, we binding to the `BlobName` property in the queue message. 124 | 125 | * The input binding is just a byte array, which makes it easy to manipulate with memory streams (no need to create new ones). Other binding types for C# are Stream, CloudBlockBlob, etc, which is very flexible. The output binding is just a stream that you just write to. 126 | 127 | ## Next steps 128 | 129 | For more information about the Azure Functions Visual Studio tooling, see the following: 130 | 131 | - [Visual Studio 2017 Tools for Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs) 132 | - [Using \.NET class libraries with Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) 133 | - [Code and test Azure functions locally](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) 134 | - Video: [Azure Functions Visual Studio Tooling](https://www.youtube.com/watch?v=BN2sIRrOt8A) 135 | - Video: [Cloud Cover: Azure Functions Local Debugging and More with Donna Malayeri](https://channel9.msdn.com/Shows/Cloud+Cover/Episode-231-Azure-Functions-Local-Debugging-and-More-with-Donna-Malayeri) 136 | -------------------------------------------------------------------------------- /function-bindings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-codercards/HEAD/function-bindings.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | settingsFilename = "CoderCards/local.settings.json" 4 | containerContent = "content" 5 | indexHtmlFile = "CoderCardsClient/index.html" 6 | 7 | def runCommand(str): 8 | return subprocess.run(str, stdout=subprocess.PIPE, shell=True).stdout.decode('utf-8').rstrip() 9 | 10 | def str2bool(v): 11 | return v.lower() in ("yes", "true", "t", "1") 12 | 13 | import fileinput, re, sys, getopt, subprocess 14 | 15 | if (len(sys.argv) < 3): 16 | print("Usage: {} ".format(sys.argv[0])) 17 | sys.exit(); 18 | 19 | storageName = sys.argv[1] # storage account name 20 | resourceGroup = sys.argv[2] # storage resource group 21 | isLocal = str2bool(sys.argv[3]) # local or prod containers 22 | 23 | containerInput = "input-local" if isLocal else "card-input" 24 | containerOutput = "output-local" if isLocal else "card-output" 25 | queueName = "local-queue" if isLocal else "input-queue" 26 | 27 | # Retrieve the Storage Account connection string 28 | connstr = runCommand('az storage account show-connection-string --name {} --resource-group {} --query connectionString --output tsv'.format(storageName, resourceGroup)) 29 | 30 | # get account URL 31 | accountUrl = \ 32 | runCommand('az storage account show --name {} -g {} --output tsv --query "{{primaryEndpoints:primaryEndpoints}}.primaryEndpoints.blob"'.format(storageName, resourceGroup)) 33 | 34 | # create containers 35 | runCommand('az storage container create --connection-string "{}" --name {}'.format(connstr, containerInput)) 36 | runCommand('az storage container create --connection-string "{}" --name {}'.format(connstr, containerOutput)) 37 | runCommand('az storage container create --connection-string "{}" --name {}'.format(connstr, containerContent)) 38 | 39 | # get SAS token for input container 40 | sasToken = runCommand('az storage container generate-sas --connection-string "{}" --name {} --permissions lrw --expiry 2018-01-01 -o tsv'.format(connstr, containerInput)) 41 | 42 | # set permissions on output and content containers 43 | runCommand('az storage container set-permission --connection-string "{}" --public-access blob -n {}'.format(connstr, containerOutput)) 44 | runCommand('az storage container set-permission --connection-string "{}" --public-access blob -n {}'.format(connstr, containerContent)) 45 | 46 | # upload index.html to storage 47 | runCommand('az storage blob upload --connection-string "{}" --container-name {} -f {} -n index.html --content-type "text/html"'.format(connstr, containerContent, indexHtmlFile)) 48 | 49 | # set CORS on blobs 50 | runCommand('az storage cors add --connection-string "{}" --origins "*" --methods GET PUT OPTIONS --allowed-headers "*" --exposed-headers "*" --max-age 200 --services b'.format(connstr)) 51 | 52 | 53 | # new settings values 54 | settingAzureWebJobsStorage = '"AzureWebJobsStorage": "{}"'.format(connstr) 55 | settingStorageUrl = '"STORAGE_URL": "{}"'.format(accountUrl) 56 | settingContainerSas = '"CONTAINER_SAS": "?{}"'.format(sasToken) 57 | 58 | # write out new settings values 59 | print(settingAzureWebJobsStorage) 60 | print(settingStorageUrl) 61 | print(settingContainerSas) 62 | 63 | # write changes to file 64 | with open(settingsFilename, 'r') as file: 65 | filedata = file.read() 66 | 67 | filedata = filedata.replace('"AzureWebJobsStorage": ""', settingAzureWebJobsStorage) \ 68 | .replace('"STORAGE_URL": ""', settingStorageUrl) \ 69 | .replace('"CONTAINER_SAS": ""', settingContainerSas) 70 | 71 | filedata = re.sub(r'"input-container": .*,', '"input-container": "{}",'.format(containerInput), filedata) 72 | filedata = re.sub(r'"output-container": .*,', '"output-container": "{}",'.format(containerOutput), filedata) 73 | filedata = re.sub(r'"input-queue": .*,', '"input-queue": "{}",'.format(queueName), filedata) 74 | 75 | with open(settingsFilename, 'w') as file: 76 | file.write(filedata) 77 | --------------------------------------------------------------------------------