From ece5f9428dd588fb905159113258bc2e98a15a00 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Sun, 18 Oct 2026 09:18:12 +1000 Subject: [PATCH 1/3] Scale PGM and PPM samples to the full range before converting them to the pixel type A non-standard maxval was applied with a brightness pass after the samples had been converted to the destination pixel type. A 16-bit sample with a small maxval was narrowed to 8 bits first, so decoding to an 8-bit pixel type such as the default Rgb24 kept only a few distinct levels (maxval 1000 gave five). The plain and binary decoders now scale every sample from [0, maxval] to the full 8-bit or 16-bit range with rounded integer arithmetic before the pixel conversion, and the extra brightness pass over the image is removed. --- src/ImageSharp/Formats/Pbm/BinaryDecoder.cs | 71 ++++++++++++++++--- src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs | 19 +---- src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs | 30 ++++++++ src/ImageSharp/Formats/Pbm/PlainDecoder.cs | 35 +++++---- .../Formats/Pbm/PbmDecoderTests.cs | 48 +++++++++++++ 5 files changed, 164 insertions(+), 39 deletions(-) create mode 100644 src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs index a465907cf0..f83d3f74b8 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs @@ -34,29 +34,30 @@ 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 { if (colorType == PbmColorType.Grayscale) { if (componentType == PbmComponentType.Byte) { - ProcessGrayscale(configuration, pixels, stream); + ProcessGrayscale(configuration, pixels, stream, maxPixelValue); } else { - ProcessWideGrayscale(configuration, pixels, stream); + ProcessWideGrayscale(configuration, pixels, stream, maxPixelValue); } } else if (colorType == PbmColorType.Rgb) { if (componentType == PbmComponentType.Byte) { - ProcessRgb(configuration, pixels, stream); + ProcessRgb(configuration, pixels, stream, maxPixelValue); } else { - ProcessWideRgb(configuration, pixels, stream); + ProcessWideRgb(configuration, pixels, stream, maxPixelValue); } } else @@ -74,7 +75,8 @@ public static void Process(Configuration configuration, Buffer2D /// The configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + /// The maximum sample value declared in the header. + private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 1; @@ -91,6 +93,8 @@ private static void ProcessGrayscale(Configuration configuration, Buffer return; } + ScaleSamples(rowSpan, maxPixelValue); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL8Bytes( configuration, @@ -109,7 +113,8 @@ private static void ProcessGrayscale(Configuration configuration, Buffer /// The configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + /// The maximum sample value declared in the header. + private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 2; @@ -129,6 +134,7 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu // The binary format stores 16-bit samples most significant byte first, // but L16 expects native (little-endian) byte order. SwapSampleBytes(rowSpan); + ScaleSamples(MemoryMarshal.Cast(rowSpan), maxPixelValue); Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL16Bytes( @@ -148,7 +154,8 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu /// The configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + /// The maximum sample value declared in the header. + private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 3; @@ -165,6 +172,8 @@ private static void ProcessRgb(Configuration configuration, Buffer2D pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb24Bytes( configuration, @@ -183,7 +192,8 @@ private static void ProcessRgb(Configuration configuration, Buffer2DThe configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + /// The maximum sample value declared in the header. + private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 6; @@ -203,6 +213,7 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D // The binary format stores 16-bit samples most significant byte first, // but Rgb48 expects native (little-endian) byte order. SwapSampleBytes(rowSpan); + ScaleSamples(MemoryMarshal.Cast(rowSpan), maxPixelValue); Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb48Bytes( @@ -227,6 +238,48 @@ private static void SwapSampleBytes(Span rowSpan) } } + /// + /// Scales a row of 8-bit samples in place from the range [0, maxval] to the full range [0, 255]. + /// Scaling happens on the samples, before they are converted to the destination pixel type, + /// so the result does not depend on the precision of that pixel type. + /// + /// The row of samples to scale. + /// The maximum sample value declared in the header. + private static void ScaleSamples(Span samples, int maxPixelValue) + { + // Samples that already use the full range are left as they are, so the common case costs nothing. + if (maxPixelValue == byte.MaxValue) + { + return; + } + + for (int i = 0; i < samples.Length; i++) + { + samples[i] = (byte)PbmSampleScaler.Scale(samples[i], maxPixelValue, byte.MaxValue); + } + } + + /// + /// Scales a row of native-endian 16-bit samples in place from the range [0, maxval] to the full range [0, 65535]. + /// Scaling happens on the samples, before they are converted to the destination pixel type, + /// so a narrower pixel type does not lose the precision of a small maxval before it is scaled. + /// + /// The row of samples to scale. + /// The maximum sample value declared in the header. + private static void ScaleSamples(Span samples, int maxPixelValue) + { + // Samples that already use the full range are left as they are, so the common case costs nothing. + if (maxPixelValue == ushort.MaxValue) + { + return; + } + + for (int i = 0; i < samples.Length; i++) + { + samples[i] = (ushort)PbmSampleScaler.Scale(samples[i], maxPixelValue, ushort.MaxValue); + } + } + /// /// Decodes binary black and white (PBM) pixel data. /// Each byte holds eight pixels, most significant bit first, and a set bit means black. diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index 938be62408..3c25ddc5cf 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; } @@ -191,21 +186,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..e8af1143e6 --- /dev/null +++ b/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Pbm; + +/// +/// Scales PGM and PPM samples from the range declared by the header maxval to the full range of the component type. +/// +internal static class PbmSampleScaler +{ + /// + /// Scales a sample from the range [0, ] to the range [0, ], + /// rounding to the nearest value. Samples outside the declared range are clamped to . + /// + /// The sample value read from the stream. + /// The maxval declared in the header, between 1 and 65535. + /// The maximum value of the component type, either 255 or 65535. + /// The scaled sample value. + public static int Scale(int value, int maxPixelValue, int targetMaxValue) + { + // Negative values can only come from an overflowing decimal sample; reinterpreting them as unsigned + // makes them clamp to maxval along with every other out-of-range sample. + uint sample = Math.Min((uint)value, (uint)maxPixelValue); + + // Adding half of the divisor before dividing rounds to the nearest value, so maxval maps exactly to + // targetMaxValue and the scale does not depend on maxval dividing the target range. + // The largest intermediate value is 65535 * 65535 + 32767, which fits in an unsigned 32-bit integer. + return (int)(((sample * (uint)targetMaxValue) + ((uint)maxPixelValue >> 1)) / (uint)maxPixelValue); + } +} diff --git a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs index 8748d90fa8..0b468d5e98 100644 --- a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs @@ -25,29 +25,30 @@ 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 { if (colorType == PbmColorType.Grayscale) { if (componentType == PbmComponentType.Byte) { - ProcessGrayscale(configuration, pixels, stream); + ProcessGrayscale(configuration, pixels, stream, maxPixelValue); } else { - ProcessWideGrayscale(configuration, pixels, stream); + ProcessWideGrayscale(configuration, pixels, stream, maxPixelValue); } } else if (colorType == PbmColorType.Rgb) { if (componentType == PbmComponentType.Byte) { - ProcessRgb(configuration, pixels, stream); + ProcessRgb(configuration, pixels, stream, maxPixelValue); } else { - ProcessWideRgb(configuration, pixels, stream); + ProcessWideRgb(configuration, pixels, stream, maxPixelValue); } } else @@ -56,7 +57,7 @@ public static void Process(Configuration configuration, Buffer2D } } - private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -71,7 +72,7 @@ private static void ProcessGrayscale(Configuration configuration, Buffer for (int x = 0; x < width; x++) { stream.ReadDecimal(out int value); - rowSpan[x] = new L8((byte)value); + rowSpan[x] = new L8((byte)PbmSampleScaler.Scale(value, maxPixelValue, byte.MaxValue)); eofReached = !stream.SkipWhitespaceAndComments(); if (eofReached) { @@ -92,7 +93,7 @@ private static void ProcessGrayscale(Configuration configuration, Buffer } } - private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -107,7 +108,7 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu for (int x = 0; x < width; x++) { stream.ReadDecimal(out int value); - rowSpan[x] = new L16((ushort)value); + rowSpan[x] = new L16((ushort)PbmSampleScaler.Scale(value, maxPixelValue, ushort.MaxValue)); eofReached = !stream.SkipWhitespaceAndComments(); if (eofReached) { @@ -128,7 +129,7 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu } } - private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -154,7 +155,11 @@ private static void ProcessRgb(Configuration configuration, Buffer2D(Configuration configuration, Buffer2D(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) + private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -201,7 +206,11 @@ 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)PbmSampleScaler.Scale(red, maxPixelValue, ushort.MaxValue), + (ushort)PbmSampleScaler.Scale(green, maxPixelValue, ushort.MaxValue), + (ushort)PbmSampleScaler.Scale(blue, maxPixelValue, ushort.MaxValue)); + eofReached = !stream.SkipWhitespaceAndComments(); if (eofReached) { diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index 778ad75897..101a28c4ad 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -163,6 +163,54 @@ public void Decode_NonStandardShortMaxPixelValue_UpscalesToFullRange() Assert.Equal(65535, image[0, 0].PackedValue); } + [Theory] + [InlineData("P2", 100)] + [InlineData("P3", 100)] + [InlineData("P5", 100)] + [InlineData("P6", 100)] + [InlineData("P2", 1000)] + [InlineData("P3", 1000)] + [InlineData("P5", 1000)] + [InlineData("P6", 1000)] + public void Decode_NonStandardMaxPixelValue_ScalesSamplesToByteRange(string magic, int maxPixelValue) + { + // Six samples make six grayscale pixels or two color pixels. They include both ends of the range + // and values that do not fall on a whole 8-bit step, so a scale that loses precision is detected. + int[] samples = [0, 7, maxPixelValue / 2, maxPixelValue - 1, maxPixelValue, 3]; + bool isRgb = magic is "P3" or "P6"; + int width = isRgb ? samples.Length / 3 : samples.Length; + + byte[] header = Encoding.ASCII.GetBytes($"{magic}\n{width} 1\n{maxPixelValue}\n"); + byte[] payload; + if (magic is "P2" or "P3") + { + payload = Encoding.ASCII.GetBytes(string.Join(' ', samples)); + } + else if (maxPixelValue > 255) + { + // Binary samples above 8 bits are stored as two bytes, most significant byte first. + payload = samples.SelectMany(s => new[] { (byte)(s >> 8), (byte)s }).ToArray(); + } + else + { + payload = samples.Select(s => (byte)s).ToArray(); + } + + using Image image = Image.Load([.. header, .. payload]); + byte[] components = new byte[width * 3]; + image.CopyPixelDataTo(components); + + for (int i = 0; i < samples.Length; i++) + { + // A grayscale sample is decoded to equal red, green and blue components, so compare the red one. + byte actual = isRgb ? components[i] : components[i * 3]; + + // A 16-bit sample is rounded once to 16 bits and again to 8 bits, so allow one step of difference. + int expected = (int)Math.Round(samples[i] * 255D / maxPixelValue, MidpointRounding.AwayFromZero); + Assert.InRange(actual, expected - 1, expected + 1); + } + } + [Theory] [WithFile(RgbPlain, PixelTypes.Rgb24)] public void PbmDecoder_Decode_Resize(TestImageProvider provider) From bec20b1c2f7225e510e6c5a22b05fdd154c94448 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Oct 2026 12:39:05 +1000 Subject: [PATCH 2/3] Preserve PBM sample precision and partial rasters with stored regression inputs --- src/ImageSharp/Formats/Pbm/BinaryDecoder.cs | 220 ++++++++++++------ .../Pbm/BufferedReadStreamExtensions.cs | 23 +- src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs | 28 ++- src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs | 26 +-- src/ImageSharp/Formats/Pbm/PlainDecoder.cs | 155 +++++++++--- .../Formats/Pbm/PbmDecoderTests.cs | 208 ++++++++++++----- tests/ImageSharp.Tests/TestImages.cs | 5 + tests/Images/Input/Pbm/samples/byte-white.pgm | 3 + .../Pbm/samples/header-decimal-overflow.pgm | 3 + .../Input/Pbm/samples/p2-above-max100.pgm | 3 + .../Input/Pbm/samples/p2-above-max1000.pgm | 3 + .../Input/Pbm/samples/p2-decimal-overflow.pgm | 3 + tests/Images/Input/Pbm/samples/p2-max100.pgm | 3 + tests/Images/Input/Pbm/samples/p2-max1000.pgm | 3 + tests/Images/Input/Pbm/samples/p2-partial.pgm | 3 + .../Images/Input/Pbm/samples/p2-zero-max.pgm | 3 + .../Input/Pbm/samples/p3-above-max100.ppm | 3 + .../Input/Pbm/samples/p3-above-max1000.ppm | 3 + .../Input/Pbm/samples/p3-decimal-overflow.ppm | 3 + tests/Images/Input/Pbm/samples/p3-max100.ppm | 3 + tests/Images/Input/Pbm/samples/p3-max1000.ppm | 3 + tests/Images/Input/Pbm/samples/p3-partial.ppm | 3 + .../Images/Input/Pbm/samples/p3-zero-max.ppm | 3 + .../Input/Pbm/samples/p5-above-max100.pgm | 3 + .../Input/Pbm/samples/p5-above-max1000.pgm | 3 + tests/Images/Input/Pbm/samples/p5-max100.pgm | 3 + tests/Images/Input/Pbm/samples/p5-max1000.pgm | 3 + tests/Images/Input/Pbm/samples/p5-partial.pgm | 3 + .../Images/Input/Pbm/samples/p5-zero-max.pgm | 3 + .../Input/Pbm/samples/p6-above-max100.ppm | 3 + .../Input/Pbm/samples/p6-above-max1000.ppm | 3 + tests/Images/Input/Pbm/samples/p6-max100.ppm | 3 + tests/Images/Input/Pbm/samples/p6-max1000.ppm | 3 + tests/Images/Input/Pbm/samples/p6-partial.ppm | 3 + .../Images/Input/Pbm/samples/p6-zero-max.ppm | 3 + tests/Images/Input/Pbm/samples/plain-eof.pbm | 3 + .../Input/Pbm/samples/raster-whitespace.pbm | 3 + .../Input/Pbm/samples/raster-whitespace.pgm | 3 + .../Input/Pbm/samples/raster-whitespace.ppm | 3 + tests/Images/Input/Pbm/samples/wide-gray.pgm | 3 + tests/Images/Input/Pbm/samples/wide-rgb.ppm | 3 + tests/Images/Input/Pbm/samples/wide-white.pgm | 3 + 42 files changed, 581 insertions(+), 189 deletions(-) create mode 100644 tests/Images/Input/Pbm/samples/byte-white.pgm create mode 100644 tests/Images/Input/Pbm/samples/header-decimal-overflow.pgm create mode 100644 tests/Images/Input/Pbm/samples/p2-above-max100.pgm create mode 100644 tests/Images/Input/Pbm/samples/p2-above-max1000.pgm create mode 100644 tests/Images/Input/Pbm/samples/p2-decimal-overflow.pgm create mode 100644 tests/Images/Input/Pbm/samples/p2-max100.pgm create mode 100644 tests/Images/Input/Pbm/samples/p2-max1000.pgm create mode 100644 tests/Images/Input/Pbm/samples/p2-partial.pgm create mode 100644 tests/Images/Input/Pbm/samples/p2-zero-max.pgm create mode 100644 tests/Images/Input/Pbm/samples/p3-above-max100.ppm create mode 100644 tests/Images/Input/Pbm/samples/p3-above-max1000.ppm create mode 100644 tests/Images/Input/Pbm/samples/p3-decimal-overflow.ppm create mode 100644 tests/Images/Input/Pbm/samples/p3-max100.ppm create mode 100644 tests/Images/Input/Pbm/samples/p3-max1000.ppm create mode 100644 tests/Images/Input/Pbm/samples/p3-partial.ppm create mode 100644 tests/Images/Input/Pbm/samples/p3-zero-max.ppm create mode 100644 tests/Images/Input/Pbm/samples/p5-above-max100.pgm create mode 100644 tests/Images/Input/Pbm/samples/p5-above-max1000.pgm create mode 100644 tests/Images/Input/Pbm/samples/p5-max100.pgm create mode 100644 tests/Images/Input/Pbm/samples/p5-max1000.pgm create mode 100644 tests/Images/Input/Pbm/samples/p5-partial.pgm create mode 100644 tests/Images/Input/Pbm/samples/p5-zero-max.pgm create mode 100644 tests/Images/Input/Pbm/samples/p6-above-max100.ppm create mode 100644 tests/Images/Input/Pbm/samples/p6-above-max1000.ppm create mode 100644 tests/Images/Input/Pbm/samples/p6-max100.ppm create mode 100644 tests/Images/Input/Pbm/samples/p6-max1000.ppm create mode 100644 tests/Images/Input/Pbm/samples/p6-partial.ppm create mode 100644 tests/Images/Input/Pbm/samples/p6-zero-max.ppm create mode 100644 tests/Images/Input/Pbm/samples/plain-eof.pbm create mode 100644 tests/Images/Input/Pbm/samples/raster-whitespace.pbm create mode 100644 tests/Images/Input/Pbm/samples/raster-whitespace.pgm create mode 100644 tests/Images/Input/Pbm/samples/raster-whitespace.ppm create mode 100644 tests/Images/Input/Pbm/samples/wide-gray.pgm create mode 100644 tests/Images/Input/Pbm/samples/wide-rgb.ppm create mode 100644 tests/Images/Input/Pbm/samples/wide-white.pgm diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs index f83d3f74b8..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; @@ -35,29 +36,44 @@ internal class BinaryDecoder /// The color type of the encoded pixels. /// The data type of the pixel components. /// 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) + 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) { - ProcessGrayscale(configuration, pixels, stream, maxPixelValue); + ProcessGrayscale(configuration, pixels, stream); } else { - ProcessWideGrayscale(configuration, pixels, stream, maxPixelValue); + ProcessWideGrayscale(configuration, pixels, stream); } } else if (colorType == PbmColorType.Rgb) { if (componentType == PbmComponentType.Byte) { - ProcessRgb(configuration, pixels, stream, maxPixelValue); + ProcessRgb(configuration, pixels, stream); } else { - ProcessWideRgb(configuration, pixels, stream, maxPixelValue); + ProcessWideRgb(configuration, pixels, stream); } } else @@ -66,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. @@ -75,8 +162,7 @@ public static void Process(Configuration configuration, Buffer2D /// The configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - /// The maximum sample value declared in the header. - private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 1; @@ -88,19 +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; } - ScaleSamples(rowSpan, maxPixelValue); + Span samples = rowSpan[..(count * bytesPerPixel)]; Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL8Bytes( configuration, - rowSpan, - pixelSpan, - width); + samples, + pixelSpan[..count], + count); } } @@ -113,8 +200,7 @@ private static void ProcessGrayscale(Configuration configuration, Buffer /// The configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - /// The maximum sample value declared in the header. - private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 2; @@ -126,22 +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); - ScaleSamples(MemoryMarshal.Cast(rowSpan), maxPixelValue); + SwapSampleBytes(samples); Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL16Bytes( configuration, - rowSpan, - pixelSpan, - width); + samples, + pixelSpan[..count], + count); } } @@ -154,8 +242,7 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu /// The configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - /// The maximum sample value declared in the header. - private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 3; @@ -167,19 +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); } } @@ -192,8 +280,7 @@ private static void ProcessRgb(Configuration configuration, Buffer2DThe configuration. /// The pixel buffer to decode into. /// The stream to read the data from. - /// The maximum sample value declared in the header. - private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) where TPixel : unmanaged, IPixel { const int bytesPerPixel = 6; @@ -205,78 +292,59 @@ 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); - ScaleSamples(MemoryMarshal.Cast(rowSpan), maxPixelValue); + SwapSampleBytes(samples); Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb48Bytes( configuration, - rowSpan, - pixelSpan, - width); - } - } - - /// - /// 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. - /// - /// The row of big-endian sample data to convert in place. - private static void SwapSampleBytes(Span rowSpan) - { - if (BitConverter.IsLittleEndian) - { - Span samples = MemoryMarshal.Cast(rowSpan); - BinaryPrimitives.ReverseEndianness(samples, samples); + samples, + pixelSpan[..count], + count); } } /// - /// Scales a row of 8-bit samples in place from the range [0, maxval] to the full range [0, 255]. - /// Scaling happens on the samples, before they are converted to the destination pixel type, - /// so the result does not depend on the precision of that pixel type. + /// Reads one encoded row and zero-fills only missing bytes in its last incomplete pixel. /// - /// The row of samples to scale. - /// The maximum sample value declared in the header. - private static void ScaleSamples(Span samples, int maxPixelValue) + /// 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) { - // Samples that already use the full range are left as they are, so the common case costs nothing. - if (maxPixelValue == byte.MaxValue) + int bytesRead = stream.Read(row); + int count = bytesRead / bytesPerPixel; + if (bytesRead % bytesPerPixel != 0) { - return; + // Keep the available channels and high sample bytes. Missing bytes must not come from pooled storage. + count++; + row.Slice(bytesRead, (count * bytesPerPixel) - bytesRead).Clear(); } - for (int i = 0; i < samples.Length; i++) - { - samples[i] = (byte)PbmSampleScaler.Scale(samples[i], maxPixelValue, byte.MaxValue); - } + return count; } /// - /// Scales a row of native-endian 16-bit samples in place from the range [0, maxval] to the full range [0, 65535]. - /// Scaling happens on the samples, before they are converted to the destination pixel type, - /// so a narrower pixel type does not lose the precision of a small maxval before it is scaled. + /// 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. /// - /// The row of samples to scale. - /// The maximum sample value declared in the header. - private static void ScaleSamples(Span samples, int maxPixelValue) + /// The row of big-endian sample data to convert in place. + private static void SwapSampleBytes(Span rowSpan) { - // Samples that already use the full range are left as they are, so the common case costs nothing. - if (maxPixelValue == ushort.MaxValue) - { - return; - } - - for (int i = 0; i < samples.Length; i++) + if (BitConverter.IsLittleEndian) { - samples[i] = (ushort)PbmSampleScaler.Scale(samples[i], maxPixelValue, ushort.MaxValue); + Span samples = MemoryMarshal.Cast(rowSpan); + BinaryPrimitives.ReverseEndianness(samples, samples); } } 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 3c25ddc5cf..4c72e1ecbb 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -133,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) { - ThrowPrematureEof(); + 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()) + { + 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) @@ -162,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 { @@ -177,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) diff --git a/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs b/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs index e8af1143e6..ef2764af03 100644 --- a/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs +++ b/src/ImageSharp/Formats/Pbm/PbmSampleScaler.cs @@ -1,30 +1,26 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.Formats.Pbm; /// -/// Scales PGM and PPM samples from the range declared by the header maxval to the full range of the component type. +/// Normalizes PGM and PPM samples without rounding them to an intermediate integer format. /// internal static class PbmSampleScaler { /// - /// Scales a sample from the range [0, ] to the range [0, ], - /// rounding to the nearest value. Samples outside the declared range are clamped to . + /// Normalizes four encoded component magnitudes to [0, 1]. Values outside the declared range saturate at its endpoints. /// - /// The sample value read from the stream. + /// The RGB sample magnitudes and an alpha magnitude equal to Maxval. /// The maxval declared in the header, between 1 and 65535. - /// The maximum value of the component type, either 255 or 65535. - /// The scaled sample value. - public static int Scale(int value, int maxPixelValue, int targetMaxValue) + /// Normalized pixel components with opaque alpha. + public static Vector4 Scale(Vector4 value, int maxPixelValue) { - // Negative values can only come from an overflowing decimal sample; reinterpreting them as unsigned - // makes them clamp to maxval along with every other out-of-range sample. - uint sample = Math.Min((uint)value, (uint)maxPixelValue); - - // Adding half of the divisor before dividing rounds to the nearest value, so maxval maps exactly to - // targetMaxValue and the scale does not depend on maxval dividing the target range. - // The largest intermediate value is 65535 * 65535 + 32767, which fits in an unsigned 32-bit integer. - return (int)(((sample * (uint)targetMaxValue) + ((uint)maxPixelValue >> 1)) / (uint)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 0b468d5e98..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; @@ -26,29 +27,44 @@ internal class PlainDecoder /// The ColorType to decode. /// Data type of the pixles components. /// 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) + 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) { - ProcessGrayscale(configuration, pixels, stream, maxPixelValue); + ProcessGrayscale(configuration, pixels, stream); } else { - ProcessWideGrayscale(configuration, pixels, stream, maxPixelValue); + ProcessWideGrayscale(configuration, pixels, stream); } } else if (colorType == PbmColorType.Rgb) { if (componentType == PbmComponentType.Byte) { - ProcessRgb(configuration, pixels, stream, maxPixelValue); + ProcessRgb(configuration, pixels, stream); } else { - ProcessWideRgb(configuration, pixels, stream, maxPixelValue); + ProcessWideRgb(configuration, pixels, stream); } } else @@ -57,7 +73,82 @@ public static void Process(Configuration configuration, Buffer2D } } - private static void ProcessGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + /// + /// 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 { int width = pixels.Width; @@ -69,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)PbmSampleScaler.Scale(value, maxPixelValue, byte.MaxValue)); + rowSpan[x] = new L8((byte)Math.Clamp(value, 0, byte.MaxValue)); + count++; eofReached = !stream.SkipWhitespaceAndComments(); if (eofReached) { @@ -83,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) { @@ -93,7 +186,7 @@ private static void ProcessGrayscale(Configuration configuration, Buffer } } - private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + private static void ProcessWideGrayscale(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -105,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)PbmSampleScaler.Scale(value, maxPixelValue, ushort.MaxValue)); + rowSpan[x] = new L16((ushort)Math.Clamp(value, 0, ushort.MaxValue)); + count++; eofReached = !stream.SkipWhitespaceAndComments(); if (eofReached) { @@ -119,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) { @@ -129,7 +224,7 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu } } - private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + private static void ProcessRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -141,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) { @@ -180,7 +277,7 @@ private static void ProcessRgb(Configuration configuration, Buffer2D(Configuration configuration, Buffer2D pixels, BufferedReadStream stream, int maxPixelValue) + private static void ProcessWideRgb(Configuration configuration, Buffer2D pixels, BufferedReadStream stream) where TPixel : unmanaged, IPixel { int width = pixels.Width; @@ -192,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) || @@ -207,10 +305,11 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D stream.ReadDecimal(out int blue); rowSpan[x] = new Rgb48( - (ushort)PbmSampleScaler.Scale(red, maxPixelValue, ushort.MaxValue), - (ushort)PbmSampleScaler.Scale(green, maxPixelValue, ushort.MaxValue), - (ushort)PbmSampleScaler.Scale(blue, maxPixelValue, ushort.MaxValue)); + (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) { @@ -221,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) { @@ -243,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) { @@ -258,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 101a28c4ad..860fe0af01 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(SampleDirectory + "wide-gray.pgm").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(SampleDirectory + "wide-rgb.ppm").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(SampleDirectory + "byte-white.pgm").Bytes); Assert.Equal(255, image[0, 0].PackedValue); } @@ -154,63 +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(SampleDirectory + "wide-white.pgm").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("P2", 100)] - [InlineData("P3", 100)] - [InlineData("P5", 100)] - [InlineData("P6", 100)] - [InlineData("P2", 1000)] - [InlineData("P3", 1000)] - [InlineData("P5", 1000)] - [InlineData("P6", 1000)] - public void Decode_NonStandardMaxPixelValue_ScalesSamplesToByteRange(string magic, int maxPixelValue) + [InlineData(SampleDirectory + "p2-max100.pgm", 100, false)] + [InlineData(SampleDirectory + "p3-max100.ppm", 100, true)] + [InlineData(SampleDirectory + "p5-max100.pgm", 100, false)] + [InlineData(SampleDirectory + "p6-max100.ppm", 100, true)] + [InlineData(SampleDirectory + "p2-max1000.pgm", 1000, false)] + [InlineData(SampleDirectory + "p3-max1000.ppm", 1000, true)] + [InlineData(SampleDirectory + "p5-max1000.pgm", 1000, false)] + [InlineData(SampleDirectory + "p6-max1000.ppm", 1000, true)] + public void Decode_NonStandardMaxPixelValue_PreservesSamplePrecision(string imagePath, int maxPixelValue, bool isRgb) { - // Six samples make six grayscale pixels or two color pixels. They include both ends of the range - // and values that do not fall on a whole 8-bit step, so a scale that loses precision is detected. - int[] samples = [0, 7, maxPixelValue / 2, maxPixelValue - 1, maxPixelValue, 3]; - bool isRgb = magic is "P3" or "P6"; - int width = isRgb ? samples.Length / 3 : samples.Length; - - byte[] header = Encoding.ASCII.GetBytes($"{magic}\n{width} 1\n{maxPixelValue}\n"); - byte[] payload; - if (magic is "P2" or "P3") + // 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++) { - payload = Encoding.ASCII.GetBytes(string.Join(' ', samples)); + 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]); } - else if (maxPixelValue > 255) + } + + /// + /// Saturates sample magnitudes independently of the selected integrity mode. + /// + /// The stored input containing samples above Maxval. + [Theory] + [InlineData(SampleDirectory + "p2-above-max100.pgm")] + [InlineData(SampleDirectory + "p3-above-max100.ppm")] + [InlineData(SampleDirectory + "p5-above-max100.pgm")] + [InlineData(SampleDirectory + "p6-above-max100.ppm")] + [InlineData(SampleDirectory + "p2-above-max1000.pgm")] + [InlineData(SampleDirectory + "p3-above-max1000.ppm")] + [InlineData(SampleDirectory + "p5-above-max1000.pgm")] + [InlineData(SampleDirectory + "p6-above-max1000.ppm")] + [InlineData(SampleDirectory + "p2-decimal-overflow.pgm")] + [InlineData(SampleDirectory + "p3-decimal-overflow.ppm")] + public void Decode_SamplesAboveMaxval_SaturateInEveryIntegrityMode(string imagePath) + { + byte[] data = TestFile.Create(imagePath).Bytes; + + foreach (SegmentIntegrityHandling integrity in Enum.GetValues()) { - // Binary samples above 8 bits are stored as two bytes, most significant byte first. - payload = samples.SelectMany(s => new[] { (byte)(s >> 8), (byte)s }).ToArray(); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + using Image image = Image.Load(options, data); + Assert.Equal(new Rgb48(65535, 65535, 65535), image[0, 0]); } - else + } + + /// + /// 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(SampleDirectory + "p2-partial.pgm", false)] + [InlineData(SampleDirectory + "p5-partial.pgm", false)] + [InlineData(SampleDirectory + "p3-partial.ppm", true)] + [InlineData(SampleDirectory + "p6-partial.ppm", 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()) { - payload = samples.Select(s => (byte)s).ToArray(); + 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]); + } } + } - using Image image = Image.Load([.. header, .. payload]); - byte[] components = new byte[width * 3]; - image.CopyPixelDataTo(components); + /// + /// Keeps raster bytes whose values are whitespace or a comment marker. + /// + /// The stored binary input. + /// True for RGB samples. + [Theory] + [InlineData(SampleDirectory + "raster-whitespace.pgm", false)] + [InlineData(SampleDirectory + "raster-whitespace.ppm", 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 i = 0; i < samples.Length; i++) + for (int x = 0; x < image.Width; x++) { - // A grayscale sample is decoded to equal red, green and blue components, so compare the red one. - byte actual = isRgb ? components[i] : components[i * 3]; + 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(SampleDirectory + "raster-whitespace.pbm").Bytes); - // A 16-bit sample is rounded once to 16 bits and again to 8 bits, so allow one step of difference. - int expected = (int)Math.Round(samples[i] * 255D / maxPixelValue, MidpointRounding.AwayFromZero); - Assert.InRange(actual, expected - 1, expected + 1); + 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(SampleDirectory + "header-decimal-overflow.pgm").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) @@ -234,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(SampleDirectory + "p2-zero-max.pgm")] + [InlineData(SampleDirectory + "p3-zero-max.ppm")] + [InlineData(SampleDirectory + "p5-zero-max.pgm")] + [InlineData(SampleDirectory + "p6-zero-max.ppm")] + 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)); @@ -249,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(SampleDirectory + "plain-eof.pbm"); 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 25bebe9e8c..751e8021c8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1358,6 +1358,11 @@ public static class BigTiff public static class Pbm { + /// + /// Stored sample normalization and raster-boundary inputs. + /// + public const string SampleDirectory = "Pbm/samples/"; + 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 From 8ef4dcb628cd1d1d39e1ccdffe1f90a2eccdf047 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Oct 2026 12:47:35 +1000 Subject: [PATCH 3/3] Use named PBM input constants instead of directory concatenation --- .../Formats/Pbm/PbmDecoderTests.cs | 70 +++++++++---------- tests/ImageSharp.Tests/TestImages.cs | 39 +++++++++-- 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index 860fe0af01..8a6227c70b 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -116,7 +116,7 @@ public void Decode_WideBinary_MatchesReferenceDecoder(TestImageProvider< [Fact] public void Decode_WideBinaryGrayscale_SamplesAreBigEndian() { - using Image image = Image.Load(TestFile.Create(SampleDirectory + "wide-gray.pgm").Bytes); + using Image image = Image.Load(TestFile.Create(WideGrayPgm).Bytes); Assert.Equal(0x8000, image[0, 0].PackedValue); Assert.Equal(0x0080, image[1, 0].PackedValue); @@ -125,7 +125,7 @@ public void Decode_WideBinaryGrayscale_SamplesAreBigEndian() [Fact] public void Decode_WideBinaryRgb_SamplesAreBigEndian() { - using Image image = Image.Load(TestFile.Create(SampleDirectory + "wide-rgb.ppm").Bytes); + using Image image = Image.Load(TestFile.Create(WideRgbPpm).Bytes); Assert.Equal(new Rgb48(0x81B5, 0x8491, 0x8671), image[0, 0]); } @@ -133,7 +133,7 @@ public void Decode_WideBinaryRgb_SamplesAreBigEndian() [Fact] public void Decode_NonStandardByteMaxPixelValue_UpscalesToFullRange() { - using Image image = Image.Load(TestFile.Create(SampleDirectory + "byte-white.pgm").Bytes); + using Image image = Image.Load(TestFile.Create(ByteWhitePgm).Bytes); Assert.Equal(255, image[0, 0].PackedValue); } @@ -141,7 +141,7 @@ public void Decode_NonStandardByteMaxPixelValue_UpscalesToFullRange() [Fact] public void Decode_NonStandardShortMaxPixelValue_UpscalesToFullRange() { - using Image image = Image.Load(TestFile.Create(SampleDirectory + "wide-white.pgm").Bytes); + using Image image = Image.Load(TestFile.Create(WideWhitePgm).Bytes); Assert.Equal(65535, image[0, 0].PackedValue); } @@ -153,14 +153,14 @@ public void Decode_NonStandardShortMaxPixelValue_UpscalesToFullRange() /// The declared maximum sample value. /// True for three-channel samples. [Theory] - [InlineData(SampleDirectory + "p2-max100.pgm", 100, false)] - [InlineData(SampleDirectory + "p3-max100.ppm", 100, true)] - [InlineData(SampleDirectory + "p5-max100.pgm", 100, false)] - [InlineData(SampleDirectory + "p6-max100.ppm", 100, true)] - [InlineData(SampleDirectory + "p2-max1000.pgm", 1000, false)] - [InlineData(SampleDirectory + "p3-max1000.ppm", 1000, true)] - [InlineData(SampleDirectory + "p5-max1000.pgm", 1000, false)] - [InlineData(SampleDirectory + "p6-max1000.ppm", 1000, true)] + [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. @@ -189,16 +189,16 @@ public void Decode_NonStandardMaxPixelValue_PreservesSamplePrecision(string imag /// /// The stored input containing samples above Maxval. [Theory] - [InlineData(SampleDirectory + "p2-above-max100.pgm")] - [InlineData(SampleDirectory + "p3-above-max100.ppm")] - [InlineData(SampleDirectory + "p5-above-max100.pgm")] - [InlineData(SampleDirectory + "p6-above-max100.ppm")] - [InlineData(SampleDirectory + "p2-above-max1000.pgm")] - [InlineData(SampleDirectory + "p3-above-max1000.ppm")] - [InlineData(SampleDirectory + "p5-above-max1000.pgm")] - [InlineData(SampleDirectory + "p6-above-max1000.ppm")] - [InlineData(SampleDirectory + "p2-decimal-overflow.pgm")] - [InlineData(SampleDirectory + "p3-decimal-overflow.ppm")] + [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; @@ -217,10 +217,10 @@ public void Decode_SamplesAboveMaxval_SaturateInEveryIntegrityMode(string imageP /// The truncated stored input. /// True for a truncated RGB pixel; false for a truncated grayscale row. [Theory] - [InlineData(SampleDirectory + "p2-partial.pgm", false)] - [InlineData(SampleDirectory + "p5-partial.pgm", false)] - [InlineData(SampleDirectory + "p3-partial.ppm", true)] - [InlineData(SampleDirectory + "p6-partial.ppm", true)] + [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; @@ -247,8 +247,8 @@ public void Decode_PartialRaster_PreservesAvailableImageData(string imagePath, b /// The stored binary input. /// True for RGB samples. [Theory] - [InlineData(SampleDirectory + "raster-whitespace.pgm", false)] - [InlineData(SampleDirectory + "raster-whitespace.ppm", true)] + [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. @@ -268,7 +268,7 @@ public void Decode_BinaryRaster_DoesNotSkipSampleBytes(string imagePath, bool is [Fact] public void Decode_BinaryBitmap_DoesNotSkipRasterByte() { - using Image image = Image.Load(TestFile.Create(SampleDirectory + "raster-whitespace.pbm").Bytes); + using Image image = Image.Load(TestFile.Create(RasterWhitespacePbm).Bytes); for (int x = 0; x < 8; x++) { @@ -282,7 +282,7 @@ public void Decode_BinaryBitmap_DoesNotSkipRasterByte() [Fact] public void Decode_OverflowingHeader_RemainsFatal() { - byte[] data = TestFile.Create(SampleDirectory + "header-decimal-overflow.pgm").Bytes; + byte[] data = TestFile.Create(HeaderDecimalOverflowPgm).Bytes; foreach (SegmentIntegrityHandling integrity in Enum.GetValues()) { @@ -319,10 +319,10 @@ public void PbmDecoder_Decode_Resize(TestImageProvider provider) } [Theory] - [InlineData(SampleDirectory + "p2-zero-max.pgm")] - [InlineData(SampleDirectory + "p3-zero-max.ppm")] - [InlineData(SampleDirectory + "p5-zero-max.pgm")] - [InlineData(SampleDirectory + "p6-zero-max.ppm")] + [InlineData(P2ZeroMaxPgm)] + [InlineData(P3ZeroMaxPpm)] + [InlineData(P5ZeroMaxPgm)] + [InlineData(P6ZeroMaxPpm)] public void Decode_MaxPixelValueZero_ThrowsInvalidImageContentException(string imagePath) { byte[] bytes = TestFile.Create(imagePath).Bytes; @@ -334,7 +334,7 @@ public void Decode_MaxPixelValueZero_ThrowsInvalidImageContentException(string i [Fact] public void PlainText_PrematureEof() { - using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(SampleDirectory + "plain-eof.pbm"); + 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 751e8021c8..f8bcf0d21e 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1358,10 +1358,41 @@ public static class BigTiff public static class Pbm { - /// - /// Stored sample normalization and raster-boundary inputs. - /// - public const string SampleDirectory = "Pbm/samples/"; + 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";