diff --git a/BlazorInputFile/FileListEntryImpl.cs b/BlazorInputFile/FileListEntryImpl.cs index 258b1dd..314558b 100644 --- a/BlazorInputFile/FileListEntryImpl.cs +++ b/BlazorInputFile/FileListEntryImpl.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; namespace BlazorInputFile { @@ -34,6 +35,11 @@ public Stream Data } } + public async Task ToImageFileAsync(string format, int maxWidth, int maxHeight) + { + return await Owner.ConvertToImageFileAsync(this, format, maxWidth, maxHeight); + } + internal void RaiseOnDataRead() { OnDataRead?.Invoke(this, null); diff --git a/BlazorInputFile/IFileListEntry.cs b/BlazorInputFile/IFileListEntry.cs index 38d02b7..9158718 100644 --- a/BlazorInputFile/IFileListEntry.cs +++ b/BlazorInputFile/IFileListEntry.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; namespace BlazorInputFile { @@ -17,6 +18,8 @@ public interface IFileListEntry Stream Data { get; } + Task ToImageFileAsync(string format, int maxWidth, int maxHeight); + event EventHandler OnDataRead; } } diff --git a/BlazorInputFile/InputFile.razor b/BlazorInputFile/InputFile.razor index 0b37811..9cd9a76 100644 --- a/BlazorInputFile/InputFile.razor +++ b/BlazorInputFile/InputFile.razor @@ -40,6 +40,16 @@ : new RemoteFileListEntryStream(JSRuntime, inputFileElement, file, MaxMessageSize, MaxBufferSize); } + internal async Task ConvertToImageFileAsync(FileListEntryImpl file, string format, int maxWidth, int maxHeight) + { + var imageFile = await JSRuntime.InvokeAsync("BlazorInputFile.toImageFile", inputFileElement, file.Id, format, maxWidth, maxHeight); + + // So that method invocations on the file can be dispatched back here + imageFile.Owner = (InputFile)(object)this; + + return imageFile; + } + void IDisposable.Dispose() { thisReference?.Dispose(); diff --git a/BlazorInputFile/wwwroot/inputfile.js b/BlazorInputFile/wwwroot/inputfile.js index 30202f0..97bceac 100644 --- a/BlazorInputFile/wwwroot/inputfile.js +++ b/BlazorInputFile/wwwroot/inputfile.js @@ -1,14 +1,14 @@ (function () { window.BlazorInputFile = { init: function init(elem, componentInstance) { - var nextFileId = 0; + elem._blazorInputFileNextFileId = 0; elem.addEventListener('change', function handleInputFileChange(event) { // Reduce to purely serializable data, plus build an index by ID elem._blazorFilesById = {}; var fileList = Array.prototype.map.call(elem.files, function (file) { var result = { - id: ++nextFileId, + id: ++elem._blazorInputFileNextFileId, lastModified: new Date(file.lastModified).toISOString(), name: file.name, size: file.size, @@ -34,6 +34,41 @@ }); }, + toImageFile(elem, fileId, format, maxWidth, maxHeight) { + var originalFile = getFileById(elem, fileId); + return createImageBitmap(originalFile.blob) + .then(function (imageBitmap) { + return new Promise(function (resolve) { + var desiredWidthRatio = Math.min(1, maxWidth / imageBitmap.width); + var desiredHeightRatio = Math.min(1, maxHeight / imageBitmap.height); + var chosenSizeRatio = Math.min(desiredWidthRatio, desiredHeightRatio); + + var canvas = document.createElement('canvas'); + canvas.width = Math.round(imageBitmap.width * chosenSizeRatio); + canvas.height = Math.round(imageBitmap.height * chosenSizeRatio); + canvas.getContext('2d').drawImage(imageBitmap, 0, 0, canvas.width, canvas.height); + return canvas.toBlob(resolve, format); + }); + }) + .then(function (resizedImageBlob) { + var result = { + id: ++elem._blazorInputFileNextFileId, + lastModified: originalFile.lastModified, + name: originalFile.name, // Note: we're not changing the file extension + size: resizedImageBlob.size, + type: format, + relativePath: originalFile.relativePath + }; + + elem._blazorFilesById[result.id] = result; + + // Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON + Object.defineProperty(result, 'blob', { value: resizedImageBlob }); + + return result; + }); + }, + readFileData: function readFileData(elem, fileId, startOffset, count) { var readPromise = getArrayBufferFromFileAsync(elem, fileId); diff --git a/samples/Sample.Core/App.razor b/samples/Sample.Core/App.razor index 2b4564e..7ec0b21 100644 --- a/samples/Sample.Core/App.razor +++ b/samples/Sample.Core/App.razor @@ -3,6 +3,7 @@ Single file Multiple files Drag/drop viewer + Image file Native upload
diff --git a/samples/Sample.Core/Pages/ImageFile.razor b/samples/Sample.Core/Pages/ImageFile.razor new file mode 100644 index 0000000..a165fcb --- /dev/null +++ b/samples/Sample.Core/Pages/ImageFile.razor @@ -0,0 +1,37 @@ +@page "/image" + +

Image file

+ +

A single file input that reads the input as an image in a chosen format with specified maximum dimensions.

+ + + +

@status

+ +@if (!string.IsNullOrEmpty(imageDataUri)) +{ + +} + +@code { + string status; + string imageDataUri; + + async Task HandleSelection(IFileListEntry[] files) + { + var rawFile = files.FirstOrDefault(); + if (rawFile != null) + { + // Load as an image file in memory + var format = "image/jpeg"; + var imageFile = await rawFile.ToImageFileAsync(format, 640, 480); + var ms = new MemoryStream(); + await imageFile.Data.CopyToAsync(ms); + + // Make a data URL so we can display it + imageDataUri = $"data:{format};base64,{Convert.ToBase64String(ms.ToArray())}"; + + status = $"Finished loading {ms.Length} bytes from {imageFile.Name}"; + } + } +}