Jump to content

All Dogs go to Heaven 2 Animated Moviebook (PC) .IWX file


Recommended Posts

  • Members
Posted
1 hour ago, Puterboy1 said:

Potrzebuję sposobu na wyodrębnienie jego zawartości dla mojego znajomego:  https://github.com/user-attachments/files/28807415/ALLDOGS.zip

 

import argparse
import struct
import sys
import threading
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class EmbeddedFile:
    kind: str
    offset: int
    size: int
    extension: str
    label: str
    details: str

    @property
    def end(self) -> int:
        return self.offset + self.size


def u16(data: bytes, offset: int) -> int:
    return struct.unpack_from("<H", data, offset)[0]


def u32(data: bytes, offset: int) -> int:
    return struct.unpack_from("<I", data, offset)[0]


def i32(data: bytes, offset: int) -> int:
    return struct.unpack_from("<i", data, offset)[0]


def find_all(data: bytes, needle: bytes):
    pos = 0
    while True:
        hit = data.find(needle, pos)
        if hit < 0:
            return
        yield hit
        pos = hit + 1


def parse_bmp(data: bytes, offset: int) -> EmbeddedFile | None:
    size = len(data)
    if offset + 54 > size or data[offset : offset + 2] != b"BM":
        return None

    file_size = u32(data, offset + 2)
    reserved_a = u16(data, offset + 6)
    reserved_b = u16(data, offset + 8)
    pixel_offset = u32(data, offset + 10)
    dib_size = u32(data, offset + 14)

    if not (26 <= file_size <= size - offset):
        return None
    if reserved_a != 0 or reserved_b != 0:
        return None
    if dib_size not in (12, 40, 52, 56, 108, 124):
        return None
    if pixel_offset < 14 + dib_size or pixel_offset > file_size:
        return None

    if dib_size == 12:
        width = u16(data, offset + 18)
        height = u16(data, offset + 20)
        planes = u16(data, offset + 22)
        bpp = u16(data, offset + 24)
        compression = 0
    else:
        width = i32(data, offset + 18)
        height = i32(data, offset + 22)
        planes = u16(data, offset + 26)
        bpp = u16(data, offset + 28)
        compression = u32(data, offset + 30)

    abs_width = abs(width)
    abs_height = abs(height)
    if not (1 <= abs_width <= 10000 and 1 <= abs_height <= 10000):
        return None
    if planes != 1 or bpp not in (1, 4, 8, 16, 24, 32):
        return None
    if compression not in (0, 1, 2, 3, 4, 5, 6):
        return None

    label = f"{abs_width}x{abs_height}_{bpp}bpp"
    details = f"BMP {abs_width}x{abs_height}, {bpp} bpp, size {file_size}"
    return EmbeddedFile("bmp", offset, file_size, ".bmp", label, details)


def parse_wave(data: bytes, offset: int) -> EmbeddedFile | None:
    size = len(data)
    if offset + 12 > size or data[offset : offset + 4] != b"RIFF":
        return None

    riff_size = u32(data, offset + 4)
    end = offset + 8 + riff_size
    if end > size or riff_size < 4 or data[offset + 8 : offset + 12] != b"WAVE":
        return None

    fmt_found = False
    data_found = False
    audio_format = 0
    channels = 0
    sample_rate = 0
    bits_per_sample = 0
    data_size = 0

    pos = offset + 12
    while pos + 8 <= end:
        chunk_id = data[pos : pos + 4]
        chunk_size = u32(data, pos + 4)
        chunk_data = pos + 8
        chunk_end = chunk_data + chunk_size
        if chunk_end > end:
            break

        if chunk_id == b"fmt " and chunk_size >= 16:
            audio_format = u16(data, chunk_data)
            channels = u16(data, chunk_data + 2)
            sample_rate = u32(data, chunk_data + 4)
            bits_per_sample = u16(data, chunk_data + 14)
            fmt_found = True
        elif chunk_id == b"data":
            data_size = chunk_size
            data_found = True

        pos = chunk_end + (chunk_size & 1)

    if not (fmt_found and data_found):
        return None
    if not (1 <= channels <= 8 and 1000 <= sample_rate <= 384000):
        return None
    if bits_per_sample not in (4, 8, 12, 16, 24, 32):
        return None

    label = f"{channels}ch_{sample_rate}hz_{bits_per_sample}bit"
    details = (
        f"WAVE format {audio_format}, {channels} ch, "
        f"{sample_rate} Hz, {bits_per_sample} bit, data {data_size}"
    )
    return EmbeddedFile("wav", offset, end - offset, ".wav", label, details)


def scan_data(data: bytes, include_bmp: bool = True, include_wav: bool = True) -> list[EmbeddedFile]:
    found: list[EmbeddedFile] = []

    if include_bmp:
        for offset in find_all(data, b"BM"):
            item = parse_bmp(data, offset)
            if item:
                found.append(item)

    if include_wav:
        for offset in find_all(data, b"RIFF"):
            item = parse_wave(data, offset)
            if item:
                found.append(item)

    found.sort(key=lambda item: (item.offset, item.kind, item.size))
    return found


def output_name(source_stem: str, index: int, item: EmbeddedFile) -> str:
    clean_label = "".join(c if c.isalnum() or c in ("_", "-") else "_" for c in item.label)
    return f"{source_stem}_{index:04d}_{item.offset:08X}_{clean_label}{item.extension}"


def extract_imx(
    input_path: Path,
    output_dir: Path,
    include_bmp: bool = True,
    include_wav: bool = True,
    use_subfolders: bool = True,
) -> tuple[list[EmbeddedFile], list[Path]]:
    data = input_path.read_bytes()
    items = scan_data(data, include_bmp=include_bmp, include_wav=include_wav)
    output_dir.mkdir(parents=True, exist_ok=True)

    written: list[Path] = []
    for index, item in enumerate(items, 1):
        folder = output_dir / item.kind if use_subfolders else output_dir
        folder.mkdir(parents=True, exist_ok=True)
        path = folder / output_name(input_path.stem, index, item)
        path.write_bytes(data[item.offset : item.end])
        written.append(path)

    return items, written


def summarize(items: list[EmbeddedFile]) -> str:
    bmp_count = sum(1 for item in items if item.kind == "bmp")
    wav_count = sum(1 for item in items if item.kind == "wav")
    total_size = sum(item.size for item in items)
    lines = [
        f"Found {len(items)} file(s): {bmp_count} BMP, {wav_count} WAVE.",
        f"Total extracted byte count: {total_size:,}",
    ]
    for item in items[:40]:
        lines.append(f"{item.kind.upper()} @ 0x{item.offset:08X}, {item.details}")
    if len(items) > 40:
        lines.append(f"... {len(items) - 40} more file(s)")
    return "\n".join(lines)


def run_gui() -> None:
    import tkinter as tk
    from tkinter import filedialog, messagebox, scrolledtext, ttk

    root = tk.Tk()
    root.title("IMX BMP/WAVE Extractor")
    root.geometry("780x520")
    root.resizable(True, True)

    default_input = Path("ALLDOGS.IMX").resolve() if Path("ALLDOGS.IMX").exists() else Path.cwd()
    input_var = tk.StringVar(value=str(default_input))
    output_var = tk.StringVar(value=str((Path.cwd() / "_alldogs_imx_extract").resolve()))
    bmp_var = tk.BooleanVar(value=True)
    wav_var = tk.BooleanVar(value=True)
    subfolders_var = tk.BooleanVar(value=True)
    status_var = tk.StringVar(value="Ready")

    main = ttk.Frame(root, padding=12)
    main.pack(fill="both", expand=True)
    main.columnconfigure(1, weight=1)
    main.rowconfigure(5, weight=1)

    def browse_input() -> None:
        path = filedialog.askopenfilename(title="Choose IMX file", filetypes=[("IMX files", "*.imx"), ("All files", "*.*")])
        if path:
            input_var.set(path)
            output_var.set(str(Path(path).with_suffix("").with_name(Path(path).stem + "_extract")))

    def browse_output() -> None:
        path = filedialog.askdirectory(title="Choose output folder")
        if path:
            output_var.set(path)

    def set_text(text: str) -> None:
        log.configure(state="normal")
        log.delete("1.0", "end")
        log.insert("1.0", text)
        log.configure(state="disabled")

    def selected_types() -> tuple[bool, bool]:
        include_bmp = bmp_var.get()
        include_wav = wav_var.get()
        if not include_bmp and not include_wav:
            raise ValueError("Select at least one file type.")
        return include_bmp, include_wav

    def scan_clicked() -> None:
        try:
            include_bmp, include_wav = selected_types()
            input_path = Path(input_var.get().strip())
            if not input_path.exists():
                raise ValueError("Input file does not exist.")
            data = input_path.read_bytes()
            items = scan_data(data, include_bmp=include_bmp, include_wav=include_wav)
            status_var.set(f"Scan complete: {len(items)} file(s).")
            set_text(summarize(items))
        except Exception as exc:
            status_var.set("Scan failed")
            messagebox.showerror("Scan failed", str(exc))

    def extract_clicked() -> None:
        def worker() -> None:
            try:
                include_bmp, include_wav = selected_types()
                input_path = Path(input_var.get().strip())
                output_dir = Path(output_var.get().strip())
                if not input_path.exists():
                    raise ValueError("Input file does not exist.")
                items, written = extract_imx(
                    input_path=input_path,
                    output_dir=output_dir,
                    include_bmp=include_bmp,
                    include_wav=include_wav,
                    use_subfolders=subfolders_var.get(),
                )
                text = summarize(items) + f"\n\nWrote {len(written)} file(s) to:\n{output_dir}"
                root.after(0, lambda: status_var.set(f"Extracted {len(written)} file(s)."))
                root.after(0, lambda: set_text(text))
                root.after(0, lambda: messagebox.showinfo("Extract complete", f"Extracted {len(written)} file(s)."))
            except Exception as exc:
                root.after(0, lambda: status_var.set("Extract failed"))
                root.after(0, lambda: messagebox.showerror("Extract failed", str(exc)))

        status_var.set("Extracting...")
        threading.Thread(target=worker, daemon=True).start()

    ttk.Label(main, text="Input IMX").grid(row=0, column=0, sticky="w", padx=6, pady=6)
    ttk.Entry(main, textvariable=input_var).grid(row=0, column=1, sticky="ew", padx=6, pady=6)
    ttk.Button(main, text="Browse", command=browse_input).grid(row=0, column=2, padx=6, pady=6)

    ttk.Label(main, text="Output Folder").grid(row=1, column=0, sticky="w", padx=6, pady=6)
    ttk.Entry(main, textvariable=output_var).grid(row=1, column=1, sticky="ew", padx=6, pady=6)
    ttk.Button(main, text="Browse", command=browse_output).grid(row=1, column=2, padx=6, pady=6)

    options = ttk.Frame(main)
    options.grid(row=2, column=0, columnspan=3, sticky="w", padx=6, pady=8)
    ttk.Checkbutton(options, text="Extract BMP", variable=bmp_var).grid(row=0, column=0, padx=8, pady=4)
    ttk.Checkbutton(options, text="Extract WAVE", variable=wav_var).grid(row=0, column=1, padx=8, pady=4)
    ttk.Checkbutton(options, text="Use subfolders", variable=subfolders_var).grid(row=0, column=2, padx=8, pady=4)

    buttons = ttk.Frame(main)
    buttons.grid(row=3, column=0, columnspan=3, sticky="e", padx=6, pady=8)
    ttk.Button(buttons, text="Scan", command=scan_clicked).grid(row=0, column=0, padx=6)
    ttk.Button(buttons, text="Extract", command=extract_clicked).grid(row=0, column=1, padx=6)

    ttk.Label(main, textvariable=status_var).grid(row=4, column=0, columnspan=3, sticky="w", padx=6, pady=4)
    log = scrolledtext.ScrolledText(main, wrap="word", state="disabled")
    log.grid(row=5, column=0, columnspan=3, sticky="nsew", padx=6, pady=6)

    root.mainloop()


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Extract BMP and WAVE files from IMX containers.")
    parser.add_argument("input", nargs="?", help="Input .imx file.")
    parser.add_argument("-o", "--output", help="Output folder. Default: <input>_extract")
    parser.add_argument("--no-bmp", action="store_true", help="Do not extract BMP files.")
    parser.add_argument("--no-wav", action="store_true", help="Do not extract WAVE files.")
    parser.add_argument("--flat", action="store_true", help="Do not create bmp/wav subfolders.")
    parser.add_argument("--scan-only", action="store_true", help="Only scan and print a summary.")
    parser.add_argument("--gui", action="store_true", help="Open the graphical interface.")
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(sys.argv[1:] if argv is None else argv)
    if args.gui or not args.input:
        run_gui()
        return 0

    input_path = Path(args.input)
    output_dir = Path(args.output) if args.output else input_path.with_suffix("").with_name(input_path.stem + "_extract")
    include_bmp = not args.no_bmp
    include_wav = not args.no_wav
    if not include_bmp and not include_wav:
        raise SystemExit("Nothing to extract: both BMP and WAVE are disabled.")

    if args.scan_only:
        items = scan_data(input_path.read_bytes(), include_bmp=include_bmp, include_wav=include_wav)
        print(summarize(items))
        return 0

    items, written = extract_imx(
        input_path=input_path,
        output_dir=output_dir,
        include_bmp=include_bmp,
        include_wav=include_wav,
        use_subfolders=not args.flat,
    )
    print(summarize(items))
    print(f"\nWrote {len(written)} file(s) to {output_dir}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...