mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Improves support for indexed colorspace images. Also adds rudimentary unit tests of PngFromPdfImageFactory.
This commit is contained in:
BIN
src/UglyToad.PdfPig.Tests/Images/Files/3x3.png
Normal file
BIN
src/UglyToad.PdfPig.Tests/Images/Files/3x3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 B |
155
src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs
Normal file
155
src/UglyToad.PdfPig.Tests/Images/PngFromPdfImageFactoryTests.cs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace UglyToad.PdfPig.Tests.Integration
|
namespace UglyToad.PdfPig.Tests.Integration
|
||||||
{
|
{
|
||||||
using Content;
|
using Content;
|
||||||
using Images.Png;
|
using UglyToad.PdfPig.Images.Png;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
public class SwedishTouringCarChampionshipTests
|
public class SwedishTouringCarChampionshipTests
|
||||||
|
|||||||
46
src/UglyToad.PdfPig.Tests/TestPdfImage.cs
Normal file
46
src/UglyToad.PdfPig.Tests/TestPdfImage.cs
Normal file
@@ -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<byte> RawBytes { get; }
|
||||||
|
|
||||||
|
public RenderingIntent RenderingIntent { get; set; } = RenderingIntent.RelativeColorimetric;
|
||||||
|
|
||||||
|
public bool IsImageMask { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<decimal> Decode { get; set; }
|
||||||
|
|
||||||
|
public bool Interpolate { get; set; }
|
||||||
|
|
||||||
|
public bool IsInlineImage { get; set; }
|
||||||
|
|
||||||
|
public ColorSpaceDetails ColorSpaceDetails { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<byte> DecodedBytes { get; set; }
|
||||||
|
|
||||||
|
public bool TryGetBytes(out IReadOnlyList<byte> bytes)
|
||||||
|
{
|
||||||
|
bytes = DecodedBytes;
|
||||||
|
return bytes != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -133,4 +133,8 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Images\" />
|
||||||
|
<Folder Include="Images\Files\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
/// change the data but for <see cref="ColorSpace.Indexed"/> it will convert the bytes which are indexes into the
|
/// 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.
|
/// real pixel data into the real pixel data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static byte[] Convert(ColorSpaceDetails details, IReadOnlyList<byte> decoded)
|
public static byte[] Convert(ColorSpaceDetails details, IReadOnlyList<byte> decoded, int bitsPerComponent, int imageWidth, int imageHeight)
|
||||||
{
|
{
|
||||||
if (decoded == null)
|
if (decoded == null)
|
||||||
{
|
{
|
||||||
@@ -33,12 +33,53 @@
|
|||||||
switch (details)
|
switch (details)
|
||||||
{
|
{
|
||||||
case IndexedColorSpaceDetails indexed:
|
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 UnwrapIndexedColorSpaceBytes(indexed, decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
return decoded.ToArray();
|
return decoded.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] UnpackIndices(IReadOnlyList<byte> input, int bitsPerComponent)
|
||||||
|
{
|
||||||
|
IEnumerable<byte> 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<byte> input)
|
private static byte[] UnwrapIndexedColorSpaceBytes(IndexedColorSpaceDetails indexed, IReadOnlyList<byte> input)
|
||||||
{
|
{
|
||||||
var multiplier = 1;
|
var multiplier = 1;
|
||||||
|
|||||||
@@ -22,16 +22,17 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
bytesPure = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails, bytesPure,
|
||||||
|
image.BitsPerComponent, image.WidthInSamples, image.HeightInSamples);
|
||||||
|
|
||||||
var numberOfComponents = actualColorSpace == ColorSpace.DeviceCMYK ? 4 : actualColorSpace == ColorSpace.DeviceRGB ? 3 : 1;
|
var numberOfComponents = actualColorSpace == ColorSpace.DeviceCMYK ? 4 : actualColorSpace == ColorSpace.DeviceRGB ? 3 : 1;
|
||||||
var is3Byte = numberOfComponents == 3;
|
var is3Byte = numberOfComponents == 3;
|
||||||
|
|
||||||
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false);
|
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)
|
if (!isCorrectlySized)
|
||||||
{
|
{
|
||||||
@@ -75,7 +76,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
bytes = builder.Save();
|
bytes = builder.Save();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
Reference in New Issue
Block a user