Skip to content

Commit 31efeae

Browse files
committed
feat: migrate WIT interface to match latest version used by chonkle
1 parent 795409e commit 31efeae

8 files changed

Lines changed: 92 additions & 81 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
run: |
3535
uv run componentize-py \
3636
--wit-path wit \
37-
--world tiff-predictor-2-python \
37+
--world codec \
3838
componentize -p src app \
3939
-o tiff-predictor-2-python.wasm
4040

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1212

1313
- Embedded `chonkle:signature` custom section in the Wasm binary, declaring codec metadata (identifier, implementation name, input/output port types).
1414

15+
### Changed
16+
17+
- Migrated WIT interface to match latest version used by `chonkle` (port-map based API).
18+
1519
## [v0.1.0] - 2026-03-05
1620

1721
Initial release.

README.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,35 @@ Supports 1, 2, and 4 bytes-per-sample data (uint8, uint16, uint32).
1515

1616
## Wasm Component interface
1717

18-
The module is built as a [Wasm Component](https://component-model.bytecodealliance.org/) and exports a `codec` interface defined in a WebAssembly Interface Type ([WIT](https://component-model.bytecodealliance.org/design/wit.html)) file:
18+
The module is built as a [Wasm Component](https://component-model.bytecodealliance.org/) and exports a `transform` interface defined in a WebAssembly Interface Type ([WIT](https://component-model.bytecodealliance.org/design/wit.html)) file:
1919

2020
```wit
21-
package cylf:tiff-predictor-2-python@0.1.0;
21+
package chonkle:codec@0.1.0;
2222
23-
interface codec {
24-
encode: func(data: list<u8>, config: string) -> result<list<u8>, string>;
25-
decode: func(data: list<u8>, config: string) -> result<list<u8>, string>;
23+
interface transform {
24+
type port-name = string;
25+
type port-map = list<tuple<port-name, list<u8>>>;
26+
27+
encode: func(inputs: port-map) -> result<port-map, string>;
28+
decode: func(inputs: port-map) -> result<port-map, string>;
2629
}
2730
28-
world tiff-predictor-2-python {
29-
export codec;
31+
world codec {
32+
export transform;
3033
}
3134
```
3235

33-
The `package` line declares a namespace, name, and version — the Component Model's equivalent of a package identifier. It scopes the interface names so they don't collide with interfaces from other packages, and it's what tooling uses to refer to this component as a dependency.
36+
Both `encode` and `decode` take a `port-map` — a list of named byte buffers — and return a `port-map` on success or a human-readable error string on failure.
37+
38+
This codec expects three input ports:
3439

35-
The `world` declares what this component exposes to the outside world. `export codec` means any host that loads this component can call the functions in the `codec` interface.
40+
- **`bytes`** — the raw pixel data
41+
- **`bytes_per_sample`** — UTF-8 digit string (`"1"`, `"2"`, or `"4"`)
42+
- **`width`** — UTF-8 digit string (e.g. `"256"`)
3643

37-
Both `encode` and `decode` take a byte array of pixel data and a JSON config string with `bytes_per_sample` and `width` keys, e.g. `{"bytes_per_sample": 2, "width": 256}`. Both also return a WIT [`result`](https://component-model.bytecodealliance.org/design/wit.html#results) — WIT's typed success-or-failure type, similar to Rust's `Result` or a checked exception in other languages. The two type parameters are the success type and the error type:
44+
It returns one output port:
3845

39-
- **`list<u8>`** (success) — WIT's byte array; contains the encoded or decoded bytes
40-
- **`string`** (failure) — a human-readable error message, e.g. for an invalid or missing config key
46+
- **`bytes`** — the encoded or decoded pixel data
4147

4248
## Prerequisites
4349

@@ -49,7 +55,7 @@ Both `encode` and `decode` take a byte array of pixel data and a JSON config str
4955
uv sync
5056
uv run componentize-py \
5157
--wit-path wit \
52-
--world tiff-predictor-2-python \
58+
--world codec \
5359
componentize -p src app \
5460
-o tiff-predictor-2-python.wasm
5561
```

src/app.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""componentize-py entry point.
22
3-
Bridges the WIT-generated Codec protocol to the pure-Python implementation
3+
Bridges the WIT-generated Transform protocol to the pure-Python implementation
44
in tiff_predictor_2.py.
55
"""
66

@@ -9,15 +9,36 @@
99
import tiff_predictor_2
1010

1111

12-
class Codec:
13-
def encode(self, data: bytes, config: str) -> bytes:
12+
class Transform:
13+
def encode(self, inputs: list[tuple[str, bytes]]) -> list[tuple[str, bytes]]:
1414
try:
15-
return tiff_predictor_2.encode(data, config)
15+
data, bps, width = _unpack_inputs(inputs)
16+
result = tiff_predictor_2.encode(data, bps, width)
17+
return [("bytes", result)]
1618
except ValueError as e:
1719
raise Err(str(e)) from e
1820

19-
def decode(self, data: bytes, config: str) -> bytes:
21+
def decode(self, inputs: list[tuple[str, bytes]]) -> list[tuple[str, bytes]]:
2022
try:
21-
return tiff_predictor_2.decode(data, config)
23+
data, bps, width = _unpack_inputs(inputs)
24+
result = tiff_predictor_2.decode(data, bps, width)
25+
return [("bytes", result)]
2226
except ValueError as e:
2327
raise Err(str(e)) from e
28+
29+
30+
def _unpack_inputs(inputs: list[tuple[str, bytes]]) -> tuple[bytes, int, int]:
31+
"""Extract and parse the three required ports from a port-map."""
32+
ports = dict(inputs)
33+
data = _require_port(ports, "bytes")
34+
bps = int(_require_port(ports, "bytes_per_sample").decode())
35+
width = int(_require_port(ports, "width").decode())
36+
return data, bps, width
37+
38+
39+
def _require_port(ports: dict[str, bytes], name: str) -> bytes:
40+
"""Look up a port by name, raising ValueError if missing."""
41+
if name not in ports:
42+
msg = f"missing required port: {name}"
43+
raise ValueError(msg)
44+
return ports[name]

src/tiff_predictor_2.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
"""
77

88
import array
9-
import json
109

1110
# bytes-per-sample -> array typecode (unsigned integers)
1211
_TYPECODES: dict[int, str] = {1: "B", 2: "H", 4: "I"}
1312

1413

15-
def decode(data: bytes, config: str) -> bytes:
14+
def decode(data: bytes, bps: int, width: int) -> bytes:
1615
"""Undo horizontal differencing via cumulative sum per row."""
17-
bps, width, height = _parse_config(config, len(data))
16+
_validate(bps, width)
17+
height = len(data) // (width * bps)
1818
mask = (1 << (bps * 8)) - 1
1919

2020
arr = array.array(_TYPECODES[bps], data)
@@ -26,12 +26,13 @@ def decode(data: bytes, config: str) -> bytes:
2626
return arr.tobytes()
2727

2828

29-
def encode(data: bytes, config: str) -> bytes:
29+
def encode(data: bytes, bps: int, width: int) -> bytes:
3030
"""Apply horizontal differencing (row-wise differences).
3131
3232
Iterates backwards so each subtraction reads the original predecessor.
3333
"""
34-
bps, width, height = _parse_config(config, len(data))
34+
_validate(bps, width)
35+
height = len(data) // (width * bps)
3536
mask = (1 << (bps * 8)) - 1
3637

3738
arr = array.array(_TYPECODES[bps], data)
@@ -43,21 +44,11 @@ def encode(data: bytes, config: str) -> bytes:
4344
return arr.tobytes()
4445

4546

46-
def _parse_config(config: str, data_len: int) -> tuple[int, int, int]:
47-
"""Parse JSON config and return (bps, width, height).
48-
49-
Raises ValueError for invalid or missing parameters.
50-
"""
51-
cfg = json.loads(config)
52-
bps = cfg.get("bytes_per_sample", 0)
53-
width = cfg.get("width", 0)
54-
47+
def _validate(bps: int, width: int) -> None:
48+
"""Validate parameters. Raises ValueError for invalid values."""
5549
if bps not in _TYPECODES:
5650
msg = f"unsupported bytes_per_sample: {bps}"
5751
raise ValueError(msg)
5852
if width <= 0:
5953
msg = f"invalid width: {width}"
6054
raise ValueError(msg)
61-
62-
height = data_len // (width * bps)
63-
return bps, width, height

tests/test_tiff_predictor_2.py

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
"""Tests for tiff_predictor_2.py
22
33
Ported from tiff-predictor-2-c/test_tiff_predictor_2.c. The Python tests
4-
call encode()/decode() with bytes + JSON config (the public API), rather
4+
call encode()/decode() with bytes + parameters (the public API), rather
55
than testing internal helpers directly.
66
"""
77

88
import array
9-
import json
109

1110
import pytest
1211

1312
from tiff_predictor_2 import decode, encode
1413

1514

16-
def _config(bps: int, width: int) -> str:
17-
return json.dumps({"bytes_per_sample": bps, "width": width})
18-
19-
2015
def _to_bytes(typecode: str, values: list[int]) -> bytes:
2116
"""Convert a list of integers to bytes via array.array (native byte order)."""
2217
return array.array(typecode, values).tobytes()
@@ -30,37 +25,37 @@ def test_bps1(self) -> None:
3025
"""[10, 5, 3, 1] -> [10, 15, 18, 19]"""
3126
data = _to_bytes("B", [10, 5, 3, 1])
3227
expected = _to_bytes("B", [10, 15, 18, 19])
33-
assert decode(data, _config(1, 4)) == expected
28+
assert decode(data, 1, 4) == expected
3429

3530
def test_bps1_wrapping(self) -> None:
3631
"""200 + 100 = 300, mod 256 = 44."""
3732
data = _to_bytes("B", [200, 100])
38-
result = array.array("B", decode(data, _config(1, 2)))
33+
result = array.array("B", decode(data, 1, 2))
3934
assert result[0] == 200
4035
assert result[1] == (200 + 100) % 256
4136

4237
def test_bps2(self) -> None:
4338
"""[1000, 500, 200] -> [1000, 1500, 1700]"""
4439
data = _to_bytes("H", [1000, 500, 200])
4540
expected = _to_bytes("H", [1000, 1500, 1700])
46-
assert decode(data, _config(2, 3)) == expected
41+
assert decode(data, 2, 3) == expected
4742

4843
def test_bps4(self) -> None:
4944
"""[100000, 50000, 25000] -> [100000, 150000, 175000]"""
5045
data = _to_bytes("I", [100000, 50000, 25000])
5146
expected = _to_bytes("I", [100000, 150000, 175000])
52-
assert decode(data, _config(4, 3)) == expected
47+
assert decode(data, 4, 3) == expected
5348

5449
def test_multirow(self) -> None:
5550
"""2 rows, width=3, bps=1 — rows decoded independently."""
5651
data = _to_bytes("B", [10, 5, 3, 20, 2, 1])
5752
expected = _to_bytes("B", [10, 15, 18, 20, 22, 23])
58-
assert decode(data, _config(1, 3)) == expected
53+
assert decode(data, 1, 3) == expected
5954

6055
def test_single_column(self) -> None:
6156
"""width=1: nothing to accumulate, buffer unchanged."""
6257
data = _to_bytes("B", [42, 99])
63-
assert decode(data, _config(1, 1)) == data
58+
assert decode(data, 1, 1) == data
6459

6560

6661
# ---- encode tests -----------------------------------------------------------
@@ -71,37 +66,37 @@ def test_bps1(self) -> None:
7166
"""[10, 15, 18, 19] -> [10, 5, 3, 1]"""
7267
data = _to_bytes("B", [10, 15, 18, 19])
7368
expected = _to_bytes("B", [10, 5, 3, 1])
74-
assert encode(data, _config(1, 4)) == expected
69+
assert encode(data, 1, 4) == expected
7570

7671
def test_bps1_wrapping(self) -> None:
7772
"""44 - 200 = -156, mod 256 = 100."""
7873
data = _to_bytes("B", [200, 44])
79-
result = array.array("B", encode(data, _config(1, 2)))
74+
result = array.array("B", encode(data, 1, 2))
8075
assert result[0] == 200
8176
assert result[1] == (44 - 200) % 256
8277

8378
def test_bps2(self) -> None:
8479
"""[1000, 1500, 1700] -> [1000, 500, 200]"""
8580
data = _to_bytes("H", [1000, 1500, 1700])
8681
expected = _to_bytes("H", [1000, 500, 200])
87-
assert encode(data, _config(2, 3)) == expected
82+
assert encode(data, 2, 3) == expected
8883

8984
def test_bps4(self) -> None:
9085
"""[100000, 150000, 175000] -> [100000, 50000, 25000]"""
9186
data = _to_bytes("I", [100000, 150000, 175000])
9287
expected = _to_bytes("I", [100000, 50000, 25000])
93-
assert encode(data, _config(4, 3)) == expected
88+
assert encode(data, 4, 3) == expected
9489

9590
def test_multirow(self) -> None:
9691
"""2 rows, width=3, bps=1 — rows encoded independently."""
9792
data = _to_bytes("B", [10, 15, 18, 20, 22, 23])
9893
expected = _to_bytes("B", [10, 5, 3, 20, 2, 1])
99-
assert encode(data, _config(1, 3)) == expected
94+
assert encode(data, 1, 3) == expected
10095

10196
def test_single_column(self) -> None:
10297
"""width=1: nothing to difference, buffer unchanged."""
10398
data = _to_bytes("B", [42, 99])
104-
assert encode(data, _config(1, 1)) == data
99+
assert encode(data, 1, 1) == data
105100

106101

107102
# ---- roundtrip tests --------------------------------------------------------
@@ -110,13 +105,11 @@ def test_single_column(self) -> None:
110105
class TestRoundtrip:
111106
def test_encode_then_decode(self) -> None:
112107
original = _to_bytes("H", [1000, 1500, 1700, 2000, 2100, 2300])
113-
cfg = _config(2, 3)
114-
assert decode(encode(original, cfg), cfg) == original
108+
assert decode(encode(original, 2, 3), 2, 3) == original
115109

116110
def test_decode_then_encode(self) -> None:
117111
original = _to_bytes("H", [1000, 1500, 1700, 2000, 2100, 2300])
118-
cfg = _config(2, 3)
119-
assert encode(decode(original, cfg), cfg) == original
112+
assert encode(decode(original, 2, 3), 2, 3) == original
120113

121114

122115
# ---- invalid config tests ---------------------------------------------------
@@ -125,24 +118,16 @@ def test_decode_then_encode(self) -> None:
125118
class TestInvalidConfig:
126119
def test_decode_bps_zero(self) -> None:
127120
with pytest.raises(ValueError, match="bytes_per_sample"):
128-
decode(b"\x01\x02\x03\x04", _config(0, 4))
121+
decode(b"\x01\x02\x03\x04", 0, 4)
129122

130123
def test_decode_width_zero(self) -> None:
131124
with pytest.raises(ValueError, match="width"):
132-
decode(b"\x01\x02\x03\x04", _config(1, 0))
133-
134-
def test_decode_empty_config(self) -> None:
135-
with pytest.raises(ValueError, match="bytes_per_sample"):
136-
decode(b"\x01\x02\x03\x04", "{}")
125+
decode(b"\x01\x02\x03\x04", 1, 0)
137126

138127
def test_encode_bps_zero(self) -> None:
139128
with pytest.raises(ValueError, match="bytes_per_sample"):
140-
encode(b"\x01\x02\x03\x04", _config(0, 4))
129+
encode(b"\x01\x02\x03\x04", 0, 4)
141130

142131
def test_encode_width_zero(self) -> None:
143132
with pytest.raises(ValueError, match="width"):
144-
encode(b"\x01\x02\x03\x04", _config(1, 0))
145-
146-
def test_encode_empty_config(self) -> None:
147-
with pytest.raises(ValueError, match="bytes_per_sample"):
148-
encode(b"\x01\x02\x03\x04", "{}")
133+
encode(b"\x01\x02\x03\x04", 1, 0)

wit/codec.wit

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package chonkle:codec@0.1.0;
2+
3+
interface transform {
4+
/// A named byte buffer — one port in the port map.
5+
type port-name = string;
6+
type port-map = list<tuple<port-name, list<u8>>>;
7+
8+
encode: func(inputs: port-map) -> result<port-map, string>;
9+
decode: func(inputs: port-map) -> result<port-map, string>;
10+
}
11+
12+
world codec {
13+
export transform;
14+
}

wit/world.wit

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)