Skip to content

File formats

The pipeline writes two file pairs: a RIP payload and an encoded print job. Each has a versioned JSON header and a packed binary file protected by CRC-32.

RIP payload: rip.json + rip.bin

rip.py writes this pair and encoder.py reads it. The binary contains each halftoned pass as 1-bit bitmaps packed with np.packbits.

// rip.json (sibling: rip.bin), example for c6n90
{
  "format": "paintress-rip",
  "format_version": "0.2.0",
  "metadata": {
    "dpi": 630, "image_width_px": 0, "nozzle_count": 90,
    "passes_per_band": 7,
    // which head this payload was made for; the encoder packs for it
    "head_layout": {
      "name": "c6n90", "nozzle_pitch_npi": 90, "column_length": 90,
      "channel_order": ["C", "M", "Y", "K", "LC", "LM"],
      "ink_map": ["K", "Y", "LM", "LC", "C", "M"],
      "band_step_nozzles": 90, "lead_in_bands": 0, "lead_in_mm": 0.0,
      "slots": [ { "column": 0, "first_nozzle": 0, "nozzle_count": 90,
                   "physical_nozzle_count": 90, "ink": "K" } ]
    },
    "ink_map": ["K", "Y", "LM", "LC", "C", "M"],
    "processing": {}
  },
  "passes": { "y_positions_mm": [], "y_deltas_mm": [] },
  "array": { "shape": [21, 6, 90, 2126], "dtype": "uint8", "packing": "packbits" },
  "data": { "file": "rip.bin", "byte_count": 0, "crc32": "0x..." }
}

The array shape is (passes, channels, nozzles, columns), so it varies with the head: (P, 6, 90, W) on c6n90, (P, 4, 60, W) on c4n180. A payload with no head_layout block is read as c6n90, which keeps older payloads loadable.

Printer job: job.json + job.bin

encoder.py writes this pair for the daemon. The binary stores 147-byte lines consecutively, in pass order. The header indexes them with per-pass line_count values and contiguous offsets.

// job.json  (sibling: job.bin)
{
  "format_version": "0.1.0",
  "geometry_fingerprint": "0x108865B9",   // the FRAME; must match the firmware
  "head_name": "c6n90",                   // WHICH HEAD; checked host-side
  "head_fingerprint": "0x9C244CD5",
  "dpi": 630, "bytes_per_line": 147, "total_passes": 42,
  "print_width_mm": 49.7, "print_height_mm": 229.0, "padded_width_mm": 60.5,
  // one entry per plumbed ink (per the encoder's ink map)
  "channel_offsets_px": { "M": 0, "C": 23, "LC": 174, "LM": 197, "Y": 348, "K": 371 },
  "passes": [ { "y_position_mm": 0.0, "y_delta_mm": 0.04, "line_count": 0 } ],
  "data": { "file": "job.bin", "byte_count": 0, "line_count": 0, "crc32": "0x..." }
}

The container is defined once, in paintress-protocol's paintress_job module, and vendored by the encoder and the daemon.

The two fingerprints

A job has separate fingerprints for its frame and head. Both are CRC-32 values calculated from canonical strings by the paintress-protocol generator.

Covers Written as Checked against A change means
Frame line size, clocks, edges, packing, PSRAM line size geometry_fingerprint the firmware's FRAME_HASH, reported at IDENTIFY as profile_hash reflash
Head head name, nozzle count, channel list head_name + head_fingerprint the daemon's --head re-RIP

The frame fingerprint describes the data sent by the firmware. Heads sharing that frame can use the same binary. The daemon checks the job's head against its machine configuration because the board cannot detect the physical head.

Key sizes

Constant Value Meaning
bytes_per_line (USB line) 147 Packed column on the wire and in the job. See Printhead data protocol
PSRAM line 196 The same column expanded to 49 32-bit words for DMA
FRAME_HASH 0x108865B9 Frame fingerprint, shared by both shipped heads
head_fingerprint 0x9C244CD5 (c6n90), 0x93255450 (c4n180) Which head a job was packed for