Skip to content

Scale PGM and PPM samples to the full range before converting them to the pixel type - #3227

Open
jafin wants to merge 1 commit into
SixLabors:mainfrom
jafin:jf/pbm-maxval-scaling
Open

jafin wants to merge 1 commit into
SixLabors:mainfrom
jafin:jf/pbm-maxval-scaling

Conversation

@jafin

@jafin jafin commented Oct 17, 2026 •

Copy link
Copy Markdown

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Problem

The decoder handled a maxval other than 255 or 65535 in two steps:

  1. It converted the raw samples to the destination pixel type.
  2. It ran a Brightness pass over the whole image.

Step 2 happened after any loss of precision in step 1. A 16-bit sample with a small maxval was narrowed to 8 bits before it was scaled. As a result, decoding to an 8-bit pixel type, such as Rgb24 (the default for Image.Load), kept only a few distinct levels. For example, with maxval 1000:

  • Sample 500 decoded to 131 instead of about 128.
  • Sample 7 decoded to 0 instead of 2.
  • The image had only five gray levels.

Change

  • The plain and binary decoders now scale each sample from [0, maxval] to the full 8-bit or 16-bit range before they convert the row to the pixel type.
  • The scaling uses rounded integer arithmetic. maxval maps exactly to the full value, and samples above maxval are clamped.
  • The new PbmSampleScaler.Scale method does the scaling for both decoders.
  • The binary decoder skips the scaling step when maxval already covers the full range.
  • The Brightness pass over the decoded image is removed. Images with a non-standard maxval now need one pass less.

Testing

The new theory Decode_NonStandardMaxPixelValue_ScalesSamplesToByteRange in PbmDecoderTests covers P2, P3, P5 and P6 with maxval 100 and 1000. It decodes to Rgb24 and compares each component with round(sample * 255 / maxval), with a tolerance of one step. The test inputs are built in memory.

On main, the four maxval 1000 cases fail and the maxval 100 cases pass. With this change, all eight cases pass.

I ran all tests in the Pbm namespace in Release on net10.0: 121 passed. These include the existing reference-image comparisons for maxval 15 inputs and the round-trip tests.

Fixes #3239

… 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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PGM/PPM images with a 16-bit maxval other than 65535 decode to only a few gray levels

1 participant