Reduce PNM Colours with a Controlled Palette Using pnmremap
You will finish with a PNM image whose pixels are restricted to a palette you provide, plus a repeatable way to handle input colours that are not exact palette entries. The examples use Netpbm 11.5.2, installed here as package version 2:11.05.02-1.1build1. Allow about fifteen minutes if your input image and palette already exist.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need the netpbm package, a readable PNM input, and a palette image. This guide does not alter the input. It writes a new image to standard output, so ordinary shell permissions are normally enough. You do not need sudo.
1. Check the installed command
Confirm which executable will run and record its version before relying on a scripted result:
$ command -v pnmremap
/usr/bin/pnmremap
$ pnmremap --version
pnmremap: Using libnetpbm from Netpbm Version: Netpbm 11.5.2
pnmremap: Built from source dated 2024-03-31 09:09:47
$ dpkg-query -W -f='${Package} ${Version}\n' netpbm
netpbm 2:11.05.02-1.1build1
The manual page documents the option as -mapfile=PALETTEFILE. Netpbm also accepts two hyphens, whitespace or an equals sign between an option and its value, and unique abbreviations. Use the full spelling in scripts so that a later option addition cannot make an abbreviation ambiguous.
2. Inspect the input and palette
A palette is simply a PNM image. Its dimensions and pixel arrangement do not matter, although a one-row image with one pixel per entry is easy to review. Duplicate entries are allowed. The output takes its tuple type and maximum sample value from the palette, so inspect both files before mapping:
$ pamfile /path/to/input.pam /path/to/palette.pam
/path/to/input.pam: PAM, 1920 by 1080 by 3 maxval 255
/path/to/palette.pam: PAM, 8 by 1 by 3 maxval 255
$ test -r /path/to/input.pam && echo 'input is readable'
$ test -r /path/to/palette.pam && echo 'palette is readable'
Your dimensions will differ. The useful checks are that both files are readable PNM-family images and that the palette has the tuple depth you intend. PNM input and palette images are supported for the normal grayscale, black-and-white and RGB cases. Exotic PAM tuple depths can fail when the two depths cannot be converted.
Checkpoint
Decide whether the palette's maximum sample value is deliberate. For example, an eight-entry palette with maxval 1 produces output with maxval 1, not a 255-level RGB image.
3. Map to the palette without dithering
The basic command maps each input pixel independently. An exact palette match remains that same palette entry. An unmatched pixel goes to the palette entry with the smallest Cartesian distance in gamma-adjusted red, green and blue brightness space:
$ pnmremap -mapfile=/path/to/palette.pam \
/path/to/input.pam > /path/to/reduced.pam
$ printf 'pnmremap status: %s\n' "$?"
pnmremap status: 0
$ pamfile /path/to/reduced.pam
/path/to/reduced.pam: PAM, 1920 by 1080 by 3 maxval 255
The default is -nofloyd, so this is ordinary per-pixel quantisation. A successful exit status says that the file was produced; it does not prove that the visual result suits your use case. Inspect the image with a trusted viewer or continue with an image comparison step.
Do not redirect straight over a useful output. Shell > truncates its destination before pnmremap starts. Write to a new name first, inspect it, then replace the old file only after you are satisfied:
$ pnmremap -mapfile=/path/to/palette.pam \
/path/to/input.pam > /path/to/reduced.pam.new
$ test -s /path/to/reduced.pam.new && echo 'new output is non-empty'
$ mv /path/to/reduced.pam.new /path/to/reduced.pam
If the conversion fails, the original reduced.pam remains untouched. Remove the incomplete .new file after checking the error. The mv command is the state-changing step; take a backup first if the old output cannot be regenerated.
4. Choose a policy for unmatched colours
Nearest-palette matching is a sensible default, but it is not always the desired rule. -firstisdefault sends every unmatched input colour to the palette pixel at the top left. -missingcolor=COLOURSPEC adds a specified fallback colour to the palette and uses it for every unmatched input colour:
$ pnmremap -mapfile=/path/to/palette.pam \
-firstisdefault /path/to/input.pam > /path/to/first-default.pam
$ pnmremap -mapfile=/path/to/palette.pam \
-missingcolor=white /path/to/input.pam > /path/to/white-default.pam
These two options require the input and palette maximum sample values to match. If the files report different maxvals, normalise them first with an appropriate Netpbm tool, then inspect the headers again. The fallback colour may already be in the palette, but it is treated as part of the palette whether or not it appears in the image.
There is no undo inside pnmremap. These commands do not change the sources, but a careless redirect can overwrite a destination. Keep the original input and use a new output path until the result has been checked.
5. Add Floyd-Steinberg dithering when banding matters
Use -floyd, or its synonym -fs, when a small palette produces visible bands. The algorithm spreads quantisation error across neighbouring pixels, creating a dot pattern that can blend into intermediate tones at normal viewing distance. It costs substantially more CPU time and can make pixel-level comparisons harder. The default remains -nofloyd, also available as -nofs.
$ pnmremap -mapfile=/path/to/palette.pam \
-floyd -randomseed=42 /path/to/input.pam > /path/to/dithered.pam
$ pamfile /path/to/dithered.pam
/path/to/dithered.pam: PAM, 1920 by 1080 by 3 maxval 255
Floyd-Steinberg uses random initial error values by default, so two runs can differ. Use -norandom for a predictable zero-initialised accumulator, or use -randomseed=42 for repeatable randomisation. Do not combine those options. The seed is useful for tests and reproducible builds; it is not a quality setting.
Checkpoint
If you need byte-for-byte repeatability, run the same command twice with a fixed seed and compare the outputs:
$ pnmremap -mapfile=/path/to/palette.pam -floyd -randomseed=42 \
/path/to/input.pam > /tmp/remap-a.pam
$ pnmremap -mapfile=/path/to/palette.pam -floyd -randomseed=42 \
/path/to/input.pam > /tmp/remap-b.pam
$ cmp --silent /tmp/remap-a.pam /tmp/remap-b.pam
$ printf 'cmp status: %s\n' "$?"
cmp status: 0
6. Diagnose failures without guessing
If pnmremap rejects an option, check its spelling against man pnmremap. All options can be abbreviated only to a shortest unique prefix, and -mapfile is mandatory. If it cannot open a file, check the exact path and permission:
$ ls -l /path/to/input.pam /path/to/palette.pam
$ test -r /path/to/input.pam && echo 'input readable'
$ test -r /path/to/palette.pam && echo 'palette readable'
A depth mismatch outside the documented RGB and grayscale conversions is an input-design problem, not something to solve by adding random flags. Convert the input or palette to a compatible PNM representation, then rerun pamfile. If a fallback option fails, compare maxvals before trying elevated privileges. Root access cannot make incompatible image metadata compatible.
For a concise mapping report, add -verbose. It reports helpful information about the mapping process but does not change the mapping rule. Keep verbose output separate from the image: redirect only standard output to the image and allow diagnostics to remain visible on the terminal.
Done means
pnmremapand its Netpbm version were confirmed on the host.- The input and palette were inspected with
pamfile, including tuple depth and maxval. - The output was written to a new path and its header was checked.
- You chose nearest matching, first-entry fallback or an explicit fallback colour deliberately.
- Dithering is enabled only when its visual trade-off is wanted, with a fixed seed when repeatability matters.
- The original input and any previous output remain recoverable.