start adding lapk extraction

This commit is contained in:
elliotcraft79 2021-06-05 20:51:04 +09:30
parent 48539684ce
commit 57b85f3f7c
8 changed files with 20548 additions and 28 deletions

20171
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -50,6 +50,7 @@
"homepage": ".", "homepage": ".",
"devDependencies": { "devDependencies": {
"eslint-loader": "^4.0.2", "eslint-loader": "^4.0.2",
"kaitai-struct": "^0.9.0",
"replace-in-file": "^6.2.0", "replace-in-file": "^6.2.0",
"yargs": "^16.2.0" "yargs": "^16.2.0"
} }

102
scripts/extract/Lapk.js Normal file
View file

@ -0,0 +1,102 @@
// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['kaitai-struct/KaitaiStream', 'LainCompress'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('kaitai-struct/KaitaiStream'), require('./lain_compress.js'));
} else {
root.Lapk = factory(root.KaitaiStream, root.LainCompress);
}
}(this, function (KaitaiStream, LainCompress) {
var Lapk = (function() {
function Lapk(_io, _parent, _root) {
this._io = _io;
this._parent = _parent;
this._root = _root || this;
this._read();
}
Lapk.prototype._read = function() {
this.magic = this._io.readBytes(4);
if (!((KaitaiStream.byteArrayCompare(this.magic, [108, 97, 112, 107]) == 0))) {
throw new KaitaiStream.ValidationNotEqualError([108, 97, 112, 107], this.magic, this._io, "/seq/0");
}
this.lapkSize = this._io.readU4le();
this._raw_data = this._io.readBytes(this.lapkSize);
var _io__raw_data = new KaitaiStream(this._raw_data);
this.data = new LapkData(_io__raw_data, this, this._root);
}
var CellHeader = Lapk.CellHeader = (function() {
function CellHeader(_io, _parent, _root) {
this._io = _io;
this._parent = _parent;
this._root = _root || this;
this._read();
}
CellHeader.prototype._read = function() {
this.cellOffset = this._io.readU4le();
this.negativeXPosition = this._io.readU2le();
this.negativeYPosition = this._io.readU2le();
this.unknown = this._io.readU4le();
}
return CellHeader;
})();
var CellData = Lapk.CellData = (function() {
function CellData(_io, _parent, _root) {
this._io = _io;
this._parent = _parent;
this._root = _root || this;
this._read();
}
CellData.prototype._read = function() {
this.width = this._io.readU2le();
this.height = this._io.readU2le();
this.chrominanceQuantisationScale = this._io.readU2le();
this.luminanceQuantisationScale = this._io.readU2le();
this.imageDataSize = this._io.readU4le();
this.runLengthCodeCount = this._io.readU4le();
this.imageData = this._io.readBytes((this.imageDataSize - 4));
this._raw_bitMask = this._io.readBytesFull();
var _process = new LainCompress();
this.bitMask = _process.decode(this._raw_bitMask);
}
return CellData;
})();
var LapkData = Lapk.LapkData = (function() {
function LapkData(_io, _parent, _root) {
this._io = _io;
this._parent = _parent;
this._root = _root || this;
this._read();
}
LapkData.prototype._read = function() {
this.cellCount = this._io.readU4le();
this.cellHeaders = new Array(this.cellCount);
for (var i = 0; i < this.cellCount; i++) {
this.cellHeaders[i] = new CellHeader(this._io, this, this._root);
}
this._raw_cellData = new Array(this.cellCount);
this.cellData = new Array(this.cellCount);
for (var i = 0; i < this.cellCount; i++) {
this._raw_cellData[i] = this._io.readBytes((i == (this.cellCount - 1) ? (((this._parent.lapkSize - 4) - (this.cellCount * 12)) - this.cellHeaders[i].cellOffset) : (this.cellHeaders[(i + 1)].cellOffset - this.cellHeaders[i].cellOffset)));
var _io__raw_cellData = new KaitaiStream(this._raw_cellData[i]);
this.cellData[i] = new CellData(_io__raw_cellData, this, this._root);
}
}
return LapkData;
})();
return Lapk;
})();
return Lapk;
}));

View file

@ -1,9 +1,10 @@
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { mkdtempSync, rmSync, mkdirSync } from "fs"; import { mkdtempSync, rmSync, mkdirSync } from "fs";
import { join } from "path"; import { join, resolve } from "path";
import { extract_video, extract_audio } from "./extract_media.mjs"; import { extract_video, extract_audio } from "./extract_media.mjs";
import { extract_voice } from "./extract_voice.mjs"; import { extract_voice } from "./extract_voice.mjs";
import { extract_lapks } from "./extract_lapks.mjs";
import yargs from "yargs"; import yargs from "yargs";
import { hideBin } from "yargs/helpers"; import { hideBin } from "yargs/helpers";
@ -35,14 +36,14 @@ const argv = yargs(hideBin(process.argv))
description: description:
"Don't delete any temporary files or directories, useful when using --tempdir (WARNING: uses 6+ GB of space)", "Don't delete any temporary files or directories, useful when using --tempdir (WARNING: uses 6+ GB of space)",
}) })
.option("no_audio", { .option("no_lapks", {
type: "boolean", type: "boolean",
description: "Don't extract audio", description: "Don't extract lapks.bin",
}).argv; }).argv;
mkdirSync(argv.tempdir, { recursive: true }); mkdirSync(argv.tempdir, { recursive: true });
const jpsxdec_jar = join("jpsxdec", "jpsxdec.jar"); const jpsxdec_jar = resolve(join("jpsxdec", "jpsxdec.jar"));
// generate disc indexes // generate disc indexes
if (!argv.no_index) { if (!argv.no_index) {
@ -74,6 +75,10 @@ if (!argv.no_voice) {
extract_voice(argv.tempdir, jpsxdec_jar); extract_voice(argv.tempdir, jpsxdec_jar);
} }
if (!argv.no_lapks) {
extract_lapks(argv.tempdir, jpsxdec_jar);
}
if (!argv.no_delete) { if (!argv.no_delete) {
rmSync(argv.tempdir, { recursive: true }); rmSync(argv.tempdir, { recursive: true });
} }

View file

@ -0,0 +1,130 @@
import { spawnSync } from "child_process";
import { readFileSync, mkdirSync, writeFileSync } from "fs";
import { join, resolve } from "path";
import KaitaiStream from "kaitai-struct/KaitaiStream.js";
import Lapk from "./Lapk.js";
export function extract_lapks(tempdir, jpsxdec_jar) {
spawnSync(
"java",
[
"-jar",
jpsxdec_jar,
"-x",
join(tempdir, "disc1.idx"),
"-i",
"LAPKS.BIN",
"-dir",
tempdir,
],
{ stdio: "inherit" }
);
const lapk_info = JSON.parse(readFileSync("lapks.json"));
let lapk_data = readFileSync(join(tempdir, "LAPKS.BIN"));
let output_folder = join("..", "..", "src", "static", "sprites", "lain");
mkdirSync(output_folder, { recursive: true });
for (let [index, lapk_entry] of lapk_info.entries()) {
let data = lapk_data.slice(
lapk_entry.offset,
lapk_entry.offset + lapk_entry.size
);
let parsed_lapk = new Lapk(new KaitaiStream(data));
let images = [];
if (index != 37) continue;
for (let [frame_number, cell_data] of parsed_lapk.data.cellData.entries()) {
const cell_header = parsed_lapk.data.cellHeaders[frame_number];
// create bitstream header (from https://github.com/m35/jpsxdec/blob/1de0518583236844b93f5fcc8a4d330a01c222b3/laintools/src/laintools/Lain_LAPKS.java#L319)
let bitstream_header = Buffer.alloc(8);
bitstream_header.writeInt8(cell_data.chrominanceQuantisationScale);
bitstream_header.writeInt8(cell_data.luminanceQuantisationScale, 1);
bitstream_header.writeInt16LE(0x3800, 2);
bitstream_header.writeInt16LE(cell_data.runLengthCodeCount, 4);
bitstream_header.writeInt16LE(0x0000, 6);
let output_bitstream = Buffer.concat(
[bitstream_header, cell_data.imageData],
cell_data.imageDataSize + 8
);
let bitstream_file = join(tempdir, `${index}_${frame_number}.bs`);
let out_frame = join(tempdir, `${index}_${frame_number}.png`);
let out_mask = join(tempdir, `${index}_${frame_number}_mask.gray`);
let out_alpha = join(tempdir, `${index}_${frame_number}_alpha.png`);
writeFileSync(bitstream_file, output_bitstream);
writeFileSync(out_mask, Buffer.from(cell_data.bitMask));
spawnSync(
"java",
[
"-jar",
jpsxdec_jar,
"-f",
resolve(bitstream_file),
"-static",
"bs",
"-dim",
`${cell_data.width}x${cell_data.height}`,
"-up",
"Lanczos3",
],
{ stdio: "inherit", cwd: tempdir }
);
spawnSync(
"convert",
[
"-background",
"none",
"(",
"-extent",
"352x367",
"xc:none",
")",
"(",
out_frame,
"(",
"-depth",
"2",
"-size",
`${cell_data.width}x${cell_data.height}`,
out_mask,
"-alpha",
"off",
")",
"-compose",
"copy-opacity",
"-composite",
")",
"-geometry",
`+${320 / 2 + 4 - cell_header.negativeXPosition}+${
352 - 1 - cell_header.negativeYPosition
}`,
"-compose",
"over",
"-composite",
out_alpha,
],
{ stdio: "inherit" }
);
images.push(out_alpha);
}
if (lapk_entry.tile != "") {
spawnSync(
"montage",
[
"-background",
"none",
"-tile",
lapk_entry.tile,
"-geometry",
"+0+0",
...images,
join(output_folder, lapk_entry.output_name)
],
{ stdio: "inherit" }
);
}
}
}

View file

@ -0,0 +1,40 @@
const KaitaiStream = require('kaitai-struct/KaitaiStream');
// based on https://github.com/magical/nlzss/blob/master/lzss3.py and https://github.com/m35/jpsxdec/blob/readme/laintools/src/laintools/Lain_Pk.java
class LainCompress {
decode (io) {
const bits = (byte) => { return [(byte >> 7) & 1, (byte >> 6) & 1,(byte >> 5) & 1,(byte >> 4) & 1,(byte >> 3) & 1,(byte >> 2) & 1, (byte >> 1) & 1, (byte) & 1] };
let compressed = new KaitaiStream(io);
let decompressed_size = compressed.readU4le();
let decompressed = Buffer.alloc(decompressed_size);
let decompressed_pos = 0;
while (decompressed_pos < decompressed_size) {
let flags = bits(compressed.readU1());
for (let flag of flags) {
if (flag === 0) {
decompressed[decompressed_pos] = compressed.readBytes(1);
decompressed_pos++;
}
else if (flag === 1) {
let offset = compressed.readU1() + 1;
let size = compressed.readU1() + 3;
for (let i = 0; i < size; i++) {
decompressed[decompressed_pos] = decompressed[decompressed_pos - offset];
decompressed_pos++;
}
}
if (decompressed_size <= decompressed_pos) break;
}
}
if (decompressed.length !== decompressed_size)
throw new Error("Decompressed size does not match the expected size!");
return decompressed;
}
}
module.exports = LainCompress;

57
scripts/extract/lapk.ksy Normal file
View file

@ -0,0 +1,57 @@
meta:
id: lapk
file-extension: bin
endian: le
ks-opaque-types: true
seq:
- id: magic
contents: 'lapk'
- id: lapk_size
type: u4
- id: data
type: lapk_data
size: lapk_size
types:
cell_header:
seq:
- id: cell_offset
type: u4
- id: negative_x_position
type: u2
- id: negative_y_position
type: u2
- id: unknown
type: u4
cell_data:
seq:
- id: width
type: u2
- id: height
type: u2
- id: chrominance_quantisation_scale
type: u2
- id: luminance_quantisation_scale
type: u2
- id: image_data_size
type: u4
- id: run_length_code_count
type: u4
- id: image_data
size: image_data_size - 4
- id: bit_mask
size-eos: true
process: lain_compress
lapk_data:
seq:
- id: cell_count
type: u4
- id: cell_headers
type: cell_header
repeat: expr
repeat-expr: cell_count
- id: cell_data
type: cell_data
size: "_index == cell_count - 1 ? _parent.lapk_size - 4 - cell_count * 12 - cell_headers[_index].cell_offset : cell_headers[_index + 1].cell_offset - cell_headers[_index].cell_offset"
repeat: expr
repeat-expr: cell_count

View file

@ -0,0 +1,62 @@
[
{ "output_name": "", "offset": 0, "size": 88748 },
{ "output_name": "", "offset": 90112, "size": 79188 },
{ "output_name": "", "offset": 169984, "size": 77196 },
{ "output_name": "", "offset": 247808, "size": 64020 },
{ "output_name": "", "offset": 313344, "size": 278740 },
{ "output_name": "", "offset": 593920, "size": 464668 },
{ "output_name": "", "offset": 1058816, "size": 305600 },
{ "output_name": "", "offset": 1366016, "size": 12304 },
{ "output_name": "", "offset": 1380352, "size": 29096 },
{ "output_name": "", "offset": 1411072, "size": 29196 },
{ "output_name": "", "offset": 1441792, "size": 144216 },
{ "output_name": "", "offset": 1587200, "size": 144384 },
{ "output_name": "", "offset": 1732608, "size": 144148 },
{ "output_name": "", "offset": 1878016, "size": 143984 },
{ "output_name": "", "offset": 2023424, "size": 450176 },
{ "output_name": "", "offset": 2473984, "size": 485652 },
{ "output_name": "", "offset": 2961408, "size": 175544 },
{ "output_name": "", "offset": 3137536, "size": 176852 },
{ "output_name": "", "offset": 3315712, "size": 277336 },
{ "output_name": "", "offset": 3594240, "size": 149004 },
{ "output_name": "", "offset": 3743744, "size": 256896 },
{ "output_name": "", "offset": 4001792, "size": 66872 },
{ "output_name": "", "offset": 4069376, "size": 51500 },
{ "output_name": "", "offset": 4122624, "size": 128272 },
{ "output_name": "", "offset": 4251648, "size": 45952 },
{ "output_name": "", "offset": 4298752, "size": 52884 },
{ "output_name": "", "offset": 4352000, "size": 282900 },
{ "output_name": "", "offset": 4636672, "size": 154348 },
{ "output_name": "", "offset": 4792320, "size": 116584 },
{ "output_name": "", "offset": 4909056, "size": 234832 },
{ "output_name": "", "offset": 5144576, "size": 405952 },
{ "output_name": "", "offset": 5552128, "size": 194376 },
{ "output_name": "", "offset": 5746688, "size": 80684 },
{ "output_name": "", "offset": 5828608, "size": 219864 },
{ "output_name": "", "offset": 6049792, "size": 285248 },
{ "output_name": "", "offset": 6336512, "size": 257924 },
{ "output_name": "", "offset": 6594560, "size": 354416 },
{ "output_name": "spin.png", "offset": 6950912, "size": 325140, "tile":"6x5" },
{ "output_name": "", "offset": 7276544, "size": 355752 },
{ "output_name": "", "offset": 7632896, "size": 371184 },
{ "output_name": "", "offset": 8005632, "size": 391168 },
{ "output_name": "", "offset": 8396800, "size": 314484 },
{ "output_name": "", "offset": 8712192, "size": 238792 },
{ "output_name": "", "offset": 8951808, "size": 475660 },
{ "output_name": "", "offset": 9428992, "size": 281608 },
{ "output_name": "", "offset": 9711616, "size": 311472 },
{ "output_name": "", "offset": 10024960, "size": 233456 },
{ "output_name": "", "offset": 10258432, "size": 339824 },
{ "output_name": "", "offset": 10598400, "size": 276968 },
{ "output_name": "", "offset": 10876928, "size": 488988 },
{ "output_name": "", "offset": 11366400, "size": 395204 },
{ "output_name": "", "offset": 11761664, "size": 432604 },
{ "output_name": "", "offset": 12195840, "size": 624836 },
{ "output_name": "", "offset": 12822528, "size": 315756 },
{ "output_name": "", "offset": 13139968, "size": 391348 },
{ "output_name": "", "offset": 13533184, "size": 12216 },
{ "output_name": "", "offset": 13545472, "size": 12236 },
{ "output_name": "", "offset": 13557760, "size": 365476 },
{ "output_name": "", "offset": 13924352, "size": 12200 }
]