@emdzej/itw-decoder 0.1.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/LICENSE +118 -0
- package/README.md +127 -0
- package/dist/decode0300.js +1380 -0
- package/dist/decode0400.js +323 -0
- package/dist/index.js +56 -0
- package/dist/itw.js +59 -0
- package/dist/png.js +35 -0
- package/package.json +30 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Michał Jaskólski
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the PolyForm Noncommercial License 1.0.0
|
|
6
|
+
* found in the LICENSE file in the root directory of this repository.
|
|
7
|
+
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.decode0400 = decode0400;
|
|
11
|
+
/**
|
|
12
|
+
* ITW 0x0400 (entropy) decoder.
|
|
13
|
+
*
|
|
14
|
+
* Faithful port of the original pipeline from Ghidra decompilation:
|
|
15
|
+
* FUN_004b5a40 — read tables from file
|
|
16
|
+
* FUN_004b6340 — parse Huffman leaf table from byte array
|
|
17
|
+
* FUN_004b6570 — build Huffman tree by weight (float comparison!)
|
|
18
|
+
* FUN_004b6250 — Huffman decode: consume bytes LSB-first, emit symbols
|
|
19
|
+
* FUN_004b57f0 — main 0x0400 pipeline: interleave + expand
|
|
20
|
+
* FUN_004b5c40 — build powers-of-two table (depth=8)
|
|
21
|
+
* FUN_004b5d20 — expand: literal copy then RLE with powers table
|
|
22
|
+
*/
|
|
23
|
+
const itw_1 = require("./itw");
|
|
24
|
+
// ─── Data structures ────────────────────────────────────────────────
|
|
25
|
+
// ByteStack mirrors FUN_004b67d0 / FUN_004b6890:
|
|
26
|
+
// field[0] = count, field[3] = data pointer (byte array)
|
|
27
|
+
class ByteStack {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.data = [];
|
|
30
|
+
}
|
|
31
|
+
get count() { return this.data.length; }
|
|
32
|
+
push(v) { this.data.push(v & 0xff); }
|
|
33
|
+
at(i) {
|
|
34
|
+
if (i < 0 || i >= this.data.length)
|
|
35
|
+
throw new itw_1.ITWError(`ByteStack out of bounds: ${i} (len=${this.data.length})`);
|
|
36
|
+
return this.data[i];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// IntStack mirrors FUN_004b6940 / FUN_004b6a10:
|
|
40
|
+
// field[0] = count, field[3] = data pointer (int32 array)
|
|
41
|
+
class IntStack {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.data = [];
|
|
44
|
+
}
|
|
45
|
+
get count() { return this.data.length; }
|
|
46
|
+
push(v) { this.data.push(v); }
|
|
47
|
+
at(i) {
|
|
48
|
+
if (i < 0 || i >= this.data.length)
|
|
49
|
+
throw new itw_1.ITWError(`IntStack out of bounds: ${i} (len=${this.data.length})`);
|
|
50
|
+
return this.data[i];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ─── FUN_004b5a40: Read payload tables from buffer ──────────────────
|
|
54
|
+
function readTables(buf, off) {
|
|
55
|
+
const intArr = new IntStack(); // DAT_00580020
|
|
56
|
+
const tableB = new ByteStack(); // DAT_00580014
|
|
57
|
+
const tableC = new ByteStack(); // DAT_0058001c
|
|
58
|
+
let p = off;
|
|
59
|
+
if (p >= buf.length)
|
|
60
|
+
throw new itw_1.ITWError("payload missing N");
|
|
61
|
+
// fread(&local_1,1,1,param_1); DAT_00580018 = local_1 & 0xff;
|
|
62
|
+
const n = buf[p++];
|
|
63
|
+
// FUN_004b6a10(DAT_00580020, DAT_00580018); — push N itself
|
|
64
|
+
intArr.push(n);
|
|
65
|
+
// then push N byte values as ints
|
|
66
|
+
for (let i = 0; i < n; i++) {
|
|
67
|
+
if (p >= buf.length)
|
|
68
|
+
throw new itw_1.ITWError("table A overruns file");
|
|
69
|
+
intArr.push(buf[p++] & 0xff);
|
|
70
|
+
}
|
|
71
|
+
// len1 = FUN_004b5750(param_1) → BE32 via two BE16
|
|
72
|
+
if (p + 4 > buf.length)
|
|
73
|
+
throw new itw_1.ITWError("missing len1");
|
|
74
|
+
const len1 = (0, itw_1.readBE32From2BE16)(buf, p);
|
|
75
|
+
p += 4;
|
|
76
|
+
if (p + len1 > buf.length)
|
|
77
|
+
throw new itw_1.ITWError("payload length exceeds file size at table B");
|
|
78
|
+
for (let i = 0; i < len1; i++) {
|
|
79
|
+
tableB.push(buf[p++]);
|
|
80
|
+
}
|
|
81
|
+
// len2 = FUN_004b5750(param_1) → BE32 via two BE16
|
|
82
|
+
if (p + 4 > buf.length)
|
|
83
|
+
throw new itw_1.ITWError("missing len2");
|
|
84
|
+
const len2 = (0, itw_1.readBE32From2BE16)(buf, p);
|
|
85
|
+
p += 4;
|
|
86
|
+
if (p + len2 > buf.length)
|
|
87
|
+
throw new itw_1.ITWError("payload length exceeds file size at table C");
|
|
88
|
+
for (let i = 0; i < len2; i++) {
|
|
89
|
+
tableC.push(buf[p++]);
|
|
90
|
+
}
|
|
91
|
+
return { intArr, tableB, tableC, n };
|
|
92
|
+
}
|
|
93
|
+
// ─── FUN_004b6340: Parse Huffman leaf table from ByteStack ──────────
|
|
94
|
+
function parseHuffLeaves(bs) {
|
|
95
|
+
if (bs.count < 4)
|
|
96
|
+
throw new itw_1.ITWError("huff table too small");
|
|
97
|
+
// count assembled as LE32: b0 + b1*256 + b2*65536 + b3*16M
|
|
98
|
+
const count = bs.at(0) + bs.at(1) * 0x100 + bs.at(2) * 0x10000 + bs.at(3) * 0x1000000;
|
|
99
|
+
let p = 4;
|
|
100
|
+
const leaves = [];
|
|
101
|
+
for (let i = 0; i < count; i++) {
|
|
102
|
+
if (p + 8 > bs.count)
|
|
103
|
+
throw new itw_1.ITWError("huff leaf overrun");
|
|
104
|
+
// byte at p+0 = symbol (stored to node+2, but read from record[0])
|
|
105
|
+
const symbol = bs.at(p);
|
|
106
|
+
// bytes p+4..p+7 = weight assembled LE, interpreted as IEEE 754 float
|
|
107
|
+
const weightBits = (bs.at(p + 4) | (bs.at(p + 5) << 8) |
|
|
108
|
+
(bs.at(p + 6) << 16) | (bs.at(p + 7) << 24)) >>> 0;
|
|
109
|
+
const floatBuf = new ArrayBuffer(4);
|
|
110
|
+
new DataView(floatBuf).setUint32(0, weightBits, true);
|
|
111
|
+
const weight = new DataView(floatBuf).getFloat32(0, true);
|
|
112
|
+
leaves.push({
|
|
113
|
+
isLeaf: 1,
|
|
114
|
+
symbol,
|
|
115
|
+
id: -1,
|
|
116
|
+
leftId: -1,
|
|
117
|
+
rightId: -1,
|
|
118
|
+
parentId: -1,
|
|
119
|
+
weight,
|
|
120
|
+
});
|
|
121
|
+
p += 8;
|
|
122
|
+
}
|
|
123
|
+
if (p + 4 > bs.count)
|
|
124
|
+
throw new itw_1.ITWError("missing trailing huff value");
|
|
125
|
+
const trailingVal = bs.at(p) + bs.at(p + 1) * 0x100 + bs.at(p + 2) * 0x10000 + bs.at(p + 3) * 0x1000000;
|
|
126
|
+
p += 4;
|
|
127
|
+
return { leaves, trailingVal, bitstreamStart: p };
|
|
128
|
+
}
|
|
129
|
+
// ─── FUN_004b6570 + FUN_004b60c0: Build Huffman tree ────────────────
|
|
130
|
+
function buildHuffTree(leaves) {
|
|
131
|
+
if (leaves.length === 0)
|
|
132
|
+
throw new itw_1.ITWError("empty huff table");
|
|
133
|
+
const nodes = [];
|
|
134
|
+
let nextId = 0;
|
|
135
|
+
// Assign IDs to leaves
|
|
136
|
+
for (const leaf of leaves) {
|
|
137
|
+
leaf.id = nextId++;
|
|
138
|
+
nodes.push(leaf);
|
|
139
|
+
}
|
|
140
|
+
// Priority queue: indices to merge
|
|
141
|
+
let queue = leaves.map(l => l.id);
|
|
142
|
+
while (queue.length > 1) {
|
|
143
|
+
// Sort by weight ascending (float comparison as in original FUN_004b60c0)
|
|
144
|
+
queue.sort((a, b) => nodes[a].weight - nodes[b].weight);
|
|
145
|
+
const leftId = queue.shift();
|
|
146
|
+
const rightId = queue.shift();
|
|
147
|
+
const left = nodes[leftId];
|
|
148
|
+
const right = nodes[rightId];
|
|
149
|
+
const parentNode = {
|
|
150
|
+
isLeaf: 0,
|
|
151
|
+
symbol: 0,
|
|
152
|
+
id: nextId++,
|
|
153
|
+
leftId: left.id,
|
|
154
|
+
rightId: right.id,
|
|
155
|
+
parentId: -1,
|
|
156
|
+
weight: left.weight + right.weight,
|
|
157
|
+
};
|
|
158
|
+
left.parentId = parentNode.id;
|
|
159
|
+
right.parentId = parentNode.id;
|
|
160
|
+
nodes.push(parentNode);
|
|
161
|
+
queue.push(parentNode.id);
|
|
162
|
+
}
|
|
163
|
+
// Root
|
|
164
|
+
const rootId = queue[0];
|
|
165
|
+
nodes[rootId].parentId = -1;
|
|
166
|
+
return nodes;
|
|
167
|
+
}
|
|
168
|
+
// ─── FUN_004b6250: Huffman decode from ByteStack ────────────────────
|
|
169
|
+
// Consumes bytes from bitstreamStart..end, 8 bits per byte LSB-first.
|
|
170
|
+
// On leaf, emits symbol to output ByteStack.
|
|
171
|
+
function huffDecode(bs, bitstreamStart, nodes, rootId, maxBits) {
|
|
172
|
+
const out = new ByteStack();
|
|
173
|
+
let curId = rootId;
|
|
174
|
+
let bitsUsed = 0;
|
|
175
|
+
for (let byteIdx = bitstreamStart; byteIdx < bs.count; byteIdx++) {
|
|
176
|
+
let byte = bs.at(byteIdx);
|
|
177
|
+
let bitsInByte = 8;
|
|
178
|
+
while (bitsInByte > 0) {
|
|
179
|
+
if (bitsUsed >= maxBits)
|
|
180
|
+
return out;
|
|
181
|
+
bitsInByte--;
|
|
182
|
+
bitsUsed++;
|
|
183
|
+
const node = nodes[curId];
|
|
184
|
+
// bit 0 → left (+8 in original), bit 1 → right (+0xC)
|
|
185
|
+
if ((byte & 1) === 0) {
|
|
186
|
+
curId = node.leftId;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
curId = node.rightId;
|
|
190
|
+
}
|
|
191
|
+
byte = byte >> 1;
|
|
192
|
+
if (curId < 0 || curId >= nodes.length) {
|
|
193
|
+
throw new itw_1.ITWError(`huffman tree traversal error: invalid nodeId ${curId}`);
|
|
194
|
+
}
|
|
195
|
+
const child = nodes[curId];
|
|
196
|
+
if (child.isLeaf !== 0) {
|
|
197
|
+
out.push(child.symbol);
|
|
198
|
+
curId = rootId; // reset to root
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
// ─── Full Huffman pipeline for one table ────────────────────────────
|
|
205
|
+
function huffDecodeTable(bs) {
|
|
206
|
+
const { leaves, trailingVal, bitstreamStart } = parseHuffLeaves(bs);
|
|
207
|
+
const nodes = buildHuffTree(leaves);
|
|
208
|
+
const rootId = nodes.length - 1; // Root is last node added
|
|
209
|
+
// trailingVal = bit budget (stored at param_1[4] in original, read as LE32)
|
|
210
|
+
return huffDecode(bs, bitstreamStart, nodes, rootId, trailingVal);
|
|
211
|
+
}
|
|
212
|
+
// ─── FUN_004b57f0: Interleave decoded B and C into intArr ───────────
|
|
213
|
+
const DEPTH = 8; // DAT_004ed104 = 8
|
|
214
|
+
function interleave(n, decodedB, decodedC, intArr) {
|
|
215
|
+
let remaining = decodedB.count + decodedC.count;
|
|
216
|
+
let idxB = 0; // ESI — index into decodedB (puVar4) data
|
|
217
|
+
let idxC = 0; // EDI — index into decodedC (puVar5) data
|
|
218
|
+
while (remaining > 0) {
|
|
219
|
+
// Guard: stop if B data is exhausted (original C code has no bounds check —
|
|
220
|
+
// it would read garbage beyond the buffer; we stop gracefully instead)
|
|
221
|
+
if (idxB >= decodedB.count)
|
|
222
|
+
break;
|
|
223
|
+
const bVal = decodedB.at(idxB);
|
|
224
|
+
if (bVal < n + DEPTH) {
|
|
225
|
+
// Push C byte then B byte; consume 2 from remaining
|
|
226
|
+
remaining -= 2;
|
|
227
|
+
// Guard: if C data is exhausted, push 0 (matches original's "read past end" behavior)
|
|
228
|
+
if (idxC < decodedC.count) {
|
|
229
|
+
intArr.push(decodedC.at(idxC));
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
intArr.push(0);
|
|
233
|
+
}
|
|
234
|
+
idxC++;
|
|
235
|
+
intArr.push(bVal);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Push just B byte; consume 1 from remaining
|
|
239
|
+
remaining -= 1;
|
|
240
|
+
intArr.push(bVal);
|
|
241
|
+
}
|
|
242
|
+
idxB++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ─── FUN_004b5d20: Expand intArr via powers table ───────────────────
|
|
246
|
+
function expand(intArr) {
|
|
247
|
+
const out = new ByteStack();
|
|
248
|
+
const data = intArr.data;
|
|
249
|
+
if (data.length === 0)
|
|
250
|
+
throw new itw_1.ITWError("empty intArr for expansion");
|
|
251
|
+
// First word = N (literal count); *param_1 = uVar2
|
|
252
|
+
const literalCount = data[0];
|
|
253
|
+
// Copy next literalCount ints into codebook (param_1[1] array)
|
|
254
|
+
const codebook = [];
|
|
255
|
+
let p = 1;
|
|
256
|
+
for (let i = 0; i < literalCount && p < data.length; i++, p++) {
|
|
257
|
+
codebook.push(data[p]);
|
|
258
|
+
}
|
|
259
|
+
// param_1[3] = depth = 8 (from FUN_004b5c40: *(param_1+0xc) = 8)
|
|
260
|
+
const depth = DEPTH;
|
|
261
|
+
// Process remaining words
|
|
262
|
+
while (p < data.length) {
|
|
263
|
+
const w = data[p];
|
|
264
|
+
if (w < literalCount + depth) {
|
|
265
|
+
// Replicate: times = 2^w, value = codebook[nextWord - depth]
|
|
266
|
+
let times = 1;
|
|
267
|
+
for (let k = 0; k < w; k++)
|
|
268
|
+
times *= 2;
|
|
269
|
+
const idx = data[p + 1];
|
|
270
|
+
p += 2;
|
|
271
|
+
// Value from codebook via: *(*(param_1[1]+0xc) + (idx - depth) * 4)
|
|
272
|
+
// param_1[1] is the int-stack whose data array is codebook
|
|
273
|
+
const cbIdx = idx - depth;
|
|
274
|
+
if (cbIdx < 0 || cbIdx >= codebook.length) {
|
|
275
|
+
throw new itw_1.ITWError(`codebook index ${cbIdx} out of range (size ${codebook.length})`);
|
|
276
|
+
}
|
|
277
|
+
const val = codebook[cbIdx];
|
|
278
|
+
for (let t = 0; t < times; t++) {
|
|
279
|
+
out.push(val);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Single value: codebook[(w - literalCount) - depth]
|
|
284
|
+
p += 1;
|
|
285
|
+
const cbIdx = (w - literalCount) - depth;
|
|
286
|
+
if (cbIdx < 0 || cbIdx >= codebook.length) {
|
|
287
|
+
throw new itw_1.ITWError(`codebook index ${cbIdx} out of range (size ${codebook.length})`);
|
|
288
|
+
}
|
|
289
|
+
out.push(codebook[cbIdx]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
// ─── Main entry point ───────────────────────────────────────────────
|
|
295
|
+
function decode0400(buf, payloadOffset, width, height) {
|
|
296
|
+
// Step 1: Read tables (FUN_004b5a40)
|
|
297
|
+
const { intArr, tableB, tableC, n } = readTables(buf, payloadOffset);
|
|
298
|
+
// Step 2: Huffman decode B and C (FUN_004b6250)
|
|
299
|
+
const decodedB = huffDecodeTable(tableB);
|
|
300
|
+
const decodedC = huffDecodeTable(tableC);
|
|
301
|
+
// Step 3: Interleave decoded B/C into intArr (FUN_004b57f0 loop)
|
|
302
|
+
interleave(n, decodedB, decodedC, intArr);
|
|
303
|
+
// Step 4: Expand via powers table (FUN_004b5c40 + FUN_004b5d20)
|
|
304
|
+
const expanded = expand(intArr);
|
|
305
|
+
// Step 5: Copy to pixel buffer (1-indexed in original: data[idx-1+iVar11])
|
|
306
|
+
// Note: Some files produce slightly fewer pixels than expected due to the
|
|
307
|
+
// original C code's lack of bounds checking in the interleave/expand pipeline.
|
|
308
|
+
// We tolerate a small shortfall (< 1% of total) and zero-fill the remainder.
|
|
309
|
+
const total = width * height;
|
|
310
|
+
if (expanded.count < total) {
|
|
311
|
+
const shortfall = total - expanded.count;
|
|
312
|
+
const shortfallPct = (shortfall / total) * 100;
|
|
313
|
+
if (shortfallPct > 1) {
|
|
314
|
+
throw new itw_1.ITWError(`decoded pixel buffer too small: got ${expanded.count}, need ${total} (${shortfallPct.toFixed(1)}% short)`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const pixels = new Uint8Array(total); // zero-filled by default
|
|
318
|
+
const copyLen = Math.min(expanded.count, total);
|
|
319
|
+
for (let i = 0; i < copyLen; i++) {
|
|
320
|
+
pixels[i] = expanded.at(i);
|
|
321
|
+
}
|
|
322
|
+
return { width, height, pixels };
|
|
323
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Copyright (c) 2026 Michał Jaskólski
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the PolyForm Noncommercial License 1.0.0
|
|
7
|
+
* found in the LICENSE file in the root directory of this repository.
|
|
8
|
+
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const commander_1 = require("commander");
|
|
14
|
+
const itw_1 = require("./itw");
|
|
15
|
+
const decode0400_1 = require("./decode0400");
|
|
16
|
+
const decode0300_1 = require("./decode0300");
|
|
17
|
+
const png_1 = require("./png");
|
|
18
|
+
const module_1 = require("module");
|
|
19
|
+
// resolve package.json relative to this file at runtime (works for both ts-node and compiled dist/)
|
|
20
|
+
const _require = (0, module_1.createRequire)(__filename);
|
|
21
|
+
const { version } = _require("../package.json");
|
|
22
|
+
const program = new commander_1.Command();
|
|
23
|
+
program
|
|
24
|
+
.name("itw-decode")
|
|
25
|
+
.description("Decode BMW TIS .ITW proprietary image files to PNG")
|
|
26
|
+
.version(version, "-V, --version", "output the current version")
|
|
27
|
+
.argument("<input>", "path to the .ITW file to decode")
|
|
28
|
+
.option("-o, --output <file>", "output PNG path (default: <input>.png)")
|
|
29
|
+
.option("-d, --dir <directory>", "output directory (default: current working directory)")
|
|
30
|
+
.action(async (input, options) => {
|
|
31
|
+
const inputPath = (0, path_1.resolve)(input);
|
|
32
|
+
const defaultName = (0, path_1.basename)(inputPath).replace(/\.itw$/i, "") + ".png";
|
|
33
|
+
const outputPath = options.output
|
|
34
|
+
? (0, path_1.resolve)(options.output)
|
|
35
|
+
: (0, path_1.join)((0, path_1.resolve)(options.dir ?? process.cwd()), defaultName);
|
|
36
|
+
const buf = (0, fs_1.readFileSync)(inputPath);
|
|
37
|
+
const { header, payloadOffset } = (0, itw_1.parseHeader)(buf);
|
|
38
|
+
let result;
|
|
39
|
+
if (header.subtype === 0x0300) {
|
|
40
|
+
result = (0, decode0300_1.decode0300)(buf, payloadOffset, header.width, header.height);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
result = (0, decode0400_1.decode0400)(buf, payloadOffset, header.width, header.height);
|
|
44
|
+
}
|
|
45
|
+
await (0, png_1.writePng)(outputPath, result.pixels, result.width, result.height);
|
|
46
|
+
console.error(`wrote ${outputPath} (${result.width}x${result.height})`);
|
|
47
|
+
});
|
|
48
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
49
|
+
if (err instanceof itw_1.ITWError) {
|
|
50
|
+
console.error(`error: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error(err);
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
package/dist/itw.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Michał Jaskólski
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the PolyForm Noncommercial License 1.0.0
|
|
6
|
+
* found in the LICENSE file in the root directory of this repository.
|
|
7
|
+
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ITWError = void 0;
|
|
11
|
+
exports.readBE16 = readBE16;
|
|
12
|
+
exports.readBE32From2BE16 = readBE32From2BE16;
|
|
13
|
+
exports.readLE32 = readLE32;
|
|
14
|
+
exports.parseHeader = parseHeader;
|
|
15
|
+
class ITWError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ITWError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
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];
|
|
26
|
+
}
|
|
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)
|
|
43
|
+
throw new ITWError("file too small for header");
|
|
44
|
+
const magic = String.fromCharCode(buf[0], buf[1], buf[2], buf[3]);
|
|
45
|
+
if (magic !== "ITW_")
|
|
46
|
+
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);
|
|
52
|
+
if (version !== 0x0100 && version !== 0x0200)
|
|
53
|
+
throw new ITWError(`unsupported version 0x${version.toString(16)}`);
|
|
54
|
+
if (subtype !== 0x0300 && subtype !== 0x0400)
|
|
55
|
+
throw new ITWError(`unsupported subtype 0x${subtype.toString(16)}`);
|
|
56
|
+
if (bpp !== 8)
|
|
57
|
+
throw new ITWError(`unsupported bpp ${bpp}`);
|
|
58
|
+
return { header: { version, width, height, bpp, subtype }, payloadOffset: 14 };
|
|
59
|
+
}
|
package/dist/png.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Michał Jaskólski
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the PolyForm Noncommercial License 1.0.0
|
|
6
|
+
* found in the LICENSE file in the root directory of this repository.
|
|
7
|
+
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.grayToRgba = grayToRgba;
|
|
11
|
+
exports.writePng = writePng;
|
|
12
|
+
const pngjs_1 = require("pngjs");
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
function grayToRgba(gray, width, height) {
|
|
15
|
+
const out = new Uint8Array(width * height * 4);
|
|
16
|
+
for (let i = 0, j = 0; i < gray.length; i++, j += 4) {
|
|
17
|
+
const v = gray[i];
|
|
18
|
+
out[j] = v;
|
|
19
|
+
out[j + 1] = v;
|
|
20
|
+
out[j + 2] = v;
|
|
21
|
+
out[j + 3] = 255;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
async function writePng(path, gray, width, height) {
|
|
26
|
+
const png = new pngjs_1.PNG({ width, height, colorType: 6 });
|
|
27
|
+
png.data = Buffer.from(grayToRgba(gray, width, height));
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
png
|
|
30
|
+
.pack()
|
|
31
|
+
.pipe((0, fs_1.createWriteStream)(path))
|
|
32
|
+
.on("finish", () => resolve())
|
|
33
|
+
.on("error", reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emdzej/itw-decoder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"description": "Decode BMW TIS .ITW proprietary image files to PNG",
|
|
6
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"itw-decode": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
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
|
+
"dependencies": {
|
|
21
|
+
"commander": "^14.0.3",
|
|
22
|
+
"pngjs": "^7.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.11.0",
|
|
26
|
+
"@types/pngjs": "^6.0.4",
|
|
27
|
+
"ts-node": "^10.9.2",
|
|
28
|
+
"typescript": "^5.4.0"
|
|
29
|
+
}
|
|
30
|
+
}
|