Skip to content

Instantly share code, notes, and snippets.

@a9udn9u
Created December 20, 2023 20:12
Show Gist options
  • Select an option

  • Save a9udn9u/d7b4d2dd79c33aef9ac805f1603a59d1 to your computer and use it in GitHub Desktop.

Select an option

Save a9udn9u/d7b4d2dd79c33aef9ac805f1603a59d1 to your computer and use it in GitHub Desktop.
Convert RDR 2 photo mode file to JPG
// @ts-check
const fsp = require('node:fs/promises');
const {basename, join} = require('path');
const INPUT_FILE_PREFIX = 'PRDR';
const OUTPUT_FILE_SUFFIX = '.jpg';
const BUFSIZE = 4192; // 4 KB
const usage = () => {
const {argv} = process;
console.error("USAGE:", basename(argv[0]), basename(argv[1]), "FILE_OR_DIR");
console.error(` FILE_OR_DIR: Input file name must starts with "${INPUT_FILE_PREFIX}"`);
}
const getInputs = async () => {
const fileOrDir = process.argv[2];
if (!fileOrDir) {
usage();
return [];
}
let stat;
try {
stat = await fsp.stat(process.argv[2]);
} catch (ex) {
console.error("Can't open file:", fileOrDir);
return [];
}
if (stat.isFile() && basename(fileOrDir).startsWith(INPUT_FILE_PREFIX)) {
return [fileOrDir];
}
if (stat.isDirectory()) {
const inputs = (await fsp.readdir(fileOrDir, {withFileTypes: true}))
.filter(e => e.isFile())
.filter(e => e.name.startsWith(INPUT_FILE_PREFIX))
.filter(e => !e.name.endsWith(OUTPUT_FILE_SUFFIX))
.map(e => join(fileOrDir, e.name));
if (!inputs.length) {
usage();
}
return inputs;
}
usage();
return [];
}
const getOutputFile = inputPath => inputPath + OUTPUT_FILE_SUFFIX;
const extract = async (state) => {
const {input, output, buffer} = state;
const {bytesRead} = await input.read(state.buffer, 0, state.buffer.byteLength);
state.bufferOffset = 0;
state.bufferLength = bytesRead;
for (let i = 0; i < bytesRead; i++) {
state.currentByte = buffer[i];
if (!state.dataBegins && state.previousByte == 0xff && state.currentByte == 0xd8) {
// Image data starts at previous byte, inclusive
state.bufferOffset = i - 1;
state.bufferLength -= state.bufferOffset;
state.dataBegins = true;
}
if (!state.dataEnds && state.previousByte == 0xff && state.currentByte == 0xd9) {
// Image data ends at previous byte, exclusive
state.bufferLength = i - 1 - state.bufferOffset;
state.dataEnds = true;
break;
}
state.previousByte = state.currentByte;
}
if (state.dataBegins && state.bufferLength > 0) {
await output.write(buffer, state.bufferOffset, state.bufferLength);
}
if (!state.dataEnds && bytesRead !== 0) {
await extract(state);
}
}
// Main routine
(async () => {
(await getInputs()).forEach(async prdr => {
const input = await fsp.open(prdr, 'r');
const output = await fsp.open(getOutputFile(prdr), 'w');
await extract({
input,
output,
buffer: Buffer.alloc(BUFSIZE),
bufferOffset: 0,
bufferLength: 0,
dataBegins: false,
dataEnds: false,
currentByte: 0x0,
previousByte: 0x0,
});
await input.close();
await output.close();
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment