Files
Dylan M. Taylorandsubha-2025 0ff43c72b3 Add craft-raster-codecs: DDS, DICOM, FITS, IFF/ILBM, PCX, PNM and XPM from PhotoCraft (#880)
* Add craft-raster-codecs: DDS, DICOM, FITS, IFF/ILBM, PCX, PNM and XPM from PhotoCraft

One module per format, moved from photocraft-codecs at 63b641b5 with its tests.
Decoders return one flat Image and take Limits; encoders take an image already in a
layout the format stores. Layout planning, colour and the document mapping stay in the app.

* craft-raster-codecs: PCX reads four 8-bit planes as RGBA and version 4 headers; pcx::dimensions

Four 8-bit planes are red, green, blue and alpha: they decode to RGBA instead of being
refused. Version 4 (PC Paintbrush for Windows) headers are accepted. pcx::dimensions gives the
pixel size from the header without decoding.

Ported from the PCX reader of storytold/designcraft#2011.

---------

Co-authored-by: subha-2025 <218455239+subha-2025@users.noreply.github.com>
2026-11-11 18:37:51 -05:00

3.4 KiB

craft-raster-codecs

Small raster image formats for the Craft apps, from PhotoCraft's photocraft-codecs: one module per format, one implementation of each. Decoders take untrusted bytes and Limits, check the declared size before allocating, and never panic. No unsafe; it builds for wasm32-unknown-unknown. Dependencies: thiserror, and rayon on native targets (the DDS encoder compresses rows of blocks in parallel).

craft-raster-codecs = { git = "https://github.com/storytold/craft-libs", rev = "<commit>" }
Module Format Read Write
dds DirectDraw Surface: uncompressed masks, luminance, BC1/BC2/BC3 (DXT1/3/5), legacy and DX10 headers; top mip level yes uncompressed and BC1-BC3, optional mip chain
dicom DICOM Part 10, uncompressed transfer syntaxes: MONOCHROME1/2, RGB, YBR_FULL, 8 and 16 bits yes no
fits FITS primary HDU or first IMAGE extension, every BITPIX, BSCALE/BZERO, header cards as text yes gray or RGB; 8-bit, 16-bit, float
iff IFF ILBM (1 to 8 planes with a palette, 24 planes, mask plane or transparent colour) and PBM, stored or ByteRun1; HAM and EHB are refused yes 24-plane ILBM with an optional mask plane
pcx ZSoft PCX: RLE scanlines, 16 and 256-colour palettes, 8-bit three-plane RGB and four-plane RGBA; a dimensions probe yes no
pnm Netpbm: PBM, PGM, PPM (plain and raw, 8 and 16 bits), PAM, PFM yes PGM, PPM, PAM, PFM
xpm X PixMap: XPM3 and XPM2, X11 colour names yes no

The model

Image is a flat raster: width, height, a ChannelLayout (gray, RGB or CMYK, each with or without straight alpha), a SampleType (U8, U16, F32) and interleaved, native-endian samples, top row first. It also carries what these formats state beside the pixels: dpi (PCX), text (FITS header cards) and warnings (a FITS cube opened as its first plane).

use craft_raster_codecs::{Limits, pnm};

let image = pnm::decode(b"P2\n2 1\n255\n0 255\n", &Limits::default())?;
assert_eq!(image.data(), &[0, 255]);
let bytes = pnm::encode(&image)?;

Each module has a looks_like_* (or is_dicom) sniffer for the format's signature. dds and fits also have has_additional_images, which says without decoding whether a file holds more than the one image decode returns (cube faces, array slices, further HDUs).

Encoders take an image already in a layout and depth the format stores, and return CodecError::Encode for any other. Choosing the nearest layout and converting to it stays in the app. The same goes for colour management, orientation, format detection across a whole format list, and mapping Image onto the app's own document.

Limits

Limits bounds width, height, pixel count and the bytes of the decoded buffer. The default is 2^18 pixels a side, 2^30 pixels and 8 GiB (1 GiB on 32-bit targets). Compressed formats (IFF, PCX) also refuse a file too small to hold what its header declares, before allocating. Text formats (plain PNM, XPM) reserve from the bytes present, not from the header.

Tests

cargo test -p craft-raster-codecs. tests/roundtrip.rs writes every layout and depth each writer stores and reads it back exactly, then truncates, bit-flips and overwrites header bytes of a valid file of every format: a reader may refuse, never panic. Two tests are ignored by default: a DDS encode timing, and a PNM peak-memory check that reads the process's RSS.