/// <inheritdoc /> public override PPImage ChangeColorType(PPImage input, SKColorType type, CancellationToken token = default) { if (input == null) { throw new ArgumentNullException($"The {nameof(input)} cannot be null"); } NewColorType = type; return(new PPImage(input.Bitmap.Copy(NewColorType), input.ImagePath) { ImageName = input.ImageName }); }
/// <summary> /// Converts a value to the <see cref="PPImage"/> /// </summary> /// <param name="value">The value to be converted</param> /// <exception cref="ArgumentException">Is thrown when <paramref name="value"/> is not <see cref="Image"/> or it's source is not <see cref="BitmapSource"/></exception> /// <returns>The resulting <see cref="PPImage"/></returns> public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (value is Image img) { if (img.Source is BitmapSource bmp) { var tmp = bmp.ToSKBitmap(); var resultPPImage = new PPImage(tmp); tmp.Dispose(); return(resultPPImage); } } throw new ArgumentException($"{nameof(value)} is not Image or it's source is not BitmapSource"); }
/// <summary> /// Performs the "Cropping" Trim operation (i.e. the Crop) /// </summary> /// <param name="input">Removes transparent pixels from border of the image permamently</param> /// <param name="token">The cancellation token</param> /// <remarks>The cropped pixels are reflected in the metadata</remarks> /// <returns>Cropped texture, unmodified <paramref name="input"/> if the cancellation was requested</returns> public override PPImage Trim(PPImage input, CancellationToken token = default) { if (input == null) { throw new ArgumentNullException(nameof(input)); } var result = trimmer.Trim(input, token); result.OriginalWidth = result.Bitmap.Width; result.OriginalHeight = result.Bitmap.Height; result.OffsetX = result.OffsetY = 0; //does not keep position return(result); }
/// <summary> /// Saves an image at a given path using a format given by <paramref name="format"/> /// </summary> /// <param name="image">The image to be saved</param> /// <param name="path">The path where the image should be saved</param> /// <param name="format">Format of the image</param> /// <param name="overwrite">Determines whether the file should be overriden if it already exists</param> /// <returns></returns> public static bool SaveBitmap(PPImage image, string path, SKEncodedImageFormat format, bool overwrite = true) { if (!overwrite && File.Exists(path)) { return(false); } using (var img = SKImage.FromBitmap(image.Bitmap)) using (var sw = File.OpenWrite(path)) using (var data = img.Encode(format, 100)) { data.SaveTo(sw); } return(true); }
/// <summary> /// Returns a neighborhood pixels of the pixel at a given index (index in the flat 1D instead of 2D array) within the given image /// </summary> /// <remarks>The neighborhood is "Moore neighborhood"</remarks> /// <param name="index">The index of the pixel for which the neighborhood should be returned</param> /// <param name="image">The image containing the pixel at the given index</param> /// <returns>The indices of the neighborhood pixels</returns> private static IEnumerable <int> GetNeighborhood(int index, PPImage image) { yield return(index - 1); yield return(index + 1); yield return(index - image.Bitmap.Width); yield return(index + image.Bitmap.Width); yield return(index - image.Bitmap.Width - 1); yield return(index - image.Bitmap.Width + 1); yield return(index + image.Bitmap.Width - 1); yield return(index + image.Bitmap.Width + 1); }
/// <inheritdoc /> public override PPImage RemoveBackground(PPImage input, CancellationToken token = default) { if (input == null) { throw new ArgumentNullException($"The {nameof(input)} cannot be null"); } var component = SelectBackground(input); //Cancellation was requested during the SelectBackground method if (component == null) { return(input); } #pragma warning disable CA2000 // The resulting PPImage is the owner of the bitmap SKBitmap bmp2 = new SKBitmap(input.Bitmap.Width, input.Bitmap.Height, input.Bitmap.ColorType, SKAlphaType.Premul) { Pixels = input.Bitmap.Pixels }; #pragma warning restore CA2000 // So it should not get disposed there bmp2.Erase(SKColors.Transparent); foreach (var pix in component) { if (token.IsCancellationRequested) { return(input); } bmp2.SetPixel(pix % input.Bitmap.Width, pix / input.Bitmap.Width, SKColors.Transparent); } return(new PPImage(bmp2, input.ImagePath) { ImageName = input.ImageName }); }
/// <summary> /// Returns an image of the texture atlas /// </summary> /// <returns>The image of the texture atlas</returns> private PPImage GetImage() { PPImage image = new PPImage(Bitmap); return(image); }
/// <inheritdoc /> public override PPImage Trim(PPImage input, CancellationToken token = default) { if (input == null) { throw new ArgumentNullException($"The {nameof(input)} cannot be null"); } int origWidth = input.Bitmap.Width; int origHeight = input.Bitmap.Height; int left = 0, right = input.Bitmap.Width, top = input.Bitmap.Height, bottom = 0; var pixels = input.Bitmap.Pixels; bool topTransparent = true, bottomTransparent = true, leftTransparent = true, rightTransparent = true; while (true) { if (token.IsCancellationRequested) { return(input); } //process top and bottom lines (horizontal) for (int i = left; i < right; i++) { if (pixels[bottom * input.Bitmap.Width + i].Alpha > alphaTolerance) { bottomTransparent = false; } if (pixels[(top - 1) * input.Bitmap.Width + i].Alpha > alphaTolerance) { topTransparent = false; } } if (bottomTransparent) { bottom++; } if (topTransparent) { top--; } //process vertical lines for (int i = bottom; i < top; i++) { if (pixels[i * input.Bitmap.Width + left].Alpha > alphaTolerance) { leftTransparent = false; } if (pixels[i * input.Bitmap.Width + (right - 1)].Alpha > alphaTolerance) { rightTransparent = false; } } if (leftTransparent) { left++; } if (rightTransparent) { right--; } if (!(bottomTransparent || topTransparent || leftTransparent || rightTransparent)) { break; } if (left >= (input.Bitmap.Width - 1) && right <= 0 && top <= 0 && bottom >= (input.Bitmap.Height - 1)) { return(new PPImage()); } } SKBitmap result = new SKBitmap(); input.Bitmap.ExtractSubset(result, new SKRectI(left, bottom, right, top)); var resultImage = new PPImage(result, input.ImagePath) { ImageName = input.ImageName, OriginalWidth = origWidth, OriginalHeight = origHeight, OffsetX = left, OffsetY = bottom //actually top in "graphic field jargon" (i.e. the lower coordinates) }; return(resultImage); }
/// <inheritdoc /> public PPImage ProcessImage(PPImage input, CancellationToken token = default) { return(ChangeColorType(input, NewColorType, token)); }
/// <summary> /// Performs the ColorType change operation /// </summary> /// <param name="input">Input image</param> /// <param name="targetColorType">The target color type</param> /// <param name="token">The cancellation token</param> /// <remarks>The input image is copied, processed and the processed version is then returned</remarks> /// <exception cref="ArgumentNullException">Is thrown when the <paramref name="input"/> is null</exception> /// <returns>An image with a color type changed to the target color type, unmodified <paramref name="input"/> when the cancellation was requested</returns> public abstract PPImage ChangeColorType(PPImage input, SKColorType targetColorType, CancellationToken token = default);
/// <inheritdoc /> public override PPImage ChangeColorType(PPImage input, CancellationToken token = default) { return(ChangeColorType(input, NewColorType, token)); }
/// <summary> /// Performs the remove background operation /// </summary> /// <param name="input">Input image</param> /// <param name="token">The cancellation token</param> /// <remarks>The input image is copied, processed and the processed version is then returned</remarks> /// <exception cref="ArgumentNullException">Is thrown when the <paramref name="input"/> is null</exception> /// <returns>An image with background removed, unmodified <paramref name="input"/> when cancellation was requested</returns> public abstract PPImage RemoveBackground(PPImage input, CancellationToken token = default);
/// <inheritdoc /> public PPImage ProcessImage(PPImage input, CancellationToken token = default) { return(RemoveBackground(input, token)); }
/// <summary> /// Performs the Extrude operation /// </summary> /// <param name="input">Input image</param> /// <param name="token">The cancellation token</param> /// <remarks>The input image is copied, processed and the processed version is then returned</remarks> /// <returns>An image extruded by a <see cref="Amount"/> of pixels</returns> public PPImage Extrude(PPImage input, CancellationToken token = default) { return(Extrude(input, Amount, token)); }
/// <inheritdoc /> public PPImage ProcessImage(PPImage input, CancellationToken token = default) { return(Trim(input, token)); }
/// <summary> /// Adds a padding, i.e. <see cref="Amount"/> of transparent pixels to each side of the <paramref name="input"/> image's border /// </summary> /// <param name="input">The input image</param> /// <param name="token">The cancellation token</param> /// <returns>The copy of the input texture with added padding or unmodified input texture if the cancellation was requested</returns> public PPImage AddPadding(PPImage input, CancellationToken token = default) { return(AddPadding(input, Amount, token)); }
/// <summary> /// Adds a padding, i.e. <paramref name="amount"/> of transparent pixels to each side of the <paramref name="input"/> image's border /// </summary> /// <param name="input">The input image</param> /// <param name="amount">Amount of transparent pixels to add</param> /// <param name="token">The cancellation token</param> /// <remarks>Changes the <see cref="Amount"/> to <paramref name="amount"/></remarks> /// <returns>The copy of the input texture with added padding or unmodified input texture if the cancellation was requested</returns> public PPImage AddPadding(PPImage input, int amount, CancellationToken token = default) { if (amount < 0) { throw new ArgumentOutOfRangeException($"The {nameof(amount)} has to be non-negative integer"); } if (input == null) { throw new ArgumentNullException($"The {nameof(input)} cannot be null"); } //Do not allow to go beyond this size (because the use of Int32.MaxValue >> 1 in the packing algorithms //And because width > height must fit into Int32.MaxValue var area = (long)(input.Bitmap.Width + amount) * (input.Bitmap.Height + amount); if (area > int.MaxValue) { return(input); } Amount = amount; #pragma warning disable CA2000 // The resulting PPImage is the owner of the bitmap SKBitmap result = new SKBitmap(input.Bitmap.Width + 2 * amount, input.Bitmap.Height + 2 * amount); #pragma warning restore CA2000 // So it should not get disposed there result.Erase(SKColors.Transparent); var srcPixels = input.Bitmap.Pixels; for (int i = 0; i < input.Bitmap.Height; i++) { for (int j = 0; j < input.Bitmap.Width; j++) { result.SetPixel(j + amount, i + amount, srcPixels[i * input.Bitmap.Width + j]); //copy } //Do not ask too often, that is the reason why the check is after the inner loop if (token.IsCancellationRequested) { return(input); } } //for (int i = 0; i < Amount; i++) //{ // if (token.IsCancellationRequested) // { // return input; // } // for (int x = 0; x < input.Bitmap.Width; x++) // { // //The strip above // result.SetPixel(x, i, SKColors.Transparent); // //The strip below // result.SetPixel(x, input.Bitmap.Height - 1 + i, SKColors.Transparent); // } // if (token.IsCancellationRequested) // { // return input; // } // for (int y = 0; y < input.Bitmap.Height; y++) // { // //The strip to the left // result.SetPixel(i, y, SKColors.Transparent); // //The strip to the right // result.SetPixel(input.Bitmap.Width - 1 + i, y, SKColors.Transparent); // } //} var resImage = new PPImage(result, input.ImagePath) { ImageName = input.ImageName, }; resImage.NoWhiteSpaceXOffset += Amount; resImage.NoWhiteSpaceYOffset += Amount; resImage.FinalWidth -= 2 * Amount; resImage.FinalHeight -= 2 * Amount; return(resImage); }
/// <summary> /// Performs the Trim operation /// </summary> /// <param name="input">Input image</param> /// <param name="token">The cancellation token</param> /// <remarks>The input image is copied, processed and the processed version is then returned</remarks> /// <exception cref="ArgumentNullException">Is thrown when the <paramref name="input"/> is null</exception> /// <returns>Trimmed image, null if the cancellation was requested</returns> public abstract PPImage Trim(PPImage input, CancellationToken token = default);
/// <summary> /// Selects a guessed background of the input image /// </summary> /// <param name="input">The input image</param> /// <param name="token">The cancellation token</param> /// <remarks>Performs a BFS</remarks> /// <returns>Returns the list of pixels having the same color as the color of the guessed background, null when cancellation was requested</returns> private List <int> SelectBackground(PPImage input, CancellationToken token = default) { SKColor bgColor = SKColors.Transparent; Dictionary <SKColor, List <int> > components = new Dictionary <SKColor, List <int> >(); int[] cornerIndices = new int[] { 0, //top-left input.Bitmap.Width - 1, //top-right (input.Bitmap.Height - 1) * input.Bitmap.Width, //bottom-left input.Bitmap.Width *input.Bitmap.Height - 1 }; //bottom-right}; var pixels = input.Bitmap.Pixels; SKColor[] cornerColors = new SKColor[4] { pixels[cornerIndices[0]], pixels[cornerIndices[1]], pixels[cornerIndices[2]], pixels[cornerIndices[3]] }; bool[] visited = new bool[input.Bitmap.Width * input.Bitmap.Height]; Dictionary <SKColor, float> colorCounts = new Dictionary <SKColor, float>(); Queue <int> toVisit = new Queue <int>(); foreach (var corner in cornerIndices) { if (token.IsCancellationRequested) { return(null); } toVisit.Enqueue(corner); visited[corner] = true; } if (cornerColors.All(x => x == cornerColors[0])) { colorCounts.Add(cornerColors[0], 0.0f); components.Add(cornerColors[0], new List <int>()); int totalVisited = 0; while (toVisit.Count > 0) { if (token.IsCancellationRequested) { return(null); } var curr = toVisit.Dequeue(); totalVisited++; foreach (var neigh in GetNeighborhood(curr, input).Where(x => x != curr && x >= 0 && x < input.Bitmap.Width * input.Bitmap.Height)) { if (visited[neigh] || pixels[neigh] != cornerColors[0]) { continue; } toVisit.Enqueue(neigh); visited[neigh] = true; } if (pixels[curr] == cornerColors[0]) { components[cornerColors[0]].Add(curr); colorCounts[cornerColors[0]] += 1.0f; } } return(components[cornerColors[0]]); } else { var uniqueColors = cornerColors.Distinct(); foreach (var x in uniqueColors) { components.Add(x, new List <int>()); colorCounts.Add(x, 0.0f); } while (toVisit.Count > 0) { if (token.IsCancellationRequested) { return(null); } var curr = toVisit.Dequeue(); foreach (var neigh in GetNeighborhood(curr, input).Where(z => z != curr && z >= 0 && z < input.Bitmap.Width * input.Bitmap.Height)) { if (visited[neigh] || !cornerColors.Contains(pixels[neigh])) { continue; } toVisit.Enqueue(neigh); visited[neigh] = true; } //if (uniqueColors.Contains(input.Pixels[curr])) //{ components[pixels[curr]].Add(curr); float x = curr % input.Bitmap.Width; float y = curr / input.Bitmap.Width; float middleX = input.Bitmap.Width / 2.0f; float middleY = input.Bitmap.Height / 2.0f; colorCounts[pixels[curr]] += Max(Abs(middleX - x), Abs(middleY - y)); //colorCounts[input.Pixels[curr]] += 1.0f; //} visited[curr] = true; } bgColor = colorCounts.OrderByDescending(x => x.Value).First().Key; return(components[bgColor]); } //return bgColor; }
/// <summary> /// Performs the Trim operation with a specified amount /// </summary> /// <param name="input">Input image</param> /// <param name="amount">Amount of pixels to be added</param> /// <param name="token">The cancellation token</param> /// <remarks> /// The input image is copied, processed and the processed version is then returned /// Changes the <see cref="Amount"/> to <paramref name="amount"/> /// </remarks> /// <exception cref="ArgumentNullException">Is thrown when the <paramref name="input"/> is null</exception> /// <exception cref="ArgumentOutOfRangeException">Is thrown when the <paramref name="amount"/> is negative</exception> /// <returns>An image extruded by a <paramref name="amount"/> of pixels, unmodified <paramref name="input"/> if cancellation was requested</returns> public PPImage Extrude(PPImage input, int amount, CancellationToken token = default) { if (amount < 0) { throw new ArgumentOutOfRangeException($"The {nameof(amount)} has to be non-negative integer"); } if (input == null) { throw new ArgumentNullException($"The {nameof(input)} cannot be null"); } //Do not allow to go beyond this size (because the use of Int32.MaxValue >> 1 in the packing algorithms //And because width > height must fit into Int32.MaxValue var area = (long)(input.Bitmap.Width + amount) * (input.Bitmap.Height + amount); if (area > int.MaxValue) { return(input); } Amount = amount; #pragma warning disable CA2000 // The resulting PPImage is the owner of the bitmap SKBitmap result = new SKBitmap(input.Bitmap.Width + 2 * amount, input.Bitmap.Height + 2 * amount); #pragma warning restore CA2000 // So it should not get disposed there result.Erase(SKColors.Transparent); var srcPixels = input.Bitmap.Pixels; for (int i = 0; i < input.Bitmap.Height; i++) { for (int j = 0; j < input.Bitmap.Width; j++) { result.SetPixel(j + amount, i + amount, srcPixels[i * input.Bitmap.Width + j]); //copy } //Do not ask too often, that is the reason why the check is after the inner loop if (token.IsCancellationRequested) { return(input); } } //top & bottow rows for (int i = 0; i < input.Bitmap.Width; i++) { for (int j = 0; j < amount; j++) { result.SetPixel(i + amount, j, srcPixels[i]); result.SetPixel(i + amount, input.Bitmap.Height + j + amount, srcPixels[srcPixels.Length - 1 - ((input.Bitmap.Width - 1) - i)]); } //Do not ask too often, that is the reason why the check is after the inner loop if (token.IsCancellationRequested) { return(input); } } //left & right columns for (int i = 0; i < input.Bitmap.Height; i++) { for (int j = 0; j < amount; j++) { result.SetPixel(j, i + amount, srcPixels[i * input.Bitmap.Width]); result.SetPixel(j + input.Bitmap.Width + amount, i + amount, srcPixels[i * input.Bitmap.Width + input.Bitmap.Width - 1]); } //Do not ask too often, that is the reason why the check is after the inner loop if (token.IsCancellationRequested) { return(input); } } //diags for (int i = 0; i < amount; i++) { for (int j = 0; j < amount; j++) { result.SetPixel(i, j, srcPixels[0]); //left top result.SetPixel(i, result.Height - j - 1, srcPixels[input.Bitmap.Width * (input.Bitmap.Height - 1)]); //left bottom result.SetPixel(result.Width - i - 1, j, srcPixels[input.Bitmap.Width - 1]); //top right result.SetPixel(result.Width - i - 1, result.Height - j - 1, srcPixels[input.Bitmap.Width * input.Bitmap.Height - 1]); //right bottom } //Do not ask too often, that is the reason why the check is after the inner loop if (token.IsCancellationRequested) { return(input); } } var resImage = new PPImage(result, input.ImagePath) { ImageName = input.ImageName }; resImage.NoWhiteSpaceXOffset += Amount; resImage.NoWhiteSpaceYOffset += Amount; resImage.FinalWidth -= 2 * Amount; resImage.FinalHeight -= 2 * Amount; return(resImage); }
/// <summary> /// Constructs the ViewModel from a <paramref name="image"/> /// </summary> /// <param name="image">Image for which the ViewModel is constructed</param> public ImageViewModel(PPImage image) { Image = image; }