Skip to content

Commit dfd4571

Browse files
committed
Load BMP, 1BPP, 2BPP, and LZ-compressed *BPP tilesets
1 parent 9d20858 commit dfd4571

File tree

3 files changed

+309
-7
lines changed

3 files changed

+309
-7
lines changed

TODO.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
## Features
44

5-
* **Load tileset graphics** (support GIF, BMP, 1BPP, 2BPP, and LZ-compressed BPP)
65
* **Generate tilemaps and tilesets from images**
76
* Support offset and length when adding tileset images
87
* Export all tilesets as a .tileset file

src/tileset.cpp

Lines changed: 299 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#pragma warning(push, 0)
22
#include <FL/fl_utf8.h>
33
#include <FL/Fl_PNG_Image.H>
4+
#include <FL/Fl_BMP_Image.H>
5+
#include <FL/Fl_Image_Surface.H>
46
#include <FL/fl_draw.H>
57
#pragma warning(pop)
68

@@ -58,16 +60,168 @@ bool Tileset::draw_tile(const Tile_State *ts, int x, int y, bool active) const {
5860
}
5961

6062
Tileset::Result Tileset::read_tiles(const char *f) {
61-
if (ends_with(f, ".png") || ends_with(f, ".PNG")) { return read_png_graphics(f); }
62-
// TODO: support BMP, GIF, 1BPP, 2BPP, 1BPP.LZ, 2BPP.LZ
63+
std::string s(f);
64+
if (ends_with(s, ".png") || ends_with(s, ".PNG")) { return read_png_graphics(f); }
65+
if (ends_with(s, ".bmp") || ends_with(s, ".BMP")) { return read_bmp_graphics(f); }
66+
if (ends_with(s, ".1bpp") || ends_with(s, ".1BPP")) { return read_1bpp_graphics(f); }
67+
if (ends_with(s, ".2bpp") || ends_with(s, ".2BPP")) { return read_2bpp_graphics(f); }
68+
if (ends_with(s, ".1bpp.lz") || ends_with(s, ".1BPP.LZ")) { return read_1bpp_lz_graphics(f); }
69+
if (ends_with(s, ".2bpp.lz") || ends_with(s, ".2BPP.LZ")) { return read_2bpp_lz_graphics(f); }
6370
return (_result = TILESET_BAD_EXT);
6471
}
6572

6673
Tileset::Result Tileset::read_png_graphics(const char *f) {
6774
Fl_PNG_Image png(f);
68-
if (png.fail()) { return (_result = TILESET_BAD_FILE); }
75+
return postprocess_graphics(&png);
76+
}
77+
78+
Tileset::Result Tileset::read_bmp_graphics(const char *f) {
79+
Fl_BMP_Image bmp(f);
80+
return postprocess_graphics(&bmp);
81+
}
82+
83+
Tileset::Result Tileset::read_1bpp_graphics(const char *f) {
84+
FILE *file = fl_fopen(f, "rb");
85+
if (!file) { return (_result = TILESET_BAD_FILE); }
86+
87+
fseek(file, 0, SEEK_END);
88+
long n = ftell(file);
89+
rewind(file);
90+
if (n % BYTES_PER_1BPP_TILE) { fclose(file); return (_result = TILESET_BAD_DIMS); }
91+
92+
uchar *data = new uchar[n];
93+
size_t r = fread(data, 1, n, file);
94+
fclose(file);
95+
if (r != (size_t)n) { delete [] data; return (_result = TILESET_BAD_FILE); }
96+
97+
return parse_1bpp_data(n, data);
98+
}
99+
100+
Tileset::Result Tileset::read_2bpp_graphics(const char *f) {
101+
FILE *file = fl_fopen(f, "rb");
102+
if (!file) { return (_result = TILESET_BAD_FILE); }
103+
104+
fseek(file, 0, SEEK_END);
105+
long n = ftell(file);
106+
rewind(file);
107+
if (n % BYTES_PER_2BPP_TILE) { fclose(file); return (_result = TILESET_BAD_DIMS); }
108+
109+
uchar *data = new uchar[n];
110+
size_t r = fread(data, 1, n, file);
111+
fclose(file);
112+
if (r != (size_t)n) { delete [] data; return (_result = TILESET_BAD_FILE); }
113+
114+
return parse_2bpp_data(n, data);
115+
}
116+
117+
static Tileset::Result decompress_lz_data(const char *f, uchar *data, size_t lim, size_t &len);
118+
119+
Tileset::Result Tileset::read_1bpp_lz_graphics(const char *f) {
120+
uchar *data = new uchar[NUM_TILES * BYTES_PER_1BPP_TILE];
121+
size_t n = 0;
122+
if (decompress_lz_data(f, data, NUM_TILES * BYTES_PER_1BPP_TILE, n) != TILESET_OK) {
123+
delete [] data;
124+
return _result;
125+
}
126+
return parse_1bpp_data(n, data);
127+
}
128+
129+
Tileset::Result Tileset::read_2bpp_lz_graphics(const char *f) {
130+
uchar *data = new uchar[NUM_TILES * BYTES_PER_2BPP_TILE];
131+
size_t n = 0;
132+
if (decompress_lz_data(f, data, NUM_TILES * BYTES_PER_2BPP_TILE, n) != TILESET_OK) {
133+
delete [] data;
134+
return _result;
135+
}
136+
return parse_2bpp_data(n, data);
137+
}
138+
139+
enum Hue { WHITE, DARK, LIGHT, BLACK };
140+
141+
Fl_Color hue_colors[NUM_HUES] = {
142+
fl_rgb_color(0xFF, 0xFF, 0xFF), fl_rgb_color(0x55, 0x55, 0x55),
143+
fl_rgb_color(0xAA, 0xAA, 0xAA), fl_rgb_color(0x00, 0x00, 0x00),
144+
};
145+
146+
static void convert_1bpp_row(uchar b, Hue *row) {
147+
// %ABCD_EFGH -> %A %B %C %D %E %F %G %H
148+
for (int i = 0; i < TILE_SIZE; i++) {
149+
int j = TILE_SIZE - i - 1;
150+
row[i] = b >> j & 1 ? BLACK : WHITE;
151+
}
152+
}
153+
154+
static void convert_2bpp_row(uchar b1, uchar b2, Hue *row) {
155+
// %ABCD_EFGH %abcd_efgh -> %Aa %Bb %Cc %Dd %Ee %Ff %GG %Hh
156+
for (int i = 0; i < TILE_SIZE; i++) {
157+
int j = TILE_SIZE - i - 1;
158+
row[i] = (Hue)((b1 >> j & 1) * 2 + (b2 >> j & 1));
159+
}
160+
}
161+
162+
Tileset::Result Tileset::parse_1bpp_data(size_t n, uchar *data) {
163+
n /= BYTES_PER_1BPP_TILE;
164+
_num_tiles = n;
165+
if (_start + _num_tiles > NUM_TILES) { delete [] data; return (_result = TILESET_TOO_LARGE); }
166+
167+
Fl_Image_Surface *surface = new Fl_Image_Surface(TILE_SIZE, (int)n * TILE_SIZE);
168+
surface->set_current();
169+
170+
Hue row[TILE_SIZE] = {};
171+
for (size_t i = 0; i < _num_tiles; i++) {
172+
for (size_t j = 0; j < TILE_SIZE; j++) {
173+
uchar b = data[i * BYTES_PER_1BPP_TILE + j];
174+
convert_1bpp_row(b, row);
175+
for (int k = 0; k < TILE_SIZE; k++) {
176+
Hue hue = row[k];
177+
fl_color(hue_colors[hue]);
178+
fl_point(k, (int)(i * TILE_SIZE + j));
179+
}
180+
}
181+
}
182+
183+
Fl_RGB_Image *img = surface->image();
184+
delete surface;
185+
Fl_Display_Device::display_device()->set_current();
186+
187+
delete [] data;
188+
return postprocess_graphics(img);
189+
}
190+
191+
Tileset::Result Tileset::parse_2bpp_data(size_t n, uchar *data) {
192+
n /= BYTES_PER_2BPP_TILE;
193+
_num_tiles = n;
194+
if (_start + _num_tiles > NUM_TILES) { delete [] data; return (_result = TILESET_TOO_LARGE); }
195+
196+
Fl_Image_Surface *surface = new Fl_Image_Surface(TILE_SIZE, (int)n * TILE_SIZE);
197+
surface->set_current();
198+
199+
Hue row[TILE_SIZE] = {};
200+
for (size_t i = 0; i < _num_tiles; i++) {
201+
for (size_t j = 0; j < TILE_SIZE; j++) {
202+
uchar b1 = data[i * BYTES_PER_2BPP_TILE + j * 2];
203+
uchar b2 = data[i * BYTES_PER_2BPP_TILE + j * 2 + 1];
204+
convert_2bpp_row(b1, b2, row);
205+
for (int k = 0; k < TILE_SIZE; k++) {
206+
Hue hue = row[k];
207+
fl_color(hue_colors[hue]);
208+
fl_point(k, (int)(i * TILE_SIZE + j));
209+
}
210+
}
211+
}
69212

70-
_image = (Fl_RGB_Image *)png.copy(png.w() * 2, png.h() * 2);
213+
Fl_RGB_Image *img = surface->image();
214+
delete surface;
215+
Fl_Display_Device::display_device()->set_current();
216+
217+
delete [] data;
218+
return postprocess_graphics(img);
219+
}
220+
221+
Tileset::Result Tileset::postprocess_graphics(Fl_RGB_Image *img) {
222+
if (!img || img->fail()) { return (_result = TILESET_BAD_FILE); }
223+
224+
_image = (Fl_RGB_Image *)img->copy(img->w() * 2, img->h() * 2);
71225
if (!_image || _image->fail()) { return (_result = TILESET_BAD_FILE); }
72226

73227
if (!refresh_inactive_image()) {
@@ -81,7 +235,7 @@ Tileset::Result Tileset::read_png_graphics(const char *f) {
81235
w /= TILE_SIZE_2X;
82236
h /= TILE_SIZE_2X;
83237
_num_tiles = w * h;
84-
if (_num_tiles > NUM_TILES) { return (_result = TILESET_TOO_LARGE); }
238+
if (_start + _num_tiles > NUM_TILES) { return (_result = TILESET_TOO_LARGE); }
85239

86240
return (_result = TILESET_OK);
87241
}
@@ -108,3 +262,143 @@ const char *Tileset::error_message(Result result) {
108262
return "Unspecified error.";
109263
}
110264
}
265+
266+
// A rundown of Pokemon Crystal's LZ compression scheme:
267+
enum Lz_Command {
268+
// Control commands occupy bits 5-7.
269+
// Bits 0-4 serve as the first parameter n for each command.
270+
LZ_LITERAL, // n values for n bytes
271+
LZ_ITERATE, // one value for n bytes
272+
LZ_ALTERNATE, // alternate two values for n bytes
273+
LZ_BLANK, // zero for n bytes
274+
// Repeater commands repeat any data that was just decompressed.
275+
// They take an additional signed parameter s to mark a relative starting point.
276+
// These wrap around (positive from the start, negative from the current position).
277+
LZ_REPEAT, // n bytes starting from s
278+
LZ_FLIP, // n bytes in reverse bit order starting from s
279+
LZ_REVERSE, // n bytes backwards starting from s
280+
// The long command is used when 5 bits aren't enough. Bits 2-4 contain a new control code.
281+
// Bits 0-1 are appended to a new byte as 8-9, allowing a 10-bit parameter.
282+
LZ_LONG // n is now 10 bits for a new control code
283+
};
284+
285+
// If 0xff is encountered instead of a command, decompression ends.
286+
#define LZ_END 0xff
287+
288+
// [sum(((b >> i) & 1) << (7 - i) for i in range(8)) for b in range(256)]
289+
static uchar bit_flipped[256] = {
290+
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
291+
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
292+
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
293+
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
294+
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
295+
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
296+
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
297+
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
298+
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
299+
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
300+
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
301+
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
302+
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
303+
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
304+
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
305+
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
306+
};
307+
308+
static Tileset::Result decompress_lz_data(const char *f, uchar *data, size_t lim, size_t &len) {
309+
FILE *file = fl_fopen(f, "rb");
310+
if (!file) { return Tileset::Result::TILESET_BAD_FILE; }
311+
312+
fseek(file, 0, SEEK_END);
313+
long n = ftell(file);
314+
rewind(file);
315+
uchar *lz_data = new uchar[n];
316+
size_t r = fread(lz_data, 1, n, file);
317+
fclose(file);
318+
if (r != (size_t)n) { delete [] lz_data; return Tileset::Result::TILESET_BAD_FILE; }
319+
320+
size_t address = 0;
321+
uchar q[2];
322+
int offset;
323+
for (;;) {
324+
uchar b = lz_data[address++];
325+
if (b == LZ_END) { break; }
326+
if (len >= lim) {
327+
delete [] lz_data;
328+
return Tileset::Result::TILESET_TOO_LARGE;
329+
}
330+
Lz_Command cmd = (Lz_Command)((b & 0xe0) >> 5);
331+
int length = 0;
332+
if (cmd == LZ_LONG) {
333+
cmd = (Lz_Command)((b & 0x1c) >> 2);
334+
length = (int)(b & 0x03) * 0x100;
335+
b = lz_data[address++];
336+
length += (int)b + 1;
337+
}
338+
else {
339+
length = (int)(b & 0x1f) + 1;
340+
}
341+
switch (cmd) {
342+
case LZ_LITERAL:
343+
// Copy data directly.
344+
for (int i = 0; i < length; i++) {
345+
data[len++] = lz_data[address++];
346+
}
347+
break;
348+
case LZ_ITERATE:
349+
// Write one byte repeatedly.
350+
b = lz_data[address++];
351+
for (int i = 0; i < length; i++) {
352+
data[len++] = b;
353+
}
354+
break;
355+
case LZ_ALTERNATE:
356+
// Write alternating bytes.
357+
q[0] = lz_data[address++];
358+
q[1] = lz_data[address++];
359+
// Copy data directly.
360+
for (int i = 0; i < length; i++) {
361+
data[len++] = q[i & 1];
362+
}
363+
break;
364+
case LZ_BLANK:
365+
// Write zeros.
366+
for (int i = 0; i < length; i++) {
367+
data[len++] = 0;
368+
}
369+
break;
370+
case LZ_REPEAT:
371+
// Repeat bytes from output.
372+
b = lz_data[address++];
373+
offset = b >= 0x80 ? (int)len - (int)(b & 0x7f) - 1 : (int)b * 0x100 + lz_data[address++];
374+
for (int i = 0; i < length; i++) {
375+
data[len++] = data[offset + i];
376+
}
377+
break;
378+
case LZ_FLIP:
379+
// Repeat flipped bytes from output.
380+
b = lz_data[address++];
381+
offset = b >= 0x80 ? (int)len - (int)(b & 0x7f) - 1 : (int)b * 0x100 + lz_data[address++];
382+
for (int i = 0; i < length; i++) {
383+
b = data[offset + i];
384+
data[len++] = bit_flipped[b];
385+
}
386+
break;
387+
case LZ_REVERSE:
388+
// Repeat reversed bytes from output.
389+
b = lz_data[address++];
390+
offset = b >= 0x80 ? (int)len - (int)(b & 0x7f) - 1 : (int)b * 0x100 + lz_data[address++];
391+
for (int i = 0; i < length; i++) {
392+
data[len++] = data[offset - i];
393+
}
394+
break;
395+
case LZ_LONG:
396+
default:
397+
delete [] lz_data;
398+
return Tileset::Result::TILESET_BAD_CMD;
399+
}
400+
}
401+
402+
delete [] lz_data;
403+
return Tileset::Result::TILESET_OK;
404+
}

src/tileset.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
#define TILE_SIZE 8
1212
#define TILE_SIZE_2X (TILE_SIZE * 2)
1313

14-
#define BYTES_PER_1BPP_TILE (TILE_SIZE * TILE_SIZE / 8)
14+
#define NUM_HUES 4
15+
#define BYTES_PER_1BPP_TILE (TILE_SIZE * TILE_SIZE / TILE_SIZE)
1516
#define BYTES_PER_2BPP_TILE (BYTES_PER_1BPP_TILE * 2)
1617

1718
#define NUM_TILES 256
@@ -40,6 +41,14 @@ class Tileset {
4041
Result read_tiles(const char *f);
4142
private:
4243
Result read_png_graphics(const char *f);
44+
Result read_bmp_graphics(const char *f);
45+
Result read_1bpp_graphics(const char *f);
46+
Result read_2bpp_graphics(const char *f);
47+
Result read_1bpp_lz_graphics(const char *f);
48+
Result read_2bpp_lz_graphics(const char *f);
49+
Result parse_1bpp_data(size_t n, uchar *data);
50+
Result parse_2bpp_data(size_t n, uchar *data);
51+
Result postprocess_graphics(Fl_RGB_Image *img);
4352
public:
4453
static const char *error_message(Result result);
4554
};

0 commit comments

Comments
 (0)