Example T1231021
Visible to All Users

Office File API – Integrate AI

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

Documentation

Example Code

Controllers/OpenAIController.cs
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); } } } }
Controllers/LanguageController.cs
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); } } } }
BusinessObjects/OpenAIClientImageHelper.cs
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; } } }
BusinessObjects/OpenAIClientHyperlinkHelper.cs
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; } } }
BusinessObjects/AzureAILanguageHelper.cs
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('_', '-'); } } }
BusinessObjects/AzureAITranslationHelper.cs
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; } } }
BusinessObjects/Helpers.cs
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; } } } }

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.