Skip to content

Commit 0ed2715

Browse files
Watermark option.
1 parent 923b0d9 commit 0ed2715

19 files changed

+365
-87
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<PackageProjectUrl>https://github.com/squidex/squidex</PackageProjectUrl>
1212
<PublishRepositoryUrl>true</PublishRepositoryUrl>
1313
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
14-
<Version>6.19.0</Version>
14+
<Version>6.20.0</Version>
1515
</PropertyGroup>
1616

1717
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">

assets/Benchmarks/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using BenchmarkDotNet.Attributes;
99
using BenchmarkDotNet.Running;
10+
using Microsoft.Extensions.DependencyInjection;
1011
using Squidex.Assets;
1112

1213
#pragma warning disable MA0048 // File name must match type name
@@ -69,7 +70,13 @@ public static async Task Main(string[] args)
6970
{
7071
if (args.Contains("--resize"))
7172
{
72-
var generator = new ImageSharpThumbnailGenerator();
73+
var httpClientFactory =
74+
new ServiceCollection()
75+
.AddHttpClient()
76+
.BuildServiceProvider()
77+
.GetRequiredService<IHttpClientFactory>();
78+
79+
var generator = new ImageSharpThumbnailGenerator(httpClientFactory);
7380

7481
await using (var source = new FileStream("file_example_PNG_1MB.png", FileMode.Open))
7582
{

assets/Squidex.Assets.ImageMagick/ImageMagickThumbnailGenerator.cs

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using System.Threading;
1212
using System.Threading.Tasks;
1313
using ImageMagick;
14-
using SixLabors.ImageSharp;
1514
using Squidex.Assets.Internal;
1615

1716
namespace Squidex.Assets;
@@ -55,36 +54,13 @@ protected override async Task CreateThumbnailCoreAsync(Stream source, string mim
5554
foreach (var image in images)
5655
{
5756
var clone = image.Clone();
58-
5957
var color = options.ParseColor();
60-
6158
if (w > 0 || h > 0)
6259
{
63-
var isCropUpsize = options.Mode == ResizeMode.CropUpsize;
64-
65-
var resizeMode = options.Mode;
66-
67-
if (isCropUpsize)
68-
{
69-
resizeMode = ResizeMode.Crop;
70-
}
71-
72-
if (w >= image.Width && h >= image.Height && resizeMode == ResizeMode.Crop && !isCropUpsize)
73-
{
74-
resizeMode = ResizeMode.BoxPad;
75-
}
60+
var resizeMode = GetResizeMode(options, w, h, image);
61+
var resizeAnchor = GetResizeAnchor(options);
7662

77-
PointF? centerCoordinates = null;
78-
79-
if (options.FocusX.HasValue && options.FocusY.HasValue)
80-
{
81-
centerCoordinates = new PointF(
82-
+(options.FocusX.Value / 2f) + 0.5f,
83-
-(options.FocusY.Value / 2f) + 0.5f
84-
);
85-
}
86-
87-
var (size, pad) = ResizeHelper.CalculateTargetLocationAndBounds(resizeMode, new Size(image.Width, image.Height), w, h, centerCoordinates);
63+
var (size, pad) = ResizeHelper.CalculateTargetLocationAndBounds(resizeMode, new Size(image.Width, image.Height), w, h, resizeAnchor);
8864

8965
var sourceRectangle = new MagickGeometry(pad.Width, pad.Height)
9066
{
@@ -94,7 +70,6 @@ protected override async Task CreateThumbnailCoreAsync(Stream source, string mim
9470
clone.Resize(sourceRectangle);
9571

9672
image.Extent(size.Width, size.Height);
97-
9873
image.CompositeClear(color);
9974
image.Composite(clone, pad.X, pad.Y, CompositeOperator.Over);
10075
}
@@ -171,6 +146,39 @@ protected override async Task FixCoreAsync(Stream source, string mimeType, Strea
171146
}
172147
}
173148

149+
private static PointF? GetResizeAnchor(ResizeOptions options)
150+
{
151+
PointF? centerCoordinates = null;
152+
153+
if (options.FocusX.HasValue && options.FocusY.HasValue)
154+
{
155+
centerCoordinates = new PointF(
156+
+(options.FocusX.Value / 2f) + 0.5f,
157+
-(options.FocusY.Value / 2f) + 0.5f
158+
);
159+
}
160+
161+
return centerCoordinates;
162+
}
163+
164+
private static ResizeMode GetResizeMode(ResizeOptions options, int w, int h, IMagickImage<byte> image)
165+
{
166+
var isCropUpsize = options.Mode == ResizeMode.CropUpsize;
167+
168+
var resizeMode = options.Mode;
169+
if (isCropUpsize)
170+
{
171+
resizeMode = ResizeMode.Crop;
172+
}
173+
174+
if (w >= image.Width && h >= image.Height && resizeMode == ResizeMode.Crop && !isCropUpsize)
175+
{
176+
resizeMode = ResizeMode.BoxPad;
177+
}
178+
179+
return resizeMode;
180+
}
181+
174182
private static MagickFormat GetFormat(string mimeType)
175183
{
176184
var format = MagickFormat.Unknown;

assets/Squidex.Assets.ImageMagick/Internal/ResizeHelper.cs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66
// ==========================================================================
77

88
using System;
9-
using SixLabors.ImageSharp;
9+
10+
#pragma warning disable MA0048 // File name must match type name
11+
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
1012

1113
namespace Squidex.Assets.Internal;
1214

15+
public readonly record struct PointF(float X, float Y);
16+
17+
public readonly record struct Size(int Width, int Height);
18+
19+
public readonly record struct Rectangle(int X, int Y, int Width, int Height);
20+
1321
internal static class ResizeHelper
1422
{
1523
public static (Size Size, Rectangle Rectangle) CalculateTargetLocationAndBounds(ResizeMode mode, Size sourceSize, int width, int height, PointF? centerCoordinates)
@@ -75,7 +83,16 @@ private static (Size Size, Rectangle Rectangle) CalculateBoxPadRectangle(
7583
var targetX = (desiredWidth - sourceWidth) / 2;
7684

7785
// Target image width and height can be different to the rectangle width and height.
78-
return (new Size(Sanitize(desiredWidth), Sanitize(desiredHeight)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight)));
86+
return (
87+
new Size(
88+
Sanitize(desiredWidth),
89+
Sanitize(desiredHeight)),
90+
new Rectangle(
91+
targetX,
92+
targetY,
93+
Sanitize(targetWidth),
94+
Sanitize(targetHeight))
95+
);
7996
}
8097

8198
// Switch to pad mode to downscale and calculate from there.
@@ -89,9 +106,9 @@ private static (Size Size, Rectangle Rectangle) CalculateCropRectangle(
89106
PointF? centerCoordinates)
90107
{
91108
float ratio;
109+
92110
var sourceWidth = source.Width;
93111
var sourceHeight = source.Height;
94-
95112
var targetX = 0;
96113
var targetY = 0;
97114
var targetWidth = desiredWidth;
@@ -154,7 +171,16 @@ private static (Size Size, Rectangle Rectangle) CalculateCropRectangle(
154171
}
155172

156173
// Target image width and height can be different to the rectangle width and height.
157-
return (new Size(Sanitize(desiredWidth), Sanitize(desiredHeight)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight)));
174+
return (
175+
new Size(
176+
Sanitize(desiredWidth),
177+
Sanitize(desiredHeight)),
178+
new Rectangle(
179+
targetX,
180+
targetY,
181+
Sanitize(targetWidth),
182+
Sanitize(targetHeight))
183+
);
158184
}
159185

160186
private static (Size Size, Rectangle Rectangle) CalculateMaxRectangle(
@@ -184,7 +210,16 @@ private static (Size Size, Rectangle Rectangle) CalculateMaxRectangle(
184210
}
185211

186212
// Replace the size to match the rectangle.
187-
return (new Size(Sanitize(targetWidth), Sanitize(targetHeight)), new Rectangle(0, 0, Sanitize(targetWidth), Sanitize(targetHeight)));
213+
return (
214+
new Size(
215+
Sanitize(targetWidth),
216+
Sanitize(targetHeight)),
217+
new Rectangle(
218+
0,
219+
0,
220+
Sanitize(targetWidth),
221+
Sanitize(targetHeight))
222+
);
188223
}
189224

190225
private static (Size Size, Rectangle Rectangle) CalculateMinRectangle(
@@ -266,7 +301,16 @@ private static (Size Size, Rectangle Rectangle) CalculatePadRectangle(
266301
}
267302

268303
// Target image width and height can be different to the rectangle width and height.
269-
return (new Size(Sanitize(desiredWidth), Sanitize(desiredHeight)), new Rectangle(targetX, targetY, Sanitize(targetWidth), Sanitize(targetHeight)));
304+
return (
305+
new Size(
306+
Sanitize(desiredWidth),
307+
Sanitize(desiredHeight)),
308+
new Rectangle(
309+
targetX,
310+
targetY,
311+
Sanitize(targetWidth),
312+
Sanitize(targetHeight))
313+
);
270314
}
271315

272316
private static int Sanitize(int input) => Math.Max(1, input);

assets/Squidex.Assets.ImageSharp/ImageSharpThumbnailGenerator.cs

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Collections.Generic;
1010
using System.IO;
1111
using System.Linq;
12+
using System.Net.Http;
1213
using System.Threading;
1314
using System.Threading.Tasks;
1415
using Blurhash.ImageSharp;
@@ -23,18 +24,21 @@
2324
using SixLabors.ImageSharp.PixelFormats;
2425
using SixLabors.ImageSharp.Processing;
2526
using Squidex.Assets.Internal;
26-
using ISImageInfo = SixLabors.ImageSharp.ImageInfo;
27-
using ISResizeMode = SixLabors.ImageSharp.Processing.ResizeMode;
28-
using ISResizeOptions = SixLabors.ImageSharp.Processing.ResizeOptions;
27+
using ImageSharpInfo = SixLabors.ImageSharp.ImageInfo;
28+
using ImageSharpMode = SixLabors.ImageSharp.Processing.ResizeMode;
29+
using ImageSharpOptions = SixLabors.ImageSharp.Processing.ResizeOptions;
2930

3031
namespace Squidex.Assets;
3132

3233
public sealed class ImageSharpThumbnailGenerator : AssetThumbnailGeneratorBase
3334
{
3435
private readonly HashSet<string> mimeTypes;
36+
private readonly IHttpClientFactory httpClientFactory;
3537

36-
public ImageSharpThumbnailGenerator()
38+
public ImageSharpThumbnailGenerator(IHttpClientFactory httpClientFactory)
3739
{
40+
this.httpClientFactory = httpClientFactory;
41+
3842
mimeTypes = Configuration.Default.ImageFormatsManager.ImageFormats.SelectMany(x => x.MimeTypes).ToHashSet();
3943
}
4044

@@ -72,39 +76,25 @@ protected override async Task CreateThumbnailCoreAsync(Stream source, string mim
7276

7377
using (var image = await Image.LoadAsync(source, ct))
7478
{
75-
image.Mutate(x => x.AutoOrient());
79+
var watermark = await GetWatermarkAsync(options, ct);
7680

77-
if (w > 0 || h > 0)
81+
image.Mutate(operation =>
7882
{
79-
var isCropUpsize = options.Mode == ResizeMode.CropUpsize;
80-
81-
if (!Enum.TryParse<ISResizeMode>(options.Mode.ToString(), true, out var resizeMode))
82-
{
83-
resizeMode = ISResizeMode.Max;
84-
}
85-
86-
if (isCropUpsize)
87-
{
88-
resizeMode = ISResizeMode.Crop;
89-
}
83+
operation.AutoOrient();
9084

91-
if (w >= image.Width && h >= image.Height && resizeMode == ISResizeMode.Crop && !isCropUpsize)
85+
if (w > 0 || h > 0)
9286
{
93-
resizeMode = ISResizeMode.BoxPad;
94-
}
87+
var resizeMode = GetResizeMode(options, w, h, image);
88+
var resizeOptions = new ImageSharpOptions { Size = new Size(w, h), Mode = resizeMode, PremultiplyAlpha = true };
9589

96-
var resizeOptions = new ISResizeOptions { Size = new Size(w, h), Mode = resizeMode, PremultiplyAlpha = true };
97-
98-
if (options.FocusX.HasValue && options.FocusY.HasValue)
99-
{
100-
resizeOptions.CenterCoordinates = new PointF(
101-
+(options.FocusX.Value / 2f) + 0.5f,
102-
-(options.FocusY.Value / 2f) + 0.5f
103-
);
104-
}
90+
if (options.FocusX.HasValue && options.FocusY.HasValue)
91+
{
92+
resizeOptions.CenterCoordinates = new PointF(
93+
+(options.FocusX.Value / 2f) + 0.5f,
94+
-(options.FocusY.Value / 2f) + 0.5f
95+
);
96+
}
10597

106-
image.Mutate(operation =>
107-
{
10898
operation.Resize(resizeOptions);
10999

110100
if (options.Background != null && Color.TryParse(options.Background, out var color))
@@ -115,15 +105,42 @@ protected override async Task CreateThumbnailCoreAsync(Stream source, string mim
115105
{
116106
operation.BackgroundColor(Color.Transparent);
117107
}
118-
});
119-
}
108+
}
109+
110+
if (watermark != null)
111+
{
112+
operation.Watermark(watermark, options.WatermarkAnchor, options.WatermarkOpacity);
113+
}
114+
});
120115

121116
var encoder = options.GetEncoder(image.Metadata.DecodedImageFormat);
122117

123118
await image.SaveAsync(destination, encoder, ct);
124119
}
125120
}
126121

122+
private static ImageSharpMode GetResizeMode(ResizeOptions options, int w, int h, Image image)
123+
{
124+
var isCropUpsize = options.Mode == ResizeMode.CropUpsize;
125+
126+
if (!Enum.TryParse<ImageSharpMode>(options.Mode.ToString(), true, out var resizeMode))
127+
{
128+
resizeMode = ImageSharpMode.Max;
129+
}
130+
131+
if (isCropUpsize)
132+
{
133+
resizeMode = ImageSharpMode.Crop;
134+
}
135+
136+
if (w >= image.Width && h >= image.Height && resizeMode == ImageSharpMode.Crop && !isCropUpsize)
137+
{
138+
resizeMode = ImageSharpMode.BoxPad;
139+
}
140+
141+
return resizeMode;
142+
}
143+
127144
protected override async Task<ImageInfo?> GetImageInfoCoreAsync(Stream source, string mimeType,
128145
CancellationToken ct = default)
129146
{
@@ -166,7 +183,25 @@ protected override async Task FixCoreAsync(Stream source, string mimeType, Strea
166183
}
167184
}
168185

169-
private static ImageInfo GetImageInfo(ISImageInfo imageInfo)
186+
private async Task<Image?> GetWatermarkAsync(ResizeOptions options,
187+
CancellationToken ct)
188+
{
189+
if (options.WatermarkUrl == null)
190+
{
191+
return null;
192+
}
193+
194+
try
195+
{
196+
return await httpClientFactory.GetImageAsync(options.WatermarkUrl, ct);
197+
}
198+
catch
199+
{
200+
return null;
201+
}
202+
}
203+
204+
private static ImageInfo GetImageInfo(ImageSharpInfo imageInfo)
170205
{
171206
var orientation = ImageOrientation.None;
172207

0 commit comments

Comments
 (0)