Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,4 @@ FodyWeavers.xsd
/CommonImageActions/wwwroot/cache
/CommonImageActions/wwwroot/test/thumbsUp.jpg
/CommonImageActions.SampleAspnetCoreProject/wwwroot/cache
/CommonImageActions.SampleAspnetCoreProject/wwwroot/test/ProjectsIcon.png
16 changes: 0 additions & 16 deletions CommonImageActions.Core.Tests/ImageProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
148 changes: 84 additions & 64 deletions CommonImageActions.Core/ImageProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;
using SkiaSharp;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -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
Expand All @@ -123,32 +117,32 @@ 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
{
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);
}
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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);
}
}

Expand All @@ -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;
}
Expand All @@ -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)
{
Expand All @@ -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;

Expand All @@ -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
Expand All @@ -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;
}

Expand All @@ -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
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion CommonImageActions.Pdf/PdfProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
}

//copy stream into memory asyncronously
byte[] imageData = null;

Check warning on line 61 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.

Check warning on line 61 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
using (var ms = new MemoryStream())
{
await imageStream.CopyToAsync(ms);
Expand All @@ -70,7 +70,7 @@

internal static async Task<byte[]> ProcessHelperAsync(byte[] pdfData, ImageActions actions)
{
byte[] returnValue = null;

Check warning on line 73 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.

Check warning on line 73 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.

//make sure pdfium is initalized
if (isPdfiumInitalized == false)
Expand Down Expand Up @@ -151,7 +151,7 @@

//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();
Expand All @@ -178,7 +178,7 @@
}
});

return returnValue;

Check warning on line 181 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 181 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}

public static byte[] ConvertFromBGRA32ToBmp(byte[] managedArray, int width, int height)
Expand Down Expand Up @@ -215,7 +215,7 @@
Array.Copy(managedArray, i * rowSize, reversedData, (height - 1 - i) * rowSize, rowSize);
}

byte[] bmpData = null;

Check warning on line 218 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.

Check warning on line 218 in CommonImageActions.Pdf/PdfProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
using (var fs = new MemoryStream())
{
fs.Write(bmpFileHeader, 0, bmpFileHeader.Length);
Expand Down
Loading
Loading