Skip to content

Add PGM, PBM, and QOI format support#99

Merged
Hexagon merged 4 commits intomainfrom
copilot/support-common-formats
Mar 22, 2026
Merged

Add PGM, PBM, and QOI format support#99
Hexagon merged 4 commits intomainfrom
copilot/support-common-formats

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 21, 2026

Extends Netpbm coverage to the full P1–P6 family and adds QOI as a modern lossless option, completing the most practical gap in common format support.

New formats

PGM — Portable GrayMap (src/formats/pgm.ts)

  • Decode: P2 (ASCII) and P5 (binary, 1 byte/pixel); maxval scaling, comments
  • Encode: P5 binary via standard luma formula (0.299R + 0.587G + 0.114B)

PBM — Portable BitMap (src/formats/pbm.ts)

  • Decode: P1 (ASCII 0/1) and P4 (packed bits, MSB-first, row-padded)
  • Encode: P4 binary with luma threshold at 128; PBM convention: 0=white, 1=black

QOI — Quite OK Image (src/formats/qoi.ts)

  • Full spec-compliant encode/decode: all six chunk types (RGB, RGBA, INDEX, DIFF, LUMA, RUN), 64-entry running color array, big-endian header, end marker
  • Encoder uses byte-wrapping arithmetic for DIFF/LUMA diffs, matching the reference phoboslab/qoi signed char semantics
  • Decoder validates end marker, checks bounds before reading chunk payloads, and throws on incomplete pixel streams or truncated/corrupt data
  • extractMetadata validates channels (3 or 4) and returns undefined for unsupported values
import { Image } from "jsr:@cross/image";

const img = await Image.decode(await Deno.readFile("photo.png"));
await Deno.writeFile("photo.qoi", await img.encode("qoi"));

const gray = await img.encode("pgm");  // P5 grayscale
const mono = await img.encode("pbm");  // P4 packed monochrome

Wiring

  • All three formats registered in src/image.ts and exported from mod.ts
  • 63 new tests (19 PGM + 19 PBM + 25 QOI including wraparound verification and negative decode tests)
  • Updated README.md, CHANGELOG.md, and docs/src/formats.md

⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/cross-org/image/sessions/87a6b200-83c3-4461-8907-3df4836d773c
Copilot AI changed the title [WIP] Continue work on fully supporting all commonly used formats Add PGM, PBM, and QOI format support Mar 21, 2026
Copilot AI requested a review from Hexagon March 21, 2026 20:56
@Hexagon Hexagon requested a review from Copilot March 21, 2026 20:57
@Hexagon
Copy link
Copy Markdown
Member

Hexagon commented Mar 21, 2026

@copilot compare qoi implementation to https://github.com/phoboslab/qoi

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds three additional image formats (PGM, PBM, QOI) to the library’s pluggable format system, expanding Netpbm coverage and introducing QOI as a modern lossless option.

Changes:

  • Implemented new format handlers: PGMFormat (P2/P5), PBMFormat (P1/P4), and QOIFormat (encode/decode).
  • Registered/exported the new formats through src/image.ts and mod.ts.
  • Added comprehensive tests and updated docs/README/changelog to reflect new format support.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/formats/qoi.ts Adds QOI encode/decode + metadata extraction.
src/formats/pgm.ts Adds PGM (P2/P5) decode + P5 encode.
src/formats/pbm.ts Adds PBM (P1/P4) decode + P4 encode.
src/image.ts Registers PGM/PBM/QOI formats for auto-detection.
mod.ts Exports the new format classes and updates module docs.
test/formats/qoi.test.ts New tests for QOI behavior and round-trips.
test/formats/pgm.test.ts New tests for PGM decode/encode and error cases.
test/formats/pbm.test.ts New tests for PBM decode/encode and error cases.
README.md Updates supported formats list/table.
docs/src/formats.md Documents new format support in matrices/details.
CHANGELOG.md Notes added format support in Unreleased section.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +95 to +99
// Reserve 8 bytes for end marker at tail
const dataEnd = data.length - 8;

while (pixelIndex < pixelCount && offset < dataEnd) {
const byte = data[offset++];
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QOI decoding currently assumes the last 8 bytes are the end marker (via dataEnd = data.length - 8) and will silently ignore them even if the marker is missing/incorrect. It also returns successfully even if the loop exits before producing width*height pixels (e.g., truncated files), leaving the output buffer partially uninitialized. Consider verifying the 8-byte end marker, and throwing if pixelIndex !== pixelCount after parsing.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +105
if (byte === QOI_OP_RGB) {
r = data[offset++];
g = data[offset++];
b = data[offset++];
// a unchanged
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decoder reads chunk payload bytes without checking that enough bytes remain before dataEnd (e.g., QOI_OP_RGB needs 3 bytes, QOI_OP_RGBA needs 4, QOI_OP_LUMA needs 1). With truncated/corrupt input this can read past the intended pixel stream and yield incorrect output instead of throwing. Add bounds checks before consuming chunk payload bytes and throw a descriptive error on premature EOF.

Copilot uses AI. Check for mistakes.
Comment on lines +317 to +328
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined> {
if (!this.canDecode(data)) return Promise.resolve(undefined);

const channels = data[12];

return Promise.resolve({
format: "qoi",
compression: "none",
frameCount: 1,
bitDepth: 8,
colorType: channels === 4 ? "rgba" : "rgb",
});
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractMetadata() treats any non-4 channels value as RGB (channels === 4 ? "rgba" : "rgb"). For malformed QOI headers (e.g., channels=0/2/5) this returns misleading metadata even though decode() rejects such files. Consider validating channels (3 or 4) here too and returning undefined (or a safe fallback) when unsupported.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +49
test("QOI: decode - invalid data throws", async () => {
const format = new QOIFormat();

await assertRejects(
async () => await format.decode(new Uint8Array([0, 1, 2, 3])),
Error,
"Invalid QOI signature",
);
});
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QOI tests cover invalid signature and encode-side length mismatch, but there are no negative decode tests for truncated/corrupt QOI streams (e.g., missing/incorrect 8-byte end marker, truncated QOI_OP_RGB/RGBA payload, or pixel stream ending before width*height). Adding these would prevent silent partial decodes if decoder validation is tightened.

Copilot uses AI. Check for mistakes.
@Hexagon
Copy link
Copy Markdown
Member

Hexagon commented Mar 21, 2026

@copilot apply changes based on the comments in this thread

…ng reference qoi.h)

Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/cross-org/image/sessions/e7a44ae9-80eb-4cd0-9a7b-f21aad38724d
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 21, 2026

@copilot compare qoi implementation to https://github.com/phoboslab/qoi

Compared against the reference phoboslab/qoi (qoi.h). The implementation matches on all spec-critical points:

  • ✅ Hash function: (r*3 + g*5 + b*7 + a*11) % 64
  • ✅ Initial pixel: {0, 0, 0, 255}
  • ✅ All 6 chunk types with correct tag values, bit layouts, and biases
  • ✅ 8-bit tags checked before 2-bit tags (per spec precedence)
  • ✅ Run length 1–62, bias -1; end marker 0x00×7 + 0x01
  • ✅ Big-endian header, decoder wrapping via & 0xff

Found one difference: the encoder was computing channel diffs as plain integers ((r - prevR) | 0) instead of using byte-wrapping arithmetic. The reference C code uses signed char which naturally wraps (e.g., 0 - 255 = 1). The QOI spec requires wraparound for DIFF/LUMA. Files decoded correctly either way, but the encoder missed DIFF/LUMA opportunities for wraparound transitions, falling through to larger RGB chunks.

Fixed in 2b44047 — encoder now uses (((r - prevR) & 0xFF) ^ 0x80) - 0x80 matching the reference's signed char semantics. Added two tests verifying wraparound encoding produces correct and compact output.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • qoiformat.org
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

…pixel detection, extractMetadata channels validation, and 7 new negative tests

Co-authored-by: Hexagon <419737+Hexagon@users.noreply.github.com>
Agent-Logs-Url: https://github.com/cross-org/image/sessions/82c277d1-99d4-4c56-b8c3-a21a2187967f
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 21, 2026

@copilot apply changes based on the comments in this thread

Applied all 4 review comments in 31dbbd6:

  1. End marker validation — decoder now verifies the 8-byte end marker (0x00×7 + 0x01) and throws "Invalid QOI end marker" if missing/incorrect
  2. Bounds checksQOI_OP_RGB, QOI_OP_RGBA, and QOI_OP_LUMA all check remaining bytes before reading payloads, throwing "Truncated QOI data: incomplete ..." on premature EOF
  3. Incomplete pixel detection — throws "Incomplete QOI image" if the loop exits before producing width×height pixels
  4. extractMetadata channels validation — returns undefined for channels other than 3 or 4

Added 7 new negative decode tests covering all of the above. Full suite passes (612 tests).

@Hexagon Hexagon marked this pull request as ready for review March 21, 2026 21:58
@Hexagon Hexagon merged commit 8c1632f into main Mar 22, 2026
4 of 5 checks passed
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.

3 participants