Process Every Image in a Netpbm Stream with pamexec
You will finish with a repeatable way to run one shell command for every image in a Netpbm stream, while keeping the input intact and checking what happened. The examples use Netpbm 11.5.2 from Debian package netpbm 2:11.05.02-1.1build1, which is the version installed on this machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a shell, pamexec, a Netpbm image stream and the command you want to run for each image. The normal examples are unprivileged. Do not add sudo unless the input or destination is genuinely protected, and check the command first as the account that will run it.
1. Check the installed command
Start by confirming which executable and Netpbm build you are using. This is read-only:
$ command -v pamexec
/usr/bin/pamexec
$ pamexec --version
pamexec: Using libnetpbm from Netpbm Version: Netpbm 11.5.2
pamexec: Built from source dated 2024-03-31 09:09:47
pamexec: Built by Debian
pamexec: Use 'man pamexec' for help.
The command's basic shape is pamexec [command] [netpbmfile]. The command is a shell command, not merely an executable name, so quote it when it contains arguments. The input file is optional and defaults to standard input. Use - explicitly when that makes a pipeline easier to read.
Checkpoint: if command -v finds a different copy, or the version differs, keep that fact beside your test results. Option details and failure behaviour can vary between Netpbm builds.
2. Confirm that the input is a stream
A Netpbm stream can contain multiple images. Before processing it, inspect the first image without changing the file:
$ pamfile /path/to/input.pam
/path/to/input.pam: PAM, 640 by 480 pixels, 3 channels, depth 8, RGB, true
$ test -s /path/to/input.pam && echo "input is non-empty"
input is non-empty
Your pamfile wording will reflect the actual format. The useful checks are that the path is readable, the file is non-empty and the first image has the dimensions and format you expect. A single-image file is still valid input, but it gives you no way to notice whether a later command accidentally stops after the first image.
For a harmless local test, make a two-image plain PGM stream. The redirection creates or replaces only the temporary file, so do not point it at a useful image:
$ printf 'P2\n1 1\n255\n7\nP2\n1 1\n255\n9\n' > /tmp/pamexec-two-images.pgm
$ pamfile /tmp/pamexec-two-images.pgm
/tmp/pamexec-two-images.pgm: PGM plain, 1 by 1 maxval 255
pamfile reports the first image. That is normal. pamexec is the part that will split the stream into one image per child invocation.
3. Run a command once per image
Use a command that consumes its complete standard input. pamfile is a useful diagnostic because it reads one image and reports its format:
$ pamexec pamfile /tmp/pamexec-two-images.pgm
stdin: PGM raw, 1 by 1 maxval 255
stdin: PGM raw, 1 by 1 maxval 255
Two lines confirm that the command ran twice. The child receives its image on standard input, so the child sees stdin rather than the original file name. Its output is written to the parent's standard output and its diagnostics go to standard error in the usual shell fashion.
To use a pipeline, leave the file argument out or pass -:
$ cat /tmp/pamexec-two-images.pgm | pamexec - pamfile
stdin: PGM raw, 1 by 1 maxval 255
stdin: PGM raw, 1 by 1 maxval 255
Do not put a second pipeline inside the command unless you understand which process receives the image. In particular, pamexec pamfile | other-command sends pamfile's text to the next command, not the original images.
4. Produce a multi-image output
The usual purpose is to transform every input image and preserve the resulting stream. The Netpbm manual gives this pattern for an animated GIF:
$ pamexec pamtogif myvideo.ppm | gifsicle --multifile > myvideo.gif
Here pamtogif receives one image at a time. gifsicle --multifile then reads the resulting sequence. The exact command after pamexec must accept the output format produced by the child. Test the first image and the final file before using a long or unattended batch.
When replacing a valuable destination, avoid truncating it before conversion succeeds. Write to a new name, verify it, then replace the old file only as an explicit final action:
$ pamexec pamtogif /path/to/source.ppm | gifsicle --multifile > /path/to/movie.gif.new
$ test -s /path/to/movie.gif.new && file /path/to/movie.gif.new
$ mv -- /path/to/movie.gif.new /path/to/movie.gif
The last command changes state and can overwrite the existing destination. Keep a backup if the old file matters. If conversion fails, remove the incomplete .new file after checking that the original is still present; do not automate that deletion until the workflow is trusted.
5. Select images before execution
pamexec processes every image it receives. If only some images are wanted, put pampick before it:
$ pampick 0,2 /path/to/input.pam | pamexec pamfile -
stdin: PAM ...
stdin: PAM ...
The exact pampick selection syntax and output depend on the installed pampick version, so check its own manual before using an index expression. The important boundary is the pipe: pampick decides which images enter the stream, and pamexec runs the child on each image that remains.
For a different workflow, pamsplit can write one file per image before you process those files. That is useful when the downstream tool cannot read standard input, but it creates temporary files and requires cleanup. Prefer the stream form when the child already accepts Netpbm input.
6. Make sure the child consumes its input
pamexec assumes the child reads all of its image from standard input. A child that exits early can leave unread bytes in the pipe. The next invocation then starts with the wrong data, producing errors or misleading results.
If the tool only needs a small part of each image, buffer each image through a temporary file and make the child read that file. The manual illustrates the pattern with cat and head:
$ pamexec "cat >/tmp/x; head --lines=3 x" /path/to/myvideo.ppm
This example is deliberately limited: every iteration reuses /tmp/x, so it is unsuitable for concurrent jobs and exposes the temporary path to other local processes. For a real batch, use a private temporary directory with restrictive permissions, a unique file per iteration or a wrapper that handles cleanup, and avoid placing untrusted image-derived names into the shell command.
7. Treat failed children as a tested edge case
Without --check, the installed command can continue after a child returns non-zero. That makes a batch look successful unless you inspect the child's output and your own logs. Test your actual failure behaviour with a harmless two-image stream:
$ pamexec 'sh -c "exit 7"' /tmp/pamexec-two-images.pgm
$ printf 'pamexec status: %s\n' "$?"
pamexec status: 0
On this Netpbm 11.5.2 build, the default run returns zero even though the child fails. Do not use that status alone as a success signal.
The --check option is intended to stop when a command has a non-zero status:
$ pamexec --check false /tmp/pamexec-two-images.pgm
pamexec: Failed on image 0: Command 'false' terminated abnormally or with nonzero exit status
There is a local version-specific trap: this installed build prints that diagnostic and then exits with status 139, which is a segmentation fault status, rather than returning a clean ordinary failure. Reproduce it in a disposable test before relying on --check in automation:
$ pamexec --check false /tmp/pamexec-two-images.pgm >/tmp/pamexec.out 2>/tmp/pamexec.err
$ printf 'status: %s\n' "$?"
status: 139
If your system behaves the same way, record the failure, do not treat status 139 as a normal child error, and consider upgrading or reporting the package issue before putting this path in production. If you need reliable batch control now, wrap the child so it records per-image results, and test the wrapper against representative inputs. Do not silently discard failed images.
Done means
- You confirmed the executable and Netpbm version used by the job.
- You checked that the input is readable, non-empty and in the expected Netpbm format.
- You verified a two-image test produces two child invocations.
- Your child command consumes all of standard input and accepts the image format it receives.
- Any replacement output is written to a new file and checked before it can replace the old one.
- You tested child failure handling, including the local
--checkstatus-139 behaviour.