@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 +17 -5
- package/dist/decode0300.js +42 -7
- package/dist/decode0400.js +4 -3
- package/dist/index.js +0 -0
- package/dist/itw.js +13 -29
- package/package.json +16 -9
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 | **
|
|
91
|
-
| 0x0300 wavelet decoded | 35,
|
|
90
|
+
| Success rate | **99.66%** (47,498 / 47,660) |
|
|
91
|
+
| 0x0300 wavelet decoded | 35,587 |
|
|
92
92
|
| 0x0400 entropy decoded | 11,911 |
|
|
93
|
-
| Failures |
|
|
93
|
+
| Failures | 162 — all genuinely malformed/truncated source files |
|
|
94
94
|
|
|
95
|
-
|
|
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.
|
package/dist/decode0300.js
CHANGED
|
@@ -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(
|
|
26
|
-
this.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
|
-
|
|
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
|
-
|
|
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
|
|
1211
|
+
const b = (0, itw_1.toBuffer)(buf);
|
|
1212
|
+
const payloadLen = b.readUInt32BE(payloadOffset);
|
|
1207
1213
|
const payloadStart = payloadOffset + 4;
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
+
};
|
package/dist/decode0400.js
CHANGED
|
@@ -51,7 +51,8 @@ class IntStack {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
// ─── FUN_004b5a40: Read payload tables from buffer ──────────────────
|
|
54
|
-
function readTables(
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
28
|
-
|
|
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
|
|
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 =
|
|
48
|
-
const width =
|
|
49
|
-
const height =
|
|
50
|
-
const bpp =
|
|
51
|
-
const subtype =
|
|
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.
|
|
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
|
+
}
|