mirror of https://github.com/abpframework/abp.git
42 changed files with 1132 additions and 717 deletions
@ -1,15 +1,21 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageCompressor // TODO: RENAME: IImageCompressorContributor
|
|||
public interface IImageCompressor |
|||
{ |
|||
//TODO: new extension method that works with byte arrays
|
|||
Task<Stream> CompressAsync(Stream stream, CancellationToken cancellationToken = default); // TODO: TryCompressAsync & remove CanCompress
|
|||
|
|||
Stream Compress(Stream stream); |
|||
|
|||
bool CanCompress(IImageFormat imageFormat); |
|||
Task<ImageProcessResult<Stream>> CompressAsync( |
|||
Stream stream, |
|||
[CanBeNull] string mimeType = null, |
|||
bool ignoreExceptions = false, |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task<ImageProcessResult<byte[]>> CompressAsync( |
|||
byte[] bytes, |
|||
[CanBeNull] string mimeType = null, |
|||
bool ignoreExceptions = false, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageCompressorContributor |
|||
{ |
|||
Task<ImageContributorResult<Stream>> TryCompressAsync(Stream stream, string mimeType = null, CancellationToken cancellationToken = default); |
|||
Task<ImageContributorResult<byte[]>> TryCompressAsync(byte[] bytes, string mimeType = null, CancellationToken cancellationToken = default); |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageCompressorManager // TODO: Rename to IImageCompressor
|
|||
{ |
|||
Task<Stream> CompressAsync( |
|||
Stream stream, |
|||
[CanBeNull] IImageFormat imageFormat = null, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -1,6 +0,0 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageCompressorSelector //TODO: Remove, merge to IImageCompressorManager
|
|||
{ |
|||
IImageCompressor FindCompressor(IImageFormat imageFormat); |
|||
} |
|||
@ -1,6 +0,0 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageFormat |
|||
{ |
|||
string MimeType { get; } |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
using System.IO; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageFormatDetector |
|||
{ |
|||
IImageFormat FindFormat(Stream image); |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageResizeParameter //TODO: Remove
|
|||
{ |
|||
int? Width { get; } |
|||
int? Height { get; } |
|||
|
|||
ImageResizeMode? Mode { get; } |
|||
} |
|||
@ -1,15 +1,13 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageResizer |
|||
{ |
|||
Task<Stream> ResizeAsync(Stream stream, IImageResizeParameter resizeParameter, |
|||
CancellationToken cancellationToken = default); //TODO: TryResizeAsync...
|
|||
Task<ImageProcessResult<Stream>> ResizeAsync(Stream stream, ImageResizeArgs resizeArgs, [CanBeNull]string mimeType = null, CancellationToken cancellationToken = default); |
|||
|
|||
Stream Resize(Stream stream, IImageResizeParameter resizeParameter); //TODO: Remove
|
|||
|
|||
bool CanResize(IImageFormat imageFormat); //TODO: Discard (merge with TryResizeAsync)
|
|||
Task<ImageProcessResult<byte[]>> ResizeAsync(byte[] bytes, ImageResizeArgs resizeArgs, [CanBeNull] string mimeType = null, CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageResizerContributor |
|||
{ |
|||
Task<ImageContributorResult<Stream>> TryResizeAsync(Stream stream, ImageResizeArgs resizeArgs, string mimeType = null, CancellationToken cancellationToken = default); |
|||
|
|||
Task<ImageContributorResult<byte[]>> TryResizeAsync(byte[] bytes, ImageResizeArgs resizeArgs, string mimeType = null, CancellationToken cancellationToken = default); |
|||
} |
|||
@ -1,6 +0,0 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public interface IImageResizerSelector |
|||
{ |
|||
IImageResizer FindResizer(IImageFormat imageFormat); |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageCompressor : IImageCompressor, ITransientDependency |
|||
{ |
|||
protected IEnumerable<IImageCompressorContributor> ImageCompressorContributors { get; } |
|||
|
|||
public ImageCompressor(IEnumerable<IImageCompressorContributor> imageCompressorContributors) |
|||
{ |
|||
ImageCompressorContributors = imageCompressorContributors; |
|||
} |
|||
|
|||
public virtual async Task<ImageProcessResult<Stream>> CompressAsync(Stream stream, string mimeType = null, bool ignoreExceptions = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
foreach (var imageCompressorContributor in ImageCompressorContributors) |
|||
{ |
|||
var result = await imageCompressorContributor.TryCompressAsync(stream, mimeType, cancellationToken); |
|||
if (!result.IsSupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (!ignoreExceptions && result.Exception != null) |
|||
{ |
|||
throw result.Exception; |
|||
} |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
return new ImageProcessResult<Stream>(result.Result, result.IsSuccess); |
|||
} |
|||
|
|||
return new ImageProcessResult<Stream>(stream, false); |
|||
} |
|||
|
|||
return new ImageProcessResult<Stream>(stream, false); |
|||
} |
|||
|
|||
public virtual async Task<ImageProcessResult<byte[]>> CompressAsync(byte[] bytes, string mimeType = null, bool ignoreExceptions = false, CancellationToken cancellationToken = default) |
|||
{ |
|||
foreach (var imageCompressorContributor in ImageCompressorContributors) |
|||
{ |
|||
var result = await imageCompressorContributor.TryCompressAsync(bytes, mimeType, cancellationToken); |
|||
if (!result.IsSupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (!ignoreExceptions && result.Exception != null) |
|||
{ |
|||
throw result.Exception; |
|||
} |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
return new ImageProcessResult<byte[]>(result.Result, result.IsSuccess); |
|||
} |
|||
|
|||
return new ImageProcessResult<byte[]>(bytes, false); |
|||
} |
|||
|
|||
return new ImageProcessResult<byte[]>(bytes, false); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageContributorResult<T> |
|||
{ |
|||
public T Result { get; } |
|||
public bool IsSuccess { get; } |
|||
public bool IsSupported { get; } |
|||
public Exception Exception { get; } |
|||
|
|||
public ImageContributorResult(T result, bool isSuccess, bool isSupported = true, Exception exception = null) |
|||
{ |
|||
Result = result; |
|||
IsSuccess = isSuccess; |
|||
IsSupported = isSupported; |
|||
Exception = exception; |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageFormat : IImageFormat |
|||
{ |
|||
public ImageFormat(string mimeType) |
|||
{ |
|||
MimeType = mimeType; |
|||
} |
|||
|
|||
public string MimeType { get; } |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageProcessResult<T> |
|||
{ |
|||
public T Result { get; } |
|||
public bool IsSuccess { get; } |
|||
|
|||
public ImageProcessResult(T result, bool isSuccess) |
|||
{ |
|||
Result = result; |
|||
IsSuccess = isSuccess; |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageResizeArgs |
|||
{ |
|||
public int Width { get; set; } |
|||
|
|||
public int Height { get; set; } |
|||
|
|||
public ImageResizeMode Mode { get; set; } = ImageResizeMode.Default; |
|||
|
|||
public ImageResizeArgs(int? width = null, int? height = null, ImageResizeMode? mode = null) |
|||
{ |
|||
if (mode.HasValue) |
|||
{ |
|||
Mode = mode.Value; |
|||
} |
|||
|
|||
if (width is < 0) |
|||
{ |
|||
throw new ArgumentException("Width cannot be negative!", nameof(width)); |
|||
} |
|||
|
|||
if (height is < 0) |
|||
{ |
|||
throw new ArgumentException("Height cannot be negative!", nameof(height)); |
|||
} |
|||
|
|||
Width = width ?? 0; |
|||
Height = height ?? 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageResizeOptions |
|||
{ |
|||
public ImageResizeMode DefaultResizeMode { get; set; } = ImageResizeMode.None; |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageResizer : IImageResizer, ITransientDependency |
|||
{ |
|||
protected IEnumerable<IImageResizerContributor> ImageResizerContributors { get; } |
|||
|
|||
protected ImageResizeOptions ImageResizeOptions { get; } |
|||
|
|||
public ImageResizer(IEnumerable<IImageResizerContributor> imageResizerContributors, IOptions<ImageResizeOptions> imageResizeOptions) |
|||
{ |
|||
ImageResizerContributors = imageResizerContributors; |
|||
ImageResizeOptions = imageResizeOptions.Value; |
|||
} |
|||
|
|||
public async Task<ImageProcessResult<Stream>> ResizeAsync(Stream stream, ImageResizeArgs resizeArgs, string mimeType = null, CancellationToken cancellationToken = default) |
|||
{ |
|||
ChangeDefaultResizeMode(resizeArgs); |
|||
|
|||
foreach (var imageResizerContributor in ImageResizerContributors) |
|||
{ |
|||
var result = await imageResizerContributor.TryResizeAsync(stream, resizeArgs, mimeType, cancellationToken); |
|||
if (!result.IsSupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (result.Exception != null) |
|||
{ |
|||
throw result.Exception; |
|||
} |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
return new ImageProcessResult<Stream>(result.Result, result.IsSuccess); |
|||
} |
|||
|
|||
return new ImageProcessResult<Stream>(stream, false); |
|||
} |
|||
|
|||
return new ImageProcessResult<Stream>(stream, false); |
|||
} |
|||
|
|||
public async Task<ImageProcessResult<byte[]>> ResizeAsync(byte[] bytes, ImageResizeArgs resizeArgs, string mimeType = null, CancellationToken cancellationToken = default) |
|||
{ |
|||
ChangeDefaultResizeMode(resizeArgs); |
|||
|
|||
foreach (var imageResizerContributor in ImageResizerContributors) |
|||
{ |
|||
var result = await imageResizerContributor.TryResizeAsync(bytes, resizeArgs, mimeType, cancellationToken); |
|||
if (!result.IsSupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (result.Exception != null) |
|||
{ |
|||
throw result.Exception; |
|||
} |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
return new ImageProcessResult<byte[]>(result.Result, result.IsSuccess); |
|||
} |
|||
|
|||
return new ImageProcessResult<byte[]>(bytes, false); |
|||
} |
|||
|
|||
return new ImageProcessResult<byte[]>(bytes, false); |
|||
} |
|||
|
|||
protected virtual void ChangeDefaultResizeMode(ImageResizeArgs resizeArgs) |
|||
{ |
|||
if (resizeArgs.Mode == ImageResizeMode.Default) |
|||
{ |
|||
resizeArgs.Mode = ImageResizeOptions.DefaultResizeMode; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
[DependsOn(typeof(AbpImageAbstractionsModule))] |
|||
public class AbpImageAspNetCoreModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class CompressImageAttribute : ActionFilterAttribute |
|||
{ |
|||
public string[] Parameters { get; } |
|||
|
|||
protected IImageCompressor ImageCompressor { get; set; } |
|||
|
|||
public CompressImageAttribute(params string[] parameters) |
|||
{ |
|||
Parameters = parameters; |
|||
} |
|||
|
|||
public async override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) |
|||
{ |
|||
var parameters = Parameters.Any() |
|||
? context.ActionArguments.Where(x => Parameters.Contains(x.Key)).ToArray() |
|||
: context.ActionArguments.ToArray(); |
|||
|
|||
ImageCompressor = context.HttpContext.RequestServices.GetRequiredService<IImageCompressor>(); |
|||
|
|||
foreach (var (key, value) in parameters) |
|||
{ |
|||
object compressedValue = value switch { |
|||
IFormFile file => await CompressImageAsync(file), |
|||
IRemoteStreamContent remoteStreamContent => await CompressImageAsync(remoteStreamContent), |
|||
Stream stream => await CompressImageAsync(stream), |
|||
IEnumerable<byte> bytes => await CompressImageAsync(bytes.ToArray()), |
|||
_ => null |
|||
}; |
|||
|
|||
if (compressedValue != null) |
|||
{ |
|||
context.ActionArguments[key] = compressedValue; |
|||
} |
|||
} |
|||
|
|||
await next(); |
|||
} |
|||
|
|||
protected async Task<IFormFile> CompressImageAsync(IFormFile file) |
|||
{ |
|||
if(file.ContentType == null || !file.ContentType.StartsWith("image/")) |
|||
{ |
|||
return file; |
|||
} |
|||
|
|||
var result = await ImageCompressor.CompressAsync(file.OpenReadStream(), file.ContentType); |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
return new FormFile(result.Result, 0, result.Result.Length, file.Name, file.FileName); |
|||
} |
|||
|
|||
return file; |
|||
} |
|||
|
|||
protected async Task<IRemoteStreamContent> CompressImageAsync(IRemoteStreamContent remoteStreamContent) |
|||
{ |
|||
if(remoteStreamContent.ContentType == null || !remoteStreamContent.ContentType.StartsWith("image/")) |
|||
{ |
|||
return remoteStreamContent; |
|||
} |
|||
|
|||
var result = await ImageCompressor.CompressAsync(remoteStreamContent.GetStream(), remoteStreamContent.ContentType); |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
var fileName = remoteStreamContent.FileName; |
|||
var contentType = remoteStreamContent.ContentType; |
|||
remoteStreamContent.Dispose(); |
|||
return new RemoteStreamContent(result.Result, fileName, contentType); |
|||
} |
|||
|
|||
return remoteStreamContent; |
|||
} |
|||
|
|||
protected async Task<Stream> CompressImageAsync(Stream stream) |
|||
{ |
|||
var result = await ImageCompressor.CompressAsync(stream); |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
await stream.DisposeAsync(); |
|||
return result.Result; |
|||
} |
|||
|
|||
return stream; |
|||
} |
|||
|
|||
protected async Task<byte[]> CompressImageAsync(byte[] bytes) |
|||
{ |
|||
return (await ImageCompressor.CompressAsync(bytes)).Result; |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ResizeImageAttribute : ActionFilterAttribute |
|||
{ |
|||
public int? Width { get; } |
|||
public int? Height { get; } |
|||
|
|||
public ImageResizeMode Mode { get; set; } |
|||
public string[] Parameters { get; } |
|||
|
|||
protected IImageResizer ImageResizer { get; set; } |
|||
|
|||
public ResizeImageAttribute(int width, int height, params string[] parameters) |
|||
{ |
|||
Width = width; |
|||
Height = height; |
|||
Parameters = parameters; |
|||
} |
|||
|
|||
public ResizeImageAttribute(int size, params string[] parameters) |
|||
{ |
|||
Width = size; |
|||
Height = size; |
|||
Parameters = parameters; |
|||
} |
|||
|
|||
public async override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) |
|||
{ |
|||
var parameters = Parameters.Any() |
|||
? context.ActionArguments.Where(x => Parameters.Contains(x.Key)).ToArray() |
|||
: context.ActionArguments.ToArray(); |
|||
|
|||
ImageResizer = context.HttpContext.RequestServices.GetRequiredService<IImageResizer>(); |
|||
|
|||
foreach (var (key, value) in parameters) |
|||
{ |
|||
object resizedValue = value switch { |
|||
IFormFile file => await ResizeImageAsync(file), |
|||
IRemoteStreamContent remoteStreamContent => await ResizeImageAsync(remoteStreamContent), |
|||
Stream stream => await ResizeImageAsync(stream), |
|||
IEnumerable<byte> bytes => await ResizeImageAsync(bytes.ToArray()), |
|||
_ => null |
|||
}; |
|||
|
|||
if (resizedValue != null) |
|||
{ |
|||
context.ActionArguments[key] = resizedValue; |
|||
} |
|||
} |
|||
|
|||
await next(); |
|||
} |
|||
|
|||
protected async Task<IFormFile> ResizeImageAsync(IFormFile file) |
|||
{ |
|||
if(file.ContentType == null || !file.ContentType.StartsWith("image/")) |
|||
{ |
|||
return file; |
|||
} |
|||
|
|||
var result = await ImageResizer.ResizeAsync(file.OpenReadStream(), new ImageResizeArgs(Width, Height, Mode), file.ContentType); |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
return new FormFile(result.Result, 0, result.Result.Length, file.Name, file.FileName); |
|||
} |
|||
|
|||
return file; |
|||
} |
|||
|
|||
protected async Task<IRemoteStreamContent> ResizeImageAsync(IRemoteStreamContent remoteStreamContent) |
|||
{ |
|||
if(remoteStreamContent.ContentType == null || !remoteStreamContent.ContentType.StartsWith("image/")) |
|||
{ |
|||
return remoteStreamContent; |
|||
} |
|||
|
|||
var result = await ImageResizer.ResizeAsync(remoteStreamContent.GetStream(), new ImageResizeArgs(Width, Height, Mode), remoteStreamContent.ContentType); |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
var fileName = remoteStreamContent.FileName; |
|||
var contentType = remoteStreamContent.ContentType; |
|||
remoteStreamContent.Dispose(); |
|||
return new RemoteStreamContent(result.Result, fileName, contentType); |
|||
} |
|||
|
|||
return remoteStreamContent; |
|||
} |
|||
|
|||
protected async Task<Stream> ResizeImageAsync(Stream stream) |
|||
{ |
|||
var result = await ImageResizer.ResizeAsync(stream, new ImageResizeArgs(Width, Height, Mode)); |
|||
|
|||
if (result.IsSuccess) |
|||
{ |
|||
await stream.DisposeAsync(); |
|||
return result.Result; |
|||
} |
|||
|
|||
return stream; |
|||
} |
|||
|
|||
protected async Task<byte[]> ResizeImageAsync(byte[] bytes) |
|||
{ |
|||
return (await ImageResizer.ResizeAsync(bytes, new ImageResizeArgs(Width, Height, Mode))).Result; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageSharpCompressOptions |
|||
{ |
|||
public int JpegQuality { get; set; } = 60; |
|||
public PngCompressionLevel PngCompressionLevel { get; set; } = PngCompressionLevel.BestCompression; |
|||
public bool PngIgnoreMetadata { get; set; } = true; |
|||
public int WebpQuality { get; set; } = 60; |
|||
} |
|||
@ -1,91 +0,0 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
using SixLabors.ImageSharp.Formats.Webp; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageSharpImageCompressor : IImageCompressor, ITransientDependency |
|||
{ |
|||
public async Task<Stream> CompressAsync(Stream stream, CancellationToken cancellationToken = default) |
|||
{ |
|||
var memoryStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); |
|||
|
|||
using var image = await SixLabors.ImageSharp.Image.LoadAsync(memoryStream, cancellationToken); |
|||
memoryStream.Position = 0; |
|||
|
|||
var format = await SixLabors.ImageSharp.Image.DetectFormatAsync(memoryStream, cancellationToken); |
|||
memoryStream.Position = 0; |
|||
|
|||
var encoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(format); |
|||
switch (encoder) |
|||
{ |
|||
case JpegEncoder jpegEncoder: |
|||
jpegEncoder.Quality = 60; |
|||
break; |
|||
case PngEncoder pngEncoder: |
|||
pngEncoder.CompressionLevel = PngCompressionLevel.BestCompression; //TODO: AbpImageSharpOptions (others too)
|
|||
pngEncoder.IgnoreMetadata = true; |
|||
break; |
|||
case WebpEncoder webPEncoder: |
|||
webPEncoder.Quality = 60; |
|||
webPEncoder.UseAlphaCompression = true; |
|||
break; |
|||
case null: |
|||
throw new NotSupportedException($"No encoder available for the given format: {format.Name}"); |
|||
} |
|||
|
|||
await image.SaveAsync(memoryStream, encoder, cancellationToken: cancellationToken); |
|||
memoryStream.SetLength(memoryStream.Position); |
|||
|
|||
return memoryStream; |
|||
} |
|||
|
|||
public Stream Compress(Stream stream) |
|||
{ |
|||
var newStream = stream.CreateMemoryStream(); |
|||
using var image = SixLabors.ImageSharp.Image.Load(newStream); |
|||
newStream.Position = 0; |
|||
var format = SixLabors.ImageSharp.Image.DetectFormat(newStream); |
|||
newStream.Position = 0; |
|||
var encoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(format); |
|||
|
|||
switch (encoder) |
|||
{ |
|||
case JpegEncoder jpegEncoder: |
|||
jpegEncoder.Quality = 60; |
|||
break; |
|||
case PngEncoder pngEncoder: |
|||
pngEncoder.CompressionLevel = PngCompressionLevel.BestCompression; |
|||
pngEncoder.IgnoreMetadata = true; |
|||
break; |
|||
case WebpEncoder webPEncoder: |
|||
webPEncoder.Quality = 60; |
|||
webPEncoder.UseAlphaCompression = true; |
|||
break; |
|||
case null: |
|||
throw new NotSupportedException($"No encoder available for provided path: {format.Name}"); |
|||
} |
|||
|
|||
image.Save(newStream, encoder); |
|||
newStream.SetLength(newStream.Position); |
|||
return newStream; |
|||
} |
|||
|
|||
|
|||
public bool CanCompress(IImageFormat format) |
|||
{ |
|||
//TODO: Use MimeTypes (after moving it to Volo.Abp.Core)
|
|||
return format?.MimeType switch { |
|||
"image/jpeg" => true, |
|||
"image/png" => true, |
|||
"image/webp" => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Options; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
using SixLabors.ImageSharp.Formats.Webp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageSharpImageCompressorContributor : IImageCompressorContributor, ITransientDependency |
|||
{ |
|||
protected ImageSharpCompressOptions Options { get; } |
|||
|
|||
public ImageSharpImageCompressorContributor(IOptions<ImageSharpCompressOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public async Task<ImageContributorResult<Stream>> TryCompressAsync(Stream stream, string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
|
|||
MemoryStream ms = null; |
|||
|
|||
try |
|||
{ |
|||
var (image, format) = await SixLabors.ImageSharp.Image.LoadWithFormatAsync(stream, cancellationToken); |
|||
|
|||
mimeType = format.DefaultMimeType; |
|||
|
|||
if (!CanCompress(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
|
|||
var encoder = image.GetConfiguration().ImageFormatsManager.FindEncoder(format); |
|||
|
|||
switch (encoder) |
|||
{ |
|||
case JpegEncoder jpegEncoder: |
|||
jpegEncoder.Quality = Options.JpegQuality; |
|||
break; |
|||
case PngEncoder pngEncoder: |
|||
pngEncoder.CompressionLevel = Options.PngCompressionLevel; |
|||
pngEncoder.IgnoreMetadata = Options.PngIgnoreMetadata; |
|||
break; |
|||
case WebpEncoder webPEncoder: |
|||
webPEncoder.Quality = Options.WebpQuality; |
|||
webPEncoder.UseAlphaCompression = true; |
|||
break; |
|||
case null: |
|||
throw new NotSupportedException($"No encoder available for the given format: {format.Name}"); |
|||
} |
|||
|
|||
ms = new MemoryStream(); |
|||
await image.SaveAsync(ms, encoder, cancellationToken: cancellationToken); |
|||
ms.Position = 0; |
|||
|
|||
return new ImageContributorResult<Stream>(ms, true); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
ms?.Dispose(); |
|||
|
|||
return new ImageContributorResult<Stream>(stream, false, true, e); |
|||
} |
|||
} |
|||
|
|||
public async Task<ImageContributorResult<byte[]>> TryCompressAsync(byte[] bytes, string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<byte[]>(bytes, false, false); |
|||
} |
|||
|
|||
using var ms = new MemoryStream(bytes); |
|||
var result = await TryCompressAsync(ms, mimeType, cancellationToken); |
|||
|
|||
if (!result.IsSuccess) |
|||
{ |
|||
return new ImageContributorResult<byte[]>(bytes, result.IsSuccess, result.IsSupported, result.Exception); |
|||
} |
|||
|
|||
var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); |
|||
result.Result.Dispose(); |
|||
return new ImageContributorResult<byte[]>(newBytes, true); |
|||
} |
|||
|
|||
private static bool CanCompress(string mimeType) |
|||
{ |
|||
return mimeType switch { |
|||
MimeTypes.Image.Jpeg => true, |
|||
MimeTypes.Image.Png => true, |
|||
MimeTypes.Image.Webp => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
using System.IO; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageSharpImageFormatDetector : IImageFormatDetector, ITransientDependency |
|||
{ |
|||
public IImageFormat FindFormat(Stream stream) |
|||
{ |
|||
var format = SixLabors.ImageSharp.Image.DetectFormat(stream); |
|||
return new ImageFormat(format.DefaultMimeType); |
|||
} |
|||
} |
|||
@ -1,100 +0,0 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using SixLabors.ImageSharp; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageSharpImageResizer : IImageResizer, ITransientDependency |
|||
{ |
|||
|
|||
public async Task<Stream> ResizeAsync(Stream stream, IImageResizeParameter resizeParameter, CancellationToken cancellationToken = default) |
|||
{ |
|||
var newStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); |
|||
using var image = await SixLabors.ImageSharp.Image.LoadAsync(newStream, cancellationToken); |
|||
ApplyMode(image, resizeParameter); |
|||
newStream.Position = 0; |
|||
var format = await SixLabors.ImageSharp.Image.DetectFormatAsync(newStream, cancellationToken); |
|||
newStream.Position = 0; |
|||
await image.SaveAsync(newStream, format, cancellationToken: cancellationToken); |
|||
newStream.SetLength(newStream.Position); |
|||
newStream.Position = 0; |
|||
return newStream; |
|||
} |
|||
|
|||
public Stream Resize(Stream stream, IImageResizeParameter resizeParameter) |
|||
{ |
|||
var newStream = stream.CreateMemoryStream(); |
|||
using var image = SixLabors.ImageSharp.Image.Load(newStream); |
|||
ApplyMode(image, resizeParameter); |
|||
newStream.Position = 0; |
|||
var format = SixLabors.ImageSharp.Image.DetectFormat(newStream); |
|||
newStream.Position = 0; |
|||
image.Save(newStream, format); |
|||
newStream.SetLength(newStream.Position); |
|||
newStream.Position = 0; |
|||
return newStream; |
|||
} |
|||
|
|||
public bool CanResize(IImageFormat imageFormat) |
|||
{ |
|||
return imageFormat?.MimeType switch |
|||
{ |
|||
"image/jpeg" => true, |
|||
"image/png" => true, |
|||
"image/gif" => true, |
|||
"image/bmp" => true, |
|||
"image/tiff" => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
private void ApplyMode(SixLabors.ImageSharp.Image image, IImageResizeParameter resizeParameter) |
|||
{ |
|||
var width = resizeParameter.Width ?? image.Width; |
|||
var height = resizeParameter.Height ?? image.Height; |
|||
|
|||
var defaultResizeOptions = new ResizeOptions { Size = new SixLabors.ImageSharp.Size(width, height) }; |
|||
|
|||
switch (resizeParameter.Mode) |
|||
{ |
|||
case null: |
|||
case ImageResizeMode.None: |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
case ImageResizeMode.Stretch: |
|||
defaultResizeOptions.Mode = ResizeMode.Stretch; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
case ImageResizeMode.BoxPad: |
|||
defaultResizeOptions.Mode = ResizeMode.BoxPad; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
case ImageResizeMode.Min: |
|||
defaultResizeOptions.Mode = ResizeMode.Min; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
case ImageResizeMode.Max: |
|||
defaultResizeOptions.Mode = ResizeMode.Max; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
case ImageResizeMode.Crop: |
|||
defaultResizeOptions.Mode = ResizeMode.Crop; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
case ImageResizeMode.Pad: |
|||
defaultResizeOptions.Mode = ResizeMode.Pad; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
case ImageResizeMode.Fill: |
|||
defaultResizeOptions.Mode = ResizeMode.Stretch; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
break; |
|||
default: |
|||
throw new NotSupportedException("Resize mode " + resizeParameter.Mode + "is not supported!"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using SixLabors.ImageSharp; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class ImageSharpImageResizerContributor : IImageResizerContributor, ITransientDependency |
|||
{ |
|||
public async Task<ImageContributorResult<Stream>> TryResizeAsync(Stream stream, ImageResizeArgs resizeArgs, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanResize(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
|
|||
var (image, format) = await SixLabors.ImageSharp.Image.LoadWithFormatAsync(stream, cancellationToken); |
|||
|
|||
mimeType = format.DefaultMimeType; |
|||
|
|||
if (!CanResize(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
|
|||
var size = new Size(); |
|||
if (resizeArgs.Width > 0) |
|||
{ |
|||
size.Width = resizeArgs.Width; |
|||
} |
|||
|
|||
if (resizeArgs.Height > 0) |
|||
{ |
|||
size.Height = resizeArgs.Height; |
|||
} |
|||
|
|||
var defaultResizeOptions = new ResizeOptions { Size = size }; |
|||
|
|||
MemoryStream ms = null; |
|||
try |
|||
{ |
|||
if (ResizeModeMap.TryGetValue(resizeArgs.Mode, out var resizeMode)) |
|||
{ |
|||
defaultResizeOptions.Mode = resizeMode; |
|||
image.Mutate(x => x.Resize(defaultResizeOptions)); |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("Resize mode " + resizeArgs.Mode + "is not supported!"); |
|||
} |
|||
|
|||
ms = new MemoryStream(); |
|||
await image.SaveAsync(ms, format, cancellationToken: cancellationToken); |
|||
ms.SetLength(ms.Position); |
|||
ms.Position = 0; |
|||
|
|||
return new ImageContributorResult<Stream>(ms, true); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
ms?.Dispose(); |
|||
return new ImageContributorResult<Stream>(stream, false, true, e); |
|||
} |
|||
} |
|||
|
|||
public async Task<ImageContributorResult<byte[]>> TryResizeAsync(byte[] bytes, ImageResizeArgs resizeArgs, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanResize(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<byte[]>(bytes, false, false); |
|||
} |
|||
|
|||
using var ms = new MemoryStream(bytes); |
|||
var result = await TryResizeAsync(ms, resizeArgs, mimeType, cancellationToken); |
|||
|
|||
if (!result.IsSuccess) |
|||
{ |
|||
return new ImageContributorResult<byte[]>(bytes, result.IsSuccess, result.IsSupported, result.Exception); |
|||
} |
|||
|
|||
var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); |
|||
result.Result.Dispose(); |
|||
|
|||
return new ImageContributorResult<byte[]>(newBytes, true); |
|||
} |
|||
|
|||
private static bool CanResize(string mimeType) |
|||
{ |
|||
return mimeType switch { |
|||
MimeTypes.Image.Jpeg => true, |
|||
MimeTypes.Image.Png => true, |
|||
MimeTypes.Image.Gif => true, |
|||
MimeTypes.Image.Bmp => true, |
|||
MimeTypes.Image.Tiff => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
private readonly static Dictionary<ImageResizeMode, ResizeMode> ResizeModeMap = new() { |
|||
{ ImageResizeMode.None, ResizeMode.Crop }, |
|||
{ ImageResizeMode.Stretch, ResizeMode.Stretch }, |
|||
{ ImageResizeMode.BoxPad, ResizeMode.BoxPad }, |
|||
{ ImageResizeMode.Min, ResizeMode.Min }, |
|||
{ ImageResizeMode.Max, ResizeMode.Max }, |
|||
{ ImageResizeMode.Crop, ResizeMode.Crop }, |
|||
{ ImageResizeMode.Pad, ResizeMode.Pad } |
|||
}; |
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using ImageMagick; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class MagickImageCompressor : IImageCompressor, ITransientDependency |
|||
{ |
|||
private readonly ImageOptimizer _optimizer = new(); |
|||
|
|||
public async Task<Stream> CompressAsync(Stream stream, CancellationToken cancellationToken = default) |
|||
{ |
|||
var newStream = await stream.CreateMemoryStreamAsync(cancellationToken:cancellationToken); |
|||
try //TODO: Remove try/catch
|
|||
{ |
|||
_optimizer.IsSupported(newStream); |
|||
newStream.Position = 0; |
|||
_optimizer.Compress(newStream); |
|||
} |
|||
catch |
|||
{ |
|||
// ignored
|
|||
} |
|||
return newStream; |
|||
} |
|||
|
|||
public Stream Compress(Stream stream) |
|||
{ |
|||
var newStream = stream.CreateMemoryStream(); |
|||
try |
|||
{ |
|||
_optimizer.IsSupported(newStream); |
|||
newStream.Position = 0; |
|||
_optimizer.Compress(newStream); |
|||
} |
|||
catch |
|||
{ |
|||
// ignored
|
|||
} |
|||
return newStream; |
|||
} |
|||
|
|||
public bool CanCompress(IImageFormat imageFormat) |
|||
{ |
|||
return imageFormat?.MimeType switch |
|||
{ |
|||
"image/jpeg" => true, |
|||
"image/png" => true, |
|||
"image/gif" => true, |
|||
"image/bmp" => true, |
|||
"image/tiff" => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using ImageMagick; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class MagickImageCompressorContributor : IImageCompressorContributor, ITransientDependency |
|||
{ |
|||
protected MagickNetCompressOptions Options { get; } |
|||
|
|||
private readonly ImageOptimizer _optimizer; |
|||
|
|||
public MagickImageCompressorContributor(IOptions<MagickNetCompressOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
_optimizer = new ImageOptimizer { |
|||
OptimalCompression = Options.OptimalCompression, IgnoreUnsupportedFormats = Options.IgnoreUnsupportedFormats |
|||
}; |
|||
} |
|||
|
|||
private static bool CanCompress(string mimeType) |
|||
{ |
|||
return mimeType switch { |
|||
MimeTypes.Image.Jpeg => true, |
|||
MimeTypes.Image.Png => true, |
|||
MimeTypes.Image.Gif => true, |
|||
MimeTypes.Image.Bmp => true, |
|||
MimeTypes.Image.Tiff => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
public async Task<ImageContributorResult<Stream>> TryCompressAsync(Stream stream, string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
|
|||
MemoryStream ms = null; |
|||
try |
|||
{ |
|||
ms = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); |
|||
|
|||
if (!_optimizer.IsSupported(ms)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
|
|||
Func<Stream, bool> compressFunc; |
|||
|
|||
if (Options.Lossless) |
|||
{ |
|||
compressFunc = _optimizer.LosslessCompress; |
|||
} |
|||
else |
|||
{ |
|||
compressFunc = _optimizer.Compress; |
|||
} |
|||
|
|||
if (compressFunc(ms)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(ms, true); |
|||
} |
|||
|
|||
ms.Dispose(); |
|||
return new ImageContributorResult<Stream>(stream, false); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
ms?.Dispose(); |
|||
return new ImageContributorResult<Stream>(stream, false, false, e); |
|||
} |
|||
} |
|||
|
|||
public async Task<ImageContributorResult<byte[]>> TryCompressAsync(byte[] bytes, string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<byte[]>(bytes, false, false); |
|||
} |
|||
|
|||
using var ms = new MemoryStream(bytes); |
|||
var result = await TryCompressAsync(ms, mimeType, cancellationToken); |
|||
|
|||
if (!result.IsSuccess) |
|||
{ |
|||
result.Result.Dispose(); |
|||
return new ImageContributorResult<byte[]>(bytes, result.IsSuccess, result.IsSupported, result.Exception); |
|||
} |
|||
|
|||
var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); |
|||
result.Result.Dispose(); |
|||
return new ImageContributorResult<byte[]>(newBytes, true); |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using System.IO; |
|||
using ImageMagick; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class MagickImageFormatDetector : IImageFormatDetector, ITransientDependency |
|||
{ |
|||
public IImageFormat FindFormat(Stream image) |
|||
{ |
|||
using var magickImage = new MagickImage(image); |
|||
var format = magickImage.FormatInfo; |
|||
return format == null ? null : new ImageFormat(format.MimeType ?? string.Empty); |
|||
} |
|||
} |
|||
@ -1,177 +0,0 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using ImageMagick; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class MagickImageResizer : IImageResizer, ITransientDependency |
|||
{ |
|||
public async Task<Stream> ResizeAsync(Stream stream, IImageResizeParameter resizeParameter, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var newStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); |
|||
try //TODO: Remove try/catch
|
|||
{ |
|||
using var image = new MagickImage(newStream); |
|||
ApplyMode(image, resizeParameter); |
|||
newStream.Position = 0; |
|||
await image.WriteAsync(newStream, cancellationToken); |
|||
newStream.SetLength(newStream.Position); |
|||
newStream.Position = 0; |
|||
} |
|||
catch |
|||
{ |
|||
// ignored
|
|||
} |
|||
|
|||
return newStream; |
|||
} |
|||
|
|||
public Stream Resize(Stream stream, IImageResizeParameter resizeParameter) |
|||
{ |
|||
var newStream = stream.CreateMemoryStream(); |
|||
try |
|||
{ |
|||
using var image = new MagickImage(newStream); |
|||
ApplyMode(image, resizeParameter); |
|||
newStream.Position = 0; |
|||
image.Write(newStream); |
|||
newStream.SetLength(newStream.Position); |
|||
newStream.Position = 0; |
|||
} |
|||
catch |
|||
{ |
|||
// ignored
|
|||
} |
|||
|
|||
return newStream; |
|||
} |
|||
|
|||
public bool CanResize(IImageFormat imageFormat) |
|||
{ |
|||
return imageFormat?.MimeType switch { |
|||
"image/jpeg" => true, |
|||
"image/png" => true, |
|||
"image/gif" => true, |
|||
"image/bmp" => true, |
|||
"image/tiff" => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
private void ApplyMode(IMagickImage image, IImageResizeParameter resizeParameter) |
|||
{ |
|||
var width = resizeParameter.Width ?? image.Width; |
|||
var height = resizeParameter.Height ?? image.Height; |
|||
var defaultMagickGeometry = new MagickGeometry(width, height); |
|||
var imageRatio = image.Height / (float)image.Width; |
|||
var percentHeight = Math.Abs(height / (float)image.Height); |
|||
var percentWidth = Math.Abs(width / (float)image.Width); |
|||
var ratio = height / (float)width; |
|||
var newWidth = width; |
|||
var newHeight = height; |
|||
switch (resizeParameter.Mode) |
|||
{ |
|||
case null: |
|||
case ImageResizeMode.None: |
|||
image.Resize(defaultMagickGeometry); |
|||
break; |
|||
case ImageResizeMode.Stretch: |
|||
defaultMagickGeometry.IgnoreAspectRatio = true; |
|||
image.Resize(defaultMagickGeometry); |
|||
break; |
|||
case ImageResizeMode.Pad: |
|||
if (percentHeight < percentWidth) |
|||
{ |
|||
newWidth = (int)Math.Round(image.Width * percentHeight); |
|||
} |
|||
else |
|||
{ |
|||
newHeight = (int)Math.Round(image.Height * percentWidth); |
|||
} |
|||
|
|||
defaultMagickGeometry.IgnoreAspectRatio = true; |
|||
image.Resize(newWidth, newHeight); |
|||
image.Extent(width, height, Gravity.Center); |
|||
break; |
|||
case ImageResizeMode.BoxPad: |
|||
int boxPadWidth = width > 0 ? width : (int)Math.Round(image.Width * percentHeight); |
|||
int boxPadHeight = height > 0 ? height : (int)Math.Round(image.Height * percentWidth); |
|||
|
|||
if (image.Width < boxPadWidth && image.Height < boxPadHeight) |
|||
{ |
|||
newWidth = boxPadWidth; |
|||
newHeight = boxPadHeight; |
|||
} |
|||
|
|||
image.Resize(newWidth, newHeight); |
|||
image.Extent(defaultMagickGeometry, Gravity.Center); |
|||
break; |
|||
case ImageResizeMode.Max: |
|||
|
|||
if (imageRatio < ratio) |
|||
{ |
|||
newHeight = (int)(image.Height * percentWidth); |
|||
} |
|||
else |
|||
{ |
|||
newWidth = (int)(image.Width * percentHeight); |
|||
} |
|||
|
|||
image.Resize(newWidth, newHeight); |
|||
break; |
|||
case ImageResizeMode.Min: |
|||
if (width > image.Width || height > image.Height) |
|||
{ |
|||
newWidth = image.Width; |
|||
newHeight = image.Height; |
|||
} |
|||
else |
|||
{ |
|||
int widthDiff = image.Width - width; |
|||
int heightDiff = image.Height - height; |
|||
|
|||
if (widthDiff > heightDiff) |
|||
{ |
|||
newWidth = (int)Math.Round(height / imageRatio); |
|||
} |
|||
else if (widthDiff < heightDiff) |
|||
{ |
|||
newHeight = (int)Math.Round(width * imageRatio); |
|||
} |
|||
else |
|||
{ |
|||
if (height > width) |
|||
{ |
|||
newHeight = (int)Math.Round(image.Height * percentWidth); |
|||
} |
|||
else |
|||
{ |
|||
newHeight = (int)Math.Round(image.Height * percentWidth); |
|||
} |
|||
} |
|||
} |
|||
|
|||
image.Resize(newWidth, newHeight); |
|||
break; |
|||
case ImageResizeMode.Crop: |
|||
defaultMagickGeometry.IgnoreAspectRatio = true; |
|||
image.Crop(width, height, Gravity.Center); |
|||
image.Resize(defaultMagickGeometry); |
|||
break; |
|||
case ImageResizeMode.Distort: |
|||
image.Distort(DistortMethod.Resize, width, height); |
|||
break; |
|||
case ImageResizeMode.Fill: |
|||
defaultMagickGeometry.IgnoreAspectRatio = true; |
|||
defaultMagickGeometry.FillArea = true; |
|||
image.Resize(defaultMagickGeometry); |
|||
break; |
|||
default: |
|||
throw new NotSupportedException("Resize mode " + resizeParameter.Mode + "is not supported!"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,286 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using ImageMagick; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class MagickImageResizerContributor : IImageResizerContributor, ITransientDependency |
|||
{ |
|||
public async Task<ImageContributorResult<Stream>> TryResizeAsync(Stream stream, ImageResizeArgs resizeArgs, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanResize(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
|
|||
MemoryStream ms = null; |
|||
try |
|||
{ |
|||
ms = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); |
|||
|
|||
using var image = new MagickImage(ms); |
|||
|
|||
if (string.IsNullOrWhiteSpace(mimeType)) |
|||
{ |
|||
var format = image.FormatInfo; |
|||
mimeType = format?.MimeType; |
|||
|
|||
if (!CanResize(mimeType)) |
|||
{ |
|||
return new ImageContributorResult<Stream>(stream, false, false); |
|||
} |
|||
} |
|||
|
|||
Resize(image, resizeArgs); |
|||
|
|||
ms.Position = 0; |
|||
await image.WriteAsync(ms, cancellationToken); |
|||
ms.SetLength(ms.Position); |
|||
ms.Position = 0; |
|||
return new ImageContributorResult<Stream>(ms, true); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
ms?.Dispose(); |
|||
return new ImageContributorResult<Stream>(stream, false, true, e); |
|||
} |
|||
} |
|||
|
|||
public Task<ImageContributorResult<byte[]>> TryResizeAsync(byte[] bytes, ImageResizeArgs resizeArgs, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanResize(mimeType)) |
|||
{ |
|||
return Task.FromResult(new ImageContributorResult<byte[]>(bytes, false, false)); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
using var image = new MagickImage(bytes); |
|||
|
|||
if (string.IsNullOrWhiteSpace(mimeType)) |
|||
{ |
|||
var format = image.FormatInfo; |
|||
mimeType = format?.MimeType; |
|||
|
|||
if (!CanResize(mimeType)) |
|||
{ |
|||
return Task.FromResult(new ImageContributorResult<byte[]>(bytes, false, false)); |
|||
} |
|||
} |
|||
|
|||
Resize(image, resizeArgs); |
|||
|
|||
return Task.FromResult(new ImageContributorResult<byte[]>(image.ToByteArray(), true)); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
return Task.FromResult(new ImageContributorResult<byte[]>(bytes, false, true, e)); |
|||
} |
|||
} |
|||
|
|||
protected virtual bool CanResize(string mimeType) |
|||
{ |
|||
return mimeType switch { |
|||
MimeTypes.Image.Jpeg => true, |
|||
MimeTypes.Image.Png => true, |
|||
MimeTypes.Image.Gif => true, |
|||
MimeTypes.Image.Bmp => true, |
|||
MimeTypes.Image.Tiff => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
protected virtual void Resize(MagickImage image, ImageResizeArgs resizeParameter) |
|||
{ |
|||
const int min = 1; |
|||
int targetWidth = resizeParameter.Width, targetHeight = resizeParameter.Height; |
|||
|
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
if (targetWidth == 0 && targetHeight > 0) |
|||
{ |
|||
targetWidth = Math.Max(min, (int)Math.Round(sourceWidth * targetHeight / (float)sourceHeight)); |
|||
} |
|||
|
|||
if (targetHeight == 0 && targetWidth > 0) |
|||
{ |
|||
targetHeight = Math.Max(min, (int)Math.Round(sourceHeight * targetWidth / (float)sourceWidth)); |
|||
} |
|||
|
|||
switch (resizeParameter.Mode) |
|||
{ |
|||
case ImageResizeMode.None: |
|||
ResizeModeNone(image, targetWidth, targetHeight); |
|||
break; |
|||
case ImageResizeMode.Stretch: |
|||
ResizeStretch(image, targetWidth, targetHeight); |
|||
break; |
|||
case ImageResizeMode.Pad: |
|||
ResizePad(image, targetWidth, targetHeight); |
|||
break; |
|||
case ImageResizeMode.BoxPad: |
|||
ResizeBoxPad(image, targetWidth, targetHeight); |
|||
break; |
|||
case ImageResizeMode.Max: |
|||
ResizeMax(image, targetWidth, targetHeight); |
|||
break; |
|||
case ImageResizeMode.Min: |
|||
ResizeMin(image, targetWidth, targetHeight); |
|||
break; |
|||
case ImageResizeMode.Crop: |
|||
ResizeCrop(image, targetWidth, targetHeight); |
|||
break; |
|||
default: |
|||
throw new NotSupportedException("Resize mode " + resizeParameter.Mode + "is not supported!"); |
|||
} |
|||
} |
|||
|
|||
protected virtual void ResizeCrop(MagickImage image, int targetWidth, int targetHeight) |
|||
{ |
|||
var defaultMagickGeometry = new MagickGeometry(targetWidth, targetHeight) { IgnoreAspectRatio = true }; |
|||
image.Crop(defaultMagickGeometry, Gravity.Center); |
|||
} |
|||
|
|||
protected virtual void ResizeMin(MagickImage image, int targetWidth, int targetHeight) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var imageRatio = CalculateRatio(sourceWidth, sourceHeight); |
|||
|
|||
var percentWidth = CalculatePercent(sourceWidth, targetWidth); |
|||
|
|||
if (targetWidth > sourceWidth || targetHeight > sourceHeight) |
|||
{ |
|||
targetWidth = sourceWidth; |
|||
targetHeight = sourceHeight; |
|||
} |
|||
else |
|||
{ |
|||
var widthDiff = sourceWidth - targetWidth; |
|||
var heightDiff = sourceHeight - targetHeight; |
|||
|
|||
if (widthDiff > heightDiff) |
|||
{ |
|||
targetWidth = (int)Math.Round(targetHeight / imageRatio); |
|||
} |
|||
else if (widthDiff < heightDiff) |
|||
{ |
|||
targetHeight = (int)Math.Round(targetWidth * imageRatio); |
|||
} |
|||
else |
|||
{ |
|||
if (targetHeight > targetWidth) |
|||
{ |
|||
targetWidth = (int)Math.Round(sourceHeight * percentWidth); |
|||
} |
|||
else |
|||
{ |
|||
targetHeight = (int)Math.Round(sourceHeight * percentWidth); |
|||
} |
|||
} |
|||
} |
|||
|
|||
image.Resize(targetWidth, targetHeight); |
|||
} |
|||
|
|||
protected virtual void ResizeMax(IMagickImage image, int targetWidth, int targetHeight) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var imageRatio = CalculateRatio(sourceWidth, sourceHeight); |
|||
var ratio = CalculateRatio(targetWidth, targetHeight); |
|||
|
|||
var percentHeight = CalculatePercent(sourceHeight, targetHeight); |
|||
var percentWidth = CalculatePercent(sourceWidth, targetWidth); |
|||
|
|||
if (imageRatio < ratio) |
|||
{ |
|||
targetHeight = (int)(sourceHeight * percentWidth); |
|||
} |
|||
else |
|||
{ |
|||
targetWidth = (int)(sourceWidth * percentHeight); |
|||
} |
|||
|
|||
image.Resize(targetWidth, targetHeight); |
|||
} |
|||
|
|||
protected virtual void ResizeBoxPad(MagickImage image, int targetWidth, int targetHeight) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var percentHeight = CalculatePercent(sourceHeight, targetHeight); |
|||
var percentWidth = CalculatePercent(sourceWidth, targetWidth); |
|||
|
|||
var newWidth = targetWidth; |
|||
var newHeight = targetHeight; |
|||
|
|||
var boxPadWidth = targetWidth > 0 ? targetWidth : (int)Math.Round(sourceWidth * percentHeight); |
|||
var boxPadHeight = targetHeight > 0 ? targetHeight : (int)Math.Round(sourceHeight * percentWidth); |
|||
|
|||
if (sourceWidth < boxPadWidth && sourceHeight < boxPadHeight) |
|||
{ |
|||
newWidth = boxPadWidth; |
|||
newHeight = boxPadHeight; |
|||
} |
|||
|
|||
image.Resize(newWidth, newHeight); |
|||
image.Extent(targetWidth, targetHeight, Gravity.Center, MagickColors.Transparent); |
|||
} |
|||
|
|||
protected virtual void ResizePad(MagickImage image, int targetWidth, int targetHeight) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var percentHeight = CalculatePercent(sourceHeight, targetHeight); |
|||
var percentWidth = CalculatePercent(sourceWidth, targetWidth); |
|||
|
|||
var newWidth = targetWidth; |
|||
var newHeight = targetHeight; |
|||
|
|||
if (percentHeight < percentWidth) |
|||
{ |
|||
newWidth = (int)Math.Round(sourceWidth * percentHeight); |
|||
} |
|||
else |
|||
{ |
|||
newHeight = (int)Math.Round(sourceHeight * percentWidth); |
|||
} |
|||
|
|||
image.Resize(newWidth, newHeight); |
|||
image.Extent(targetWidth, targetHeight, Gravity.Center, MagickColors.Transparent); |
|||
} |
|||
|
|||
protected virtual float CalculatePercent(int imageHeightOrWidth, int heightOrWidth) |
|||
{ |
|||
return heightOrWidth / (float)imageHeightOrWidth; |
|||
} |
|||
|
|||
protected virtual float CalculateRatio(int width, int height) |
|||
{ |
|||
return height / (float)width; |
|||
} |
|||
|
|||
protected virtual void ResizeStretch(IMagickImage image, int targetWidth, int targetHeight) |
|||
{ |
|||
image.Resize(new MagickGeometry(targetWidth, targetHeight) { IgnoreAspectRatio = true }); |
|||
} |
|||
|
|||
protected virtual void ResizeModeNone(IMagickImage image, int targetWidth, int targetHeight) |
|||
{ |
|||
image.Resize(targetWidth, targetHeight); |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Image; |
|||
|
|||
public class MagickNetCompressOptions |
|||
{ |
|||
public bool OptimalCompression { get; set; } |
|||
public bool IgnoreUnsupportedFormats { get; set; } |
|||
public bool Lossless { get; set; } |
|||
} |
|||
@ -1,78 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Mvc.Controllers; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
//Rename: CompressImageAttribute
|
|||
//Also introduce ResizeImageAttribute(...)
|
|||
public class AbpImageCompressActionFilterAttribute : ActionFilterAttribute |
|||
{ |
|||
public string[] Parameters { get; } |
|||
|
|||
public AbpImageCompressActionFilterAttribute(params string[] parameters) |
|||
{ |
|||
Parameters = parameters; |
|||
} |
|||
|
|||
public async override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) |
|||
{ |
|||
var parameters = Parameters.Any() |
|||
? context.ActionArguments.Where(x => Parameters.Contains(x.Key)).ToArray() |
|||
: context.ActionArguments.ToArray(); |
|||
|
|||
var parameterDescriptors = context.ActionDescriptor.Parameters.OfType<ControllerParameterDescriptor>(); |
|||
var parameterMap = new Dictionary<string, ParameterInfo>(); |
|||
foreach (var parameterDescriptor in parameterDescriptors) |
|||
{ |
|||
var parameter = parameterDescriptor.ParameterInfo; |
|||
var attribute = parameter.GetCustomAttribute<AbpOriginalImageAttribute>(); |
|||
if (attribute == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
parameterMap.Add(attribute.Parameter, parameter); |
|||
} |
|||
|
|||
foreach (var (key, value) in parameters) |
|||
{ |
|||
object compressedValue = value switch { |
|||
IFormFile file => await file.CompressImageAsync(context.HttpContext.RequestServices), |
|||
IRemoteStreamContent remoteStreamContent => await remoteStreamContent.CompressImageAsync(context.HttpContext.RequestServices), |
|||
Stream stream => await stream.CompressImageAsync(context.HttpContext.RequestServices), |
|||
_ => null |
|||
}; |
|||
|
|||
if (parameterMap.TryGetValue(key, out var parameterInfo)) |
|||
{ |
|||
if (parameterInfo.Name != null) |
|||
{ |
|||
context.ActionArguments.Add(parameterInfo.Name, value); |
|||
} |
|||
else if (value is IDisposable disposable) |
|||
{ |
|||
disposable.Dispose(); |
|||
} |
|||
} |
|||
else if (value is IDisposable disposable) |
|||
{ |
|||
disposable.Dispose(); |
|||
} |
|||
|
|||
if (compressedValue != null) |
|||
{ |
|||
context.ActionArguments[key] = compressedValue; |
|||
} |
|||
} |
|||
|
|||
await next(); |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
[DependsOn(typeof(AbpImageModule))] |
|||
public class AbpImageWebModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
[AttributeUsage(AttributeTargets.Parameter)] |
|||
public class AbpOriginalImageAttribute : Attribute //TODO: Remove
|
|||
{ |
|||
public AbpOriginalImageAttribute(string parameter) |
|||
{ |
|||
Parameter = parameter; |
|||
} |
|||
|
|||
public string Parameter { get; } |
|||
} |
|||
@ -1,77 +0,0 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Image; |
|||
|
|||
public static class IFormFileExtensions //TODO: Remove
|
|||
{ |
|||
public async static Task<IFormFile> CompressImageAsync( |
|||
this IFormFile formFile, |
|||
IImageFormat imageFormat, |
|||
IImageCompressor imageCompressor, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!formFile.ContentType.StartsWith("image")) |
|||
{ |
|||
return formFile; |
|||
} |
|||
|
|||
if (!imageCompressor.CanCompress(imageFormat)) |
|||
{ |
|||
return formFile; |
|||
} |
|||
|
|||
var compressedImageStream = await formFile.OpenReadStream().CompressImageAsync(imageFormat, imageCompressor, cancellationToken); |
|||
|
|||
var newFormFile = new FormFile(compressedImageStream, 0, compressedImageStream.Length, formFile.Name, |
|||
formFile.FileName) { Headers = formFile.Headers }; |
|||
|
|||
return newFormFile; |
|||
} |
|||
|
|||
public async static Task<IFormFile> CompressImageAsync(this IFormFile formFile, IServiceProvider serviceProvider) |
|||
{ |
|||
var imageFormatDetector = serviceProvider.GetRequiredService<IImageFormatDetector>(); |
|||
var imageCompressorSelector = serviceProvider.GetRequiredService<IImageCompressorSelector>(); |
|||
var format = imageFormatDetector.FindFormat(formFile.OpenReadStream()); |
|||
return await formFile.CompressImageAsync(format, imageCompressorSelector.FindCompressor(format)); |
|||
} |
|||
|
|||
public static async Task<IFormFile> ResizeImageAsync( |
|||
this IFormFile formFile, |
|||
IImageResizeParameter imageResizeParameter, |
|||
IImageFormat imageFormat, |
|||
IImageResizer imageResizer, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!formFile.ContentType.StartsWith("image")) |
|||
{ |
|||
return formFile; |
|||
} |
|||
|
|||
if (!imageResizer.CanResize(imageFormat)) |
|||
{ |
|||
return formFile; |
|||
} |
|||
|
|||
var resizedImageStream = |
|||
await formFile.OpenReadStream().ResizeImageAsync(imageResizeParameter, imageFormat, imageResizer, cancellationToken); |
|||
|
|||
var newFormFile = new FormFile(resizedImageStream, 0, resizedImageStream.Length, formFile.Name, |
|||
formFile.FileName) { Headers = formFile.Headers }; |
|||
|
|||
return newFormFile; |
|||
} |
|||
|
|||
public static Task<IFormFile> ResizeImageAsync(this IFormFile formFile, |
|||
IImageResizeParameter imageResizeParameter, IServiceProvider serviceProvider) |
|||
{ |
|||
var imageFormatDetector = serviceProvider.GetRequiredService<IImageFormatDetector>(); |
|||
var imageResizerSelector = serviceProvider.GetRequiredService<IImageResizerSelector>(); |
|||
var format = imageFormatDetector.FindFormat(formFile.OpenReadStream()); |
|||
return formFile.ResizeImageAsync(imageResizeParameter, format, imageResizerSelector.FindResizer(format)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue