Introduction

When displaying an image on a web page, it's recommended that the alt attribute holds a textual replacement for the image. This is mandatory and incredibly useful for accessibility—screen readers read the attribute value out to their users so they know what the image means.

To assist with this ChatGPT with a paid subscription, a developer can create a custom GPT that, once one or more images are uploaded, can produce alternative text for an image using instructions and rules provided by the user.

Learn how to use a custom GPT to create alternative text, which is then saved to a text file in a C# project. The project stores the image and alternate text in an SQL-Server database, and then, in an ASP.NET Core project, the image with the alternative text from the database is rendered.

The benefits of storing images and alternative text are twofold: consistency for multiple projects and the ease of making changes in one place after the text is in the database.

Example for alternative text from using a custom GPT

src="Wine.png" alt="Glass of red wine with a clear stem, filled halfway, on a white background." />

Not bad but we could remove "on a white background".

Best practices

Dictate that images should be stored in the file system instead of in a database. In the example used, the photos represent categories for products in a modified version of the Microsoft NorthWind database where no image exceeds nine kilobytes, which breaks the no images in a database. Still, in this case, it's acceptable.

The developer decides whether images should be stored in a database or the file system; alternative text can be saved to a database.

Custom GPT

GPTs are custom versions of ChatGPT that users can tailor for specific tasks or topics by combining instructions, knowledge, and capabilities. They can be as simple or as complex as needed, addressing anything from language learning to technical support. Plus, Team, and Enterprise users can start creating GPTs at chatgpt.com/create.

how to create a GPT.

Generate text for image alt attribute

Using the instructions above and the following instructions, is the finished product.

alternative text custom GPT

Example

Given the following image.

Image description

Produces

src="sample.png" alt="Bridge crossing a river with fall foliage on both sides and a snow-covered mountain in the distance." />

For generating images to stored in the database, upload all images at once.

screen shot for above

Note
In some cases ChatGPT will not use the file names for the uploaded images, if this happens re-prompt with "Use the file name for each img"

Save results to SQL-Server

Copy the results

src="Beverages.png" alt="Table with a wine bottle, cups, a teapot, and a decorative plate in the background." />

 src="Condiments.png" alt="Assorted jars, cups, and a salt shaker arranged in front of a plate." />

 src="Confections.png" alt="Whole pie with a golden crust and sliced pieces on a wooden table." />

 src="DairyProducts.png" alt="Blocks of cheese on a plate with wrapped cheese packages behind them." />

 src="GrainsCereals.png" alt="Baked bread loaves with a sliced portion on a red and white cloth." />

 src="MeatPoultry.png" alt="Cooked meat on a white plate with corn and other vegetables around it." />

 src="Produce.png" alt="Bowls of various dumplings and a tray of packaged frozen dumplings." />

 src="Seafood.png" alt="Blue floral dishware with crepes or pancakes on a platter." />

 src="Wine.png" alt="Glass of red wine on a white background." />

🛑 Create the database WorkingImages, followed by running CreateDatabase.sql under DataScripts in the AlterateImageApp project.

Paste into a text file in the console project.

The following class

public partial class FileOperations
{

    public static List<ImageAltText> ReadAltTexts(string fileName) =>
    (
        from line in File.ReadLines(fileName) 
        select ImageAltTextRegex().Match(line) into match 
        where match.Success select new ImageAltText
        {
            Src = match.Groups[1].Value, 
            Alt = match.Groups[2].Value
        }).ToList();


    [GeneratedRegex(@"", RegexOptions.IgnoreCase, "en-US")]
    private static partial Regex ImageAltTextRegex();
}

Which produces.

output from using the method ReadAltTexts

The next step uses Dapper to first truncate the table and reset the identity column using the method below, AddRange.

Next, to validate that the images can be read back, the method Write creates each image in a local folder. This is optional and can be commented out.

using System.Text.RegularExpressions;
using AlternateImageApp.Models;
using ConsoleConfigurationLibrary.Classes;
using Dapper;
using Microsoft.Data.SqlClient;

namespace AlternateImageApp.Classes;

/// 
/// Provides operations for managing and manipulating image alt text data within a database.
public partial class DataOperations
{
    public static void AddRange(List<ImageAltText> list, string filePath)
    {

        TruncateTable("Categories");

        using var db = new SqlConnection(AppConnections.Instance.MainConnection);

        foreach (var item in list)
        {
            var bytes = File.ReadAllBytes(Path.Combine(filePath, item.Src));

            db.Execute(
                """
                INSERT INTO Categories (Name, Ext, AltText, Photo) 
                VALUES (@Name, @Ext, @AltText, @Photo)
                """, 
                new
                {
                    item.Name,
                    Ext = item.Ext,
                    AltText = item.Alt, 
                    Photo = bytes
                });
        }
    }

    public static void Write()
    {
        using var db = new SqlConnection(AppConnections.Instance.MainConnection);
        var results = db.Query<ImageAltText>(
            """
            SELECT Id, Name as Src, Ext, AltText as Alt, Photo 
            FROM dbo.Categories
            """).ToList();

        if (!Directory.Exists("Result"))
        {
            Directory.CreateDirectory("Result");
        }

        foreach (var item in results)
        {
            File.WriteAllBytes(Path.Combine("Result", item.FileName), item.Photo);
        }

    }

    public static void TruncateTable(string tableName)
    {
        if (string.IsNullOrWhiteSpace(tableName) || !IsValidSqlIdentifier(tableName))
            throw new ArgumentException("Invalid table name.", nameof(tableName));

        var sql = 
            $"""
             TRUNCATE TABLE dbo.{tableName};
             DBCC CHECKIDENT ('dbo.{tableName}', RESEED, 1);
             """;

        using var cn = new SqlConnection(AppConnections.Instance.MainConnection);
        cn.Execute(sql);
    }

    private static bool IsValidSqlIdentifier(string name)
    {
        return SqlIdentifierRegex().IsMatch(name);
    }

    [GeneratedRegex(@"^[A-Za-z_][A-Za-z0-9_]*$")]
    private static partial Regex SqlIdentifierRegex();
}

ASP.NET Core project

page displaying our images with alternate text

For true validation, create an ASP.NET Core project using EF Core to ensure the images can be displayed as shown above.

EF Power Tools, a Visual Studio extension, was used to reverse engineer the database.

The connection string is stored in appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=.\\SQLEXPRESS;Initial Catalog=WorkingImages;Integrated Security=True;Encrypt=False"
  }
}

Dependency injection setup in Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddRazorPages();

        builder.Services.AddDbContext<Context>(options =>
            options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

        var app = builder.Build();
...

Index.cshtml

@page
@using CategoriesApplication1.Classes
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}





 class="container">

     class="row">
         class="col-12">
             class="fs-4">Categories
        
    
    @for (var index = 0; index < Model.Categories.Count; index += 3)
    {

        <div class="row mb-4">

            @for (var innerIndex = index; innerIndex < index + 3 && innerIndex < Model.Categories.Count; innerIndex++)
            {
                <div class="col-md-4">

                     class="rotate">

                         src="data:image/png;base64,@Convert.ToBase64String(@Model.Categories[innerIndex].Photo)"
                             alt="@Model.Categories[innerIndex].AltText"
                             class="img-fluid "/>

                    

                     class="mt-3">@Model.Categories[innerIndex].Name.SplitCase()

                
            }
}