diff --git a/OpenAIChatGPTBlazor/Components/EditImageOptions.razor b/OpenAIChatGPTBlazor/Components/EditImageOptions.razor new file mode 100644 index 0000000..025cdb8 --- /dev/null +++ b/OpenAIChatGPTBlazor/Components/EditImageOptions.razor @@ -0,0 +1,60 @@ +@using OpenAI.Images +@using Microsoft.AspNetCore.Components.Forms + + +
+
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + +
+
+
+
+ +@code { + private string _size = "1024x1024"; + private string _quality = "high"; + private string _inputFidelity = "standard"; + + public GeneratedImageSize GetImageSize() + { + var parts = _size.Split("x"); + return new GeneratedImageSize(int.Parse(parts[0]), int.Parse(parts[1])); + } + + public GeneratedImageQuality GetImageQuality() + { + return new GeneratedImageQuality(_quality); + } + + public string GetInputFidelity() + { + return _inputFidelity; + } +} diff --git a/OpenAIChatGPTBlazor/Components/Layout/NavMenu.razor b/OpenAIChatGPTBlazor/Components/Layout/NavMenu.razor index 97ae135..aafb78d 100644 --- a/OpenAIChatGPTBlazor/Components/Layout/NavMenu.razor +++ b/OpenAIChatGPTBlazor/Components/Layout/NavMenu.razor @@ -18,6 +18,11 @@ Image Gen + diff --git a/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor b/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor new file mode 100644 index 0000000..386bba4 --- /dev/null +++ b/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor @@ -0,0 +1,93 @@ +@page "/EditImage" +@rendermode InteractiveServer +@using System.Globalization +@using Microsoft.Extensions.Options; +@using Microsoft.FeatureManagement; +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@inject IConfiguration Configuration +@inject IJSRuntime JS +@inject IFeatureManager FeatureManager +@inject IOptionsMonitor OpenAIOptions + +Image Editor + +
+
+

+ Image Editor using OpenAI +

+
+
+ + @if (_loading) + { +
+
+

... please wait ...

+ } + @if (_warningMessage.Length > 0) + { +
+ Warning! @_warningMessage. +
+ } + + @if (_originalImageDataUrl != null) + { +
+
+
Original Image:
+ +
+
+ } + + @if (_editedImageDataUrl != null) + { +
+
+
Edited Image:
+ +
+ +
+
+
+ } +
+
+ +
diff --git a/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor.cs b/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor.cs new file mode 100644 index 0000000..0cb64c7 --- /dev/null +++ b/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor.cs @@ -0,0 +1,243 @@ +using System; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; +using OpenAI; +using OpenAI.Images; + +namespace OpenAIChatGPTBlazor.Components.Pages +{ + public partial class EditImage : ComponentBase, IAsyncDisposable + { + [Inject(Key = "OpenAi_Image")] + public OpenAIClient OpenAIClient { get; set; } = null!; + + private CancellationTokenSource? _editCancellationTokenSource; + private string _warningMessage = string.Empty; + private bool _loading = false; + private EditImageOptions _optionsComponent = new(); + private IJSObjectReference? _module; + + private string _prompt = string.Empty; + private IBrowserFile? _uploadedFile; + private string? _originalImageDataUrl; + private string? _editedImageDataUrl; + private BinaryData? _editedImageData; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _module = await JS.InvokeAsync( + "import", + $"./Components/Pages/EditImage.razor.js?v={DateTime.Now.Ticks}" + ); + } + } + + private async Task OnFileSelected(InputFileChangeEventArgs e) + { + try + { + _warningMessage = string.Empty; + _uploadedFile = e.File; + + if (_uploadedFile != null) + { + // Validate file size (50 MB limit for GPT-image-1) + const long maxFileSize = 50 * 1024 * 1024; // 50 MB + if (_uploadedFile.Size > maxFileSize) + { + _warningMessage = "File size must be less than 50 MB."; + _uploadedFile = null; + return; + } + + // Validate file type + if (!_uploadedFile.ContentType.StartsWith("image/")) + { + _warningMessage = "Please select a valid image file (PNG or JPG)."; + _uploadedFile = null; + return; + } + + // Read and display the uploaded image (read full stream) + using var stream = _uploadedFile.OpenReadStream(maxFileSize); + var buffer = new byte[_uploadedFile.Size]; + int totalRead = 0; + while (totalRead < buffer.Length) + { + int read = await stream.ReadAsync( + buffer, + totalRead, + buffer.Length - totalRead + ); + if (read == 0) + break; + totalRead += read; + } + + var base64 = Convert.ToBase64String(buffer); + _originalImageDataUrl = $"data:{_uploadedFile.ContentType};base64,{base64}"; + + // Clear previous edit result + _editedImageDataUrl = null; + _editedImageData = null; + } + } + catch (Exception ex) + { + _warningMessage = $"Error uploading file: {ex.Message}"; + _uploadedFile = null; + } + finally + { + StateHasChanged(); + } + } + + private async Task OnEditClick() => await RunEdit(); + + private async Task OnPromptKeydown(KeyboardEventArgs e) + { + if ((e.Key == "Enter" || e.Key == "NumpadEnter") && e.CtrlKey) + { + await RunEdit(); + } + } + + private void OnAbortClick() => AbortEdit(); + + private async Task RunEdit() + { + if (_uploadedFile == null || string.IsNullOrWhiteSpace(_prompt)) + return; + + try + { + _loading = true; + _warningMessage = string.Empty; + StateHasChanged(); + + _editCancellationTokenSource?.Dispose(); + _editCancellationTokenSource = new CancellationTokenSource(); + + // Read the image file (read full stream) + const long maxFileSize = 50 * 1024 * 1024; // 50 MB + using var stream = _uploadedFile.OpenReadStream(maxFileSize); + var buffer = new byte[_uploadedFile.Size]; + int totalRead = 0; + while (totalRead < buffer.Length) + { + int read = await stream.ReadAsync( + buffer, + totalRead, + buffer.Length - totalRead, + _editCancellationTokenSource.Token + ); + if (read == 0) + break; + totalRead += read; + } + var imageStream = new System.IO.MemoryStream(buffer); + + // Use the OpenAI SDK for image editing + var imageClient = OpenAIClient.GetImageClient("gpt-image-1"); + + // Create image edit options + var editOptions = new ImageEditOptions() + { + Size = _optionsComponent.GetImageSize(), + // ResponseFormat = GeneratedImageFormat.Bytes, + }; + + // Call the SDK method for image editing + var result = await imageClient.GenerateImageEditAsync( + imageStream, + _uploadedFile.Name, + _prompt, + editOptions, + _editCancellationTokenSource.Token + ); + + if (result?.Value?.ImageBytes != null) + { + _editedImageData = result.Value.ImageBytes; + var base64 = Convert.ToBase64String(_editedImageData.ToArray()); + _editedImageDataUrl = $"data:image/png;base64,{base64}"; + } + + _loading = false; + } + catch (TaskCanceledException) + when (_editCancellationTokenSource?.IsCancellationRequested == true) + { + // Gracefully handle cancellation + _loading = false; + } + catch (Exception ex) + { + _warningMessage = ex.Message; + _loading = false; + } + finally + { + StateHasChanged(); + } + } + + private void AbortEdit() + { + try + { + if ( + _editCancellationTokenSource?.Token != null + && _editCancellationTokenSource.Token.CanBeCanceled + ) + { + _editCancellationTokenSource.Cancel(); + } + } + catch (Exception ex) + { + _warningMessage = ex.Message; + } + } + + private async Task DownloadEditedImage() + { + if (_editedImageData == null) + return; + + try + { + var fileName = $"edited_image_{DateTime.Now:yyyyMMdd_HHmmss}.png"; + var streamRef = new DotNetStreamReference(_editedImageData.ToStream()); + + if (_module is not null) + { + await _module.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef); + } + else + { + _warningMessage = "JavaScript module not loaded. Please refresh the page."; + } + } + catch (Exception ex) + { + _warningMessage = $"Error downloading image: {ex.Message}"; + } + } + + public async ValueTask DisposeAsync() + { + if (_module is not null) + { + await _module.DisposeAsync(); + } + } + } +} diff --git a/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor.js b/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor.js new file mode 100644 index 0000000..81a6e2a --- /dev/null +++ b/OpenAIChatGPTBlazor/Components/Pages/EditImage.razor.js @@ -0,0 +1,12 @@ +// Download function for EditImage page +export async function downloadFileFromStream(fileName, contentStreamReference) { + const arrayBuffer = await contentStreamReference.arrayBuffer(); + const blob = new Blob([arrayBuffer]); + const url = URL.createObjectURL(blob); + const anchorElement = document.createElement('a'); + anchorElement.href = url; + anchorElement.download = fileName ?? ''; + anchorElement.click(); + anchorElement.remove(); + URL.revokeObjectURL(url); +} diff --git a/OpenAIChatGPTBlazor/Components/Pages/GenerateImage.razor b/OpenAIChatGPTBlazor/Components/Pages/GenerateImage.razor index a820747..a5b77b5 100644 --- a/OpenAIChatGPTBlazor/Components/Pages/GenerateImage.razor +++ b/OpenAIChatGPTBlazor/Components/Pages/GenerateImage.razor @@ -8,7 +8,7 @@ @inject IFeatureManager FeatureManager @inject IOptionsMonitor OpenAIOptions -My DALL-E +Image Generation
diff --git a/OpenAIChatGPTBlazor/OpenAIChatGPTBlazor.csproj b/OpenAIChatGPTBlazor/OpenAIChatGPTBlazor.csproj index 185f505..07c6356 100644 --- a/OpenAIChatGPTBlazor/OpenAIChatGPTBlazor.csproj +++ b/OpenAIChatGPTBlazor/OpenAIChatGPTBlazor.csproj @@ -9,7 +9,7 @@ - + @@ -21,10 +21,6 @@ all runtime; build; native; contentfiles; analyzers - - - - \ No newline at end of file