@emdzej/itw-decoder 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ ITW is a proprietary image format used in BMW's Technical Information System (TI
13
13
  - **Subtype 0x0300** — Wavelet compression (biorthogonal wavelet transform, ~5:1–14:1 ratio)
14
14
  - **Subtype 0x0400** — Entropy compression (Huffman + RLE interleave)
15
15
 
16
- All images are 8-bit grayscale, typically 316×238 pixels.
16
+ All images are 8-bit grayscale, typically 316×238 or 631×474 pixels.
17
17
 
18
18
  For a full technical description of the format and decoding algorithms see [`docs/HOW_IT_WORKS.md`](docs/HOW_IT_WORKS.md).
19
19
 
@@ -87,12 +87,14 @@ Tested against the full 47,660-file GRAFIK corpus:
87
87
 
88
88
  | Metric | Value |
89
89
  |--------|-------|
90
- | Success rate | **98.67%** (47,028 / 47,660) |
91
- | 0x0300 wavelet decoded | 35,117 |
90
+ | Success rate | **99.66%** (47,498 / 47,660) |
91
+ | 0x0300 wavelet decoded | 35,587 |
92
92
  | 0x0400 entropy decoded | 11,911 |
93
- | Failures | 632 — all genuinely malformed/truncated source files |
93
+ | Failures | 162 — all genuinely malformed/truncated source files |
94
94
 
95
- The dominant failure reason is "wavelet payload overruns file" (470 truncated files), not decoder bugs.
95
+ ### Encoder bug: 256-byte payload length overrun
96
+
97
+ 586 wavelet (0x0300) files in the corpus declare a payload length exactly 256 bytes larger than the actual file data. This is a systematic bug in the original BMW ITW encoder — the payload data is complete, only the length field is wrong. The decoder tolerates this by allowing small overruns (up to 512 bytes) while still rejecting truly truncated files.
96
98
 
97
99
  ## Project structure
98
100
 
@@ -125,3 +127,13 @@ pnpm dev # run via ts-node (no build step)
125
127
  ## License
126
128
 
127
129
  [PolyForm Noncommercial License 1.0.0](LICENSE) — free for personal, research, and noncommercial use; commercial use is not permitted.
130
+
131
+ ## Right to Repair
132
+
133
+ The [Right to Repair](https://repair.eu) movement advocates for consumers' ability to fix the products they own — from electronics to vehicles — without being locked out by manufacturers through proprietary tools, paywalled documentation, or artificial restrictions.
134
+
135
+ **I build these tools because I believe repair is a fundamental right, not a privilege.**
136
+
137
+ Too often, service manuals, diagnostic software, and technical documentation are kept behind closed doors — unavailable to individuals even when they're willing to pay. This wasn't always the case. Products once shipped with schematics and repair guides as standard. The increasing complexity of modern technology doesn't change the fact that capable people exist who can — and should be allowed to — use that information.
138
+
139
+ These projects exist to preserve access to technical knowledge and ensure that owners aren't left at the mercy of vendors who may discontinue support, charge prohibitive fees, or simply refuse service.
@@ -7,6 +7,7 @@
7
7
  * https://polyformproject.org/licenses/noncommercial/1.0.0
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports._internals = void 0;
10
11
  exports.decode0300 = decode0300;
11
12
  const zlib_1 = require("zlib");
12
13
  const itw_1 = require("./itw");
@@ -22,8 +23,8 @@ const DAT_004ed118 = 0x80; // int: extra bits flag mask
22
23
  const DAT_004ed11c = 5; // int: buffer size multiplier
23
24
  // ─── Cursor: tracks read position into the payload ─────────────────────────
24
25
  class Cursor {
25
- constructor(buf, offset) {
26
- this.buf = buf;
26
+ constructor(input, offset) {
27
+ this.buf = (0, itw_1.toBuffer)(input);
27
28
  this.pos = offset;
28
29
  }
29
30
  readByte() {
@@ -32,12 +33,16 @@ class Cursor {
32
33
  return this.buf[this.pos++];
33
34
  }
34
35
  readBE16() {
35
- const v = (0, itw_1.readBE16)(this.buf, this.pos);
36
+ if (this.pos + 2 > this.buf.length)
37
+ throw new itw_1.ITWError("cursor overrun (BE16)");
38
+ const v = this.buf.readUInt16BE(this.pos);
36
39
  this.pos += 2;
37
40
  return v;
38
41
  }
39
42
  readBE32() {
40
- const v = (0, itw_1.readBE32From2BE16)(this.buf, this.pos);
43
+ if (this.pos + 4 > this.buf.length)
44
+ throw new itw_1.ITWError("cursor overrun (BE32)");
45
+ const v = this.buf.readUInt32BE(this.pos);
41
46
  this.pos += 4;
42
47
  return v;
43
48
  }
@@ -1203,10 +1208,17 @@ function decode0300(buf, payloadOffset, width, height, opts) {
1203
1208
  // Read BE32 payload length, then the payload starts after it
1204
1209
  if (payloadOffset + 4 > buf.length)
1205
1210
  throw new itw_1.ITWError("missing wavelet length");
1206
- const payloadLen = (0, itw_1.readBE32From2BE16)(buf, payloadOffset);
1211
+ const b = (0, itw_1.toBuffer)(buf);
1212
+ const payloadLen = b.readUInt32BE(payloadOffset);
1207
1213
  const payloadStart = payloadOffset + 4;
1208
- if (payloadStart + payloadLen > buf.length)
1209
- throw new itw_1.ITWError("wavelet payload overruns file");
1214
+ // Tolerate declared payload length exceeding file size — 586 files in the BMW TIS
1215
+ // corpus have a payload length exactly 256 bytes larger than the actual file data.
1216
+ // This is a systematic encoder bug; the actual wavelet data fits within the file.
1217
+ if (payloadStart + payloadLen > buf.length) {
1218
+ const over = (payloadStart + payloadLen) - buf.length;
1219
+ if (over > 512)
1220
+ throw new itw_1.ITWError("wavelet payload overruns file by " + over + " bytes");
1221
+ }
1210
1222
  // Build Fischer tables
1211
1223
  const baseTable = buildBaseTable(); // cumulative counts — used to build diff table
1212
1224
  const diffTable = buildDiffTable(baseTable); // exact counts T(q,m) — used by fischerDecode for unranking
@@ -1378,3 +1390,26 @@ function decode0300(buf, payloadOffset, width, height, opts) {
1378
1390
  }
1379
1391
  return { width, height, pixels };
1380
1392
  }
1393
+ /** @internal — exported for testing only */
1394
+ exports._internals = {
1395
+ deriveMirror,
1396
+ buildFilterCoeffs,
1397
+ initFilters,
1398
+ buildBaseTable,
1399
+ buildDiffTable,
1400
+ buildRankTable,
1401
+ tableLookup,
1402
+ fischerDecode,
1403
+ splitEvenOdd,
1404
+ calcBandSize,
1405
+ levelScaleFactor,
1406
+ q15ToFloat,
1407
+ hexToFloat,
1408
+ polyphaseConvolve1D,
1409
+ matrixCreate,
1410
+ matrixGet,
1411
+ matrixSet,
1412
+ edgeExtend,
1413
+ Cursor,
1414
+ Bitstream,
1415
+ };
@@ -51,7 +51,8 @@ class IntStack {
51
51
  }
52
52
  }
53
53
  // ─── FUN_004b5a40: Read payload tables from buffer ──────────────────
54
- function readTables(buf, off) {
54
+ function readTables(input, off) {
55
+ const buf = (0, itw_1.toBuffer)(input);
55
56
  const intArr = new IntStack(); // DAT_00580020
56
57
  const tableB = new ByteStack(); // DAT_00580014
57
58
  const tableC = new ByteStack(); // DAT_0058001c
@@ -71,7 +72,7 @@ function readTables(buf, off) {
71
72
  // len1 = FUN_004b5750(param_1) → BE32 via two BE16
72
73
  if (p + 4 > buf.length)
73
74
  throw new itw_1.ITWError("missing len1");
74
- const len1 = (0, itw_1.readBE32From2BE16)(buf, p);
75
+ const len1 = buf.readUInt32BE(p);
75
76
  p += 4;
76
77
  if (p + len1 > buf.length)
77
78
  throw new itw_1.ITWError("payload length exceeds file size at table B");
@@ -81,7 +82,7 @@ function readTables(buf, off) {
81
82
  // len2 = FUN_004b5750(param_1) → BE32 via two BE16
82
83
  if (p + 4 > buf.length)
83
84
  throw new itw_1.ITWError("missing len2");
84
- const len2 = (0, itw_1.readBE32From2BE16)(buf, p);
85
+ const len2 = buf.readUInt32BE(p);
85
86
  p += 4;
86
87
  if (p + len2 > buf.length)
87
88
  throw new itw_1.ITWError("payload length exceeds file size at table C");
package/dist/index.js CHANGED
File without changes
package/dist/itw.js CHANGED
@@ -8,9 +8,7 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.ITWError = void 0;
11
- exports.readBE16 = readBE16;
12
- exports.readBE32From2BE16 = readBE32From2BE16;
13
- exports.readLE32 = readLE32;
11
+ exports.toBuffer = toBuffer;
14
12
  exports.parseHeader = parseHeader;
15
13
  class ITWError extends Error {
16
14
  constructor(message) {
@@ -19,36 +17,22 @@ class ITWError extends Error {
19
17
  }
20
18
  }
21
19
  exports.ITWError = ITWError;
22
- function readBE16(buf, off) {
23
- if (off + 2 > buf.length)
24
- throw new ITWError(`readBE16 out of range @${off}`);
25
- return (buf[off] << 8) | buf[off + 1];
20
+ /** Ensure we have a Buffer (wraps plain Uint8Array if needed). */
21
+ function toBuffer(buf) {
22
+ return Buffer.isBuffer(buf) ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
26
23
  }
27
- function readBE32From2BE16(buf, off) {
28
- const hi = readBE16(buf, off);
29
- const lo = readBE16(buf, off + 2);
30
- return (hi << 16) | lo;
31
- }
32
- function readLE32(buf, off) {
33
- if (off + 4 > buf.length)
34
- throw new ITWError(`readLE32 out of range @${off}`);
35
- return buf[off] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24);
36
- }
37
- function parseHeader(buf) {
38
- // Header layout (from Ghidra FUN_004b5680 + FUN_004b5780):
39
- // 6 x BE16 in FUN_004b5680: magic(2), magic(2), version, width, height, bpp
40
- // 1 x BE16 in FUN_004b5780: subtype
41
- // Total = 7 x BE16 = 14 bytes
42
- if (buf.length < 14)
24
+ function parseHeader(input) {
25
+ if (input.length < 14)
43
26
  throw new ITWError("file too small for header");
44
- const magic = String.fromCharCode(buf[0], buf[1], buf[2], buf[3]);
27
+ const buf = toBuffer(input);
28
+ const magic = buf.toString('ascii', 0, 4);
45
29
  if (magic !== "ITW_")
46
30
  throw new ITWError("bad magic");
47
- const version = readBE16(buf, 4);
48
- const width = readBE16(buf, 6);
49
- const height = readBE16(buf, 8);
50
- const bpp = readBE16(buf, 0x0a);
51
- const subtype = readBE16(buf, 0x0c);
31
+ const version = buf.readUInt16BE(4);
32
+ const width = buf.readUInt16BE(6);
33
+ const height = buf.readUInt16BE(8);
34
+ const bpp = buf.readUInt16BE(0x0a);
35
+ const subtype = buf.readUInt16BE(0x0c);
52
36
  if (version !== 0x0100 && version !== 0x0200)
53
37
  throw new ITWError(`unsupported version 0x${version.toString(16)}`);
54
38
  if (subtype !== 0x0300 && subtype !== 0x0400)
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@emdzej/itw-decoder",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "commonjs",
5
5
  "description": "Decode BMW TIS .ITW proprietary image files to PNG",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/emdzej/itw-decoder"
10
+ },
7
11
  "main": "dist/index.js",
8
12
  "bin": {
9
13
  "itw-decode": "dist/index.js"
@@ -11,12 +15,6 @@
11
15
  "files": [
12
16
  "dist"
13
17
  ],
14
- "scripts": {
15
- "build": "tsc",
16
- "start": "node dist/index.js",
17
- "dev": "ts-node src/index.ts",
18
- "decode": "ts-node src/index.ts"
19
- },
20
18
  "dependencies": {
21
19
  "commander": "^14.0.3",
22
20
  "pngjs": "^7.0.0"
@@ -25,6 +23,15 @@
25
23
  "@types/node": "^20.11.0",
26
24
  "@types/pngjs": "^6.0.4",
27
25
  "ts-node": "^10.9.2",
28
- "typescript": "^5.4.0"
26
+ "typescript": "^5.4.0",
27
+ "vitest": "^4.1.1"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "start": "node dist/index.js",
32
+ "dev": "ts-node src/index.ts",
33
+ "decode": "ts-node src/index.ts",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest"
29
36
  }
30
- }
37
+ }