Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code improvements #9

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
ext_modules = [bitmap_decompression, ima_adpcm_decompression])
except:
# RELY ON THE PYTHON FALLBACK.
warnings.warn('The C decompression binaries are not available on this installation. Sounds and bitmaps might not export.')
warnings.warn('WARNING: The C decompression binaries are not available on this installation. Sounds and bitmaps might not export.')
setup(name = 'MediaStation')

# BUILD THE IMA ADPCM DECOMPRESSOR.
Expand Down
18 changes: 7 additions & 11 deletions src/MediaStation/Assets/Asset.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/python3

from dataclasses import dataclass
from enum import IntEnum
Expand Down Expand Up @@ -325,7 +326,6 @@ def _read_section(self, section_type, chunk):

elif section_type == 0x03ed:
# This should only occur in version 1 games.
#
# This type should be only used for LKASB Zazu minigame,
# so it's okay to hardcode the constant 5.
self.mouse = {
Expand All @@ -348,8 +348,7 @@ def _read_section(self, section_type, chunk):
self.unks.append({hex(section_type): Datum(chunk).d})

elif section_type >= 0x0514 and section_type < 0x0519:
# These data are constant across the LKASB constellation
# minigame. I will ignore them.
# These data are constant across the LKASB constellation minigame. I will ignore them.
self.unks.append({hex(section_type): Datum(chunk).d})

elif section_type == 0x0519:
Expand All @@ -361,8 +360,7 @@ def _read_section(self, section_type, chunk):
self.palette = chunk.read(0x300)

elif section_type == 0x05dc:
# It's only not 0.0 in the 'Read to me' and 'Read and play'
# images of Dalmatians. So I will ignore it.
# It's only not 0.0 in the 'Read to me' and 'Read and play' images of Dalmatians. So I will ignore it.
self.unks.append({hex(section_type): Datum(chunk).d})

elif section_type == 0x05dd:
Expand Down Expand Up @@ -412,8 +410,7 @@ def _read_section(self, section_type, chunk):
self.unks.append({hex(section_type): Datum(chunk).d})

elif section_type == 0x776: # IMAGE_SET
# I think this is just a marker for the beginning of the image set
# data (0x778s), so I think we can just ignore this.
# I think this is just a marker for the beginning of the image set data (0x778s), so I think we can just ignore this.
self.unks.append({hex(section_type): Datum(chunk).d})
self.bitmap_declarations = []

Expand All @@ -422,8 +419,7 @@ def _read_section(self, section_type, chunk):
self.bitmap_declarations.append(bitmap_declaration)

elif section_type == 0x779: # IMAGE_SET
# TODO: Figure out what this is. I just split it out here so I
# wouldn't forget about it.
# TODO: Figure out what this is. I just split it out here so I wouldn't forget about it.
self.unk_bitmap_set_bounding_box = Datum(chunk).d

elif section_type >= 0x0773 and section_type <= 0x0780:
Expand Down Expand Up @@ -453,7 +449,7 @@ def _read_section(self, section_type, chunk):
try:
self.sound_encoding = Sound.Encoding(raw_sound_encoding)
except:
raise ValueError(f'Received unknown sound encoding specifier: 0x{raw_sound_encoding:04x}')
raise ValueError(f'ERROR: Received unknown sound encoding specifier: 0x{raw_sound_encoding:04x}')

else:
raise BinaryParsingError(f'Unknown section type: 0x{section_type:0>4x}', chunk.stream)
raise BinaryParsingError(f'ERROR: Unknown section type: 0x{section_type:0>4x}', chunk.stream)
41 changes: 18 additions & 23 deletions src/MediaStation/Assets/Bitmap.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/python3

import io
from enum import IntEnum
Expand All @@ -10,47 +11,44 @@
from ..Primitives.Datum import Datum

# ATTEMPT TO IMPORT THE C-BASED DECOMPRESSION LIBRARY.
# We will fall back to the pure Python implementation if it doesn't work, but there is easily a
# 10x slowdown with pure Python.
# We will fall back to the pure Python implementation if it doesn't work, but there is easily a 10x slowdown with pure Python.
try:
import MediaStationBitmapRle
rle_c_loaded = True
except ImportError:
print('WARNING: The C bitmap decompression binary is not available on this installation. Bitmaps will not be exported.')
rle_c_loaded = False

## A base header for a bitmap.
# A base header for a bitmap.
class BitmapHeader:
## Reads a bitmap header from the binary stream at its current position.
## \param[in] stream - A binary stream that supports the read method.
# Reads a bitmap header from the binary stream at its current position.
# \param[in] stream - A binary stream that supports the read method.
def __init__(self, stream):
self._header_size_in_bytes = Datum(stream).d
self.dimensions = Datum(stream).d
self.compression_type = Bitmap.CompressionType(Datum(stream).d)
# TODO: Figure out what this is.
# This has something to do with the width of the bitmap but is always
# a few pixels off from the width. And in rare cases it seems to be
# the true width!
# This has something to do with the width of the bitmap but is always a few pixels off from the width.
# And in rare cases it seems to be the true width!
self.unk2 = Datum(stream).d

@property
def _is_compressed(self) -> bool:
return (self.compression_type != Bitmap.CompressionType.UNCOMPRESSED) and \
(self.compression_type != Bitmap.CompressionType.UNCOMPRESSED_2)

## A single, still bitmap.
# A single, still bitmap.
class Bitmap(RectangularBitmap):
class CompressionType(IntEnum):
UNCOMPRESSED = 0
RLE_COMPRESSED = 1
UNCOMPRESSED_2 = 7
UNK1 = 6

## Reads a bitmap from the binary stream at its current position.
## \param[in] stream - A binary stream that supports the read method.
## \param[in] dimensions - The dimensions of the image, if they are known beforehand
## (like in an asset header). Otherwise, the dimensions will be read
## from the image.
# Reads a bitmap from the binary stream at its current position.
# \param[in] stream - A binary stream that supports the read method.
# \param[in] dimensions - The dimensions of the image, if they are known beforehand
# (like in an asset header). Otherwise, the dimensions will be read from the image.
def __init__(self, chunk, header_class = BitmapHeader):
super().__init__()
self.name = None
Expand All @@ -76,19 +74,16 @@ def __init__(self, chunk, header_class = BitmapHeader):
# VERIFY THAT THE WIDTH IS CORRECT.
if len(self._pixels) != (self._width * self._height):
# TODO: This was to enable
# Hunchback:346.CXT:img_q13_BackgroundPanelA to export
# properly. It turns out the true width was in fact
# what's in the header rather than what's actually stored
# in the width. I don't know the other cases where this might
# happen, or what regressions might be caused.
# Hunchback:346.CXT:img_q13_BackgroundPanelA to export properly.
# It turns out the true width was in fact what's in the header rather than what's actually stored in the width.
# I don't know the other cases where this might happen, or what regressions might be caused.
if len(self._pixels) == (self.header.unk2 * self._height):
self._width = self.header.unk2
print(f'WARNING: Found and corrected mismatched width in uncompressed bitmap. Header: {self.header.unk2}. Width: {self._width}. Resetting width to header.')
else:
print(f'WARNING: Found mismatched width in uncompressed bitmap. Header: {self.header.unk2}. Width: {self._width}. This image might not be exported correctly.')

## Calculates the total number of bytes the uncompressed image
## (pixels) should occupy, rounded up to the closest whole byte.
# Calculates the total number of bytes the uncompressed image (pixels) should occupy, rounded up to the closest whole byte.
@property
def _expected_bitmap_length_in_bytes(self) -> int:
return self.width * self.height
Expand All @@ -101,8 +96,8 @@ def export(self, root_directory_path: str, command_line_arguments):
def decompress_bitmap(self):
self._pixels = MediaStationBitmapRle.decompress(self._raw, self.width, self.height)

## \return The decompressed pixels that represent this image.
## The number of bytes is the same as the product of the width and the height.
# \return The decompressed pixels that represent this image.
# The number of bytes is the same as the product of the width and the height.
@property
def pixels(self) -> bytes:
if self._pixels is None:
Expand Down
16 changes: 8 additions & 8 deletions src/MediaStation/Assets/BitmapRle.c
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

/// Actually decompresses the Media Station RLE stream, and easily provides a 10x performance improvement
/// over the pure Python implementation.
// Actually decompresses the Media Station RLE stream, and easily provides a 10x performance improvement
// over the pure Python implementation.
static PyObject *method_decompress_media_station_rle(PyObject *self, PyObject *args) {
// READ THE PARAMETERS FROM PYTHON.
char *compressed_image;
Expand Down Expand Up @@ -30,7 +30,7 @@ static PyObject *method_decompress_media_station_rle(PyObject *self, PyObject *a
keyframe_image = NULL;
}

// MAKE SURE THE PARAMETERS ARE SANE.
// MAKE SURE THE PARAMETERS ARE SAME.
// The full width and full height are optional, so if they are not provided
// assume the full width and height is the same as the width and height for
// this specific bitmap.
Expand Down Expand Up @@ -68,7 +68,7 @@ static PyObject *method_decompress_media_station_rle(PyObject *self, PyObject *a
if (decompressed_image_object == NULL) {
// TODO: We really should use Py_DECREF here I think, but since the
// program will currently just quit it isn't a big deal.
PyErr_Format(PyExc_RuntimeError, "BitmapRle.c: Failed to allocate decompressed image data buffer.");
PyErr_Format(PyExc_RuntimeError, "BitmapRle.c::PyBytes_FromStringAndSize(): Failed to allocate decompressed image data buffer.");
return NULL;
}
char *decompressed_image = PyBytes_AS_STRING(decompressed_image_object);
Expand Down Expand Up @@ -202,15 +202,15 @@ static PyObject *method_decompress_media_station_rle(PyObject *self, PyObject *a
return decompressed_image_object;
}

/// Defines the Python methods callable in this module.
// Defines the Python methods callable in this module.
static PyMethodDef MediaStationBitmapRleDecompressionMethod[] = {
{"decompress", method_decompress_media_station_rle, METH_VARARGS, "Decompresses raw Media Station RLE-encoded streams into an image bitmap (8-bit indexed color) and a transparency bitmap (monochrome, but still 8-bit for simplicity)."},
// An entry of nulls must be provided to indicate we're done.
{NULL, NULL, 0, NULL}
};

/// Defines the Python module itself. Because the module requires references to
/// each of the methods, the module must be defined after the methods.
// Defines the Python module itself. Because the module requires references to
// each of the methods, the module must be defined after the methods.
static struct PyModuleDef MediaStationBitmapRleModule = {
PyModuleDef_HEAD_INIT,
"BitmapRle",
Expand All @@ -222,7 +222,7 @@ static struct PyModuleDef MediaStationBitmapRleModule = {
MediaStationBitmapRleDecompressionMethod
};

/// Called when a Python script inputs this module for the first time.
// Called when a Python script inputs this module for the first time.
PyMODINIT_FUNC PyInit_MediaStationBitmapRle(void) {
return PyModule_Create(&MediaStationBitmapRleModule);
}
22 changes: 11 additions & 11 deletions src/MediaStation/Assets/BitmapSet.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/python3

import os
from pathlib import Path
Expand All @@ -7,25 +8,24 @@
from ..Primitives.Datum import Datum
from .Bitmap import Bitmap, BitmapHeader

## This is not an animation or a sprite but just a collection of static bitmaps.
## It is a fairly rare asset type, having only been observed in:
## - Hercules, 1531.CXT. Seems to be some sort of changeable background.
## This is denoted in the PROFILE._ST by a strange line:
## "# image_7d12g_Background 15000 15001 15002 15003 15004 15005 15006 15007 15008 15009 15010 15011 15012 15013"
## Indeed, there are 14 images in this set.
# This is not an animation or a sprite but just a collection of static bitmaps.
# It is a fairly rare asset type, having only been observed in:
# - Hercules, 1531.CXT. Seems to be some sort of changeable background.
# This is denoted in the PROFILE._ST by a strange line:
# "# image_7d12g_Background 15000 15001 15002 15003 15004 15005 15006 15007 15008 15009 15010 15011 15012 15013"
# Indeed, there are 14 images in this set.

## Each bitmap is declared in the asset header.
# Each bitmap is declared in the asset header.
class BitmapSetBitmapDeclaration:
def __init__(self, stream):
self.index = Datum(stream).d
# This is the ID as reported in PROFILE.ST.
# Using the example above, it's something like 15000, 15001,
# and so forth. Should increase along with the indices
# Using the example above, it's something like 15000, 15001, and so forth. Should increase along with the indices
self.id = Datum(stream).d
# This includes the space requried for the header.
self.chunk_length_in_bytes = Datum(stream).d

## The bitmap header for one of the bitmaps in the bitmap set.
# The bitmap header for one of the bitmaps in the bitmap set.
class BitmapSetBitmapHeader(BitmapHeader):
def __init__(self, stream):
# Specifies the position of the bitmap in the bitmap set.
Expand Down Expand Up @@ -58,7 +58,7 @@ def read_chunk(self, chunk):
# VERIFY BITMAP DATA WILL NOT BE LOST.
existing_bitmap_with_same_index = self.bitmaps.get(bitmap.header.index)
if existing_bitmap_with_same_index is not None:
# Interestingly, in Hercules the same images that occur in the first subfile
# Interestingly, in Hercules the same images that occur in the first subfile
# also seem to occur in later subfiles (with the same index). To make sure
# no data is being lost, we will just ensure that the bitmap being replaced
# has the exact same pixel data as the bitmap replacing it.
Expand Down
15 changes: 8 additions & 7 deletions src/MediaStation/Assets/Font.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/python3

import os
from pathlib import Path
Expand All @@ -9,7 +10,7 @@
from ..Primitives.Point import Point
from .Bitmap import Bitmap, BitmapHeader

## A single glyph (bitmap) in a font.
# A single glyph (bitmap) in a font.
class FontGlyph(Bitmap):
def __init__(self, chunk):
# A special bitmap header is not needed because the extra
Expand All @@ -19,21 +20,21 @@ def __init__(self, chunk):
self.unk2 = Datum(chunk).d
super().__init__(chunk)

## A font is a collection of glyphs.
## Fonts have a very similar structure as sprites, but fonts are of course
## not animations so they do not derive from the animation class.
# A font is a collection of glyphs.
# Fonts have a very similar structure as sprites, but fonts are of course
# not animations so they do not derive from the animation class.
class Font:
def __init__(self, header):
self.name = None
self.glyphs = []

## Adds a glyph to the font collection.
# Adds a glyph to the font collection.
def append(self, chunk):
font_glyph = FontGlyph(chunk)
self.glyphs.append(font_glyph)

## Since the font is not an animation, each character in the font should
## always be exported separately.
# Since the font is not an animation, each character in the font should
# always be exported separately.
def export(self, root_directory_path, command_line_arguments):
frame_export_directory_path = os.path.join(root_directory_path, self.name)
Path(frame_export_directory_path).mkdir(parents = True, exist_ok = True)
Expand Down
Loading
Loading