The following project integrates AI capabilities into a DevExpress-powered Office File API Web API application. This project uses the following AI services:
- OpenAI API generates descriptions for images, charts and hyperlinks in Microsoft Word and Excel files.
- Azure AI Language API detects the language for text paragraphs in Word files.
- Azure AI Translator API translates paragraph text to the chosen language in Word files.
[!note]
Before you incorporate this solution in your app, please be sure to read and understand terms and conditions of using AI services.
Implementation Details
The project uses the Azure.AI.OpenAI, Azure.AI.TextAnalytics and Azure.AI.Translation.Text NuGet packages. Azure.AI.OpenAI adapts OpenAI's REST APIs for use in non-Azure OpenAI development. Azure.AI.TextAnalytics and Azure.AI.Translation.Text require an Azure subscription. Once you obtain it, create a Language resource and a Translator resource (or a single multi-service resource) in the Azure portal to get your keys and endpoints for client authentication.
OpenAIController
includes endpoints to generate image, chart and hyperlink descriptions. The OpenAIClientImageHelper
class sends a request to describe an image and obtains a string with a response. The OpenAIClientHyperlinkHelper
class sends a request to describe an hyperlink and obtains a string with a response. The OpenAIClientImageHelper.DescribeImageAsync
and OpenAIClientHyperlinkHelper.DescribeHyperlinkAsync
methods are executed within the corresponding endpoints.
For Excel files, charts are converted to images to obtain relevant descriptions.
LanguageController
includes the endpoint to detect the language for text paragraphs and generate paragraph transaltions. The AzureAILanguageHelper
class sends a request to detect the language of the specified text and returns the language name in the "ISO 693-1" format. The AzureAITranslationHelper
class sends a request to translate the given text to the specified language and returns the transaled text string. The AzureAILanguageHelper.DetectTextLanguage
and AzureAITranslationHelper.TranslateText
methods are executed in the GenerateLanguageSettingsForParagraphs
endpoint.
Files to Review
- OpenAIController.cs
- LanguageController.cs
- OpenAIClientImageHelper.cs
- OpenAIClientHyperlinkHelper.cs
- AzureAILanguageHelper.cs
- AzureAITranslationHelper.cs
- Helpers.cs
Documentation
- Office File API — Enhance Accessibility in Office Documents (Word & Excel) using OpenAI Models
- Office File API — Enhance Accessibility in Office Documents using OpenAI Models (Part 2)
Example Code
C#using DevExpress.Office.Utils;
using DevExpress.Spreadsheet;
using DevExpress.XtraPrinting.Native;
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
using Microsoft.AspNetCore.Mvc;
using RichEditOpenAIWebApi.BusinessObjects;
using Swashbuckle.AspNetCore.Annotations;
using System.Globalization;
using System.Net;
namespace RichEditOpenAIWebApi.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
public class OpenAIController : ControllerBase
{
// Insert your OpenAI key
string openAIApiKey = "";
[HttpPost]
[SwaggerResponse((int)HttpStatusCode.OK, "Download a file", typeof(FileContentResult))]
public async Task<IActionResult> GenerateImageAltText(IFormFile documentWithImage, [FromQuery] RichEditFormat outputFormat)
{
try
{
var imageHelper = new OpenAIClientImageHelper(openAIApiKey);
using (var wordProcessor = new RichEditDocumentServer())
{
await RichEditHelper.LoadFile(wordProcessor, documentWithImage);
wordProcessor.IterateSubDocuments((document) =>
{
foreach (var shape in document.Shapes)
{
if (shape.Type == DevExpress.XtraRichEdit.API.Native.ShapeType.Picture && string.IsNullOrEmpty(shape.AltText))
shape.AltText = imageHelper.DescribeImageAsync(shape.PictureFormat.Picture).Result;
}
});
Stream result = RichEditHelper.SaveDocument(wordProcessor, outputFormat);
string contentType = RichEditHelper.GetContentType(outputFormat);
string outputStringFormat = outputFormat.ToString().ToLower();
return File(result, contentType, $"result.{outputStringFormat}");
}
}
catch (Exception e)
{
return StatusCode(500, e.Message + Environment.NewLine + e.StackTrace);
}
}
[HttpPost]
[SwaggerResponse((int)HttpStatusCode.OK, "Download a file", typeof(FileContentResult))]
public async Task<IActionResult> GenerateChartAltText(IFormFile documentWithImage, [FromQuery] SpreadsheetFormat outputFormat)
{
try
{
var imageHelper = new OpenAIClientImageHelper(openAIApiKey);
using (var workbook = new Workbook())
{
await SpreadsheetHelper.LoadWorkbook(workbook, documentWithImage);
foreach (var worksheet in workbook.Worksheets)
{
foreach (var chart in worksheet.Charts)
{
OfficeImage image = chart.ExportToImage();
chart.AlternativeText = imageHelper.DescribeImageAsync(image).Result;
}
}
Stream result = SpreadsheetHelper.SaveDocument(workbook, outputFormat);
string contentType = SpreadsheetHelper.GetContentType(outputFormat);
string outputStringFormat = outputFormat.ToString().ToLower();
return File(result, contentType, $"result.{outputStringFormat}");
}
}
catch (Exception e)
{
return StatusCode(500, e.Message + Environment.NewLine + e.StackTrace);
}
}
[HttpPost]
[SwaggerResponse((int)HttpStatusCode.OK, "Download a file", typeof(FileContentResult))]
public async Task<IActionResult> GenerateHyperlinkDescriptionForWord(IFormFile documentWithHyperlinks, [FromQuery] RichEditFormat outputFormat)
{
try
{
var hyperlinkHelper = new OpenAIClientHyperlinkHelper(openAIApiKey);
using (var wordProcessor = new RichEditDocumentServer())
{
await RichEditHelper.LoadFile(wordProcessor, documentWithHyperlinks);
wordProcessor.IterateSubDocuments(async (document) =>
{
foreach (var hyperlink in document.Hyperlinks)
{
if (string.IsNullOrEmpty(hyperlink.ToolTip) || hyperlink.ToolTip == hyperlink.NavigateUri)
{
hyperlink.ToolTip = hyperlinkHelper.DescribeHyperlinkAsync(hyperlink.NavigateUri).Result;
}
}
});
Stream result = RichEditHelper.SaveDocument(wordProcessor, outputFormat);
string contentType = RichEditHelper.GetContentType(outputFormat);
string outputStringFormat = outputFormat.ToString().ToLower();
return File(result, contentType, $"result.{outputStringFormat}");
}
}
catch (Exception e)
{
return StatusCode(500, e.Message + Environment.NewLine + e.StackTrace);
}
}
[HttpPost]
[SwaggerResponse((int)HttpStatusCode.OK, "Download a file", typeof(FileContentResult))]
public async Task<IActionResult> GenerateHyperlinkDescriptionForSpreadsheet(IFormFile documentWithHyperlinks, [FromQuery] SpreadsheetFormat outputFormat)
{
try
{
var hyperlinkHelper = new OpenAIClientHyperlinkHelper(openAIApiKey);
using (var workbook = new Workbook())
{
await SpreadsheetHelper.LoadWorkbook(workbook, documentWithHyperlinks);
foreach (var worksheet in workbook.Worksheets)
{
foreach (var hyperlink in worksheet.Hyperlinks)
{
if(hyperlink.IsExternal && (string.IsNullOrEmpty(hyperlink.TooltipText) || hyperlink.TooltipText == hyperlink.Uri))
hyperlink.TooltipText = hyperlinkHelper.DescribeHyperlinkAsync(hyperlink.Uri).Result;
}
}
Stream result = SpreadsheetHelper.SaveDocument(workbook, outputFormat);
string contentType = SpreadsheetHelper.GetContentType(outputFormat);
string outputStringFormat = outputFormat.ToString().ToLower();
return File(result, contentType, $"result.{outputStringFormat}");
}
}
catch (Exception e)
{
return StatusCode(500, e.Message + Environment.NewLine + e.StackTrace);
}
}
}
}
C#using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
using Microsoft.AspNetCore.Mvc;
using RichEditOpenAIWebApi.BusinessObjects;
using Swashbuckle.AspNetCore.Annotations;
using System.Globalization;
using System.Net;
namespace RichEditOpenAIWebApi.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
public class LanguageController : ControllerBase
{
// Insert your Azure key and end point for the Language Service
string languageAzureKey = "";
Uri languageEndPoint = new Uri("");
// Insert your Azure key and end point for the Translation Service
string translationAzureKey = "";
Uri translationEndPoint = new Uri("");
[HttpPost]
[SwaggerResponse((int)HttpStatusCode.OK, "Download a file", typeof(FileContentResult))]
public async Task<IActionResult> GenerateLanguageSettingsForParagraphs(IFormFile documentWithHyperlinks, [FromQuery] RichEditFormat outputFormat)
{
try
{
var languageHelper = new AzureAILanguageHelper(languageAzureKey, languageEndPoint);
var translationHelper = new AzureAITranslationHelper(translationAzureKey, translationEndPoint);
using (var server = new RichEditDocumentServer())
{
await RichEditHelper.LoadFile(server, documentWithHyperlinks);
server.IterateSubDocuments(async (document) =>
{
foreach (var paragraph in document.Paragraphs)
{
CharacterProperties cp = document.BeginUpdateCharacters(paragraph.Range);
string paragraphText = document.GetText(paragraph.Range);
if (cp.Language.Value.Latin == null && !string.IsNullOrWhiteSpace(paragraphText))
{
CultureInfo? culture = null;
string language = languageHelper.DetectTextLanguage(paragraphText).Result;
try
{
culture = new CultureInfo((language));
}
catch { }
finally
{
if (culture != null)
{
cp.Language = new DevExpress.XtraRichEdit.Model.LangInfo(culture, null, null);
if (language != "en")
{
Comment comment = document.Comments.Create(paragraph.Range, "Translator");
SubDocument commentDoc = comment.BeginUpdate();
string translatedText = translationHelper.TranslateText(paragraphText, language, "en").Result;
commentDoc.InsertText(commentDoc.Range.Start, $"Detected Language: {language}\r\nTranslation (en): {translatedText}");
comment.EndUpdate(commentDoc);
}
}
}
}
document.EndUpdateCharacters(cp);
}
});
Stream result = RichEditHelper.SaveDocument(server, outputFormat);
string contentType = RichEditHelper.GetContentType(outputFormat);
string outputStringFormat = outputFormat.ToString().ToLower();
return File(result, contentType, $"result.{outputStringFormat}");
}
}
catch (Exception e)
{
return StatusCode(500, e.Message + Environment.NewLine + e.StackTrace);
}
}
}
}
C#using Azure;
using Azure.AI.OpenAI;
using DevExpress.Drawing;
using DevExpress.Office.Utils;
namespace RichEditOpenAIWebApi.BusinessObjects {
class OpenAIClientImageHelper {
OpenAIClient client;
internal OpenAIClientImageHelper(string openAIApiKey) {
client = new OpenAIClient(openAIApiKey, new OpenAIClientOptions());
}
string ConvertDXImageToBase64String(DXImage image) {
using (MemoryStream stream = new MemoryStream()) {
image.Save(stream, DXImageFormat.Png);
byte[] imageBytes = stream.ToArray();
return Convert.ToBase64String(imageBytes);
}
}
internal async Task<string> DescribeImageAsync(OfficeImage image) {
return await GetImageDescription(image.GetImageBytes(OfficeImageFormat.Png));
}
internal async Task<string> GetImageDescription(byte[] imageBytes) {
ChatCompletionsOptions chatCompletionsOptions = new() {
DeploymentName = "gpt-4-vision-preview",
Messages =
{
new ChatRequestSystemMessage("You are a helpful assistant that describes hyperlinks."),
new ChatRequestUserMessage(
new ChatMessageTextContentItem("Give a description of this image in no more than 10 words"),
new ChatMessageImageContentItem(BinaryData.FromBytes(imageBytes), "image/png"))
},
MaxTokens = 300
};
Response<ChatCompletions> chatResponse = await client.GetChatCompletionsAsync(chatCompletionsOptions);
ChatChoice choice = chatResponse.Value.Choices[0];
return choice.Message.Content;
}
}
}
C#using Azure;
using Azure.AI.OpenAI;
using DevExpress.Drawing;
using DevExpress.Office.Utils;
namespace RichEditOpenAIWebApi.BusinessObjects
{
public class OpenAIClientHyperlinkHelper
{
OpenAIClient client;
internal OpenAIClientHyperlinkHelper(string openAIApiKey)
{
client = new OpenAIClient(openAIApiKey, new OpenAIClientOptions());
}
internal async Task<string> DescribeHyperlinkAsync(string link)
{
ChatCompletionsOptions chatCompletionsOptions = new()
{
DeploymentName = "gpt-4-vision-preview",
Messages =
{
new ChatRequestSystemMessage("You are a helpful assistant that describes images."),
new ChatRequestUserMessage(
new ChatMessageTextContentItem("Give a description of this hyperlink URI in 10-20 words"),
new ChatMessageTextContentItem(link))
},
MaxTokens = 300
};
Response<ChatCompletions> chatResponse = await client.GetChatCompletionsAsync(chatCompletionsOptions);
ChatChoice choice = chatResponse.Value.Choices[0];
return choice.Message.Content;
}
}
}
C#using Azure;
using Azure.AI.TextAnalytics;
namespace RichEditOpenAIWebApi.BusinessObjects
{
public class AzureAILanguageHelper
{
private TextAnalyticsClient client;
internal AzureAILanguageHelper(string key, Uri endpoint)
{
AzureKeyCredential azureCredential = new AzureKeyCredential(key);
client = new TextAnalyticsClient(endpoint, azureCredential);
}
internal async Task<string> DetectTextLanguage(string text)
{
DetectedLanguage detectedLanguage = await client.DetectLanguageAsync(text);
return detectedLanguage.Iso6391Name.Replace('_', '-');
}
}
}
C#using Azure;
using Azure.AI.Translation.Text;
namespace RichEditOpenAIWebApi.BusinessObjects
{
public class AzureAITranslationHelper
{
TextTranslationClient client;
internal AzureAITranslationHelper(string key, Uri endpoint, string region = "global")
{
AzureKeyCredential azureCredential = new AzureKeyCredential(key);
client = new TextTranslationClient(azureCredential, endpoint, region);
}
internal async Task<string> TranslateText(string text, string sourceLanguage, string targetLanguage)
{
Response<IReadOnlyList<TranslatedTextItem>> response = await client.TranslateAsync(targetLanguage, text, sourceLanguage);
TranslatedTextItem translatedTextItem = response.Value.First();
return translatedTextItem.Translations[0].Text;
}
}
}
C#using DevExpress.Spreadsheet;
using DevExpress.XtraRichEdit;
using RichEditDocumentFormat = DevExpress.XtraRichEdit.DocumentFormat;
using RichEditEncryptionSettings = DevExpress.XtraRichEdit.API.Native.EncryptionSettings;
using SpreadsheetDocumentFormat = DevExpress.Spreadsheet.DocumentFormat;
using SpreadsheetEncryptionSettings = DevExpress.Spreadsheet.EncryptionSettings;
namespace RichEditOpenAIWebApi.BusinessObjects
{
public static class RichEditHelper
{
public static async Task LoadFile(RichEditDocumentServer server, IFormFile file)
{
using (var stream = new MemoryStream())
{
await file.CopyToAsync(stream);
stream.Seek(0, SeekOrigin.Begin);
server.LoadDocument(stream);
}
}
public static Stream SaveDocument(RichEditDocumentServer server, RichEditFormat outputFormat, RichEditEncryptionSettings? encryptionSettings = null)
{
MemoryStream resultStream = new MemoryStream();
if (outputFormat == RichEditFormat.Pdf)
server.ExportToPdf(resultStream);
else
{
RichEditDocumentFormat documentFormat = new RichEditDocumentFormat((int)outputFormat);
if (documentFormat == RichEditDocumentFormat.Html)
server.Options.Export.Html.EmbedImages = true;
if (encryptionSettings == null)
server.SaveDocument(resultStream, documentFormat);
else
server.SaveDocument(resultStream, documentFormat, encryptionSettings);
}
resultStream.Seek(0, SeekOrigin.Begin);
return resultStream;
}
public static string GetContentType(RichEditFormat documentFormat)
{
switch (documentFormat)
{
case RichEditFormat.Doc:
case RichEditFormat.Dot:
return "application/msword";
case RichEditFormat.Docm:
case RichEditFormat.Docx:
case RichEditFormat.Dotm:
case RichEditFormat.Dotx:
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case RichEditFormat.ePub:
return "application/epub+zip";
case RichEditFormat.Mht:
case RichEditFormat.Html:
return "text/html";
case RichEditFormat.Odt:
return "application/vnd.oasis.opendocument.text";
case RichEditFormat.Txt:
return "text/plain";
case RichEditFormat.Rtf:
return "application/rtf";
case RichEditFormat.Xml:
return "application/xml";
case RichEditFormat.Pdf:
return "application/pdf";
default: return string.Empty;
}
}
public static async Task<string> FileToBase64String(IFormFile file)
{
using (var stream = new MemoryStream())
{
await file.CopyToAsync(stream);
stream.Seek(0, SeekOrigin.Begin);
byte[] imageBytes = stream.ToArray();
return Convert.ToBase64String(imageBytes);
}
}
}
public static class SpreadsheetHelper
{
public static async Task LoadWorkbook(Workbook workbook, IFormFile file)
{
using (var stream = new MemoryStream())
{
await file.CopyToAsync(stream);
stream.Seek(0, SeekOrigin.Begin);
workbook.LoadDocument(stream);
}
}
public static Stream SaveDocument(Workbook workbook, SpreadsheetFormat outputFormat, SpreadsheetEncryptionSettings? encryptionSettings = null)
{
MemoryStream resultStream = new MemoryStream();
SpreadsheetDocumentFormat documentFormat = new SpreadsheetDocumentFormat((int)outputFormat);
if (outputFormat == SpreadsheetFormat.Pdf)
workbook.ExportToPdf(resultStream);
else
{
if (encryptionSettings == null)
workbook.SaveDocument(resultStream, documentFormat);
else
workbook.SaveDocument(resultStream, documentFormat, encryptionSettings);
}
resultStream.Seek(0, SeekOrigin.Begin);
return resultStream;
}
public static string GetContentType(SpreadsheetFormat documentFormat)
{
switch (documentFormat)
{
case SpreadsheetFormat.Xls:
case SpreadsheetFormat.Xlt:
return "application/msword";
case SpreadsheetFormat.Xlsx:
case SpreadsheetFormat.Xlsb:
case SpreadsheetFormat.Xlsm:
case SpreadsheetFormat.Xltm:
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case SpreadsheetFormat.Html:
return "text/html";
case SpreadsheetFormat.XmlSpreadsheet2003:
return "application/xml";
case SpreadsheetFormat.Pdf:
return "application/pdf";
default: return string.Empty;
}
}
}
}