Skip to content

Commit d979183

Browse files
committed
add implementation
1 parent a35c8e2 commit d979183

File tree

6 files changed

+157
-0
lines changed

6 files changed

+157
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
venv
2+
.vscode

examples/hello-ncurses.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pyxp
2+
image_layers = pyxp.load("hello.xp")
3+
layer = image_layers[0]
4+
pos = lambda x, y: x + y * layer.height
5+
6+
from curses import wrapper
7+
import _curses
8+
import locale
9+
10+
def main(stdscr):
11+
locale.setlocale(locale.LC_ALL, '')
12+
stdscr.clear()
13+
for i in range(layer.width):
14+
for j in range(layer.height):
15+
tile = layer.tiles[pos(i, j)]
16+
try:
17+
char = tile.ascii_code.decode("cp437")
18+
char = char.replace(chr(0), "")
19+
stdscr.addstr(i, j, char)
20+
except _curses.error:
21+
pass
22+
23+
stdscr.refresh()
24+
stdscr.getkey()
25+
26+
wrapper(main)

examples/hello.xp

456 Bytes
Binary file not shown.

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build-system]
2+
requires = [
3+
"setuptools>=42",
4+
"wheel"
5+
]
6+
build-backend = "setuptools.build_meta"

setup.cfg

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[metadata]
2+
name = pyxp-mattlink
3+
version = 0.0.1
4+
author = Matt Link
5+
author_email = author@example.com
6+
description = A small REXPaint .xp file loader
7+
long_description = file: README.md
8+
long_description_content_type = text/markdown
9+
url = https://github.com/mattlink/pyxp
10+
project_urls =
11+
Bug Tracker = https://github.com/mattlink/pyxp/issues
12+
classifiers =
13+
Programming Language :: Python :: 3
14+
License :: OSI Approved :: GPL
15+
Operating System :: OS Independent
16+
17+
[options]
18+
package_dir =
19+
= src
20+
packages = find:
21+
python_requires = >=3.6
22+
23+
[options.packages.find]
24+
where = src

src/pyxp/__init__.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import gzip
2+
from typing import List
3+
from dataclasses import dataclass
4+
5+
META_SIZE = 8
6+
META_OFFSETS = {
7+
"version": (0,4),
8+
"layers": (4,8),
9+
}
10+
11+
# offsets relative to a given layer context (starting byte)
12+
LAYER_META_SIZE = 8
13+
LAYER_META_OFFSETS = {
14+
"width": (0, 4),
15+
"height": (4, 8),
16+
}
17+
18+
# offests relative to given image contxt within layer
19+
TILE_SIZE = 10
20+
TILE_OFFSETS = {
21+
"ascii": (0, 4),
22+
"fg_r": (4, 5),
23+
"fg_g": (5, 6),
24+
"fg_b": (6, 7),
25+
"bg_r": (7, 8),
26+
"bg_g": (8, 9),
27+
"bg_b": (9, 10),
28+
}
29+
30+
def load_offset_raw(xp_data: bytes, offsets: dict, offset_key: str) -> str:
31+
offset = offsets.get(offset_key)
32+
assert offset, f"No offset found for {offset_key}"
33+
return xp_data[offset[0]:offset[1]]
34+
35+
def load_offset(xp_data: bytes, offsets: dict, offset_key: str) -> int:
36+
offset = offsets.get(offset_key)
37+
assert offset, f"No offset found for {offset_key}"
38+
return int.from_bytes(
39+
xp_data[offset[0]:offset[1]],
40+
byteorder="little"
41+
)
42+
43+
44+
@dataclass
45+
class Tile:
46+
ascii_code: str
47+
fg_r: str
48+
fg_g: str
49+
fg_b: str
50+
bg_r: str
51+
bg_g: str
52+
bg_b: str
53+
54+
55+
@dataclass
56+
class ImageLayer:
57+
width: int
58+
height: int
59+
tiles: List[Tile]
60+
61+
62+
def load(file_name: str) -> List[ImageLayer]:
63+
images = []
64+
65+
xp_data = gzip.open(file_name).read()
66+
67+
version = load_offset(xp_data, META_OFFSETS, "version")
68+
layers = load_offset(xp_data, META_OFFSETS, "layers")
69+
70+
# Reset offset context (we're done parsing metadata)
71+
xp_data = xp_data[META_SIZE:]
72+
73+
for layer in range(layers):
74+
image_width = load_offset(xp_data, LAYER_META_OFFSETS, "width")
75+
image_height = load_offset(xp_data, LAYER_META_OFFSETS, "height")
76+
77+
image = ImageLayer(image_width, image_height, [])
78+
79+
# Reset layer offset context
80+
xp_data = xp_data[LAYER_META_SIZE:]
81+
82+
num_tiles = image_width * image_height
83+
for tile in range(num_tiles):
84+
85+
ascii_code = load_offset_raw(xp_data, TILE_OFFSETS, "ascii")
86+
fg_r = load_offset(xp_data, TILE_OFFSETS, "fg_r")
87+
fg_g = load_offset(xp_data, TILE_OFFSETS, "fg_g")
88+
fg_b = load_offset(xp_data, TILE_OFFSETS, "fg_b")
89+
bg_r = load_offset(xp_data, TILE_OFFSETS, "bg_r")
90+
bg_g = load_offset(xp_data, TILE_OFFSETS, "bg_g")
91+
bg_b = load_offset(xp_data, TILE_OFFSETS, "bg_b")
92+
93+
image.tiles.append(Tile(ascii_code, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b))
94+
95+
# Reset tile offset context
96+
xp_data = xp_data[TILE_SIZE:]
97+
98+
images.append(image)
99+
100+
return images

0 commit comments

Comments
 (0)