diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
index a465907cf0..9974c19153 100644
--- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
+++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Buffers.Binary;
+using System.Numerics;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@@ -34,9 +35,25 @@ internal class BinaryDecoder
/// The stream to read the data from.
/// The color type of the encoded pixels.
/// The data type of the pixel components.
- public static void Process(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, PbmColorType colorType, PbmComponentType componentType)
+ /// The maximum sample value declared in the header. Unused for black and white.
+ public static void Process(
+ Configuration configuration,
+ Buffer2D pixels,
+ BufferedReadStream stream,
+ PbmColorType colorType,
+ PbmComponentType componentType,
+ int maxPixelValue)
where TPixel : unmanaged, IPixel
{
+ // Full-range inputs keep the existing byte and ushort conversion paths.
+ // Other ranges need normalized floats until the destination pixel format performs its final packing.
+ int fullRange = componentType == PbmComponentType.Short ? ushort.MaxValue : byte.MaxValue;
+ if (colorType != PbmColorType.BlackAndWhite && maxPixelValue != fullRange)
+ {
+ ProcessScaled(configuration, pixels, stream, colorType, componentType, maxPixelValue);
+ return;
+ }
+
if (colorType == PbmColorType.Grayscale)
{
if (componentType == PbmComponentType.Byte)
@@ -65,6 +82,77 @@ public static void Process(Configuration configuration, Buffer2D
}
}
+ ///
+ /// Normalizes binary samples before the destination pixel format packs them.
+ ///
+ /// The destination pixel format.
+ /// The configuration.
+ /// The destination pixels.
+ /// The input stream at the raster.
+ /// The encoded color type.
+ /// The encoded sample size.
+ /// The declared maximum sample value.
+ private static void ProcessScaled(
+ Configuration configuration,
+ Buffer2D pixels,
+ BufferedReadStream stream,
+ PbmColorType colorType,
+ PbmComponentType componentType,
+ int maxPixelValue)
+ where TPixel : unmanaged, IPixel
+ {
+ bool isRgb = colorType == PbmColorType.Rgb;
+ bool isWide = componentType == PbmComponentType.Short;
+ int sampleSize = isWide ? sizeof(ushort) : sizeof(byte);
+ int bytesPerPixel = sampleSize * (isRgb ? 3 : 1);
+ int width = pixels.Width;
+
+ // Rent both rows once. Encoded bytes stay separate from the float row, so widening cannot overwrite unread samples.
+ using IMemoryOwner encoded = configuration.MemoryAllocator.Allocate(width * bytesPerPixel);
+ using IMemoryOwner vectors = configuration.MemoryAllocator.Allocate(width);
+ Span encodedRow = encoded.GetSpan();
+ Span vectorRow = vectors.GetSpan();
+
+ for (int y = 0; y < pixels.Height; y++)
+ {
+ int count = ReadRow(stream, encodedRow, bytesPerPixel);
+ if (count == 0)
+ {
+ return;
+ }
+
+ for (int x = 0; x < count; x++)
+ {
+ int offset = x * bytesPerPixel;
+ int red = isWide ? BinaryPrimitives.ReadUInt16BigEndian(encodedRow.Slice(offset, sampleSize)) : encodedRow[offset];
+ int green = red;
+ int blue = red;
+ if (isRgb)
+ {
+ offset += sampleSize;
+ green = isWide ? BinaryPrimitives.ReadUInt16BigEndian(encodedRow.Slice(offset, sampleSize)) : encodedRow[offset];
+ offset += sampleSize;
+ blue = isWide ? BinaryPrimitives.ReadUInt16BigEndian(encodedRow.Slice(offset, sampleSize)) : encodedRow[offset];
+ }
+
+ // RGB holds the encoded magnitudes. Alpha uses Maxval so normalization produces exactly opaque alpha.
+ vectorRow[x] = PbmSampleScaler.Scale(new Vector4(red, green, blue, maxPixelValue), maxPixelValue);
+ }
+
+ // Convert only complete pixels. A short final row must not copy unused pooled memory into the image.
+ PixelOperations.Instance.FromVector4Destructive(
+ configuration,
+ vectorRow[..count],
+ pixels.DangerousGetRowSpan(y)[..count],
+ PixelConversionModifiers.Scale);
+
+ if (count < width)
+ {
+ return;
+ }
+ }
+ }
+
///
/// Decodes 8-bit binary grayscale (PGM) pixel data.
/// Each pixel is a single byte that holds its luminance value.
@@ -86,17 +174,20 @@ private static void ProcessGrayscale(Configuration configuration, Buffer
for (int y = 0; y < height; y++)
{
- if (stream.Read(rowSpan) < rowSpan.Length)
+ int count = ReadRow(stream, rowSpan, bytesPerPixel);
+ if (count == 0)
{
return;
}
+ Span samples = rowSpan[..(count * bytesPerPixel)];
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromL8Bytes(
configuration,
- rowSpan,
- pixelSpan,
- width);
+ samples,
+ pixelSpan[..count],
+ count);
}
}
@@ -121,21 +212,24 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu
for (int y = 0; y < height; y++)
{
- if (stream.Read(rowSpan) < rowSpan.Length)
+ int count = ReadRow(stream, rowSpan, bytesPerPixel);
+ if (count == 0)
{
return;
}
+ Span samples = rowSpan[..(count * bytesPerPixel)];
+
// The binary format stores 16-bit samples most significant byte first,
// but L16 expects native (little-endian) byte order.
- SwapSampleBytes(rowSpan);
+ SwapSampleBytes(samples);
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromL16Bytes(
configuration,
- rowSpan,
- pixelSpan,
- width);
+ samples,
+ pixelSpan[..count],
+ count);
}
}
@@ -160,17 +254,20 @@ private static void ProcessRgb(Configuration configuration, Buffer2D samples = rowSpan[..(count * bytesPerPixel)];
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromRgb24Bytes(
configuration,
- rowSpan,
- pixelSpan,
- width);
+ samples,
+ pixelSpan[..count],
+ count);
}
}
@@ -195,24 +292,48 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D
for (int y = 0; y < height; y++)
{
- if (stream.Read(rowSpan) < rowSpan.Length)
+ int count = ReadRow(stream, rowSpan, bytesPerPixel);
+ if (count == 0)
{
return;
}
+ Span samples = rowSpan[..(count * bytesPerPixel)];
+
// The binary format stores 16-bit samples most significant byte first,
// but Rgb48 expects native (little-endian) byte order.
- SwapSampleBytes(rowSpan);
+ SwapSampleBytes(samples);
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromRgb48Bytes(
configuration,
- rowSpan,
- pixelSpan,
- width);
+ samples,
+ pixelSpan[..count],
+ count);
}
}
+ ///
+ /// Reads one encoded row and zero-fills only missing bytes in its last incomplete pixel.
+ ///
+ /// The input stream.
+ /// The reusable encoded row.
+ /// The encoded size of one pixel.
+ /// The number of available pixels, including an incomplete final pixel.
+ private static int ReadRow(BufferedReadStream stream, Span row, int bytesPerPixel)
+ {
+ int bytesRead = stream.Read(row);
+ int count = bytesRead / bytesPerPixel;
+ if (bytesRead % bytesPerPixel != 0)
+ {
+ // Keep the available channels and high sample bytes. Missing bytes must not come from pooled storage.
+ count++;
+ row.Slice(bytesRead, (count * bytesPerPixel) - bytesRead).Clear();
+ }
+
+ return count;
+ }
+
///
/// Reverses the byte order of each 16-bit sample in the given row when the host is little-endian.
/// The binary PGM and PPM formats store multi-byte samples most significant byte first.
diff --git a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
index 3b0e41a02d..4865e93c66 100644
--- a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
+++ b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
@@ -62,8 +62,19 @@ public static bool SkipWhitespaceAndComments(this BufferedReadStream stream)
/// It's up to the call site to handle such a situation.
///
public static bool ReadDecimal(this BufferedReadStream stream, out int value)
+ => stream.ReadDecimal(out value, out _);
+
+ ///
+ /// Reads a decimal value and reports values above the supported integer range without wrapping.
+ ///
+ /// The input stream.
+ /// The parsed value, saturated at .
+ /// True if the decimal value exceeds .
+ /// False if EOF was reached. The final digits are retained in .
+ public static bool ReadDecimal(this BufferedReadStream stream, out int value, out bool overflow)
{
value = 0;
+ overflow = false;
while (true)
{
int current = stream.ReadByte();
@@ -78,7 +89,17 @@ public static bool ReadDecimal(this BufferedReadStream stream, out int value)
break;
}
- value = (value * 10) + current;
+ // Keep consuming digits after overflow so the next sample starts at the correct byte.
+ // Sample conversion can saturate this value. Header parsing rejects an overflowing field.
+ if (value > (int.MaxValue - current) / 10)
+ {
+ value = int.MaxValue;
+ overflow = true;
+ }
+ else
+ {
+ value = (value * 10) + current;
+ }
}
return true;
diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
index 938be62408..4c72e1ecbb 100644
--- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
+++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
@@ -6,7 +6,6 @@
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Pbm;
@@ -65,10 +64,6 @@ protected override Image Decode(BufferedReadStream stream, Cance
Buffer2D pixels = image.GetRootFramePixelBuffer();
this.ProcessPixels(stream, pixels);
- if (this.NeedsUpscaling())
- {
- this.ProcessUpscaling(image);
- }
return image;
}
@@ -138,19 +133,25 @@ private void ProcessHeader(BufferedReadStream stream)
}
if (!stream.SkipWhitespaceAndComments() ||
- !stream.ReadDecimal(out int width) ||
+ !stream.ReadDecimal(out int width, out bool widthOverflow) || widthOverflow ||
!stream.SkipWhitespaceAndComments() ||
- !stream.ReadDecimal(out int height) ||
- !stream.SkipWhitespaceAndComments())
+ !stream.ReadDecimal(out int height, out bool heightOverflow) || heightOverflow)
+ {
+ ThrowInvalidHeader();
+ }
+
+ // ReadDecimal consumed the separator after height. Only PBM's binary raster starts here;
+ // other formats still need a Maxval token, and plain PBM still needs to skip token whitespace.
+ if ((this.colorType != PbmColorType.BlackAndWhite || this.encoding == PbmEncoding.Plain) && !stream.SkipWhitespaceAndComments())
{
- ThrowPrematureEof();
+ ThrowInvalidHeader();
}
if (this.colorType != PbmColorType.BlackAndWhite)
{
- if (!stream.ReadDecimal(out this.maxPixelValue))
+ if (!stream.ReadDecimal(out this.maxPixelValue, out bool maxValueOverflow) || maxValueOverflow)
{
- ThrowPrematureEof();
+ ThrowInvalidHeader();
}
if (this.maxPixelValue <= 0 || this.maxPixelValue >= 65536)
@@ -167,7 +168,12 @@ private void ProcessHeader(BufferedReadStream stream)
this.componentType = PbmComponentType.Byte;
}
- stream.SkipWhitespaceAndComments();
+ // The binary formats have exactly one separator before the raster. Its bytes can equal whitespace or '#'.
+ // Plain samples are decimal tokens, so their leading whitespace and comments still need to be skipped.
+ if (this.encoding == PbmEncoding.Plain)
+ {
+ stream.SkipWhitespaceAndComments();
+ }
}
else
{
@@ -182,8 +188,9 @@ private void ProcessHeader(BufferedReadStream stream)
meta.ColorType = this.colorType;
meta.ComponentType = this.componentType;
+ // Reject an incomplete header or a decimal field that exceeds the supported integer range.
[DoesNotReturn]
- static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header.");
+ static void ThrowInvalidHeader() => throw new InvalidImageContentException("Invalid or incomplete PBM header.");
}
private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels)
@@ -191,21 +198,11 @@ private void ProcessPixels(BufferedReadStream stream, Buffer2D p
{
if (this.encoding == PbmEncoding.Binary)
{
- BinaryDecoder.Process(this.configuration, pixels, stream, this.colorType, this.componentType);
+ BinaryDecoder.Process(this.configuration, pixels, stream, this.colorType, this.componentType, this.maxPixelValue);
}
else
{
- PlainDecoder.Process(this.configuration, pixels, stream, this.colorType, this.componentType);
+ PlainDecoder.Process(this.configuration, pixels, stream, this.colorType, this.componentType, this.maxPixelValue);
}
}
-
- private void ProcessUpscaling(Image image)
- where TPixel : unmanaged, IPixel
- {
- int maxAllocationValue = this.componentType == PbmComponentType.Short ? 65535 : 255;
- float factor = maxAllocationValue / (float)this.maxPixelValue;
- image.Mutate(x => x.Brightness(factor));
- }
-
- private bool NeedsUpscaling() => this.colorType != PbmColorType.BlackAndWhite && this.maxPixelValue is not 255 and not 65535;
}
diff --git a/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs b/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs
new file mode 100644
index 0000000000..ef2764af03
--- /dev/null
+++ b/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.ImageSharp.Formats.Pbm;
+
+///
+/// Normalizes PGM and PPM samples without rounding them to an intermediate integer format.
+///
+internal static class PbmSampleScaler
+{
+ ///
+ /// Normalizes four encoded component magnitudes to [0, 1]. Values outside the declared range saturate at its endpoints.
+ ///
+ /// The RGB sample magnitudes and an alpha magnitude equal to Maxval.
+ /// The maxval declared in the header, between 1 and 65535.
+ /// Normalized pixel components with opaque alpha.
+ public static Vector4 Scale(Vector4 value, int maxPixelValue)
+ {
+ // Maxval is a validated positive integer. Division maps Maxval to exactly one, including the alpha lane.
+ // Saturation is a numeric conversion rule and does not depend on SegmentIntegrityHandling.
+ value = Numerics.Clamp(value, Vector4.Zero, new Vector4(maxPixelValue));
+ return value / maxPixelValue;
+ }
+}
diff --git a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs
index 8748d90fa8..b36487da50 100644
--- a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs
+++ b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
+using System.Numerics;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -25,9 +26,25 @@ internal class PlainDecoder
/// The stream to read the data from.
/// The ColorType to decode.
/// Data type of the pixles components.
- public static void Process(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, PbmColorType colorType, PbmComponentType componentType)
+ /// The maximum sample value declared in the header. Unused for black and white.
+ public static void Process(
+ Configuration configuration,
+ Buffer2D pixels,
+ BufferedReadStream stream,
+ PbmColorType colorType,
+ PbmComponentType componentType,
+ int maxPixelValue)
where TPixel : unmanaged, IPixel
{
+ // Full-range inputs keep the existing byte and ushort conversion paths.
+ // Other ranges need normalized floats until the destination pixel format performs its final packing.
+ int fullRange = componentType == PbmComponentType.Short ? ushort.MaxValue : byte.MaxValue;
+ if (colorType != PbmColorType.BlackAndWhite && maxPixelValue != fullRange)
+ {
+ ProcessScaled(configuration, pixels, stream, colorType, maxPixelValue);
+ return;
+ }
+
if (colorType == PbmColorType.Grayscale)
{
if (componentType == PbmComponentType.Byte)
@@ -56,6 +73,81 @@ public static void Process(Configuration configuration, Buffer2D
}
}
+ ///
+ /// Normalizes decimal samples before the destination pixel format packs them.
+ ///
+ /// The destination pixel format.
+ /// The configuration.
+ /// The destination pixels.
+ /// The input stream at the raster.
+ /// The encoded color type.
+ /// The declared maximum sample value.
+ private static void ProcessScaled(
+ Configuration configuration,
+ Buffer2D pixels,
+ BufferedReadStream stream,
+ PbmColorType colorType,
+ int maxPixelValue)
+ where TPixel : unmanaged, IPixel
+ {
+ // Header parsing can reach EOF before the first sample. Retain the destination's default pixels in that case.
+ if (stream.Position == stream.Length)
+ {
+ return;
+ }
+
+ bool isRgb = colorType == PbmColorType.Rgb;
+ int width = pixels.Width;
+ using IMemoryOwner vectors = configuration.MemoryAllocator.Allocate(width);
+ Span vectorRow = vectors.GetSpan();
+
+ for (int y = 0; y < pixels.Height; y++)
+ {
+ int count = 0;
+ bool hasNext = true;
+ for (int x = 0; x < width; x++)
+ {
+ // ReadDecimal retains a final number even when EOF immediately follows its last digit.
+ hasNext = stream.ReadDecimal(out int red) && stream.SkipWhitespaceAndComments();
+ int green = red;
+ int blue = red;
+ if (isRgb)
+ {
+ // Preserve available channels if the last pixel is incomplete. Missing channels remain zero.
+ green = 0;
+ blue = 0;
+ if (hasNext)
+ {
+ hasNext = stream.ReadDecimal(out green) && stream.SkipWhitespaceAndComments();
+ if (hasNext)
+ {
+ hasNext = stream.ReadDecimal(out blue) && stream.SkipWhitespaceAndComments();
+ }
+ }
+ }
+
+ vectorRow[x] = PbmSampleScaler.Scale(new Vector4(red, green, blue, maxPixelValue), maxPixelValue);
+ count++;
+ if (!hasNext)
+ {
+ break;
+ }
+ }
+
+ // Do not copy the unused part of a pooled row after EOF. Earlier rows and available samples remain decoded.
+ PixelOperations.Instance.FromVector4Destructive(
+ configuration,
+ vectorRow[..count],
+ pixels.DangerousGetRowSpan(y)[..count],
+ PixelConversionModifiers.Scale);
+
+ if (!hasNext)
+ {
+ return;
+ }
+ }
+ }
+
private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream)
where TPixel : unmanaged, IPixel
{
@@ -68,10 +160,12 @@ private static void ProcessGrayscale(Configuration configuration, Buffer
bool eofReached = false;
for (int y = 0; y < height; y++)
{
+ int count = 0;
for (int x = 0; x < width; x++)
{
stream.ReadDecimal(out int value);
- rowSpan[x] = new L8((byte)value);
+ rowSpan[x] = new L8((byte)Math.Clamp(value, 0, byte.MaxValue));
+ count++;
eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
@@ -82,8 +176,8 @@ private static void ProcessGrayscale(Configuration configuration, Buffer
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromL8(
configuration,
- rowSpan,
- pixelSpan);
+ rowSpan[..count],
+ pixelSpan[..count]);
if (eofReached)
{
@@ -104,10 +198,12 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu
bool eofReached = false;
for (int y = 0; y < height; y++)
{
+ int count = 0;
for (int x = 0; x < width; x++)
{
stream.ReadDecimal(out int value);
- rowSpan[x] = new L16((ushort)value);
+ rowSpan[x] = new L16((ushort)Math.Clamp(value, 0, ushort.MaxValue));
+ count++;
eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
@@ -118,8 +214,8 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromL16(
configuration,
- rowSpan,
- pixelSpan);
+ rowSpan[..count],
+ pixelSpan[..count]);
if (eofReached)
{
@@ -140,6 +236,7 @@ private static void ProcessRgb(Configuration configuration, Buffer2D(Configuration configuration, Buffer2D(Configuration configuration, Buffer2D pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromRgb24(
configuration,
- rowSpan,
- pixelSpan);
+ rowSpan[..count],
+ pixelSpan[..count]);
if (eofReached)
{
@@ -187,6 +289,7 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D
bool eofReached = false;
for (int y = 0; y < height; y++)
{
+ int count = 0;
for (int x = 0; x < width; x++)
{
if (!stream.ReadDecimal(out int red) ||
@@ -201,7 +304,12 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D
stream.ReadDecimal(out int blue);
- rowSpan[x] = new Rgb48((ushort)red, (ushort)green, (ushort)blue);
+ rowSpan[x] = new Rgb48(
+ (ushort)Math.Clamp(red, 0, ushort.MaxValue),
+ (ushort)Math.Clamp(green, 0, ushort.MaxValue),
+ (ushort)Math.Clamp(blue, 0, ushort.MaxValue));
+
+ count++;
eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
@@ -212,8 +320,8 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromRgb48(
configuration,
- rowSpan,
- pixelSpan);
+ rowSpan[..count],
+ pixelSpan[..count]);
if (eofReached)
{
@@ -234,11 +342,13 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu
bool eofReached = false;
for (int y = 0; y < height; y++)
{
+ int count = 0;
for (int x = 0; x < width; x++)
{
stream.ReadDecimal(out int value);
rowSpan[x] = value == 0 ? White : Black;
+ count++;
eofReached = !stream.SkipWhitespaceAndComments();
if (eofReached)
{
@@ -249,8 +359,8 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromL8(
configuration,
- rowSpan,
- pixelSpan);
+ rowSpan[..count],
+ pixelSpan[..count]);
if (eofReached)
{
diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
index 778ad75897..8a6227c70b 100644
--- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Text;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.PixelFormats;
@@ -117,12 +116,7 @@ public void Decode_WideBinary_MatchesReferenceDecoder(TestImageProvider<
[Fact]
public void Decode_WideBinaryGrayscale_SamplesAreBigEndian()
{
- // Per the Netpbm specification, 16-bit samples store the most significant byte first.
- byte[] header = Encoding.ASCII.GetBytes("P5\n2 1\n65535\n");
- byte[] samples = [0x80, 0x00, 0x00, 0x80];
- byte[] data = [.. header, .. samples];
-
- using Image image = Image.Load(data);
+ using Image image = Image.Load(TestFile.Create(WideGrayPgm).Bytes);
Assert.Equal(0x8000, image[0, 0].PackedValue);
Assert.Equal(0x0080, image[1, 0].PackedValue);
@@ -131,12 +125,7 @@ public void Decode_WideBinaryGrayscale_SamplesAreBigEndian()
[Fact]
public void Decode_WideBinaryRgb_SamplesAreBigEndian()
{
- // Per the Netpbm specification, 16-bit samples store the most significant byte first.
- byte[] header = Encoding.ASCII.GetBytes("P6\n1 1\n65535\n");
- byte[] samples = [0x81, 0xB5, 0x84, 0x91, 0x86, 0x71];
- byte[] data = [.. header, .. samples];
-
- using Image image = Image.Load(data);
+ using Image image = Image.Load(TestFile.Create(WideRgbPpm).Bytes);
Assert.Equal(new Rgb48(0x81B5, 0x8491, 0x8671), image[0, 0]);
}
@@ -144,9 +133,7 @@ public void Decode_WideBinaryRgb_SamplesAreBigEndian()
[Fact]
public void Decode_NonStandardByteMaxPixelValue_UpscalesToFullRange()
{
- byte[] data = Encoding.ASCII.GetBytes("P2\n1 1\n100\n100");
-
- using Image image = Image.Load(data);
+ using Image image = Image.Load(TestFile.Create(ByteWhitePgm).Bytes);
Assert.Equal(255, image[0, 0].PackedValue);
}
@@ -154,15 +141,161 @@ public void Decode_NonStandardByteMaxPixelValue_UpscalesToFullRange()
[Fact]
public void Decode_NonStandardShortMaxPixelValue_UpscalesToFullRange()
{
- byte[] header = Encoding.ASCII.GetBytes("P5\n1 1\n1000\n");
- byte[] samples = [0x03, 0xE8];
- byte[] data = [.. header, .. samples];
-
- using Image image = Image.Load(data);
+ using Image image = Image.Load(TestFile.Create(WideWhitePgm).Bytes);
Assert.Equal(65535, image[0, 0].PackedValue);
}
+ ///
+ /// Preserves non-standard sample ranges when packing both byte and ushort destination pixels.
+ ///
+ /// The stored PGM or PPM input.
+ /// The declared maximum sample value.
+ /// True for three-channel samples.
+ [Theory]
+ [InlineData(P2Max100Pgm, 100, false)]
+ [InlineData(P3Max100Ppm, 100, true)]
+ [InlineData(P5Max100Pgm, 100, false)]
+ [InlineData(P6Max100Ppm, 100, true)]
+ [InlineData(P2Max1000Pgm, 1000, false)]
+ [InlineData(P3Max1000Ppm, 1000, true)]
+ [InlineData(P5Max1000Pgm, 1000, false)]
+ [InlineData(P6Max1000Ppm, 1000, true)]
+ public void Decode_NonStandardMaxPixelValue_PreservesSamplePrecision(string imagePath, int maxPixelValue, bool isRgb)
+ {
+ // The six stored samples are 0, 7, Maxval/2, Maxval-1, Maxval and 3.
+ // These fixed expectations round their fractions once at the destination precision.
+ byte[] expected8 = maxPixelValue == 100 ? [0, 18, 128, 252, 255, 8] : [0, 2, 128, 255, 255, 1];
+ ushort[] expected16 = maxPixelValue == 100 ? [0, 4587, 32768, 64880, 65535, 1966] : [0, 459, 32768, 65469, 65535, 197];
+ byte[] data = TestFile.Create(imagePath).Bytes;
+ using Image image8 = Image.Load(data);
+ using Image image16 = Image.Load(data);
+ int width = isRgb ? 2 : 6;
+ Assert.Equal(new Size(width, 1), image8.Size);
+ Assert.Equal(image8.Size, image16.Size);
+
+ for (int x = 0; x < width; x++)
+ {
+ int red = isRgb ? x * 3 : x;
+ int green = isRgb ? red + 1 : red;
+ int blue = isRgb ? red + 2 : red;
+ Assert.Equal(new Rgb24(expected8[red], expected8[green], expected8[blue]), image8[x, 0]);
+ Assert.Equal(new Rgb48(expected16[red], expected16[green], expected16[blue]), image16[x, 0]);
+ }
+ }
+
+ ///
+ /// Saturates sample magnitudes independently of the selected integrity mode.
+ ///
+ /// The stored input containing samples above Maxval.
+ [Theory]
+ [InlineData(P2AboveMax100Pgm)]
+ [InlineData(P3AboveMax100Ppm)]
+ [InlineData(P5AboveMax100Pgm)]
+ [InlineData(P6AboveMax100Ppm)]
+ [InlineData(P2AboveMax1000Pgm)]
+ [InlineData(P3AboveMax1000Ppm)]
+ [InlineData(P5AboveMax1000Pgm)]
+ [InlineData(P6AboveMax1000Ppm)]
+ [InlineData(P2DecimalOverflowPgm)]
+ [InlineData(P3DecimalOverflowPpm)]
+ public void Decode_SamplesAboveMaxval_SaturateInEveryIntegrityMode(string imagePath)
+ {
+ byte[] data = TestFile.Create(imagePath).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ using Image image = Image.Load(options, data);
+ Assert.Equal(new Rgb48(65535, 65535, 65535), image[0, 0]);
+ }
+ }
+
+ ///
+ /// Preserves the available pixels and channels when a non-standard raster ends early.
+ ///
+ /// The truncated stored input.
+ /// True for a truncated RGB pixel; false for a truncated grayscale row.
+ [Theory]
+ [InlineData(P2PartialPgm, false)]
+ [InlineData(P5PartialPgm, false)]
+ [InlineData(P3PartialPpm, true)]
+ [InlineData(P6PartialPpm, true)]
+ public void Decode_PartialRaster_PreservesAvailableImageData(string imagePath, bool isRgb)
+ {
+ byte[] data = TestFile.Create(imagePath).Bytes;
+ Rgb24[] expected = isRgb
+ ? [new(0, 2, 128), new(255, 0, 0)]
+ : [new(0, 0, 0), new(18, 18, 18), new(255, 255, 255), new(128, 128, 128), default, default];
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ using Image image = Image.Load(options, data);
+ Assert.Equal(isRgb ? new Size(2, 1) : new Size(3, 2), image.Size);
+
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], image[i % image.Width, i / image.Width]);
+ }
+ }
+ }
+
+ ///
+ /// Keeps raster bytes whose values are whitespace or a comment marker.
+ ///
+ /// The stored binary input.
+ /// True for RGB samples.
+ [Theory]
+ [InlineData(RasterWhitespacePgm, false)]
+ [InlineData(RasterWhitespacePpm, true)]
+ public void Decode_BinaryRaster_DoesNotSkipSampleBytes(string imagePath, bool isRgb)
+ {
+ // Stored magnitudes 9, 10, 13, 32 and 35 are TAB, LF, CR, space and '#'. They belong to the raster.
+ byte[] expected = [23, 26, 33, 82, 89, 255];
+ using Image image = Image.Load(TestFile.Create(imagePath).Bytes);
+
+ for (int x = 0; x < image.Width; x++)
+ {
+ int red = isRgb ? x * 3 : x;
+ Assert.Equal(new Rgb24(expected[red], expected[isRgb ? red + 1 : red], expected[isRgb ? red + 2 : red]), image[x, 0]);
+ }
+ }
+
+ ///
+ /// Keeps a binary PBM raster byte whose value equals ASCII space.
+ ///
+ [Fact]
+ public void Decode_BinaryBitmap_DoesNotSkipRasterByte()
+ {
+ using Image image = Image.Load(TestFile.Create(RasterWhitespacePbm).Bytes);
+
+ for (int x = 0; x < 8; x++)
+ {
+ Assert.Equal(x == 2 ? 0 : 255, image[x, 0].PackedValue);
+ }
+ }
+
+ ///
+ /// Rejects an overflowing header dimension before allocating the image.
+ ///
+ [Fact]
+ public void Decode_OverflowingHeader_RemainsFatal()
+ {
+ byte[] data = TestFile.Create(HeaderDecimalOverflowPgm).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ Assert.Throws(() => Image.Identify(options, data));
+ Assert.Throws(() =>
+ {
+ using Image image = Image.Load(options, data);
+ });
+ }
+ }
+
+
[Theory]
[WithFile(RgbPlain, PixelTypes.Rgb24)]
public void PbmDecoder_Decode_Resize(TestImageProvider provider)
@@ -186,13 +319,13 @@ public void PbmDecoder_Decode_Resize(TestImageProvider provider)
}
[Theory]
- [InlineData("P2")]
- [InlineData("P3")]
- [InlineData("P5")]
- [InlineData("P6")]
- public void Decode_MaxPixelValueZero_ThrowsInvalidImageContentException(string magic)
+ [InlineData(P2ZeroMaxPgm)]
+ [InlineData(P3ZeroMaxPpm)]
+ [InlineData(P5ZeroMaxPgm)]
+ [InlineData(P6ZeroMaxPpm)]
+ public void Decode_MaxPixelValueZero_ThrowsInvalidImageContentException(string imagePath)
{
- byte[] bytes = Encoding.ASCII.GetBytes($"{magic} 1 1 0\n0");
+ byte[] bytes = TestFile.Create(imagePath).Bytes;
using MemoryStream stream = new(bytes);
Assert.Throws(() => Image.Load(stream));
@@ -201,8 +334,7 @@ public void Decode_MaxPixelValueZero_ThrowsInvalidImageContentException(string m
[Fact]
public void PlainText_PrematureEof()
{
- byte[] bytes = Encoding.ASCII.GetBytes($"P1\n100 100\n1 0 1 0 1 0");
- using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(bytes);
+ using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(PlainEofPbm);
Assert.True(eofHitCounter.EofHitCount <= 2);
Assert.Equal(new Size(100, 100), eofHitCounter.Image.Size);
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 6966a32a78..96d099ea2e 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1358,6 +1358,42 @@ public static class BigTiff
public static class Pbm
{
+ public const string WideGrayPgm = "Pbm/samples/wide-gray.pgm";
+ public const string WideRgbPpm = "Pbm/samples/wide-rgb.ppm";
+ public const string ByteWhitePgm = "Pbm/samples/byte-white.pgm";
+ public const string WideWhitePgm = "Pbm/samples/wide-white.pgm";
+ public const string P2Max100Pgm = "Pbm/samples/p2-max100.pgm";
+ public const string P3Max100Ppm = "Pbm/samples/p3-max100.ppm";
+ public const string P5Max100Pgm = "Pbm/samples/p5-max100.pgm";
+ public const string P6Max100Ppm = "Pbm/samples/p6-max100.ppm";
+ public const string P2Max1000Pgm = "Pbm/samples/p2-max1000.pgm";
+ public const string P3Max1000Ppm = "Pbm/samples/p3-max1000.ppm";
+ public const string P5Max1000Pgm = "Pbm/samples/p5-max1000.pgm";
+ public const string P6Max1000Ppm = "Pbm/samples/p6-max1000.ppm";
+ public const string P2AboveMax100Pgm = "Pbm/samples/p2-above-max100.pgm";
+ public const string P3AboveMax100Ppm = "Pbm/samples/p3-above-max100.ppm";
+ public const string P5AboveMax100Pgm = "Pbm/samples/p5-above-max100.pgm";
+ public const string P6AboveMax100Ppm = "Pbm/samples/p6-above-max100.ppm";
+ public const string P2AboveMax1000Pgm = "Pbm/samples/p2-above-max1000.pgm";
+ public const string P3AboveMax1000Ppm = "Pbm/samples/p3-above-max1000.ppm";
+ public const string P5AboveMax1000Pgm = "Pbm/samples/p5-above-max1000.pgm";
+ public const string P6AboveMax1000Ppm = "Pbm/samples/p6-above-max1000.ppm";
+ public const string P2DecimalOverflowPgm = "Pbm/samples/p2-decimal-overflow.pgm";
+ public const string P3DecimalOverflowPpm = "Pbm/samples/p3-decimal-overflow.ppm";
+ public const string P2PartialPgm = "Pbm/samples/p2-partial.pgm";
+ public const string P5PartialPgm = "Pbm/samples/p5-partial.pgm";
+ public const string P3PartialPpm = "Pbm/samples/p3-partial.ppm";
+ public const string P6PartialPpm = "Pbm/samples/p6-partial.ppm";
+ public const string RasterWhitespacePgm = "Pbm/samples/raster-whitespace.pgm";
+ public const string RasterWhitespacePpm = "Pbm/samples/raster-whitespace.ppm";
+ public const string RasterWhitespacePbm = "Pbm/samples/raster-whitespace.pbm";
+ public const string HeaderDecimalOverflowPgm = "Pbm/samples/header-decimal-overflow.pgm";
+ public const string P2ZeroMaxPgm = "Pbm/samples/p2-zero-max.pgm";
+ public const string P3ZeroMaxPpm = "Pbm/samples/p3-zero-max.ppm";
+ public const string P5ZeroMaxPgm = "Pbm/samples/p5-zero-max.pgm";
+ public const string P6ZeroMaxPpm = "Pbm/samples/p6-zero-max.ppm";
+ public const string PlainEofPbm = "Pbm/samples/plain-eof.pbm";
+
public const string BlackAndWhitePlain = "Pbm/blackandwhite_plain.pbm";
public const string BlackAndWhiteBinary = "Pbm/blackandwhite_binary.pbm";
public const string GrayscaleBinary = "Pbm/rings.pgm";
diff --git a/tests/Images/Input/Pbm/samples/byte-white.pgm b/tests/Images/Input/Pbm/samples/byte-white.pgm
new file mode 100644
index 0000000000..b6ccc0049c
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/byte-white.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b504c73b4f6c935d685ec532c26594027db7f78792016a5661734263a77ea94b
+size 15
diff --git a/tests/Images/Input/Pbm/samples/header-decimal-overflow.pgm b/tests/Images/Input/Pbm/samples/header-decimal-overflow.pgm
new file mode 100644
index 0000000000..c7da08e46a
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/header-decimal-overflow.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de66e826dc5dd2afa7d9cc940c51dd3903aa173da95d552356a53e967ccbe53f
+size 23
diff --git a/tests/Images/Input/Pbm/samples/p2-above-max100.pgm b/tests/Images/Input/Pbm/samples/p2-above-max100.pgm
new file mode 100644
index 0000000000..7fab382084
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p2-above-max100.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8d32a9b102793b2b0091e9a40806ca02a7a237ea3893d1e58b3615806a9850ae
+size 15
diff --git a/tests/Images/Input/Pbm/samples/p2-above-max1000.pgm b/tests/Images/Input/Pbm/samples/p2-above-max1000.pgm
new file mode 100644
index 0000000000..126230863d
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p2-above-max1000.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:766b9194741228357f5f11170e605ba89d58b29a17d16a94d4d6016aefbde19f
+size 17
diff --git a/tests/Images/Input/Pbm/samples/p2-decimal-overflow.pgm b/tests/Images/Input/Pbm/samples/p2-decimal-overflow.pgm
new file mode 100644
index 0000000000..c1d01a2d2d
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p2-decimal-overflow.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8cae8e11025d477ae84c360acd07605d9681bcbd51e523f6677945209663b4df
+size 23
diff --git a/tests/Images/Input/Pbm/samples/p2-max100.pgm b/tests/Images/Input/Pbm/samples/p2-max100.pgm
new file mode 100644
index 0000000000..024d12eda4
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p2-max100.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:42c24a166bfa44069a845aba75fe1bd72e8a9df0ed8b151def58efec09eac051
+size 27
diff --git a/tests/Images/Input/Pbm/samples/p2-max1000.pgm b/tests/Images/Input/Pbm/samples/p2-max1000.pgm
new file mode 100644
index 0000000000..6b6520f876
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p2-max1000.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c65ac654430f6680adc6e86dac2696061227651ace44834a2795e21bf2ec804a
+size 31
diff --git a/tests/Images/Input/Pbm/samples/p2-partial.pgm b/tests/Images/Input/Pbm/samples/p2-partial.pgm
new file mode 100644
index 0000000000..b4c53668c7
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p2-partial.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2f8703fe912eac9765da2e4f1d88ea55afe96ffe03083a174be48294a79b34d2
+size 22
diff --git a/tests/Images/Input/Pbm/samples/p2-zero-max.pgm b/tests/Images/Input/Pbm/samples/p2-zero-max.pgm
new file mode 100644
index 0000000000..b02cdfc5aa
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p2-zero-max.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3438f701e8325e944a6e86222cbe06d357624ba48d23e15ed7b21c76c3780a06
+size 11
diff --git a/tests/Images/Input/Pbm/samples/p3-above-max100.ppm b/tests/Images/Input/Pbm/samples/p3-above-max100.ppm
new file mode 100644
index 0000000000..3dca522722
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p3-above-max100.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3778b4fab3940ae9828a702f5aa0768dcc82606e7a85e6e4898bcd62538f1756
+size 23
diff --git a/tests/Images/Input/Pbm/samples/p3-above-max1000.ppm b/tests/Images/Input/Pbm/samples/p3-above-max1000.ppm
new file mode 100644
index 0000000000..369cc5eb9d
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p3-above-max1000.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c655cce160d471e488e611645daee790bf076bf378c5e3bc62d2ceb9ffb1de0c
+size 27
diff --git a/tests/Images/Input/Pbm/samples/p3-decimal-overflow.ppm b/tests/Images/Input/Pbm/samples/p3-decimal-overflow.ppm
new file mode 100644
index 0000000000..c9806ecdd1
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p3-decimal-overflow.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9e9f61c8bfae421ea566deec59a62d368613c4d8fa74b930fa71d9dbefb36fd6
+size 45
diff --git a/tests/Images/Input/Pbm/samples/p3-max100.ppm b/tests/Images/Input/Pbm/samples/p3-max100.ppm
new file mode 100644
index 0000000000..b17618ffab
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p3-max100.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:47ac5dcdc664833737b4d254253149bb922736f2c46c4729452c153d895ec114
+size 27
diff --git a/tests/Images/Input/Pbm/samples/p3-max1000.ppm b/tests/Images/Input/Pbm/samples/p3-max1000.ppm
new file mode 100644
index 0000000000..68e13913c9
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p3-max1000.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1daa7ccd16aae0e28efbfe2fcb9eb73f7a5ad00d7dbe63e9ebc952e4da44d285
+size 31
diff --git a/tests/Images/Input/Pbm/samples/p3-partial.ppm b/tests/Images/Input/Pbm/samples/p3-partial.ppm
new file mode 100644
index 0000000000..c49f3e1255
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p3-partial.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0299a7fbe75c6b0530139bdb5696dcea8ba05df782860b6d0ae7e5397cf0f4ce
+size 25
diff --git a/tests/Images/Input/Pbm/samples/p3-zero-max.ppm b/tests/Images/Input/Pbm/samples/p3-zero-max.ppm
new file mode 100644
index 0000000000..c81e6c72a7
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p3-zero-max.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2ecf882400921ae6e28aa4bdf42eb535a606763b85f830e816d74d3dcf06b46b
+size 11
diff --git a/tests/Images/Input/Pbm/samples/p5-above-max100.pgm b/tests/Images/Input/Pbm/samples/p5-above-max100.pgm
new file mode 100644
index 0000000000..bdf0f17cc5
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p5-above-max100.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a2591c4faf7b3607479d3b611f8cdaee64be9ec0babbf07afc2fd0cab8a609b9
+size 12
diff --git a/tests/Images/Input/Pbm/samples/p5-above-max1000.pgm b/tests/Images/Input/Pbm/samples/p5-above-max1000.pgm
new file mode 100644
index 0000000000..878aa36806
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p5-above-max1000.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f9f62a7a19714e0d79653e024df026a1b2224b03c63713f26e95720cdb9fbd7
+size 14
diff --git a/tests/Images/Input/Pbm/samples/p5-max100.pgm b/tests/Images/Input/Pbm/samples/p5-max100.pgm
new file mode 100644
index 0000000000..afdd1f740e
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p5-max100.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5254a063f3779cb152f03aa287bb0280f03915ad1ae27f50adb2c22251df0428
+size 17
diff --git a/tests/Images/Input/Pbm/samples/p5-max1000.pgm b/tests/Images/Input/Pbm/samples/p5-max1000.pgm
new file mode 100644
index 0000000000..676259beba
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p5-max1000.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:db981a459cf2a15a390885dbc7a82939a1b24b0dc12808deaaac1207b782cc5c
+size 24
diff --git a/tests/Images/Input/Pbm/samples/p5-partial.pgm b/tests/Images/Input/Pbm/samples/p5-partial.pgm
new file mode 100644
index 0000000000..346e3ddf44
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p5-partial.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb0ba909c47a6538d5d164210870807472c86f7c189ed0fddab9745b6dfbe3ba
+size 15
diff --git a/tests/Images/Input/Pbm/samples/p5-zero-max.pgm b/tests/Images/Input/Pbm/samples/p5-zero-max.pgm
new file mode 100644
index 0000000000..03ca4de70b
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p5-zero-max.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8d861ebeb6429288f4b10ccf8a548c9bd41c14642bebb66349083eba3aa1446e
+size 11
diff --git a/tests/Images/Input/Pbm/samples/p6-above-max100.ppm b/tests/Images/Input/Pbm/samples/p6-above-max100.ppm
new file mode 100644
index 0000000000..42b4a1604d
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p6-above-max100.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dbbdb23bfba375c81079478418e3857587a6566d4623d286b5a168c1b0e6cfe7
+size 14
diff --git a/tests/Images/Input/Pbm/samples/p6-above-max1000.ppm b/tests/Images/Input/Pbm/samples/p6-above-max1000.ppm
new file mode 100644
index 0000000000..f354e92118
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p6-above-max1000.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bb80c650d4881877075521b5a5f83c967f49afe0a8c78bde69adf776fe8783a6
+size 18
diff --git a/tests/Images/Input/Pbm/samples/p6-max100.ppm b/tests/Images/Input/Pbm/samples/p6-max100.ppm
new file mode 100644
index 0000000000..9fcd64daaf
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p6-max100.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b493e4fbb02266b93ed6191a8516bcbd6d0a1ec41ccfa6844fa83ee051b96bf
+size 17
diff --git a/tests/Images/Input/Pbm/samples/p6-max1000.ppm b/tests/Images/Input/Pbm/samples/p6-max1000.ppm
new file mode 100644
index 0000000000..33d1b42c33
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p6-max1000.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bbcdfa970dea62de9759ed64e3c0eee991a24673b89051314aaa8fc43cd53aa4
+size 24
diff --git a/tests/Images/Input/Pbm/samples/p6-partial.ppm b/tests/Images/Input/Pbm/samples/p6-partial.ppm
new file mode 100644
index 0000000000..53571c6371
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p6-partial.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a88484b7e16a5dfc14a848c2bd6c80d794fefce6d406fee270b6c04333c26f17
+size 20
diff --git a/tests/Images/Input/Pbm/samples/p6-zero-max.ppm b/tests/Images/Input/Pbm/samples/p6-zero-max.ppm
new file mode 100644
index 0000000000..5683e09464
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/p6-zero-max.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:51b422fbb31335d33ab4cb6f18704def69170d5f00f5c63279d8b21850aa070d
+size 11
diff --git a/tests/Images/Input/Pbm/samples/plain-eof.pbm b/tests/Images/Input/Pbm/samples/plain-eof.pbm
new file mode 100644
index 0000000000..f1072fe8ce
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/plain-eof.pbm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5acf9a95c51d8b9e42c187819562bcd5ec5dd2d39625dd3475f4bb0e322dd806
+size 22
diff --git a/tests/Images/Input/Pbm/samples/raster-whitespace.pbm b/tests/Images/Input/Pbm/samples/raster-whitespace.pbm
new file mode 100644
index 0000000000..7dbb415fe1
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/raster-whitespace.pbm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6470a98f812b2387d9a5cbed40318fe929aa386b7a379e0614c443180e0dbc88
+size 8
diff --git a/tests/Images/Input/Pbm/samples/raster-whitespace.pgm b/tests/Images/Input/Pbm/samples/raster-whitespace.pgm
new file mode 100644
index 0000000000..d443487d88
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/raster-whitespace.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1c6fba3b36fb532bb782d3a1db826d466d1a072d8ae90fd05fc1a3880fc62473
+size 17
diff --git a/tests/Images/Input/Pbm/samples/raster-whitespace.ppm b/tests/Images/Input/Pbm/samples/raster-whitespace.ppm
new file mode 100644
index 0000000000..680ce18386
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/raster-whitespace.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9447269a73d06ec0d9e20df94bb5ce1ba29511a2e5c2055ad9eb2f308fb30861
+size 17
diff --git a/tests/Images/Input/Pbm/samples/wide-gray.pgm b/tests/Images/Input/Pbm/samples/wide-gray.pgm
new file mode 100644
index 0000000000..7aff68904d
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/wide-gray.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b97102586b316c98db7622ef5c5d365dd98d56cca708e33054e65a40d8a00a7e
+size 17
diff --git a/tests/Images/Input/Pbm/samples/wide-rgb.ppm b/tests/Images/Input/Pbm/samples/wide-rgb.ppm
new file mode 100644
index 0000000000..f9f53e1946
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/wide-rgb.ppm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c987b903345e5c66b67583d1b831e9be9ccfde6f6119f6e3c6aec059026b8295
+size 19
diff --git a/tests/Images/Input/Pbm/samples/wide-white.pgm b/tests/Images/Input/Pbm/samples/wide-white.pgm
new file mode 100644
index 0000000000..3849d491cb
--- /dev/null
+++ b/tests/Images/Input/Pbm/samples/wide-white.pgm
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e16043fc6ff04104a3552deff58c24eb479a10e8e2816560600c7b95ed9874c1
+size 14