Introduction
GitHub recently introduced an option to customize our profile by giving a separate magical repository. This repository name should be the same as your username, for example,
here. This is how you can find this secret repository.
GitHub secret repository
This opens endless opportunities for what you can do to your GitHub home page. For example, I am showing the image of the titles of my recent 5 blog posts on my home page automatically. I did it using the Azure Function which will return the image, and all I had to do in this repository was to use it. Overall it was fun.
Custom GitHub home page
Here in this post, we will see how we can develop an Azure Function solution that returns the above-mentioned image.
Develop an Azure function
The first thing that you need to do is to create a new Azure Function. Go to your Azure portal, and search for Azure Function. The creation process is the same as we would create any other Azure resource.
Once you create the Azure function, you are good to go and develop an Azure function application in Visual Studio. Here, I am using Visual Studio 2019.
Create Azure function using Visual Studio
We are going to create an HttpTrigger Azure function, so make sure you select that option on the next screen. If you need a basic introduction about the HttpTrigger Azure function. I named my function as GetLatestPosts, and feel free to give any name you wish. This Azure function has the preceding jobs to do.
- Get the latest posts details from the feed of my blog
- Create an image with the feed data
- Send back the image as a stream
Get the latest posts details from the feed
You can usually get the feed data by going to {yoursitelink}/feed. For example, I can get my feed data by going to https://sibeeshpassion.com/feed/. So in our function, we will get this data using an XmlReader and Deserialize it using SyndicationFeed. Please be noted that the Syndication namespace is part of the System.ServiceModel and you should install the Nuget package System.ServiceModel.Syndication. Below is the function used to retrieve the latest 5 blog posts' details.
- public static IEnumerable < SyndicationItem > GetLatestFeeds() {
- var reader = XmlReader.Create(Configuration.BlogLink);
- var feed = SyndicationFeed.Load(reader);
- reader.Close();
- return feed.Items.Take(5);
- }
Create an image with titles
Now we need a function to generate an image and write the blog posts title to that image dynamically. To do this, we are using the Nuget package called SixLabors.ImageSharp. Feel free to use any packages you like, the only thing that matters is that we need an image with the blog posts titles. The SixLabors.ImageSharp is indeed an amazing library, I can recommend you to give it a try. This is how your Nuget package installed window should look now:
Required Nuget packages
Now we can write the function as shown below.
- private static string WriteOnImage(IEnumerable < SyndicationItem > feedItems) {
- var titles = string.Join(", ", feedItems.Select(s => s.Title.Text).ToList());
- using
- var img = new Image < Rgba32 > (Configuration.ImageWidth, Configuration.ImageHeight);
- var font = System Fonts.CreateFont(Configuration.Font, Configuration.FontSize);
- img.Mutate(ctx => ctx.ApplyScalingWaterMark(font, titles, Color.Black, 5, true));
- return img.ToBase64String(PngFormat.Instance);
- }
As you can see that there is a Configuration class, from where we take all the configurations. Let’ write that now. You can also use Environment variables here, I may update my repository in the coming days.
- namespace GitHub Funcs {
- public static class Configuration {
- public static string BlogLink {
- get;
- set;
- } = "https://sibeeshpassion.com/feed";
- public static int ImageWidth {
- get;
- set;
- } = 850;
- public static int ImageHeight {
- get;
- set;
- } = 100;
- public static string ContentType {
- get;
- set;
- } = "image/png";
- public static string Font {
- get;
- set;
- } = "Arial";
- public static int FontSize {
- get;
- set;
- } = 5;
- }
- }
The SixLabors.ImageSharp has an inbuilt extension function called ToBase64String, which will return the image as base64 string. Did you notice that we are using another extension method here, “ApplyScalingWaterMark”? Let’s create a class for our Extension methods so that it will be handy in the future.
- using SixLabors.Fonts;
- using SixLabors.ImageSharp;
- using SixLabors.ImageSharp.Drawing.Processing;
- using SixLabors.ImageSharp.Processing;
- using System;
- namespace GitHub Funcs.ExtensionMethods {
- public static class ImageSharpExtensions {
- public static IImageProcessingContextApplyScalingWaterMark(thisIImageProcessingContextprocessingContext, Font font, string text, Color color, float padding, bool wordwrap) {
- if (wordwrap) {
- return processing Context.ApplyScalingWaterMarkWordWrap(font, text, color, padding);
- } else {
- return processing Context.ApplyScalingWaterMarkSimple(font, text, color, padding);
- }
- }
- private static IImage Processing Context Apply Scaling WaterMarkSimple(thisIImageProcessingContextprocessingContext, Font font, string text, Color color, float padding) {
- Size imgSize = processingContext.GetCurrentSize();
- float target Width = img Size.Width - (padding * 2);
- float target Height = img Size.Height - (padding * 2);
-
- Font Rectangle size = Text Measurer.Measure(text, newRendererOptions(font));
-
- float scaling Factor = Math.Min(imgSize.Width / size.Width, imgSize.Height / size.Height);
-
- Fonts caled Font = new Font(font, scalingFactor * font.Size);
- var center = new PointF(imgSize.Width / 2, imgSize.Height / 2);
- var textGraphicOptions = new TextGraphicsOptions() {
- Text Options = {
- Horizontal Alignment = Horizontal Alignment.Center,
- Vertical Alignment = Vertical Alignment.Center
- }
- };
- return processing Context.DrawText(textGraphicOptions, text, scaledFont, color, center);
- }
- private static IImageProcessing ContextApply ScalingWaterMark WordWrap(thisIImageProcessingContextprocessingContext, Font font, string text, Color color, float padding) {
- Size imgSize = processing Context.GetCurrentSize();
- floattar get Width = img Size.Width - (padding * 2);
- floattar get Height = imgSize.Height - (padding * 2);
- float target Min Height = imgSize.Height - (padding * 3);
-
-
- var scaled Font = font;
- Font Rectangles = new FontRectangle(0, 0, float.MaxValue, float.MaxValue);
- float scale Factor = (scaledFont.Size / 2);
- int trap Count = (int) scaledFont.Size * 2;
- if (trap Count < 10) {
- trap Count = 10;
- }
- bool is TooSmall = false;
- while ((s.Height > targetHeight || s.Height < targetMinHeight) && trapCount > 0) {
- if (s.Height > target Height) {
- if (isTooSmall) {
- scale Factor = scale Factor / 2;
- }
- scaled Font = new Font(scaledFont, scaledFont.Size - scaleFactor);
- isTooSmall = false;
- }
- if (s.Height < targetMinHeight) {
- if (!isTooSmall) {
- scale Factor = scale Factor / 2;
- }
- scaled Font = new Font(scaledFont, scaledFont.Size + scaleFactor);
- isTooSmall = true;
- }
- trapCount--;
- s = TextMeasurer.Measure(text, newRendererOptions(scaledFont) {
- Wrapping Width = target Width
- });
- }
- var center = new PointF(padding, imgSize.Height / 2);
- var textGraphicOptions = new TextGraphicsOptions() {
- Text Options = {
- Horizontal Alignment = Horizontal Alignment.Left,
- Vertical Alignment = Vertical Alignment.Center,
- WrapText Width = target Width
- }
- };
- return processing Context.DrawText(textGraphicOptions, text, scaledFont, color, center);
- }
- }
- }
Azure function to return a stream
As all the supporting methods are ready, let's write our main function. The ultimate job of this function is to convert the base64 string to a stream and return the stream. It's as simple as that.
- [FunctionName("GetLatestPosts")]
- public static FileStreamResultRun([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequestrequest, ILoggerlog) {
- try {
- varbase String = WriteOnImage(GetLatestFeeds());
-
- string convert = baseString.Replace("data:image/png;base64,", String.Empty);
- var bytes = Convert.FromBase64String(convert);
- var result = new FileStreamResult(newMemoryStream(bytes), Configuration.ContentType);
- log.LogInformation("Returning stream now!");
- request.HttpContext.Response.Headers.Add("Cache-Control", "s-maxage=1, stale-while-revalidate");
- return result;;
- } catch (System.Exceptionex) {
- log.LogError($ "Something went wrong: {ex}");
- throwex;
- }
- }
Once your Azure function is ready, please remember to publish the same to Azure. To publish your Azure Function app, just right click on your project and click Publish and then set up your publish target by choosing the existing Azure Function App, remember we have created one earlier?
Develop secret GitHub readme repository
Here, I am assuming that you already created your secret repository. And if yes, you can update the “readme” content as follows. Please remember that this is just a sample, you can update it as you wish.
Hi there π, feel free to check out my blog and youtube channel!
Below are the titles of the latest 5 posts from my blog (This is an automated message using Azure Function. Azure is Love!)
- π I’m currently working on Azure IoT Hub, IoT Central, Raspberry Pi
- π± I’m currently learning a lot of new IoT topics
- π― I’m looking to collaborate on any MVP product in IoT
- β‘ Fun fact: Well, this readme file will not be enough!
Boom!. We did it. Now go to your GitHub home page, and I am sure that you will love your new beautiful, detailed home page.
Update
As GitHub has a very low timeout, sometimes the image created was not showing in my profile. Thus, I had to change the mechanism. I changed my Azure function to a time trigger function that runs every hour and the main functionality of this function now is to generate the image and upload to the Azure blob storage. Now I can directly get the blob image URL in the Readme file. No more timeout issues. Following are the codes of the new Azure function. You can also see this in the Dev branch of the repository.
- using GitHubFuncs.ExtensionMethods;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.Azure.WebJobs;
- using Microsoft.Extensions.Logging;
- using Microsoft.WindowsAzure.Storage;
- using SixLabors.Fonts;
- using SixLabors.ImageSharp;
- using SixLabors.ImageSharp.Formats.Png;
- using SixLabors.ImageSharp.PixelFormats;
- using SixLabors.ImageSharp.Processing;
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Net.Mime;
- using System.ServiceModel.Syndication;
- using System.Threading.Tasks;
- using System.Xml;
- namespace GitHubFuncs {
- public class GetLatestPosts {
- private static ILogger_logger;
- private
- const string_fileName = "latestpost.png";
- private
- const string_blobContainerName = "github";
- [FunctionName("GetLatestPosts")]
- public async TaskRun([TimerTrigger("0 0 */1 * * *")] TimerInfomyTimer, ILoggerlog) {
- try {
- _logger = log;
- await UplaodImageToStorage();
- } catch (System.Exceptionex) {
- log.LogError($ "Something went wrong: {ex}");
- throwex;
- }
- }
- private static async Task < bool > UplaodImageToStorage() {
- try {
- var base String = WriteOnImage(GetLatestFeeds());
- string convert = baseString.Replace("data:image/png;base64,", String.Empty);
- var bytes = Convert.FromBase64String(convert);
- var stream = newMemoryStream(bytes);
- if (CloudStorageAccount.TryParse(Environment.GetEnvironmentVariable("AzureWebJobsStorage"), outCloudStorageAccountcloudStorageAccount)) {
- var cloud BlobClient = cloud StorageAccount.CreateCloudBlobClient();
- var cloud BlobContainer = cloud BlobClient.GetContainerReference(_blobContainerName);
- var cloud BlockBlob = cloud BlobContainer.GetBlockBlobReference(_fileName);
- cloud BlockBlob.Properties.ContentType = "image/png";
- await cloud BlockBlob.UploadFromStreamAsync(stream);
- _logger.LogInformation("Uploaded new image");
- } else {
- _logger.LogError("Error in connection");
- }
- } catch (Exceptionex) {
- _logger.LogError(ex.Message);
- }
- return false;
- }
- private static string WriteOnImage(IEnumerable < SyndicationItem > feedItems) {
- var titles = string.Join(", ", feedItems.Select(s => s.Title.Text).ToList());
- using
- var img = new Image < Rgba32 > (Configuration.ImageWidth, Configuration.ImageHeight);
- var font = SystemFonts.CreateFont(Configuration.Font, Configuration.FontSize);
- img.Mutate(ctx => ctx.ApplyScalingWaterMark(font, titles, Color.Black, 5, true));
- return img.ToBase64String(PngFormat.Instance);
- }
- public static IEnumerable < SyndicationItem > GetLatestFeeds() {
- var reader = XmlReader.Create(Configuration.BlogLink);
- var feed = SyndicationFeed.Load(reader);
- reader.Close();
- return feed.Items.Take(5);
- }
- }
- }
Source Code
Please feel free to check out the repositories here:
- https://github.com/SibeeshVenu/GitHubFuncs
- https://github.com/SibeeshVenu/sibeeshvenu
Your turn. What do you think?
Thanks a lot for reading. Did I miss anything that you may think is needed in this article? Did you find this post useful? Please do not forget to share your feedback!