diff --git a/src/UglyToad.PdfPig.Tests/Images/Files/3x3.png b/src/UglyToad.PdfPig.Tests/Images/Files/3x3.png new file mode 100644 index 00000000..9f10a433 Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Images/Files/3x3.png differ diff --git a/src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs b/src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs new file mode 100644 index 00000000..9654e815 --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs @@ -0,0 +1,155 @@ +namespace UglyToad.PdfPig.Tests.Images +{ + using System; + using System.IO; + using System.Linq; + using UglyToad.PdfPig.Graphics.Colors; + using UglyToad.PdfPig.Images.Png; + using Xunit; + + public class PngFromPdfImageFactoryTests + { + private static readonly byte[] RgbBlack = new byte[] { 0, 0, 0 }; + private static readonly byte[] RgbWhite = new byte[] { 255, 255, 255 }; + private static readonly byte[][] RgbPalette = new[] { RgbBlack, RgbWhite }; + + private static readonly byte[] CmykBlack = new byte[] { 0, 0, 0, 255 }; + private static readonly byte[] CmykWhite = new byte[] { 0, 0, 0, 0 }; + + private static readonly byte GrayscaleBlack = 0; + private static readonly byte GrayscaleWhite = 255; + + [Fact] + public void CanGeneratePngFromDeviceRgbImageData() + { + var pixels = new[] + { + RgbWhite, RgbBlack, RgbWhite, + RgbBlack, RgbWhite, RgbBlack, + RgbWhite, RgbBlack, RgbWhite + }; + + var image = new TestPdfImage + { + ColorSpaceDetails = DeviceRgbColorSpaceDetails.Instance, + DecodedBytes = pixels.SelectMany(b => b).ToArray(), + WidthInSamples = 3, + HeightInSamples = 3 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.Equal(LoadImage("3x3.png"), bytes); + } + + [Fact] + public void CanGeneratePngFromDeviceCMYKImageData() + { + var pixels = new[] + { + CmykWhite, CmykBlack, CmykWhite, + CmykBlack, CmykWhite, CmykBlack, + CmykWhite, CmykBlack, CmykWhite + }; + + var image = new TestPdfImage + { + ColorSpaceDetails = DeviceCmykColorSpaceDetails.Instance, + DecodedBytes = pixels.SelectMany(b => b).ToArray(), + WidthInSamples = 3, + HeightInSamples = 3 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.Equal(LoadImage("3x3.png"), bytes); + } + + [Fact] + public void CanGeneratePngFromDeviceGrayscaleImageData() + { + var pixels = new[] + { + GrayscaleWhite, GrayscaleBlack, GrayscaleWhite, + GrayscaleBlack, GrayscaleWhite, GrayscaleBlack, + GrayscaleWhite, GrayscaleBlack, GrayscaleWhite + }; + + var image = new TestPdfImage + { + ColorSpaceDetails = DeviceGrayColorSpaceDetails.Instance, + DecodedBytes = pixels, + WidthInSamples = 3, + HeightInSamples = 3 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.Equal(LoadImage("3x3.png"), bytes); + } + + [Fact] + public void CanGeneratePngFromIndexedImageData8bpc() + { + var indices = new byte[] + { + 1, 0, 1, + 0, 1, 0, + 1, 0, 1 + }; + + var image = new TestPdfImage + { + ColorSpaceDetails = new IndexedColorSpaceDetails(DeviceRgbColorSpaceDetails.Instance, 1, RgbPalette.SelectMany(b => b).ToArray()), + DecodedBytes = indices, + WidthInSamples = 3, + HeightInSamples = 3 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.Equal(LoadImage("3x3.png"), bytes); + } + + [Fact] + public void CanGeneratePngFromIndexedImageData1bpc() + { + // Indices for a 3x3 RGB image, each index is represented by a single bit + // 1, 0, 1, + // 0, 1, 0, + // 1, 0, 1 + // + // A scanline must be at least one byte wide, so the remaining five bits are padding: + // Byte 0: 10100000 (bits #7 and #5 are 1) + // Byte 1: 01000000 (bit #6 is 1) + // Byte 2: 10100000 (bits #7 and #5 are 1) + // |||||||| + // Bit # : 76543210 + + var lines = new byte[3]; + lines[0] |= (1 << 7); // Set bit #7 to 1 + lines[0] |= (1 << 5); // Set bit #5 to 1 + + lines[1] |= (1 << 6); // Set bit #6 to 1 + + lines[2] |= (1 << 7); // Set bit #7 to 1 + lines[2] |= (1 << 5); // Set bit #5 to 1 + + var colorTable = RgbPalette.SelectMany(b => b).ToArray(); + + var image = new TestPdfImage + { + ColorSpaceDetails = new IndexedColorSpaceDetails(DeviceRgbColorSpaceDetails.Instance, 1, colorTable), + DecodedBytes = lines, + WidthInSamples = 3, + HeightInSamples = 3, + BitsPerComponent = 1 + }; + + Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes)); + Assert.Equal(LoadImage("3x3.png"), bytes); + } + + private static byte[] LoadImage(string name) + { + var folder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Images", "Files")); + return File.ReadAllBytes(Path.Combine(folder, name)); + } + } +} diff --git a/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs b/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs index 64124edf..29b98354 100644 --- a/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs +++ b/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs @@ -1,7 +1,7 @@ namespace UglyToad.PdfPig.Tests.Integration { using Content; - using Images.Png; + using UglyToad.PdfPig.Images.Png; using Xunit; public class SwedishTouringCarChampionshipTests diff --git a/src/UglyToad.PdfPig.Tests/TestPdfImage.cs b/src/UglyToad.PdfPig.Tests/TestPdfImage.cs new file mode 100644 index 00000000..b32e20fe --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/TestPdfImage.cs @@ -0,0 +1,46 @@ +namespace UglyToad.PdfPig.Tests +{ + using System.Collections.Generic; + using UglyToad.PdfPig.Content; + using UglyToad.PdfPig.Core; + using UglyToad.PdfPig.Graphics.Colors; + using UglyToad.PdfPig.Graphics.Core; + using UglyToad.PdfPig.Images.Png; + + public class TestPdfImage : IPdfImage + { + public PdfRectangle Bounds { get; set; } + + public int WidthInSamples { get; set; } + + public int HeightInSamples { get; set; } + + public ColorSpace? ColorSpace => IsImageMask ? null : ColorSpaceDetails.Type; + + public int BitsPerComponent { get; set; } = 8; + + public IReadOnlyList RawBytes { get; } + + public RenderingIntent RenderingIntent { get; set; } = RenderingIntent.RelativeColorimetric; + + public bool IsImageMask { get; set; } + + public IReadOnlyList Decode { get; set; } + + public bool Interpolate { get; set; } + + public bool IsInlineImage { get; set; } + + public ColorSpaceDetails ColorSpaceDetails { get; set; } + + public IReadOnlyList DecodedBytes { get; set; } + + public bool TryGetBytes(out IReadOnlyList bytes) + { + bytes = DecodedBytes; + return bytes != null; + } + + public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes); + } +} diff --git a/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj b/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj index 8322c54b..7d1c6f49 100644 --- a/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj +++ b/src/UglyToad.PdfPig.Tests/UglyToad.PdfPig.Tests.csproj @@ -133,4 +133,8 @@ + + + + diff --git a/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs b/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs index 098aaaef..643c5deb 100644 --- a/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs +++ b/src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs @@ -18,7 +18,7 @@ /// change the data but for it will convert the bytes which are indexes into the /// real pixel data into the real pixel data. /// - public static byte[] Convert(ColorSpaceDetails details, IReadOnlyList decoded) + public static byte[] Convert(ColorSpaceDetails details, IReadOnlyList decoded, int bitsPerComponent, int imageWidth, int imageHeight) { if (decoded == null) { @@ -32,11 +32,52 @@ switch (details) { - case IndexedColorSpaceDetails indexed: - return UnwrapIndexedColorSpaceBytes(indexed, decoded); + case IndexedColorSpaceDetails indexed: + if (bitsPerComponent != 8) + { + // To ease unwrapping further below the indices are unpacked to occupy a single byte each + decoded = UnpackIndices(decoded, bitsPerComponent); + + // Remove padding bytes when the stride width differs from the image width + var stride = (imageWidth * bitsPerComponent + 7) / 8; + var strideWidth = stride * (8 / bitsPerComponent); + if (strideWidth != imageWidth) + { + decoded = RemoveStridePadding(decoded.ToArray(), strideWidth, imageWidth, imageHeight); + } + } + + return UnwrapIndexedColorSpaceBytes(indexed, decoded); } return decoded.ToArray(); + } + + private static byte[] UnpackIndices(IReadOnlyList input, int bitsPerComponent) + { + IEnumerable Unpack(byte b) + { + // Enumerate bits in bitsPerComponent-sized chunks from MSB to LSB, masking on the appropriate bits + for (int i = 8 - bitsPerComponent; i >= 0; i -= bitsPerComponent) + { + yield return (byte)((b >> i) & ((int)Math.Pow(2, bitsPerComponent) - 1)); + } + } + + return input.SelectMany(b => Unpack(b)).ToArray(); + } + + private static byte[] RemoveStridePadding(byte[] input, int strideWidth, int imageWidth, int imageHeight) + { + var result = new byte[imageWidth * imageHeight]; + for (int y = 0; y < imageHeight; y++) + { + int sourceIndex = y * strideWidth; + int targetIndex = y * imageWidth; + Array.Copy(input, sourceIndex, result, targetIndex, imageWidth); + } + + return result; } private static byte[] UnwrapIndexedColorSpaceBytes(IndexedColorSpaceDetails indexed, IReadOnlyList input) diff --git a/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs index 222e12e5..5fe9166c 100644 --- a/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs +++ b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs @@ -1,8 +1,8 @@ namespace UglyToad.PdfPig.Images.Png { using Content; - using Graphics.Colors; - + using Graphics.Colors; + internal static class PngFromPdfImageFactory { public static bool TryGenerate(IPdfImage image, out byte[] bytes) @@ -22,16 +22,17 @@ return false; } - bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure); - try { - var numberOfComponents = actualColorSpace == ColorSpace.DeviceCMYK ? 4 : actualColorSpace == ColorSpace.DeviceRGB ? 3 : 1; + bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure, + image.BitsPerComponent, image.WidthInSamples, image.HeightInSamples); + + var numberOfComponents = actualColorSpace == ColorSpace.DeviceCMYK ? 4 : actualColorSpace == ColorSpace.DeviceRGB ? 3 : 1; var is3Byte = numberOfComponents == 3; - + var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false); - var isCorrectlySized = bytesPure.Count == (image.WidthInSamples * image.HeightInSamples * (image.BitsPerComponent / 8) * numberOfComponents); + var isCorrectlySized = bytesPure.Count == (image.WidthInSamples * image.HeightInSamples * numberOfComponents); if (!isCorrectlySized) { @@ -75,7 +76,6 @@ } bytes = builder.Save(); - return true; } catch @@ -84,6 +84,6 @@ } return false; - } + } } }