diff --git a/.gitignore b/.gitignore index 6ef84b8..d10c047 100644 --- a/.gitignore +++ b/.gitignore @@ -365,3 +365,4 @@ FodyWeavers.xsd /CommonImageActions/wwwroot/cache /CommonImageActions/wwwroot/test/thumbsUp.jpg /CommonImageActions.SampleAspnetCoreProject/wwwroot/cache +/CommonImageActions.SampleAspnetCoreProject/wwwroot/test/ProjectsIcon.png diff --git a/CommonImageActions.Core.Tests/ImageProcessorTests.cs b/CommonImageActions.Core.Tests/ImageProcessorTests.cs index 849f7a7..d634678 100644 --- a/CommonImageActions.Core.Tests/ImageProcessorTests.cs +++ b/CommonImageActions.Core.Tests/ImageProcessorTests.cs @@ -115,22 +115,6 @@ public async Task ProcessVirtualImageAsync_ShouldReturnProcessedImage() Assert.NotEmpty(result); } - [Fact] - public void EncodeSkiaImage_ShouldReturnEncodedImage() - { - // Arrange - var bitmap = new SKBitmap(100, 100); - using var canvas = new SKCanvas(bitmap); - using var newImage = new SkiaImage(bitmap); - var actions = new ImageActions(); - - // Act - var result = ImageProcessor.EncodeSkiaImage(newImage, actions); - - // Assert - Assert.NotNull(result); - } - [Fact] public void GetInitials_ShouldReturnInitials() { diff --git a/CommonImageActions.Core/ImageProcessor.cs b/CommonImageActions.Core/ImageProcessor.cs index e3882b2..f9d9f51 100644 --- a/CommonImageActions.Core/ImageProcessor.cs +++ b/CommonImageActions.Core/ImageProcessor.cs @@ -1,5 +1,4 @@ using Microsoft.Maui.Graphics; -using Microsoft.Maui.Graphics.Skia; using SkiaSharp; using System; using System.Collections.Generic; @@ -91,25 +90,20 @@ await Task.Run(() => if (isVirtual) { - Color virtualImageColor = null; + SKColor virtualImageColor; //set the text color if (!string.IsNullOrEmpty(actions.ImageColor)) { - //try regular - if (Color.TryParse(actions.ImageColor, out var newColor)) - { - virtualImageColor = newColor; - } //try hex - else if (Color.TryParse($"#{actions.ImageColor}", out var newColorFromHex)) + if (SKColor.TryParse($"#{actions.ImageColor}", out var newColorFromHex)) { virtualImageColor = newColorFromHex; } //fall back to white if they both fail else { - virtualImageColor = Colors.Black; + virtualImageColor = SKColors.Black; } } else if (actions.ChooseImageColorFromTextValue.HasValue @@ -123,24 +117,24 @@ await Task.Run(() => { backgroundColor = $"#{backgroundColor}"; } - if (Color.TryParse(backgroundColor, out var newColor)) + if (SKColor.TryParse(backgroundColor, out var newColor)) { virtualImageColor = newColor; } else { - virtualImageColor = Colors.Black; + virtualImageColor = SKColors.Black; } } else { - virtualImageColor = Colors.Black; + virtualImageColor = SKColors.Black; } var newBitmap = new SKBitmap(100, 100); using var canvas = new SKCanvas(newBitmap); - canvas.Clear(virtualImageColor.AsSKColor()); - using var newImage = new SkiaImage(newBitmap); + canvas.Clear(virtualImageColor); + using var newImage = SKImage.FromBitmap(newBitmap); encodedImage = EncodeSkiaImage(newImage, actions); } else @@ -148,7 +142,7 @@ await Task.Run(() => using var stream = new MemoryStream(imageData); using var codec = SKCodec.Create(stream); using var originalBitmap = SKBitmap.Decode(codec); - using var newImage = new SkiaImage(originalBitmap); + using var newImage = SKImage.FromBitmap(originalBitmap); encodedImage = EncodeSkiaImage(newImage, actions, codec); } @@ -162,7 +156,7 @@ await Task.Run(() => return encodedImage.ToArray(); } - public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActions, SKCodec codec = null) + public static SKData EncodeSkiaImage(SKImage newImage, ImageActions imageActions, SKCodec codec = null) { //make sure image was loaded successfully if (newImage == null) @@ -215,8 +209,10 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio } // Create a new bitmap with the new dimensions - var skBmp = new SkiaBitmapExportContext(imageActions.Width.Value, imageActions.Height.Value, 1.0f); - var canvas = skBmp.Canvas; + var skBmp = new SKBitmap(imageActions.Width.Value, imageActions.Height.Value); + var recorder = new SKPictureRecorder(); + var rect = GetSKRectByWidthAndHeight(0, 0, imageActions.Width.Value, imageActions.Height.Value); + var canvas = recorder.BeginRecording(rect); //if no shape specified, but a corner radius is then set shape to rounded rectangle if (imageActions.Shape.HasValue == false && imageActions.CornerRadius.HasValue) @@ -239,30 +235,33 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio var centerX = imageActions.Width.Value / 2; var centerY = imageActions.Height.Value / 2; - var a = new PathF(); - a.AppendCircle(centerX, centerY, radius); - canvas.ClipPath(a); + var a = new SKPath(); + a.AddCircle(centerX, centerY, radius); + canvas.ClipPath(a, antialias:true); } else if (imageActions.Shape == ImageShape.Ellipse) { - var a = new PathF(); + var a = new SKPath(); var centerX = imageActions.Width.Value / 2; var centerY = imageActions.Height.Value / 2; - a.AppendEllipse(0, 0, imageActions.Width.Value, imageActions.Height.Value); - canvas.ClipPath(a); + var r = GetSKRectByWidthAndHeight(0, 0, imageActions.Width.Value, imageActions.Height.Value); + a.AddOval(r); + canvas.ClipPath(a, antialias:true); } else if (imageActions.Shape == ImageShape.RoundedRectangle) { - var a = new PathF(); + var a = new SKPath(); + var r = GetSKRectByWidthAndHeight(0, 0, imageActions.Width.Value, imageActions.Height.Value); if (imageActions.CornerRadius.HasValue) { - a.AppendRoundedRectangle(0, 0, imageActions.Width.Value, imageActions.Height.Value, imageActions.CornerRadius.Value); + + a.AddRoundRect(r, imageActions.CornerRadius.Value, imageActions.CornerRadius.Value); } else { - a.AppendRoundedRectangle(0, 0, imageActions.Width.Value, imageActions.Height.Value, CornerRadius); + a.AddRoundRect(r, CornerRadius, CornerRadius); } - canvas.ClipPath(a); + canvas.ClipPath(a, antialias:true); } } @@ -279,33 +278,33 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio break; case SKEncodedOrigin.TopRight: - canvas.Rotate(180, imageActions.Width.Value / 2, imageActions.Height.Value / 2); + canvas.RotateDegrees(180, imageActions.Width.Value / 2, imageActions.Height.Value / 2); break; case SKEncodedOrigin.BottomRight: - canvas.Rotate(180, imageActions.Width.Value / 2, imageActions.Height.Value / 2); + canvas.RotateDegrees(180, imageActions.Width.Value / 2, imageActions.Height.Value / 2); break; case SKEncodedOrigin.BottomLeft: break; case SKEncodedOrigin.LeftTop: - canvas.Rotate(90, imageActions.Width.Value / 2, imageActions.Height.Value / 2); + canvas.RotateDegrees(90, imageActions.Width.Value / 2, imageActions.Height.Value / 2); isOddRotation = true; break; case SKEncodedOrigin.RightTop: - canvas.Rotate(90, imageActions.Width.Value / 2, imageActions.Height.Value / 2); + canvas.RotateDegrees(90, imageActions.Width.Value / 2, imageActions.Height.Value / 2); isOddRotation = true; break; case SKEncodedOrigin.RightBottom: - canvas.Rotate(270, imageActions.Width.Value / 2, imageActions.Height.Value / 2); + canvas.RotateDegrees(270, imageActions.Width.Value / 2, imageActions.Height.Value / 2); isOddRotation = true; break; case SKEncodedOrigin.LeftBottom: - canvas.Rotate(270, imageActions.Width.Value / 2, imageActions.Height.Value / 2); + canvas.RotateDegrees(270, imageActions.Width.Value / 2, imageActions.Height.Value / 2); isOddRotation = true; break; } @@ -320,6 +319,11 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio rotationOffsetX = rotationOffsetY * -1; } + var imagePaint = new SKPaint + { + FilterQuality = SKFilterQuality.High + }; + //write to the canvas switch (imageActions.Mode) { @@ -330,11 +334,13 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio case ImageMode.Max: if (isOddRotation) { - canvas.DrawImage(newImage, rotationOffsetX, rotationOffsetY, imageActions.Height.Value, imageActions.Width.Value); + var drawRect = GetSKRectByWidthAndHeight(rotationOffsetX, rotationOffsetY, imageActions.Height.Value, imageActions.Width.Value); + canvas.DrawImage(newImage, drawRect, paint:imagePaint); } else { - canvas.DrawImage(newImage, 0, 0, imageActions.Width.Value, imageActions.Height.Value); + var drawRect = GetSKRectByWidthAndHeight(0, 0, imageActions.Width.Value, imageActions.Height.Value); + canvas.DrawImage(newImage, drawRect, paint: imagePaint); } break; @@ -347,9 +353,11 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio } var fitScaledWidth = (int)(newImage.Width * fitScale); var fitScaledHeight = (int)(newImage.Height * fitScale); - var fitOffsetX = (imageActions.Width.Value - fitScaledWidth) / 2; - var fitOffsetY = (imageActions.Height.Value - fitScaledHeight) / 2; - canvas.DrawImage(newImage, fitOffsetX, fitOffsetY, fitScaledWidth, fitScaledHeight); + var fitOffsetX = (imageActions.Width.Value - fitScaledWidth) / 2f; + var fitOffsetY = (imageActions.Height.Value - fitScaledHeight) / 2f; + var drawRect2 = GetSKRectByWidthAndHeight(fitOffsetX, fitOffsetY, fitScaledWidth, fitScaledHeight); + + canvas.DrawImage(newImage, drawRect2); break; //zoom in and fill canvas while maintaing aspect ratio @@ -363,7 +371,8 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio var scaledHeight = (int)(newImage.Height * scale); var offsetX = (imageActions.Width.Value - scaledWidth) / 2; var offsetY = (imageActions.Height.Value - scaledHeight) / 2; - canvas.DrawImage(newImage, offsetX, offsetY, scaledWidth, scaledHeight); + var drawRect3 = GetSKRectByWidthAndHeight(offsetX, offsetY, scaledWidth, scaledHeight); + canvas.DrawImage(newImage, drawRect3, paint: imagePaint); break; } @@ -377,55 +386,58 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio textToPrint = GetInitials(imageActions.Text); } - var myFont = new Font("Arial", weight: 800); + var myTypeface = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Black, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); var myFontSize = (int)(imageActions.Height.Value * 0.85); - canvas.Font = myFont; + + // Set up paint for text + using var paint = new SKPaint + { + Typeface = myTypeface, + IsAntialias = true, + TextSize = myFontSize, + Color = SKColors.Black, // Default text color + TextAlign = SKTextAlign.Center + }; //calculate string size where height is image height to get scale of text - var textSize = canvas.GetStringSize(textToPrint, myFont, myFontSize); + var textSize = paint.MeasureText(textToPrint); //specify the max width that is wanted var maxWidth = imageActions.Width.Value * 0.75; // it needs to fit in the image, so if it is too narrow then we need to shrink down the font - if (textSize.Width > maxWidth) + if (textSize > maxWidth) { - myFontSize = (int)((maxWidth / textSize.Width) * myFontSize); + myFontSize = (int)((maxWidth / textSize) * myFontSize); } - //calculate the text size again with the new font size - var point = new Point( - x: (skBmp.Width - textSize.Width) / 2, - y: (skBmp.Height - textSize.Height) / 2); - var myTextRectangle = new Rect(point, textSize); - canvas.FontSize = myFontSize; + // Calculate maximum font size to fit text within the image + paint.TextSize = myFontSize; //set the text color if (!string.IsNullOrEmpty(imageActions.TextColor)) { - //try regular - if (Color.TryParse(imageActions.TextColor, out var newColor)) - { - canvas.FontColor = newColor; - } //try hex - else if (Color.TryParse($"#{imageActions.TextColor}", out var newColorFromHex)) + if (SKColor.TryParse($"#{imageActions.TextColor}", out var newColorFromHex)) { - canvas.FontColor = newColorFromHex; + paint.Color = newColorFromHex; } //fall back to white if they both fail else { - canvas.FontColor = Colors.White; + paint.Color = SKColors.White; } } else { - canvas.FontColor = Colors.White; + paint.Color = SKColors.White; } - canvas.DrawString(textToPrint, myTextRectangle, HorizontalAlignment.Center, VerticalAlignment.Center, TextFlow.OverflowBounds); + // Calculate text position + var x = imageActions.Width.Value / 2f; + var y = (imageActions.Height.Value / 2f) - ((paint.FontMetrics.Ascent + paint.FontMetrics.Descent) / 2); + canvas.DrawText(textToPrint, x, y, paint); } //set export format @@ -444,24 +456,32 @@ public static SKData EncodeSkiaImage(SkiaImage newImage, ImageActions imageActio //set encoding quality SKData encodedImage = null; + var picture = recorder.EndRecording(); + var ouputSize = new SKSizeI(imageActions.Width.Value, imageActions.Height.Value); + var outputImage = SKImage.FromPicture(picture, ouputSize); switch (exportImageType) { default: - encodedImage = skBmp.SKImage.Encode(exportImageType, 100); + encodedImage = outputImage.Encode(exportImageType, 100); break; case SKEncodedImageFormat.Jpeg: - encodedImage = skBmp.SKImage.Encode(SKEncodedImageFormat.Jpeg, JpegQuality); + encodedImage = outputImage.Encode(SKEncodedImageFormat.Jpeg, JpegQuality); break; case SKEncodedImageFormat.Gif: - encodedImage = skBmp.SKImage.Encode(SKEncodedImageFormat.Gif, GifQuality); + encodedImage = outputImage.Encode(SKEncodedImageFormat.Gif, GifQuality); break; } return encodedImage; } + public static SKRect GetSKRectByWidthAndHeight(float left, float top, float width, float height) + { + return new SKRect(left, top, width + left, top + height); + } + public static UInt64 CalculateHash(string read) { UInt64 hashedValue = 3074457345618258791ul; diff --git a/CommonImageActions.Pdf/PdfProcessor.cs b/CommonImageActions.Pdf/PdfProcessor.cs index e5acad4..6788490 100644 --- a/CommonImageActions.Pdf/PdfProcessor.cs +++ b/CommonImageActions.Pdf/PdfProcessor.cs @@ -151,7 +151,7 @@ await Task.Run(() => //convert into skia format using var originalBitmap = SKBitmap.Decode(bmpData); - using var newImage = new SkiaImage(originalBitmap); + using var newImage = SKImage.FromBitmap(originalBitmap); //process skia image into encoded image returnValue = ImageProcessor.EncodeSkiaImage(newImage, actions).ToArray(); diff --git a/CommonImageActions.SampleAspnetCoreProject/wwwroot/index.html b/CommonImageActions.SampleAspnetCoreProject/wwwroot/index.html index b9b729b..62434a5 100644 --- a/CommonImageActions.SampleAspnetCoreProject/wwwroot/index.html +++ b/CommonImageActions.SampleAspnetCoreProject/wwwroot/index.html @@ -27,7 +27,7 @@
Stretch (Default)

Max
- /test/thumbsUp.jpg?s=circle&w=100&h=100&m=zoom&f=png + /test/thumbsUp.jpg?s=circle&w=100&h=100&m=Max&f=png @@ -37,7 +37,7 @@
Max

Fit
- /test/thumbsUp.jpg?s=ellipse&w=100&h=100&m=zoom&f=png + /test/thumbsUp.jpg?s=ellipse&w=100&h=100&m=Fit&f=png @@ -132,16 +132,6 @@
Initials
-
-
-

- -

-
Color Named
- /test/thumbsUp.jpg?w=100&h=100&f=png&t=DustinG&in=true&tc=blue -
-
-

@@ -155,10 +145,10 @@

Color Hex

- +

-
Background Color (Manual)
- /test/thumbsUp.jpg?w=100&h=100&f=png&t=DustinG&in=true&tc=00ff00&c=red +
Background Color (Manual Hex)
+ /test/thumbsUp.jpg?w=100&h=100&f=png&t=DustinG&in=true&tc=00ff00&c=0000ff
diff --git a/README.md b/README.md index 107ac49..c2803ce 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,8 @@ app.UseCommonImageActions( | corner, cr | Integer | The corner radius when shape is RoundedRectangle. Default is 10. | | text, t | String | The text to display on the image | | initials, in | Boolean | When true will only display initials of text. For example DustinG is displayed as DG. | -| color, c | String (ffccff or blue) | Set a color for the image | -| textColor, tc | String (ffccff or blue) | Set the color of the text | +| color, c | String ffccff | Set a color for the image | +| textColor, tc | String ffccff | Set the color of the text | | colorFromText, ft | Boolean | When true a color will be generated based on a hash of the text. The list of colors can be updated in `ImageProcessor.BackgroundColours`. | | format, f | Bmp, Gif, Ico, Jpeg, Png, Wbmp, Webp, Pkm, Ktx, Astc, Dng, Heif, Avif | What format to export the resulting image as. Default is png. | | password, pw | String | (pdf only) password to open pdf |