mirror of https://github.com/abpframework/abp.git
449 changed files with 8328 additions and 3617 deletions
@ -1,9 +1,9 @@ |
|||
<Project> |
|||
<ItemGroup> |
|||
<ItemGroup Condition="'$(Configuration)' == 'Release'"> |
|||
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.1" PrivateAssets="All" /> |
|||
<PackageReference Include="Fody" Version="6.6.1"> |
|||
<PrivateAssets>All</PrivateAssets> |
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> |
|||
</PackageReference> |
|||
</ItemGroup> |
|||
</Project> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,307 @@ |
|||
# Image Manipulation |
|||
ABP Framework provides services to compress and resize images and implements these services with popular [ImageSharp](https://sixlabors.com/products/imagesharp/) and [Magick.NET](https://github.com/dlemstra/Magick.NET) libraries. You can use these services in your reusable modules, libraries and applications, so you don't depend on a specific imaging library. |
|||
|
|||
> The image resizer/compressor system is designed to be extensible. You can implement your own image resizer/compressor contributor and use it in your application. |
|||
|
|||
## Installation |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
### Using the ABP CLI |
|||
|
|||
Open a command line terminal in the folder of your project (.csproj file) and type the following command: |
|||
|
|||
```bash |
|||
abp add-package Volo.Abp.Imaging.Abstractions |
|||
``` |
|||
|
|||
### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.Imaging.Abstractions](https://www.nuget.org/packages/Volo.Abp.Imaging.Abstractions) NuGet package to your project: |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.Imaging.Abstractions |
|||
``` |
|||
|
|||
2. Add the `AbpImagingAbstractionsModule` to the dependency list of your module: |
|||
|
|||
```csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpImagingAbstractionsModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
``` |
|||
|
|||
## Providers |
|||
|
|||
ABP Framework provides two image resizer/compressor implementations out of the box: |
|||
|
|||
* [Magick.NET](#magicknet-provider) |
|||
* [ImageSharp](#imagesharp-provider) |
|||
|
|||
You should install one of these provides to make it actually working. |
|||
|
|||
> If none of the provider packages installed into your application, compress/resize operations return the untouched input image. |
|||
|
|||
## IImageResizer |
|||
|
|||
You can [inject](Dependency-Injection.md) the `IImageResizer` service and use it for image resize operations. Here is the available methods of the `IImageResizer` service: |
|||
|
|||
```csharp |
|||
public interface IImageResizer |
|||
{ |
|||
/* Works with a Stream object that represents an image */ |
|||
Task<ImageResizeResult<Stream>> ResizeAsync( |
|||
Stream stream, |
|||
ImageResizeArgs resizeArgs, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
|
|||
/* Works with a byte array that contains an image file */ |
|||
Task<ImageResizeResult<byte[]>> ResizeAsync( |
|||
byte[] bytes, |
|||
ImageResizeArgs resizeArgs, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
} |
|||
``` |
|||
|
|||
**Example usage:** |
|||
|
|||
```csharp |
|||
var result = await _imageResizer.ResizeAsync( |
|||
stream, /* A stream object that represents an image */ |
|||
new ImageResizeArgs |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Mode = ImageResizeMode.Crop |
|||
}, |
|||
mimeType: "image/jpeg" |
|||
); |
|||
``` |
|||
|
|||
> You can use `MimeTypes.Image.Jpeg` constant instead of the `image/jpeg` magic string used in that example. |
|||
|
|||
### ImageResizeArgs |
|||
|
|||
The `ImageResizeArgs` is a class that is used to define the resize operation parameters. It has the following properties: |
|||
|
|||
* `Width`: The width of the resized image. |
|||
* `Height`: The height of the resized image. |
|||
* `Mode`: The resize mode (see the [ImageResizeMode](#imageresizemode) section for more information). |
|||
|
|||
### ImageResizeMode |
|||
|
|||
The `ImageResizeMode` is an enum that is used to define the resize mode. It has the following values: |
|||
|
|||
```csharp |
|||
public enum ImageResizeMode : byte |
|||
{ |
|||
None = 0, |
|||
Stretch = 1, |
|||
BoxPad = 2, |
|||
Min = 3, |
|||
Max = 4, |
|||
Crop = 5, |
|||
Pad = 6, |
|||
Default = 7 |
|||
} |
|||
``` |
|||
|
|||
### ImageResizeResult |
|||
|
|||
The `ImageResizeResult` is a generic class that is used to return the result of the image resize operations. It has the following properties: |
|||
|
|||
* `Result`: The resized image (stream or byte array). |
|||
* `State`: The result of the resize operation (type: `ImageProcessState`). |
|||
|
|||
### ImageProcessState |
|||
|
|||
The `ImageProcessState` is an enum that is used to return the the result of the image resize operations. It has the following values: |
|||
|
|||
```csharp |
|||
public enum ImageProcessState : byte |
|||
{ |
|||
Done = 1, |
|||
Canceled = 2, |
|||
Unsupported = 3, |
|||
} |
|||
``` |
|||
|
|||
### ImageResizeOptions |
|||
|
|||
`ImageResizeOptions` is an [options object](Options.md) that is used to configure the image resize system. It has the following properties: |
|||
|
|||
* `DefaultResizeMode`: The default resize mode. (Default: `ImageResizeMode.None`) |
|||
|
|||
## IImageCompressor |
|||
|
|||
You can [inject](Dependency-Injection.md) the `IImageCompressor` service and use it for image compression operations. Here is the available methods of the `IImageCompressor` service: |
|||
|
|||
```csharp |
|||
public interface IImageCompressor |
|||
{ |
|||
/* Works with a Stream object that represents an image */ |
|||
Task<ImageCompressResult<Stream>> CompressAsync( |
|||
Stream stream, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
|
|||
/* Works with a byte array that contains an image file */ |
|||
Task<ImageCompressResult<byte[]>> CompressAsync( |
|||
byte[] bytes, |
|||
string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
} |
|||
``` |
|||
|
|||
**Example usage:** |
|||
|
|||
```csharp |
|||
var result = await _imageCompressor.CompressAsync( |
|||
stream, /* A stream object that represents an image */ |
|||
mimeType: "image/jpeg" |
|||
); |
|||
``` |
|||
|
|||
### ImageCompressResult |
|||
|
|||
The `ImageCompressResult` is a generic class that is used to return the result of the image compression operations. It has the following properties: |
|||
|
|||
* `Result`: The compressed image (stream or byte array). |
|||
* `State`: The result of the compress operation (type: `ImageProcessState`). |
|||
|
|||
### ImageProcessState |
|||
|
|||
The `ImageProcessState` is an enum that is used to return the the result of the image compress operations. It has the following values: |
|||
|
|||
```csharp |
|||
public enum ImageProcessState : byte |
|||
{ |
|||
Done = 1, |
|||
Canceled = 2, |
|||
Unsupported = 3, |
|||
} |
|||
``` |
|||
|
|||
## Magick.NET Provider |
|||
|
|||
`Volo.Abp.Imaging.MagickNet` NuGet package implements the image operations using the [Magick.NET](https://github.com/dlemstra/Magick.NET) library. If you want to use that library as image process provider, add the `Volo.Abp.Imaging.MagickNet` NuGet package to your project, then add `AbpImagingMagickNetModule` to your [module](Module-Development-Basics.md)'s dependency list: |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpImagingMagickNetModule))] |
|||
public class MyModule : AbpModule |
|||
{ |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
### Configuration |
|||
|
|||
`MagickNetCompressOptions` is an [options object](Options.md) that is used to configure the Magick.NET image compression system. It has the following properties: |
|||
|
|||
* `OptimalCompression`: Indicates whether the optimal compression is enabled or not. (Default: `false`) |
|||
* `IgnoreUnsupportedFormats`: Indicates whether the unsupported formats are ignored or not. (Default: `false`) |
|||
* `Lossless`: Indicates whether the lossless compression is enabled or not. (Default: `false`) |
|||
|
|||
## ImageSharp Provider |
|||
|
|||
`Volo.Abp.Imaging.ImageSharp` NuGet package implements the image operations using the [ImageSharp](https://github.com/SixLabors/ImageSharp) library. If you want to use that library as image process provider, add the `Volo.Abp.Imaging.ImageSharp` NuGet package to your project, then add `AbpImagingImageSharpModule` to your [module](Module-Development-Basics.md)'s dependency list: |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpImagingImageSharpModule))] |
|||
public class MyModule : AbpModule |
|||
{ |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
### Configuration |
|||
|
|||
`ImageSharpCompressOptions` is an [options object](Options.md) that is used to configure the ImageSharp image compression system. It has the following properties: |
|||
|
|||
* `DefaultQuality`: The default quality of the JPEG and WebP encoders. (Default: `75`) |
|||
* [`JpegEncoder`](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder.html): The JPEG encoder. (Default: `JpegEncoder` with `Quality` set to `DefaultQuality`) |
|||
* [`PngEncoder`](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Formats.Png.PngEncoder.html): The PNG encoder. (Default: `PngEncoder` with `IgnoreMetadata` set to `true` and `CompressionLevel` set to `PngCompressionLevel.BestCompression`) |
|||
* [`WebPEncoder`](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Formats.Webp.WebpEncoder.html): The WebP encoder. (Default: `WebPEncoder` with `Quality` set to `DefaultQuality`) |
|||
|
|||
**Example usage:** |
|||
|
|||
```csharp |
|||
Configure<ImageSharpCompressOptions>(options => |
|||
{ |
|||
options.JpegEncoder = new JpegEncoder |
|||
{ |
|||
Quality = 60 |
|||
}; |
|||
options.PngEncoder = new PngEncoder |
|||
{ |
|||
CompressionLevel = PngCompressionLevel.BestCompression |
|||
}; |
|||
options.WebPEncoder = new WebPEncoder |
|||
{ |
|||
Quality = 65 |
|||
}; |
|||
}); |
|||
``` |
|||
|
|||
## ASP.NET Core Integration |
|||
|
|||
`Volo.Abp.Imaging.AspNetCore` NuGet package defines attributes for controller actions that can automatically compress and/or resize uploaded files. |
|||
|
|||
To use the ASP.NET Core integration, add the `Volo.Abp.Imaging.AspNetCore` NuGet package to your project and add the `AbpImagingAspNetCoreModule` module to your module's dependency list: |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpImagingAspNetCoreModule))] |
|||
public class MyModule : AbpModule |
|||
{ |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
### CompressImageAttribute |
|||
|
|||
The `CompressImageAttribute` is used to compress the image before. `IFormFile`, `IRemoteStreamContent`, `Stream` and `IEnumrable<byte>` types are supported. It has the following properties: |
|||
|
|||
* `Parameters`: Names of the the parameters that are used to configure the image compression system. This is useful if your action has some non-image parameters. If you don't specify the parameters names, all of the method parameters are considered as image. |
|||
|
|||
**Example usage:** |
|||
|
|||
```csharp |
|||
[HttpPost] |
|||
[CompressImage] /* Compresses the given file (automatically determines the file mime type) */ |
|||
public async Task<IActionResult> Upload(IFormFile file) |
|||
{ |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
### ResizeImageAttribute |
|||
|
|||
The `ResizeImageAttribute` is used to resize the image before requesting the action. `IFormFile`, `IRemoteStreamContent`, `Stream` and `IEnumrable<byte>` types are supported. It has the following properties: |
|||
|
|||
* `Parameters`: Names of the the parameters that are used to configure the image resize system. This is useful if your action has some non-image parameters. If you don't specify the parameters names, all of the method parameters are considered as image. |
|||
* `Width`: Target width of the resized image. |
|||
* `Height`: Target height of the resized image. |
|||
* `Mode`: The resize mode (see the [ImageResizeMode](#imageresizemode) section for more information). |
|||
|
|||
**Example usage:** |
|||
|
|||
```csharp |
|||
[HttpPost] |
|||
[ResizeImage(Width = 100, Height = 100, Mode = ImageResizeMode.Crop)] |
|||
public async Task<IActionResult> Upload(IFormFile file) |
|||
{ |
|||
//... |
|||
} |
|||
``` |
|||
@ -1,10 +1,39 @@ |
|||
# How to Fix "Filename too long" Error on Windows |
|||
|
|||
If you encounter the "filename too long" or "unzip" error on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). |
|||
If you encounter the "filename too long" or "unzip" error on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 255 characters. |
|||
|
|||
## Solution 1 |
|||
Try [enabling the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). |
|||
|
|||
If you face long path errors related to Git, try the following command to enable long paths in Windows. |
|||
``` |
|||
git config --system core.longpaths true |
|||
``` |
|||
|
|||
See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path |
|||
See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path |
|||
|
|||
|
|||
## Solution 2 |
|||
|
|||
You may encounter a "DirectoryNotFoundException - Could not find a part of the path" exception in Windows while using certain .NET MAUI build tools. This is related to some 32 bit .NET MAUI build tools. To resolve this issue, you can try placing the solution in the root directory of your drive, such as `C:\Projects\`. However, please note that this solution is specific to this particular exception and may not be applicable to all cases of the Windows long path issue. |
|||
|
|||
|
|||
## Solution 3 |
|||
|
|||
You can define an alias for a path in Windows by creating a symbolic link using the `mklink` command in the command prompt. Here's an example: |
|||
|
|||
``` |
|||
mklink /D C:\MyProject C:\my\long\path\to\solution\ |
|||
``` |
|||
|
|||
> Your **solution (.sln)** file should be in `C:\my\long\path\to\solution\`. Keep in mind that, if you have relative paths in your .csproj file, it will not work! |
|||
|
|||
This command creates a symbolic link named `MyProject` in the root of the `C:` drive that points to the `C:\my\long\path\to\solution\` directory. You can then use `C:\MyProject` to access the contents of the `C:\my\long\path\to\solution\` directory. |
|||
|
|||
> Note that you need to run the command prompt as an administrator to the create symbolic links. |
|||
|
|||
Then you can try building your project with `dotnet build` command. |
|||
|
|||
``` |
|||
dotnet build C:\MyProject\MyProjectName.sln |
|||
``` |
|||
|
|||
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 112 KiB |
@ -0,0 +1,22 @@ |
|||
using Microsoft.AspNetCore.Mvc.Rendering; |
|||
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
|||
using Microsoft.AspNetCore.Razor.TagHelpers; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers; |
|||
|
|||
[HtmlTargetElement("script")] |
|||
[HtmlTargetElement("body")] |
|||
public class ScriptNonceTagHelper : AbpTagHelper |
|||
{ |
|||
[HtmlAttributeNotBound] |
|||
[ViewContext] |
|||
public ViewContext ViewContext { get; set; } |
|||
public override void Process(TagHelperContext context, TagHelperOutput output) |
|||
{ |
|||
if (ViewContext.HttpContext.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) && nonce is string nonceString && !string.IsNullOrEmpty(nonceString)) |
|||
{ |
|||
output.Attributes.Add("nonce", nonceString); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using System.Text; |
|||
using Microsoft.AspNetCore.Mvc.Infrastructure; |
|||
using Microsoft.AspNetCore.WebUtilities; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Infrastructure; |
|||
|
|||
/// <summary>
|
|||
/// https://github.com/dotnet/aspnetcore/issues/40928#issuecomment-1450063613
|
|||
/// </summary>
|
|||
public class AbpMemoryPoolHttpResponseStreamWriterFactory : IHttpResponseStreamWriterFactory |
|||
{ |
|||
public const int DefaultBufferSize = 32 * 1024; |
|||
|
|||
private readonly ArrayPool<byte> _bytePool; |
|||
private readonly ArrayPool<char> _charPool; |
|||
|
|||
public AbpMemoryPoolHttpResponseStreamWriterFactory(ArrayPool<byte> bytePool, ArrayPool<char> charPool) |
|||
{ |
|||
_bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); |
|||
_charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); |
|||
} |
|||
|
|||
public TextWriter CreateWriter(Stream stream, Encoding encoding) |
|||
{ |
|||
if (stream == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(stream)); |
|||
} |
|||
|
|||
if (encoding == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(encoding)); |
|||
} |
|||
|
|||
return new HttpResponseStreamWriter(stream, encoding, DefaultBufferSize, _bytePool, _charPool); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Controllers; |
|||
|
|||
[AttributeUsage(AttributeTargets.Class)] |
|||
public class ReplaceControllersAttribute : Attribute |
|||
{ |
|||
public Type[] ControllerTypes { get; } |
|||
|
|||
public ReplaceControllersAttribute(params Type[] controllerTypes) |
|||
{ |
|||
ControllerTypes = controllerTypes ?? Type.EmptyTypes; |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using Microsoft.AspNetCore.Html; |
|||
using Microsoft.AspNetCore.Mvc.Rendering; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Security; |
|||
|
|||
public static class AbpSecurityHeaderNonceHelper |
|||
{ |
|||
public static string GetScriptNonce(this IHtmlHelper htmlHelper) |
|||
{ |
|||
if (htmlHelper.ViewContext.HttpContext.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) && nonce is string nonceString && !string.IsNullOrEmpty(nonceString)) |
|||
{ |
|||
return nonceString; |
|||
} |
|||
|
|||
return string.Empty; |
|||
} |
|||
|
|||
public static IHtmlContent GetScriptNonceAttribute(this IHtmlHelper htmlHelper) |
|||
{ |
|||
var nonce = htmlHelper.GetScriptNonce(); |
|||
return nonce == string.Empty ? HtmlString.Empty : new HtmlString($"nonce=\"{nonce}\""); |
|||
} |
|||
} |
|||
@ -1,17 +1,28 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Security; |
|||
|
|||
public class AbpSecurityHeadersOptions |
|||
{ |
|||
public bool UseContentSecurityPolicyHeader { get; set; } |
|||
|
|||
|
|||
public bool UseContentSecurityPolicyScriptNonce { get; set; } |
|||
|
|||
public string ContentSecurityPolicyValue { get; set; } |
|||
|
|||
public Dictionary<string, string> Headers { get; } |
|||
|
|||
public List<Func<HttpContext, Task<bool>>> IgnoredScriptNonceSelectors { get; } |
|||
|
|||
public List<string> IgnoredScriptNoncePaths { get; } |
|||
|
|||
public AbpSecurityHeadersOptions() |
|||
{ |
|||
Headers = new Dictionary<string, string>(); |
|||
IgnoredScriptNonceSelectors = new List<Func<HttpContext, Task<bool>>>(); |
|||
IgnoredScriptNoncePaths = new List<string>(); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.Cli.Args; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Cli.Commands; |
|||
|
|||
public class ClearDownloadCacheCommand : IConsoleCommand, ITransientDependency |
|||
{ |
|||
public const string Name = "clear-download-cache"; |
|||
|
|||
public ILogger<ClearDownloadCacheCommand> Logger { get; set; } |
|||
|
|||
public ClearDownloadCacheCommand() |
|||
{ |
|||
Logger = NullLogger<ClearDownloadCacheCommand>.Instance; |
|||
} |
|||
|
|||
public async Task ExecuteAsync(CommandLineArgs commandLineArgs) |
|||
{ |
|||
Logger.LogInformation("Clearing the templates download cache..."); |
|||
foreach (var cacheFile in Directory.GetFiles(CliPaths.TemplateCache, "*.zip")) |
|||
{ |
|||
Logger.LogInformation($"Deleting {cacheFile}"); |
|||
try |
|||
{ |
|||
File.Delete(cacheFile); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Logger.LogError(e, $"Could not delete {cacheFile}"); |
|||
} |
|||
} |
|||
Logger.LogInformation("Done."); |
|||
await Task.CompletedTask; |
|||
} |
|||
|
|||
public string GetUsageInfo() |
|||
{ |
|||
var sb = new StringBuilder(); |
|||
|
|||
sb.AppendLine(""); |
|||
sb.AppendLine("Usage:"); |
|||
sb.AppendLine(" abp clear-download-cache"); |
|||
sb.AppendLine(""); |
|||
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); |
|||
|
|||
return sb.ToString(); |
|||
} |
|||
|
|||
public string GetShortDescription() |
|||
{ |
|||
return "Clears the templates download cache."; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.framework" |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net7.0</TargetFrameworks> |
|||
<PackageId>Volo.Abp.Imaging.Abstractions</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.Threading\Volo.Abp.Threading.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
[DependsOn(typeof(AbpThreadingModule))] |
|||
public class AbpImagingAbstractionsModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public interface IImageCompressor |
|||
{ |
|||
Task<ImageCompressResult<Stream>> CompressAsync( |
|||
Stream stream, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
|
|||
Task<ImageCompressResult<byte[]>> CompressAsync( |
|||
byte[] bytes, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public interface IImageCompressorContributor |
|||
{ |
|||
Task<ImageCompressResult<Stream>> TryCompressAsync( |
|||
Stream stream, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default); |
|||
Task<ImageCompressResult<byte[]>> TryCompressAsync( |
|||
byte[] bytes, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public interface IImageResizer |
|||
{ |
|||
Task<ImageResizeResult<Stream>> ResizeAsync( |
|||
Stream stream, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
|
|||
Task<ImageResizeResult<byte[]>> ResizeAsync( |
|||
byte[] bytes, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default |
|||
); |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public interface IImageResizerContributor |
|||
{ |
|||
Task<ImageResizeResult<Stream>> TryResizeAsync( |
|||
Stream stream, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task<ImageResizeResult<byte[]>> TryResizeAsync( |
|||
byte[] bytes, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageCompressResult<T> : ImageProcessResult<T> |
|||
{ |
|||
public ImageCompressResult(T result, ImageProcessState state) : base(result, state) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageCompressor : IImageCompressor, ITransientDependency |
|||
{ |
|||
protected IEnumerable<IImageCompressorContributor> ImageCompressorContributors { get; } |
|||
|
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
|
|||
public ImageCompressor(IEnumerable<IImageCompressorContributor> imageCompressorContributors, ICancellationTokenProvider cancellationTokenProvider) |
|||
{ |
|||
ImageCompressorContributors = imageCompressorContributors; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
} |
|||
|
|||
public virtual async Task<ImageCompressResult<Stream>> CompressAsync( |
|||
[NotNull] Stream stream, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
Check.NotNull(stream, nameof(stream)); |
|||
|
|||
foreach (var imageCompressorContributor in ImageCompressorContributors) |
|||
{ |
|||
var result = await imageCompressorContributor.TryCompressAsync(stream, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); |
|||
|
|||
if (result.State == ImageProcessState.Unsupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
return new ImageCompressResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
public virtual async Task<ImageCompressResult<byte[]>> CompressAsync( |
|||
[NotNull] byte[] bytes, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
Check.NotNull(bytes, nameof(bytes)); |
|||
|
|||
foreach (var imageCompressorContributor in ImageCompressorContributors) |
|||
{ |
|||
var result = await imageCompressorContributor.TryCompressAsync(bytes, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); |
|||
|
|||
if (result.State == ImageProcessState.Unsupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
return new ImageCompressResult<byte[]>(bytes, ImageProcessState.Unsupported); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public abstract class ImageProcessResult<T> |
|||
{ |
|||
public T Result { get; } |
|||
public ImageProcessState State { get; } |
|||
|
|||
protected ImageProcessResult(T result, ImageProcessState state) |
|||
{ |
|||
Result = result; |
|||
State = state; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public enum ImageProcessState : byte |
|||
{ |
|||
Done = 1, |
|||
Canceled = 2, |
|||
Unsupported = 3, |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageResizeArgs |
|||
{ |
|||
private int _width; |
|||
public int Width |
|||
{ |
|||
get => _width; |
|||
set |
|||
{ |
|||
if (value < 0) |
|||
{ |
|||
throw new ArgumentException("Width cannot be negative!", nameof(value)); |
|||
} |
|||
|
|||
_width = value; |
|||
} |
|||
} |
|||
|
|||
private int _height; |
|||
public int Height |
|||
{ |
|||
get => _height; |
|||
set |
|||
{ |
|||
if (value < 0) |
|||
{ |
|||
throw new ArgumentException("Height cannot be negative!", nameof(value)); |
|||
} |
|||
|
|||
_height = value; |
|||
} |
|||
} |
|||
|
|||
public ImageResizeMode Mode { get; set; } = ImageResizeMode.Default; |
|||
|
|||
public ImageResizeArgs(int? width = null, int? height = null, ImageResizeMode? mode = null) |
|||
{ |
|||
if (mode.HasValue) |
|||
{ |
|||
Mode = mode.Value; |
|||
} |
|||
|
|||
Width = width ?? 0; |
|||
Height = height ?? 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public enum ImageResizeMode : byte |
|||
{ |
|||
None = 0, |
|||
Stretch = 1, |
|||
BoxPad = 2, |
|||
Min = 3, |
|||
Max = 4, |
|||
Crop = 5, |
|||
Pad = 6, |
|||
Default = 7 |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageResizeOptions |
|||
{ |
|||
public ImageResizeMode DefaultResizeMode { get; set; } = ImageResizeMode.None; |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageResizeResult<T> : ImageProcessResult<T> |
|||
{ |
|||
public ImageResizeResult(T result, ImageProcessState state) : base(result, state) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageResizer : IImageResizer, ITransientDependency |
|||
{ |
|||
protected IEnumerable<IImageResizerContributor> ImageResizerContributors { get; } |
|||
|
|||
protected ImageResizeOptions ImageResizeOptions { get; } |
|||
|
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
|
|||
public ImageResizer( |
|||
IEnumerable<IImageResizerContributor> imageResizerContributors, |
|||
IOptions<ImageResizeOptions> imageResizeOptions, |
|||
ICancellationTokenProvider cancellationTokenProvider) |
|||
{ |
|||
ImageResizerContributors = imageResizerContributors; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
ImageResizeOptions = imageResizeOptions.Value; |
|||
} |
|||
|
|||
public virtual async Task<ImageResizeResult<Stream>> ResizeAsync( |
|||
[NotNull] Stream stream, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
Check.NotNull(stream, nameof(stream)); |
|||
|
|||
ChangeDefaultResizeMode(resizeArgs); |
|||
|
|||
foreach (var imageResizerContributor in ImageResizerContributors) |
|||
{ |
|||
var result = await imageResizerContributor.TryResizeAsync(stream, resizeArgs, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); |
|||
|
|||
if (result.State == ImageProcessState.Unsupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
return new ImageResizeResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
public virtual async Task<ImageResizeResult<byte[]>> ResizeAsync( |
|||
[NotNull] byte[] bytes, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
Check.NotNull(bytes, nameof(bytes)); |
|||
|
|||
ChangeDefaultResizeMode(resizeArgs); |
|||
|
|||
foreach (var imageResizerContributor in ImageResizerContributors) |
|||
{ |
|||
var result = await imageResizerContributor.TryResizeAsync(bytes, resizeArgs, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); |
|||
|
|||
if (result.State == ImageProcessState.Unsupported) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
return new ImageResizeResult<byte[]>(bytes, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
protected virtual void ChangeDefaultResizeMode(ImageResizeArgs resizeArgs) |
|||
{ |
|||
if (resizeArgs.Mode == ImageResizeMode.Default) |
|||
{ |
|||
resizeArgs.Mode = ImageResizeOptions.DefaultResizeMode; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.framework" |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<PackageId>Volo.Abp.Imaging.AspNetCore</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<IsPackable>true</IsPackable> |
|||
<OutputType>Library</OutputType> |
|||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.Imaging.Abstractions\Volo.Abp.Imaging.Abstractions.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
[DependsOn(typeof(AbpImagingAbstractionsModule))] |
|||
public class AbpImagingAspNetCoreModule : 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.Imaging; |
|||
|
|||
public class CompressImageAttribute : ActionFilterAttribute |
|||
{ |
|||
public string[] Parameters { get; } |
|||
|
|||
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(); |
|||
|
|||
var imageCompressor = context.HttpContext.RequestServices.GetRequiredService<IImageCompressor>(); |
|||
|
|||
foreach (var (key, value) in parameters) |
|||
{ |
|||
object compressedValue = value switch { |
|||
IFormFile file => await CompressImageAsync(file, imageCompressor), |
|||
IRemoteStreamContent remoteStreamContent => await CompressImageAsync(remoteStreamContent, imageCompressor), |
|||
Stream stream => await CompressImageAsync(stream, imageCompressor), |
|||
IEnumerable<byte> bytes => await CompressImageAsync(bytes.ToArray(), imageCompressor), |
|||
_ => null |
|||
}; |
|||
|
|||
if (compressedValue != null) |
|||
{ |
|||
context.ActionArguments[key] = compressedValue; |
|||
} |
|||
} |
|||
|
|||
await next(); |
|||
} |
|||
|
|||
protected virtual async Task<IFormFile> CompressImageAsync(IFormFile file, IImageCompressor imageCompressor) |
|||
{ |
|||
if(file.Headers == null || file.ContentType == null || !file.ContentType.StartsWith("image/")) |
|||
{ |
|||
return file; |
|||
} |
|||
|
|||
var result = await imageCompressor.CompressAsync(file.OpenReadStream(), file.ContentType); |
|||
|
|||
if (result.State != ImageProcessState.Done) |
|||
{ |
|||
return file; |
|||
} |
|||
|
|||
return new FormFile(result.Result, 0, result.Result.Length, file.Name, file.FileName) { |
|||
Headers = file.Headers, |
|||
}; |
|||
} |
|||
|
|||
protected virtual async Task<IRemoteStreamContent> CompressImageAsync(IRemoteStreamContent remoteStreamContent, IImageCompressor imageCompressor) |
|||
{ |
|||
if(remoteStreamContent.ContentType == null || !remoteStreamContent.ContentType.StartsWith("image/")) |
|||
{ |
|||
return remoteStreamContent; |
|||
} |
|||
|
|||
var result = await imageCompressor.CompressAsync(remoteStreamContent.GetStream(), remoteStreamContent.ContentType); |
|||
|
|||
if (result.State != ImageProcessState.Done) |
|||
{ |
|||
return remoteStreamContent; |
|||
} |
|||
|
|||
var fileName = remoteStreamContent.FileName; |
|||
var contentType = remoteStreamContent.ContentType; |
|||
remoteStreamContent.Dispose(); |
|||
return new RemoteStreamContent(result.Result, fileName, contentType); |
|||
} |
|||
|
|||
protected virtual async Task<Stream> CompressImageAsync(Stream stream, IImageCompressor imageCompressor) |
|||
{ |
|||
var result = await imageCompressor.CompressAsync(stream); |
|||
|
|||
if (result.State != ImageProcessState.Done) |
|||
{ |
|||
return stream; |
|||
} |
|||
|
|||
await stream.DisposeAsync(); |
|||
return result.Result; |
|||
} |
|||
|
|||
protected virtual async Task<byte[]> CompressImageAsync(byte[] bytes, IImageCompressor imageCompressor) |
|||
{ |
|||
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.Imaging; |
|||
|
|||
public class ResizeImageAttribute : ActionFilterAttribute |
|||
{ |
|||
public int? Width { get; } |
|||
public int? Height { get; } |
|||
|
|||
public ImageResizeMode Mode { get; set; } |
|||
public string[] Parameters { get; } |
|||
|
|||
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(); |
|||
|
|||
var imageResizer = context.HttpContext.RequestServices.GetRequiredService<IImageResizer>(); |
|||
|
|||
foreach (var (key, value) in parameters) |
|||
{ |
|||
object resizedValue = value switch { |
|||
IFormFile file => await ResizeImageAsync(file, imageResizer), |
|||
IRemoteStreamContent remoteStreamContent => await ResizeImageAsync(remoteStreamContent, imageResizer), |
|||
Stream stream => await ResizeImageAsync(stream, imageResizer), |
|||
IEnumerable<byte> bytes => await ResizeImageAsync(bytes.ToArray(), imageResizer), |
|||
_ => null |
|||
}; |
|||
|
|||
if (resizedValue != null) |
|||
{ |
|||
context.ActionArguments[key] = resizedValue; |
|||
} |
|||
} |
|||
|
|||
await next(); |
|||
} |
|||
|
|||
protected virtual async Task<IFormFile> ResizeImageAsync(IFormFile file, IImageResizer imageResizer) |
|||
{ |
|||
if(file.Headers == null || 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.State != ImageProcessState.Done) |
|||
{ |
|||
return file; |
|||
} |
|||
|
|||
return new FormFile(result.Result, 0, result.Result.Length, file.Name, file.FileName) { |
|||
Headers = file.Headers |
|||
}; |
|||
} |
|||
|
|||
protected virtual async Task<IRemoteStreamContent> ResizeImageAsync(IRemoteStreamContent remoteStreamContent, IImageResizer imageResizer) |
|||
{ |
|||
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.State != ImageProcessState.Done) |
|||
{ |
|||
return remoteStreamContent; |
|||
} |
|||
|
|||
var fileName = remoteStreamContent.FileName; |
|||
var contentType = remoteStreamContent.ContentType; |
|||
remoteStreamContent.Dispose(); |
|||
return new RemoteStreamContent(result.Result, fileName, contentType); |
|||
} |
|||
|
|||
protected virtual async Task<Stream> ResizeImageAsync(Stream stream, IImageResizer imageResizer) |
|||
{ |
|||
var result = await imageResizer.ResizeAsync(stream, new ImageResizeArgs(Width, Height, Mode)); |
|||
|
|||
if (result.State != ImageProcessState.Done) |
|||
{ |
|||
return stream; |
|||
} |
|||
|
|||
await stream.DisposeAsync(); |
|||
return result.Result; |
|||
} |
|||
|
|||
protected virtual async Task<byte[]> ResizeImageAsync(byte[] bytes, IImageResizer imageResizer) |
|||
{ |
|||
return (await imageResizer.ResizeAsync(bytes, new ImageResizeArgs(Width, Height, Mode))).Result; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;</TargetFrameworks> |
|||
<PackageId>Volo.Abp.Imaging.ImageSharp</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.Imaging.Abstractions\Volo.Abp.Imaging.Abstractions.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.4" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.framework" |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
[DependsOn(typeof(AbpImagingAbstractionsModule))] |
|||
public class AbpImagingImageSharpModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using SixLabors.ImageSharp.Formats; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
using SixLabors.ImageSharp.Formats.Webp; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageSharpCompressOptions |
|||
{ |
|||
public IImageEncoder JpegEncoder { get; set; } |
|||
|
|||
public IImageEncoder PngEncoder { get; set; } |
|||
|
|||
public IImageEncoder WebpEncoder { get; set; } |
|||
|
|||
public int DefaultQuality { get; set; } = 75; |
|||
public ImageSharpCompressOptions() |
|||
{ |
|||
JpegEncoder = new JpegEncoder { |
|||
Quality = DefaultQuality |
|||
}; |
|||
|
|||
PngEncoder = new PngEncoder { |
|||
CompressionLevel = PngCompressionLevel.BestCompression, |
|||
IgnoreMetadata = true |
|||
}; |
|||
|
|||
WebpEncoder = new WebpEncoder { |
|||
Quality = DefaultQuality |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.Options; |
|||
using SixLabors.ImageSharp; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Formats; |
|||
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.Imaging; |
|||
|
|||
public class ImageSharpImageCompressorContributor : IImageCompressorContributor, ITransientDependency |
|||
{ |
|||
protected ImageSharpCompressOptions Options { get; } |
|||
|
|||
public ImageSharpImageCompressorContributor(IOptions<ImageSharpCompressOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
} |
|||
|
|||
public virtual async Task<ImageCompressResult<Stream>> TryCompressAsync( |
|||
Stream stream, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageCompressResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
var (image, format) = await Image.LoadWithFormatAsync(stream, cancellationToken); |
|||
|
|||
if (!CanCompress(format.DefaultMimeType)) |
|||
{ |
|||
return new ImageCompressResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
var memoryStream = await GetStreamFromImageAsync(image, format, cancellationToken); |
|||
|
|||
if (memoryStream.Length < stream.Length) |
|||
{ |
|||
return new ImageCompressResult<Stream>(memoryStream, ImageProcessState.Done); |
|||
} |
|||
|
|||
memoryStream.Dispose(); |
|||
return new ImageCompressResult<Stream>(stream, ImageProcessState.Canceled); |
|||
} |
|||
|
|||
public virtual async Task<ImageCompressResult<byte[]>> TryCompressAsync( |
|||
byte[] bytes, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageCompressResult<byte[]>(bytes, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
using var ms = new MemoryStream(bytes); |
|||
var result = await TryCompressAsync(ms, mimeType, cancellationToken); |
|||
|
|||
if (result.State != ImageProcessState.Done) |
|||
{ |
|||
return new ImageCompressResult<byte[]>(bytes, result.State); |
|||
} |
|||
|
|||
var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); |
|||
result.Result.Dispose(); |
|||
return new ImageCompressResult<byte[]>(newBytes, result.State); |
|||
} |
|||
|
|||
protected virtual bool CanCompress(string mimeType) |
|||
{ |
|||
return mimeType switch { |
|||
MimeTypes.Image.Jpeg => true, |
|||
MimeTypes.Image.Png => true, |
|||
MimeTypes.Image.Webp => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
protected virtual async Task<Stream> GetStreamFromImageAsync( |
|||
Image image, |
|||
IImageFormat format, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
var memoryStream = new MemoryStream(); |
|||
|
|||
try |
|||
{ |
|||
await image.SaveAsync(memoryStream, GetEncoder(format), cancellationToken: cancellationToken); |
|||
|
|||
memoryStream.Position = 0; |
|||
|
|||
return memoryStream; |
|||
} |
|||
catch |
|||
{ |
|||
memoryStream.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
protected virtual IImageEncoder GetEncoder(IImageFormat format) |
|||
{ |
|||
switch (format.DefaultMimeType) |
|||
{ |
|||
case MimeTypes.Image.Jpeg: |
|||
return Options.JpegEncoder ?? new JpegEncoder(); |
|||
case MimeTypes.Image.Png: |
|||
return Options.PngEncoder ?? new PngEncoder(); |
|||
case MimeTypes.Image.Webp: |
|||
return Options.WebpEncoder ?? new WebpEncoder(); |
|||
default: |
|||
throw new NotSupportedException($"No encoder available for the given format: {format.Name}"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using SixLabors.ImageSharp; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class ImageSharpImageResizerContributor : IImageResizerContributor, ITransientDependency |
|||
{ |
|||
public virtual async Task<ImageResizeResult<Stream>> TryResizeAsync( |
|||
Stream stream, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanResize(mimeType)) |
|||
{ |
|||
return new ImageResizeResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
var (image, format) = await Image.LoadWithFormatAsync(stream, cancellationToken); |
|||
|
|||
if (!CanResize(format.DefaultMimeType)) |
|||
{ |
|||
return new ImageResizeResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
if (ResizeModeMap.TryGetValue(resizeArgs.Mode, out var resizeMode)) |
|||
{ |
|||
image.Mutate(x => x.Resize(new ResizeOptions { Size = GetSize(resizeArgs), Mode = resizeMode })); |
|||
} |
|||
else |
|||
{ |
|||
throw new NotSupportedException("Resize mode " + resizeArgs.Mode + "is not supported!"); |
|||
} |
|||
|
|||
var memoryStream = new MemoryStream(); |
|||
|
|||
try |
|||
{ |
|||
await image.SaveAsync(memoryStream, format, cancellationToken: cancellationToken); |
|||
memoryStream.Position = 0; |
|||
return new ImageResizeResult<Stream>(memoryStream, ImageProcessState.Done); |
|||
} |
|||
catch |
|||
{ |
|||
memoryStream.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public virtual async Task<ImageResizeResult<byte[]>> TryResizeAsync( |
|||
byte[] bytes, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanResize(mimeType)) |
|||
{ |
|||
return new ImageResizeResult<byte[]>(bytes, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
using var ms = new MemoryStream(bytes); |
|||
|
|||
var result = await TryResizeAsync(ms, resizeArgs, mimeType, cancellationToken); |
|||
|
|||
if (result.State != ImageProcessState.Done) |
|||
{ |
|||
return new ImageResizeResult<byte[]>(bytes, result.State); |
|||
} |
|||
|
|||
var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); |
|||
|
|||
result.Result.Dispose(); |
|||
|
|||
return new ImageResizeResult<byte[]>(newBytes, result.State); |
|||
} |
|||
|
|||
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, |
|||
MimeTypes.Image.Webp => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
protected Dictionary<ImageResizeMode, ResizeMode> ResizeModeMap = new() { |
|||
{ ImageResizeMode.None, default }, |
|||
{ ImageResizeMode.Stretch, ResizeMode.Stretch }, |
|||
{ ImageResizeMode.BoxPad, ResizeMode.BoxPad }, |
|||
{ ImageResizeMode.Min, ResizeMode.Min }, |
|||
{ ImageResizeMode.Max, ResizeMode.Max }, |
|||
{ ImageResizeMode.Crop, ResizeMode.Crop }, |
|||
{ ImageResizeMode.Pad, ResizeMode.Pad } |
|||
}; |
|||
|
|||
private static Size GetSize(ImageResizeArgs resizeArgs) |
|||
{ |
|||
var size = new Size(); |
|||
|
|||
if (resizeArgs.Width > 0) |
|||
{ |
|||
size.Width = resizeArgs.Width; |
|||
} |
|||
|
|||
if (resizeArgs.Height > 0) |
|||
{ |
|||
size.Height = resizeArgs.Height; |
|||
} |
|||
|
|||
return size; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.framework" |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net7.0</TargetFrameworks> |
|||
<PackageId>Volo.Abp.Imaging.MagicNet</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.Imaging.Abstractions\Volo.Abp.Imaging.Abstractions.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.1.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
[DependsOn(typeof(AbpImagingAbstractionsModule))] |
|||
public class AbpImagingMagickNetModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using ImageMagick; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class MagickImageCompressorContributor : IImageCompressorContributor, ITransientDependency |
|||
{ |
|||
protected MagickNetCompressOptions Options { get; } |
|||
|
|||
protected readonly ImageOptimizer Optimizer; |
|||
|
|||
public MagickImageCompressorContributor(IOptions<MagickNetCompressOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
Optimizer = new ImageOptimizer |
|||
{ |
|||
OptimalCompression = Options.OptimalCompression, |
|||
IgnoreUnsupportedFormats = Options.IgnoreUnsupportedFormats |
|||
}; |
|||
} |
|||
|
|||
public virtual async Task<ImageCompressResult<Stream>> TryCompressAsync( |
|||
Stream stream, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageCompressResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
var memoryStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); |
|||
|
|||
try |
|||
{ |
|||
if (!Optimizer.IsSupported(memoryStream)) |
|||
{ |
|||
return new ImageCompressResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
if (Compress(memoryStream)) |
|||
{ |
|||
return new ImageCompressResult<Stream>(memoryStream, ImageProcessState.Done); |
|||
} |
|||
|
|||
memoryStream.Dispose(); |
|||
|
|||
return new ImageCompressResult<Stream>(stream, ImageProcessState.Canceled); |
|||
} |
|||
catch |
|||
{ |
|||
memoryStream.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public virtual async Task<ImageCompressResult<byte[]>> TryCompressAsync( |
|||
byte[] bytes, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) |
|||
{ |
|||
return new ImageCompressResult<byte[]>(bytes, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
using var memoryStream = new MemoryStream(bytes); |
|||
var result = await TryCompressAsync(memoryStream, mimeType, cancellationToken); |
|||
|
|||
if (result.State != ImageProcessState.Done) |
|||
{ |
|||
return new ImageCompressResult<byte[]>(bytes, result.State); |
|||
} |
|||
|
|||
var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); |
|||
|
|||
result.Result.Dispose(); |
|||
|
|||
return new ImageCompressResult<byte[]>(newBytes, result.State); |
|||
} |
|||
|
|||
protected virtual bool CanCompress(string mimeType) |
|||
{ |
|||
return mimeType switch { |
|||
MimeTypes.Image.Jpeg => true, |
|||
MimeTypes.Image.Png => true, |
|||
MimeTypes.Image.Gif => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
protected virtual bool Compress(Stream stream) |
|||
{ |
|||
return Options.Lossless ? Optimizer.LosslessCompress(stream) : Optimizer.Compress(stream); |
|||
} |
|||
} |
|||
@ -0,0 +1,325 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using ImageMagick; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http; |
|||
|
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class MagickImageResizerContributor : IImageResizerContributor, ITransientDependency |
|||
{ |
|||
private const int Min = 1; |
|||
|
|||
public virtual async Task<ImageResizeResult<Stream>> TryResizeAsync( |
|||
Stream stream, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!mimeType.IsNullOrWhiteSpace() && !CanResize(mimeType)) |
|||
{ |
|||
return new ImageResizeResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
var memoryStream = await stream.CreateMemoryStreamAsync(cancellationToken: cancellationToken); |
|||
|
|||
try |
|||
{ |
|||
using var image = new MagickImage(memoryStream); |
|||
|
|||
if (mimeType.IsNullOrWhiteSpace() && !CanResize(image.FormatInfo?.MimeType)) |
|||
{ |
|||
return new ImageResizeResult<Stream>(stream, ImageProcessState.Unsupported); |
|||
} |
|||
|
|||
Resize(image, resizeArgs); |
|||
|
|||
memoryStream.Position = 0; |
|||
await image.WriteAsync(memoryStream, cancellationToken); |
|||
memoryStream.SetLength(memoryStream.Position); |
|||
memoryStream.Position = 0; |
|||
|
|||
return new ImageResizeResult<Stream>(memoryStream, ImageProcessState.Done); |
|||
} |
|||
catch |
|||
{ |
|||
memoryStream.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public virtual Task<ImageResizeResult<byte[]>> TryResizeAsync( |
|||
byte[] bytes, |
|||
ImageResizeArgs resizeArgs, |
|||
[CanBeNull] string mimeType = null, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
if (!mimeType.IsNullOrWhiteSpace() && !CanResize(mimeType)) |
|||
{ |
|||
return Task.FromResult(new ImageResizeResult<byte[]>(bytes, ImageProcessState.Unsupported)); |
|||
} |
|||
|
|||
using var image = new MagickImage(bytes); |
|||
|
|||
if (mimeType.IsNullOrWhiteSpace() && !CanResize(image.FormatInfo?.MimeType)) |
|||
{ |
|||
return Task.FromResult(new ImageResizeResult<byte[]>(bytes, ImageProcessState.Unsupported)); |
|||
} |
|||
|
|||
Resize(image, resizeArgs); |
|||
|
|||
return Task.FromResult(new ImageResizeResult<byte[]>(image.ToByteArray(), ImageProcessState.Done)); |
|||
} |
|||
|
|||
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, |
|||
MimeTypes.Image.Webp => true, |
|||
_ => false |
|||
}; |
|||
} |
|||
|
|||
protected virtual void Resize(MagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
ApplyResizeMode(image, resizeArgs); |
|||
} |
|||
|
|||
protected virtual void ApplyResizeMode(MagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
switch (resizeArgs.Mode) |
|||
{ |
|||
case ImageResizeMode.None: |
|||
ResizeModeNone(image, resizeArgs); |
|||
break; |
|||
case ImageResizeMode.Stretch: |
|||
ResizeStretch(image, resizeArgs); |
|||
break; |
|||
case ImageResizeMode.Pad: |
|||
ResizePad(image, resizeArgs); |
|||
break; |
|||
case ImageResizeMode.BoxPad: |
|||
ResizeBoxPad(image, resizeArgs); |
|||
break; |
|||
case ImageResizeMode.Max: |
|||
ResizeMax(image, resizeArgs); |
|||
break; |
|||
case ImageResizeMode.Min: |
|||
ResizeMin(image, resizeArgs); |
|||
break; |
|||
case ImageResizeMode.Crop: |
|||
ResizeCrop(image, resizeArgs); |
|||
break; |
|||
default: |
|||
throw new NotSupportedException("Resize mode " + resizeArgs.Mode + "is not supported!"); |
|||
} |
|||
} |
|||
|
|||
|
|||
protected virtual int GetTargetHeight(ImageResizeArgs resizeArgs, int min, int sourceWidth, int sourceHeight) |
|||
{ |
|||
if (resizeArgs.Height == 0 && resizeArgs.Width > 0) |
|||
{ |
|||
return Math.Max(min, (int)Math.Round(sourceHeight * resizeArgs.Width / (float)sourceWidth)); |
|||
} |
|||
|
|||
return resizeArgs.Height; |
|||
} |
|||
|
|||
protected virtual int GetTargetWidth(ImageResizeArgs resizeArgs, int min, int sourceWidth, int sourceHeight) |
|||
{ |
|||
if (resizeArgs.Width == 0 && resizeArgs.Height > 0) |
|||
{ |
|||
return Math.Max(min, (int)Math.Round(sourceWidth * resizeArgs.Height / (float)sourceHeight)); |
|||
} |
|||
|
|||
return resizeArgs.Width; |
|||
} |
|||
|
|||
protected virtual void ResizeModeNone(IMagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
image.Resize( |
|||
GetTargetWidth(resizeArgs, Min, sourceWidth, sourceHeight), |
|||
GetTargetHeight(resizeArgs, Min, sourceWidth, sourceHeight) |
|||
); |
|||
} |
|||
|
|||
protected virtual void ResizeStretch(IMagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
image.Resize( |
|||
new MagickGeometry( |
|||
GetTargetWidth(resizeArgs, Min, sourceWidth, sourceHeight), |
|||
GetTargetHeight(resizeArgs, Min, sourceWidth, sourceHeight)) { IgnoreAspectRatio = true }); |
|||
} |
|||
|
|||
protected virtual void ResizePad(MagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var targetWidth = GetTargetWidth(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
var targetHeight = GetTargetHeight(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
|
|||
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 void ResizeBoxPad(MagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var targetWidth = GetTargetWidth(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
var targetHeight = GetTargetHeight(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
|
|||
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 ResizeMax(IMagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var imageRatio = CalculateRatio(sourceWidth, sourceHeight); |
|||
|
|||
var targetWidth = GetTargetWidth(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
var targetHeight = GetTargetHeight(resizeArgs, Min, 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 ResizeMin(MagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var imageRatio = CalculateRatio(sourceWidth, sourceHeight); |
|||
|
|||
var targetWidth = GetTargetWidth(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
var targetHeight = GetTargetHeight(resizeArgs, Min, 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 ResizeCrop(MagickImage image, ImageResizeArgs resizeArgs) |
|||
{ |
|||
var sourceWidth = image.Width; |
|||
var sourceHeight = image.Height; |
|||
|
|||
var targetWidth = GetTargetWidth(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
var targetHeight = GetTargetHeight(resizeArgs, Min, sourceWidth, sourceHeight); |
|||
|
|||
image.Extent( |
|||
targetWidth, |
|||
targetHeight, |
|||
Gravity.Center, |
|||
MagickColors.Transparent); |
|||
|
|||
image.Crop( |
|||
new MagickGeometry( |
|||
targetWidth, |
|||
targetHeight) { IgnoreAspectRatio = true }, |
|||
Gravity.Center); |
|||
} |
|||
|
|||
protected virtual float CalculatePercent(int imageHeightOrWidth, int heightOrWidth) |
|||
{ |
|||
return heightOrWidth / (float)imageHeightOrWidth; |
|||
} |
|||
|
|||
protected virtual float CalculateRatio(int width, int height) |
|||
{ |
|||
return height / (float)width; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Imaging; |
|||
|
|||
public class MagickNetCompressOptions |
|||
{ |
|||
public bool OptimalCompression { get; set; } |
|||
public bool IgnoreUnsupportedFormats { get; set; } |
|||
public bool Lossless { get; set; } |
|||
} |
|||
@ -1,9 +1,10 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Uow; |
|||
|
|||
public interface ITransactionApi : IDisposable |
|||
{ |
|||
Task CommitAsync(); |
|||
Task CommitAsync(CancellationToken cancellationToken = default); |
|||
} |
|||
|
|||
@ -0,0 +1,50 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Controllers; |
|||
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Controllers; |
|||
|
|||
[Area("abp")] |
|||
[RemoteService(Name = "abp")] |
|||
[ReplaceControllers(typeof(AbpApplicationConfigurationController), typeof(AbpApplicationLocalizationController))] |
|||
public class ReplaceBuiltInController : AbpController |
|||
{ |
|||
[HttpGet("api/abp/application-configuration")] |
|||
public virtual Task<MyApplicationConfigurationDto> GetAsync(MyApplicationConfigurationRequestOptions options) |
|||
{ |
|||
return Task.FromResult(new MyApplicationConfigurationDto() |
|||
{ |
|||
Random = options.Random |
|||
}); |
|||
} |
|||
|
|||
[HttpGet("api/abp/application-localization")] |
|||
public virtual Task<MyApplicationLocalizationDto> GetAsync(MyApplicationLocalizationRequestDto input) |
|||
{ |
|||
return Task.FromResult(new MyApplicationLocalizationDto() |
|||
{ |
|||
Random = input.Random |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public class MyApplicationConfigurationRequestOptions : ApplicationConfigurationRequestOptions |
|||
{ |
|||
public string Random { get; set; } |
|||
} |
|||
|
|||
public class MyApplicationConfigurationDto : ApplicationConfigurationDto |
|||
{ |
|||
public string Random { get; set; } |
|||
} |
|||
|
|||
public class MyApplicationLocalizationRequestDto : ApplicationLocalizationRequestDto |
|||
{ |
|||
public string Random { get; set; } |
|||
} |
|||
|
|||
public class MyApplicationLocalizationDto : ApplicationLocalizationDto |
|||
{ |
|||
public string Random { get; set; } |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System; |
|||
using System.Net; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Shouldly; |
|||
using Volo.Abp.AspNetCore.Mvc.Localization; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Controllers; |
|||
|
|||
public class ReplaceBuiltInController_Tests : AspNetCoreMvcTestBase |
|||
{ |
|||
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) |
|||
{ |
|||
services.Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ControllersToRemove.Add(typeof(AbpLanguagesController)); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Test() |
|||
{ |
|||
var random = Guid.NewGuid().ToString("N"); |
|||
|
|||
(await GetResponseAsObjectAsync<MyApplicationConfigurationDto>("api/abp/application-configuration?random=" + random)).Random.ShouldBe(random); |
|||
(await GetResponseAsObjectAsync<MyApplicationLocalizationDto>("api/abp/application-localization?CultureName=en&random=" + random)).Random.ShouldBe(random); |
|||
|
|||
(await GetResponseAsync("Abp/Languages/Switch", HttpStatusCode.NotFound)).StatusCode.ShouldBe(HttpStatusCode.NotFound); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Threading; |
|||
|
|||
public class SemaphoreSlimExtensions_Tests |
|||
{ |
|||
[Fact] |
|||
public async Task LockAsync_Test() |
|||
{ |
|||
var semaphore = new SemaphoreSlim(0, 1); |
|||
|
|||
await Assert.ThrowsAsync<TimeoutException>(async () => |
|||
{ |
|||
await semaphore.LockAsync(10); |
|||
}); |
|||
|
|||
semaphore = new SemaphoreSlim(1, 1); |
|||
using (await semaphore.LockAsync()) |
|||
{ |
|||
semaphore.CurrentCount.ShouldBe(0); |
|||
} |
|||
semaphore.CurrentCount.ShouldBe(1); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.TestApp.Domain; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Uow; |
|||
|
|||
public class UnitOfWork_CancellationToken_Tests : EntityFrameworkCoreTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Should_Cancel_Test() |
|||
{ |
|||
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin(isTransactional: true)) |
|||
{ |
|||
await Assert.ThrowsAsync<TaskCanceledException>(async () => |
|||
{ |
|||
var cst = new CancellationTokenSource(); |
|||
cst.Cancel(); |
|||
|
|||
await GetRequiredService<IBasicRepository<Person, Guid>>().InsertAsync(new Person(Guid.NewGuid(), "Adam", 42)); |
|||
|
|||
await uow.CompleteAsync(cst.Token); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.test" |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue