Skip to content

Commit

Permalink
Handle unpacking components for 16 bits per component images and use …
Browse files Browse the repository at this point in the history
…Span in RemoveStridePadding
  • Loading branch information
BobLd committed Sep 29, 2024
1 parent 5c168f9 commit ad78532
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 43 deletions.
19 changes: 19 additions & 0 deletions src/UglyToad.PdfPig.Tests/Integration/ColorSpaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ public void IndexedDeviceNColorSpaceImages()
}
}

[Fact]
public void BitsPerComponents16()
{
var path = IntegrationHelpers.GetDocumentPath("MOZILLA-3136-0.pdf");

using (var document = PdfDocument.Open(path))
{
var page1 = document.GetPage(3);
var images1 = page1.GetImages().ToArray();

var image9 = images1[9];

Assert.Equal(16 , image9.BitsPerComponent);

Assert.True(image9.TryGetPng(out byte[] bytes_3_9));
File.WriteAllBytes(Path.Combine(OutputFolder, "MOZILLA-3136-0_3_9_16bits.png"), bytes_3_9);
}
}

[Fact]
public void DeviceNColorSpaceImages()
{
Expand Down
104 changes: 61 additions & 43 deletions src/UglyToad.PdfPig/Images/ColorSpaceDetailsByteConverter.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,73 @@
namespace UglyToad.PdfPig.Images
{
using Content;
namespace UglyToad.PdfPig.Images
{
using Content;
using Graphics.Colors;
using System;
using System.Linq;

/// <summary>
/// Utility for working with the bytes in <see cref="IPdfImage"/>s and converting according to their <see cref="ColorSpaceDetails"/>.s
/// </summary>
public static class ColorSpaceDetailsByteConverter
{
/// <summary>
/// Converts the output bytes (if available) of <see cref="IPdfImage.TryGetBytesAsMemory"/>
/// to actual pixel values using the <see cref="IPdfImage.ColorSpaceDetails"/>. For most images this doesn't
/// change the data but for <see cref="ColorSpace.Indexed"/> it will convert the bytes which are indexes into the
/// real pixel data into the real pixel data.
/// </summary>
public static ReadOnlySpan<byte> Convert(ColorSpaceDetails details, ReadOnlySpan<byte> decoded, int bitsPerComponent, int imageWidth, int imageHeight)
{
if (decoded.IsEmpty)
{
return [];
}

if (details is null)
{
return decoded;
using System;

/// <summary>
/// Utility for working with the bytes in <see cref="IPdfImage"/>s and converting according to their <see cref="ColorSpaceDetails"/>.s
/// </summary>
public static class ColorSpaceDetailsByteConverter
{
/// <summary>
/// Converts the output bytes (if available) of <see cref="IPdfImage.TryGetBytesAsMemory"/>
/// to actual pixel values using the <see cref="IPdfImage.ColorSpaceDetails"/>. For most images this doesn't
/// change the data but for <see cref="ColorSpace.Indexed"/> it will convert the bytes which are indexes into the
/// real pixel data into the real pixel data.
/// </summary>
public static ReadOnlySpan<byte> Convert(ColorSpaceDetails details, ReadOnlySpan<byte> decoded, int bitsPerComponent, int imageWidth, int imageHeight)
{
if (decoded.IsEmpty)
{
return [];
}

if (details is null)
{
return decoded;
}

// TODO - We should aim at removing this alloc.
// The decoded input variable needs to become a Span<byte>
Span<byte> data = decoded.ToArray();

if (bitsPerComponent != 8)
{
// Unpack components such that they occupy one byte each
decoded = UnpackComponents(decoded, bitsPerComponent);
data = UnpackComponents(data, bitsPerComponent);
}

// Remove padding bytes when the stride width differs from the image width
var bytesPerPixel = details.NumberOfColorComponents;
var strideWidth = decoded.Length / imageHeight / bytesPerPixel;
var strideWidth = data.Length / imageHeight / bytesPerPixel;
if (strideWidth != imageWidth)
{
decoded = RemoveStridePadding(decoded.ToArray(), strideWidth, imageWidth, imageHeight, bytesPerPixel);
data = RemoveStridePadding(data, strideWidth, imageWidth, imageHeight, bytesPerPixel);
}

decoded = details.Transform(decoded);

return decoded;
return details.Transform(data);
}

private static byte[] UnpackComponents(ReadOnlySpan<byte> input, int bitsPerComponent)
private static Span<byte> UnpackComponents(Span<byte> input, int bitsPerComponent)
{
if (bitsPerComponent == 16) // Example with MOZILLA-3136-0.pdf (page 3)
{
int size = input.Length / 2;
var unpacked16 = input.Slice(0, size); // In place

for (int b = 0; b < size; ++b)
{
int i = 2 * b;
// Convert to UInt16 and divide by 256
unpacked16[b] = (byte)((ushort)(input[i + 1] | input[i] << 8) / 256);
}

return unpacked16;
}

int end = 8 - bitsPerComponent;
var unpacked = new byte[input.Length * (int)Math.Ceiling((end + 1) / (double)bitsPerComponent)];

Span<byte> unpacked = new byte[input.Length * (int)Math.Ceiling((end + 1) / (double)bitsPerComponent)];

int right = (int)Math.Pow(2, bitsPerComponent) - 1;

Expand All @@ -65,19 +82,20 @@ private static byte[] UnpackComponents(ReadOnlySpan<byte> input, int bitsPerComp
}

return unpacked;
}

private static byte[] RemoveStridePadding(byte[] input, int strideWidth, int imageWidth, int imageHeight, int multiplier)
}


private static Span<byte> RemoveStridePadding(Span<byte> input, int strideWidth, int imageWidth, int imageHeight, int multiplier)
{
var result = new byte[imageWidth * imageHeight * multiplier];
Span<byte> result = new byte[imageWidth * imageHeight * multiplier];
for (int y = 0; y < imageHeight; y++)
{
int sourceIndex = y * strideWidth;
int targetIndex = y * imageWidth;
Array.Copy(input, sourceIndex, result, targetIndex, imageWidth);
input.Slice(sourceIndex, imageWidth).CopyTo(result.Slice(targetIndex, imageWidth));
}

return result;
}
}
}
}
}
}

0 comments on commit ad78532

Please sign in to comment.