/* gi_blorb.c: Blorb library layer for Glk API. gi_blorb version 1.6.1. Designed by Andrew Plotkin http://eblong.com/zarf/glk/ This file is copyright 1998-2017 by Andrew Plotkin. It is distributed under the MIT license; see the "LICENSE" file. */ #include "glk.h" #include "gi_blorb.h" #ifndef NULL #define NULL 0 #endif #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif /* The magic macro of endian conversion. */ #define giblorb_native4(v) \ ( (((glui32)((v)[3]) ) & 0x000000ff) \ | (((glui32)((v)[2]) << 8) & 0x0000ff00) \ | (((glui32)((v)[1]) << 16) & 0x00ff0000) \ | (((glui32)((v)[0]) << 24) & 0xff000000)) /* More four-byte constants. */ #define giblorb_ID_FORM (giblorb_make_id('F', 'O', 'R', 'M')) #define giblorb_ID_IFRS (giblorb_make_id('I', 'F', 'R', 'S')) #define giblorb_ID_RIdx (giblorb_make_id('R', 'I', 'd', 'x')) /* giblorb_chunkdesc_t: Describes one chunk of the Blorb file. */ typedef struct giblorb_chunkdesc_struct { glui32 type; glui32 len; glui32 startpos; /* start of chunk header */ glui32 datpos; /* start of data (either startpos or startpos+8) */ void *ptr; /* pointer to malloc'd data, if loaded */ int auxdatnum; /* entry in the auxsound/auxpict array; -1 if none. This only applies to chunks that represent resources. (Currently, only images.) */ } giblorb_chunkdesc_t; /* giblorb_resdesc_t: Describes one resource in the Blorb file. */ typedef struct giblorb_resdesc_struct { glui32 usage; glui32 resnum; glui32 chunknum; } giblorb_resdesc_t; /* giblorb_auxpict_t: Extra information about an image. */ typedef struct giblorb_auxpict_struct { int loaded; glui32 width; glui32 height; char *alttext; } giblorb_auxpict_t; /* giblorb_map_t: Holds the complete description of an open Blorb file. */ struct giblorb_map_struct { glui32 inited; /* holds giblorb_Inited_Magic if the map structure is valid */ strid_t file; int numchunks; giblorb_chunkdesc_t *chunks; /* list of chunk descriptors */ int numresources; giblorb_resdesc_t *resources; /* list of resource descriptors */ giblorb_resdesc_t **ressorted; /* list of pointers to descriptors in map->resources -- sorted by usage and resource number. */ giblorb_auxpict_t *auxpict; }; #define giblorb_Inited_Magic (0xB7012BED) /* Static variables. */ static int lib_inited = FALSE; static giblorb_err_t giblorb_initialize(void); static giblorb_err_t giblorb_initialize_map(giblorb_map_t *map); static giblorb_err_t giblorb_image_get_size_jpeg(unsigned char *ptr, glui32 length, giblorb_auxpict_t *auxpict); static giblorb_err_t giblorb_image_get_size_png(unsigned char *ptr, glui32 length, giblorb_auxpict_t *auxpict); static void giblorb_qsort(giblorb_resdesc_t **list, int len); static giblorb_resdesc_t *giblorb_bsearch(giblorb_resdesc_t *sample, giblorb_resdesc_t **list, int len); static void *giblorb_malloc(glui32 len); static void *giblorb_realloc(void *ptr, glui32 len); static void giblorb_free(void *ptr); static giblorb_err_t giblorb_initialize() { return giblorb_err_None; } giblorb_err_t giblorb_create_map(strid_t file, giblorb_map_t **newmap) { giblorb_err_t err; giblorb_map_t *map; glui32 readlen; glui32 nextpos, totallength; giblorb_chunkdesc_t *chunks; int chunks_size, numchunks; char buffer[16]; *newmap = NULL; if (!lib_inited) { err = giblorb_initialize(); if (err) return err; lib_inited = TRUE; } /* First, chew through the file and index the chunks. */ glk_stream_set_position(file, 0, seekmode_Start); readlen = glk_get_buffer_stream(file, buffer, 12); if (readlen != 12) return giblorb_err_Read; if (giblorb_native4(buffer+0) != giblorb_ID_FORM) return giblorb_err_Format; if (giblorb_native4(buffer+8) != giblorb_ID_IFRS) return giblorb_err_Format; totallength = giblorb_native4(buffer+4) + 8; nextpos = 12; chunks_size = 8; numchunks = 0; chunks = (giblorb_chunkdesc_t *)giblorb_malloc(sizeof(giblorb_chunkdesc_t) * chunks_size); while (nextpos < totallength) { glui32 type, len; int chunum; giblorb_chunkdesc_t *chu; glk_stream_set_position(file, nextpos, seekmode_Start); readlen = glk_get_buffer_stream(file, buffer, 8); if (readlen != 8) { giblorb_free(chunks); return giblorb_err_Read; } type = giblorb_native4(buffer+0); len = giblorb_native4(buffer+4); if (numchunks >= chunks_size) { chunks_size *= 2; chunks = (giblorb_chunkdesc_t *)giblorb_realloc(chunks, sizeof(giblorb_chunkdesc_t) * chunks_size); } chunum = numchunks; chu = &(chunks[chunum]); numchunks++; chu->type = type; chu->startpos = nextpos; if (type == giblorb_ID_FORM) { chu->datpos = nextpos; chu->len = len+8; } else { chu->datpos = nextpos+8; chu->len = len; } chu->ptr = NULL; chu->auxdatnum = -1; nextpos = nextpos + len + 8; if (nextpos & 1) nextpos++; if (nextpos > totallength) { giblorb_free(chunks); return giblorb_err_Format; } } /* The basic IFF structure seems to be ok, and we have a list of chunks. Now we allocate the map structure itself. */ map = (giblorb_map_t *)giblorb_malloc(sizeof(giblorb_map_t)); if (!map) { giblorb_free(chunks); return giblorb_err_Alloc; } map->inited = giblorb_Inited_Magic; map->file = file; map->chunks = chunks; map->numchunks = numchunks; map->resources = NULL; map->ressorted = NULL; map->numresources = 0; /*map->releasenum = 0; map->zheader = NULL; map->resolution = NULL; map->palettechunk = -1; map->palette = NULL; map->auxsound = NULL;*/ map->auxpict = NULL; /* Now we do everything else involved in loading the Blorb file, such as building resource lists. */ err = giblorb_initialize_map(map); if (err) { giblorb_destroy_map(map); return err; } *newmap = map; return giblorb_err_None; } static giblorb_err_t giblorb_initialize_map(giblorb_map_t *map) { /* It is important that the map structure be kept valid during this function. If this returns an error, giblorb_destroy_map() will be called. */ int ix, jx; giblorb_result_t chunkres; giblorb_err_t err; char *ptr; glui32 len; glui32 numres; int gotindex = FALSE; int pictcount = 0; for (ix=0; ixnumchunks; ix++) { giblorb_chunkdesc_t *chu = &map->chunks[ix]; switch (chu->type) { case giblorb_ID_RIdx: /* Resource index chunk: build the resource list and sort it. */ if (gotindex) return giblorb_err_Format; /* duplicate index chunk */ err = giblorb_load_chunk_by_number(map, giblorb_method_Memory, &chunkres, ix); if (err) return err; ptr = chunkres.data.ptr; len = chunkres.length; numres = giblorb_native4(ptr+0); if (numres) { int ix2; giblorb_resdesc_t *resources = NULL; giblorb_resdesc_t **ressorted = NULL; if (len != numres*12+4) return giblorb_err_Format; /* bad length field */ resources = (giblorb_resdesc_t *)giblorb_malloc(numres * sizeof(giblorb_resdesc_t)); if (!resources) { return giblorb_err_Alloc; } ressorted = (giblorb_resdesc_t **)giblorb_malloc(numres * sizeof(giblorb_resdesc_t *)); if (!ressorted) { giblorb_free(resources); return giblorb_err_Alloc; } ix2 = 0; for (jx=0; jxusage = giblorb_native4(ptr+jx*12+4); res->resnum = giblorb_native4(ptr+jx*12+8); respos = giblorb_native4(ptr+jx*12+12); while (ix2 < map->numchunks && map->chunks[ix2].startpos < respos) ix2++; if (ix2 >= map->numchunks || map->chunks[ix2].startpos != respos) { /* start pos does not match a real chunk */ giblorb_free(resources); giblorb_free(ressorted); return giblorb_err_Format; } res->chunknum = ix2; ressorted[jx] = res; } /* Sort a resource list (actually a list of pointers to structures in map->resources.) This makes it easy to find resources by usage and resource number. */ giblorb_qsort(ressorted, numres); map->numresources = numres; map->resources = resources; map->ressorted = ressorted; } giblorb_unload_chunk(map, ix); gotindex = TRUE; break; case giblorb_ID_JPEG: case giblorb_ID_PNG: chu->auxdatnum = pictcount; pictcount++; break; } } if (pictcount) { map->auxpict = (giblorb_auxpict_t *)giblorb_malloc(pictcount * sizeof(giblorb_auxpict_t)); if (!map->auxpict) return giblorb_err_Alloc; for (ix=0; ixauxpict[ix]); auxpict->loaded = FALSE; auxpict->width = 0; auxpict->height = 0; auxpict->alttext = NULL; } } return giblorb_err_None; } giblorb_err_t giblorb_destroy_map(giblorb_map_t *map) { int ix; if (!map || !map->chunks || map->inited != giblorb_Inited_Magic) return giblorb_err_NotAMap; if (map->auxpict) { giblorb_free(map->auxpict); map->auxpict = NULL; } for (ix=0; ixnumchunks; ix++) { giblorb_chunkdesc_t *chu = &(map->chunks[ix]); if (chu->ptr) { giblorb_free(chu->ptr); chu->ptr = NULL; } } if (map->chunks) { giblorb_free(map->chunks); map->chunks = NULL; } map->numchunks = 0; if (map->resources) { giblorb_free(map->resources); map->resources = NULL; } if (map->ressorted) { giblorb_free(map->ressorted); map->ressorted = NULL; } map->numresources = 0; map->file = NULL; map->inited = 0; giblorb_free(map); return giblorb_err_None; } /* Chunk-handling functions. */ giblorb_err_t giblorb_load_chunk_by_type(giblorb_map_t *map, glui32 method, giblorb_result_t *res, glui32 type, glui32 count) { int ix; for (ix=0; ix < map->numchunks; ix++) { if (map->chunks[ix].type == type) { if (count == 0) break; count--; } } if (ix >= map->numchunks) { return giblorb_err_NotFound; } return giblorb_load_chunk_by_number(map, method, res, ix); } giblorb_err_t giblorb_load_chunk_by_number(giblorb_map_t *map, glui32 method, giblorb_result_t *res, glui32 chunknum) { giblorb_chunkdesc_t *chu; if (chunknum >= map->numchunks) return giblorb_err_NotFound; chu = &(map->chunks[chunknum]); switch (method) { case giblorb_method_DontLoad: /* do nothing */ break; case giblorb_method_FilePos: res->data.startpos = chu->datpos; break; case giblorb_method_Memory: if (!chu->ptr) { glui32 readlen; void *dat = giblorb_malloc(chu->len); if (!dat) return giblorb_err_Alloc; glk_stream_set_position(map->file, chu->datpos, seekmode_Start); readlen = glk_get_buffer_stream(map->file, dat, chu->len); if (readlen != chu->len) return giblorb_err_Read; chu->ptr = dat; } res->data.ptr = chu->ptr; break; } res->chunknum = chunknum; res->length = chu->len; res->chunktype = chu->type; return giblorb_err_None; } giblorb_err_t giblorb_load_resource(giblorb_map_t *map, glui32 method, giblorb_result_t *res, glui32 usage, glui32 resnum) { giblorb_resdesc_t sample; giblorb_resdesc_t *found; sample.usage = usage; sample.resnum = resnum; found = giblorb_bsearch(&sample, map->ressorted, map->numresources); if (!found) return giblorb_err_NotFound; return giblorb_load_chunk_by_number(map, method, res, found->chunknum); } giblorb_err_t giblorb_unload_chunk(giblorb_map_t *map, glui32 chunknum) { giblorb_chunkdesc_t *chu; if (chunknum >= map->numchunks) return giblorb_err_NotFound; chu = &(map->chunks[chunknum]); if (chu->ptr) { giblorb_free(chu->ptr); chu->ptr = NULL; } return giblorb_err_None; } giblorb_err_t giblorb_count_resources(giblorb_map_t *map, glui32 usage, glui32 *num, glui32 *min, glui32 *max) { int ix; int count; glui32 val; glui32 minval, maxval; count = 0; minval = 0; maxval = 0; for (ix=0; ixnumresources; ix++) { if (map->resources[ix].usage == usage) { val = map->resources[ix].resnum; if (count == 0) { count++; minval = val; maxval = val; } else { count++; if (val < minval) minval = val; if (val > maxval) maxval = val; } } } if (num) *num = count; if (min) *min = minval; if (max) *max = maxval; return giblorb_err_None; } giblorb_err_t giblorb_load_image_info(giblorb_map_t *map, glui32 resnum, giblorb_image_info_t *res) { giblorb_resdesc_t sample; giblorb_resdesc_t *found; glui32 chunknum; giblorb_chunkdesc_t *chu; giblorb_auxpict_t *auxpict; sample.usage = giblorb_ID_Pict; sample.resnum = resnum; found = giblorb_bsearch(&sample, map->ressorted, map->numresources); if (!found) return giblorb_err_NotFound; chunknum = found->chunknum; if (chunknum >= map->numchunks) return giblorb_err_NotFound; chu = &(map->chunks[chunknum]); if (chu->auxdatnum < 0) return giblorb_err_NotFound; auxpict = &(map->auxpict[chu->auxdatnum]); if (!auxpict->loaded) { giblorb_result_t res; giblorb_err_t err = giblorb_load_chunk_by_number(map, giblorb_method_Memory, &res, chunknum); if (err) return err; if (chu->type == giblorb_ID_JPEG) err = giblorb_image_get_size_jpeg(res.data.ptr, res.length, auxpict); else if (chu->type == giblorb_ID_PNG) err = giblorb_image_get_size_png(res.data.ptr, res.length, auxpict); else err = giblorb_err_Format; giblorb_unload_chunk(map, chunknum); if (err) return err; auxpict->loaded = TRUE; } res->chunktype = chu->type; res->width = auxpict->width; res->height = auxpict->height; res->alttext = auxpict->alttext; return giblorb_err_None; } static giblorb_err_t giblorb_image_get_size_jpeg(unsigned char *arr, glui32 length, giblorb_auxpict_t *auxpict) { int pos = 0; while (pos < length) { unsigned char marker; int chunklen; if (arr[pos] != 0xFF) { /* error: find_dimensions_jpeg: marker is not 0xFF */ return giblorb_err_Format; } while (arr[pos] == 0xFF) pos += 1; marker = arr[pos]; pos += 1; if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD9)) { /* marker type has no data */ continue; } chunklen = (arr[pos+0] << 8) | (arr[pos+1]); if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC8) { if (chunklen < 7) { /* error: find_dimensions_jpeg: SOF block is too small */ return giblorb_err_Format; } auxpict->height = (arr[pos+3] << 8) | (arr[pos+4]); auxpict->width = (arr[pos+5] << 8) | (arr[pos+6]); return giblorb_err_None; } pos += chunklen; } /* error: find_dimensions_jpeg: no SOF marker found */ return giblorb_err_Format; } static giblorb_err_t giblorb_image_get_size_png(unsigned char *arr, glui32 length, giblorb_auxpict_t *auxpict) { int pos = 0; if (length < 8) return giblorb_err_Format; if (arr[0] != 0x89 || arr[1] != 'P' || arr[2] != 'N' || arr[3] != 'G') { /* error: find_dimensions_png: PNG signature does not match */ return giblorb_err_Format; } pos += 8; while (pos < length) { glui32 chunklen = giblorb_native4(arr+pos); glui32 chunktype; pos += 4; chunktype = giblorb_native4(arr+pos); pos += 4; if (chunktype == giblorb_make_id('I', 'H', 'D', 'R')) { auxpict->width = giblorb_native4(arr+pos); pos += 4; auxpict->height = giblorb_native4(arr+pos); pos += 4; return giblorb_err_None; } pos += chunklen; pos += 4; /* skip CRC */ } /* error: find_dimensions_png: no PNG header block found */ return giblorb_err_Format; } /* Sorting and searching. */ static int sortsplot(giblorb_resdesc_t *v1, giblorb_resdesc_t *v2) { if (v1->usage < v2->usage) return -1; if (v1->usage > v2->usage) return 1; if (v1->resnum < v2->resnum) return -1; if (v1->resnum > v2->resnum) return 1; return 0; } static void giblorb_qsort(giblorb_resdesc_t **list, int len) { int ix, jx, res; giblorb_resdesc_t *tmpptr, *pivot; if (len < 6) { /* The list is short enough for a bubble-sort. */ for (jx=len-1; jx>0; jx--) { for (ix=0; ix 0) { tmpptr = list[ix]; list[ix] = list[ix+1]; list[ix+1] = tmpptr; } } } } else { /* Split the list. */ pivot = list[len/2]; ix=0; jx=len; while (1) { while (ix < jx-1 && sortsplot(list[ix], pivot) < 0) ix++; while (ix < jx-1 && sortsplot(list[jx-1], pivot) > 0) jx--; if (ix >= jx-1) break; tmpptr = list[ix]; list[ix] = list[jx-1]; list[jx-1] = tmpptr; } ix++; /* Sort the halves. */ giblorb_qsort(list+0, ix); giblorb_qsort(list+ix, len-ix); } } giblorb_resdesc_t *giblorb_bsearch(giblorb_resdesc_t *sample, giblorb_resdesc_t **list, int len) { int top, bot, val, res; bot = 0; top = len; while (bot < top) { val = (top+bot) / 2; res = sortsplot(list[val], sample); if (res == 0) return list[val]; if (res < 0) { bot = val+1; } else { top = val; } } return NULL; } /* Boring utility functions. If your platform doesn't support ANSI malloc(), feel free to edit these however you like. */ #include /* The OS-native header file -- you can edit this too. */ static void *giblorb_malloc(glui32 len) { return malloc(len); } static void *giblorb_realloc(void *ptr, glui32 len) { return realloc(ptr, len); } static void giblorb_free(void *ptr) { free(ptr); }