-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmngToPngs.mjs
executable file
·133 lines (103 loc) · 3.67 KB
/
mngToPngs.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env node
// mngToPngs: a command-line tool to convert an .MNG file generated by MAME
// (using -mngwrite) to a sequence of .PNG files.
//
// It's a quicky-and-dirty script, and is only intended for the simple .MNG
// profile that MAME emits.
//
// PNG spec: http://www.libpng.org/pub/png/spec/
// MNG spec: http://www.libpng.org/pub/mng/spec/
//
// Note: generation on the MAME side is done in lib/util/png.cpp
import { existsSync, mkdirSync, openSync, readSync, writeFileSync } from "fs";
import { basename } from "path";
/** Magic header for PNG files */
const pngHeader = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
/** Image tags that should be copied verbatim to the .PNG */
const tagsToCopy = new Set(["IHDR", "IDAT", "IEND"]);
/** Stream out the tagged chunks from the .MNG file */
function* getChunks(fd) {
let offset = 0;
// Skip past the .MNG magic header. Ideally we'd verify it.
{
const fileHeaderSize = 8;
const fileHeader = Buffer.alloc(fileHeaderSize)
readSync(fd, fileHeader, 0, fileHeaderSize, 0); // TODO: test
offset += fileHeaderSize;
}
while (true) {
// Read the chunk header (tag and size)
const chunkHeaderSize = 8;
const chunkHeader = Buffer.alloc(chunkHeaderSize)
readSync(fd, chunkHeader, 0, chunkHeaderSize, offset); // TODO: test
offset += chunkHeaderSize;
const size = chunkHeader.readUInt32BE(0);
const tag =
String.fromCharCode(chunkHeader.readUInt8(4)) +
String.fromCharCode(chunkHeader.readUInt8(5)) +
String.fromCharCode(chunkHeader.readUInt8(6)) +
String.fromCharCode(chunkHeader.readUInt8(7));
const payloadAndCrcSize = size + 4;
const payloadAndCrc = Buffer.alloc(payloadAndCrcSize);
readSync(fd, payloadAndCrc, 0, payloadAndCrcSize, offset); // TODO: test
offset += payloadAndCrcSize;
const chunk = Buffer.concat([chunkHeader, payloadAndCrc]);
yield [tag, chunk];
}
}
function getCliArgs() {
if (process.argv.length < 4) {
console.error(`Usage: (command) (input.mng) (output directory)`);
process.exit(1);
}
const [,,filename, dirname] = process.argv;
try {
if (!existsSync(dirname)) {
mkdirSync(dirname);
}
} catch (_) {
console.error(`Error: can't create output directory '${dirname}'`);
process.exit(1);
}
let fh;
try {
fh = openSync(filename);
} catch (_) {
console.error(`Error: can't open input file '${filename}'`);
process.exit(1);
}
return [filename, fh, dirname];
}
const [filename, fd, dirname] = getCliArgs();
// Pass 1: count frames, just so we can make the frame counter have a
// consistent number of digits
//
// Although it's tempting to use the nominal frame count in the file header,
// MAME currently (v0.269) reports it as zero. So yes, we stream the whole
// video through twice.
let frames = 0;
for (const [tag] of getChunks(fd)) {
if (tag === "IHDR") frames++;
if (tag === "MEND") break;
}
const numDigits = Math.ceil(Math.log10(frames));
// Pass 2: emit the copied frames
let frame = 0;
const [filestem] = basename(filename).split(".");
/** Staged chunks for whichever frame is current */
let chunksToOutput = [];
for (const [tag, chunk] of getChunks(fd)) {
if (tagsToCopy.has(tag)) chunksToOutput.push(chunk);
// End-of-image tag: close up the staged file
if (tag === "IEND") {
const filename = `${dirname}/${filestem}_${String(frame).padStart(numDigits, "0")}.png`;
console.log("Writing", filename);
// TODO: catch errors
writeFileSync(filename, Buffer.concat([pngHeader, ...chunksToOutput]));
// Open up the chunks for the next frame
chunksToOutput = [];
frame++;
}
// End-of-movie tag: done
if (tag === "MEND") break;
}