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