An example of using APNG support in stb_image.h
/* | |
Tests APNG support in stb_image.h by rendering the APNG via SDL. No animation occurs if the loaded file is a | |
normal PNG. | |
*/ | |
#include <assert.h> | |
#include <stdio.h> | |
#include <SDL2/SDL.h> | |
#define STB_IMAGE_IMPLEMENTATION | |
#include "stb_image.h" | |
// Settings | |
static const int screen_width = 1024; | |
static const int screen_height = 768; | |
static const int y_flip = 0; // Set to 1 to flip vertically on load | |
static const int draw_bounds = 0; // Set to 1 to draw frame bounds | |
static const int forced_delay_ms = 0; // Set to >0 to ignore frame times and force the specified delay between frames | |
static const int print_dbginfo = 1; // Set to 1 to print debug information on load | |
static int actual_screen_width = 0; | |
static int actual_screen_height = 0; | |
static const char *dispose_ops[] = { | |
"STBI_APNG_dispose_op_none", | |
"STBI_APNG_dispose_op_background", | |
"STBI_APNG_dispose_op_previous" | |
}; | |
static const char *blend_ops[] = { | |
"STBI_APNG_blend_op_source", | |
"STBI_APNG_blend_op_over" | |
}; | |
int main(int argc, char **argv) | |
{ | |
int ec = -1; | |
FILE *f = NULL; | |
int width = 0; | |
int height = 0; | |
int orig_format = 0; | |
int req_format = STBI_rgb_alpha; | |
int depth = 0; | |
int pitch_coeff = 0; | |
unsigned char *data = NULL; | |
size_t dir_offset = 0; | |
Uint32 rmask = 0x000000ff; | |
Uint32 gmask = 0x0000ff00; | |
Uint32 bmask = 0x00ff0000; | |
Uint32 amask = (req_format == STBI_rgb) ? 0 : 0xff000000; | |
Uint32 delay_ms = 0; | |
Uint32 last_render_ms = 0; | |
unsigned int current_frame = 0; | |
unsigned int num_plays = 0; | |
SDL_Window *win = NULL; | |
SDL_Renderer *renderer = NULL; | |
SDL_Surface **surfaces = NULL; | |
SDL_Texture **textures = NULL; | |
stbi__apng_directory *dir = NULL; | |
stbi__uint32 num_frames = 0; | |
stbi__context s; | |
stbi__uint32 i; | |
if (argc < 2) { | |
fprintf(stderr, "must provide image path\n"); | |
goto out; | |
} | |
stbi_set_flip_vertically_on_load(y_flip); | |
if (!(f = stbi__fopen(argv[1], "rb"))) { | |
fprintf(stderr, "can't fopen\n"); | |
goto out; | |
} | |
stbi__start_file(&s, f); | |
if (!stbi__png_test(&s)) { | |
fprintf(stderr, "image isn't png\n"); | |
fclose(f); f = NULL; | |
goto free_image; | |
} | |
data = stbi__apng_load_8bit(&s, &width, &height, &orig_format, req_format, &dir_offset); | |
// If dir_offset is > 0 then the loaded file is an APNG -- if dir_offset is == 0 then the loaded file is a | |
// normal PNG with no extra information packed into the data buffer. | |
if (data && dir_offset > 0) { | |
dir = (stbi__apng_directory *) (data + dir_offset); | |
// Sanity check that the data actually came from stbi. | |
if (dir->type != STBI__STRUCTURE_TYPE_APNG_DIRECTORY) { | |
fprintf(stderr, "invalid apng directory\n"); | |
fclose(f); f = NULL; | |
goto free_image; | |
} | |
if (print_dbginfo) { | |
fprintf(stderr, "dir_offset : %lu\n", dir_offset); | |
fprintf(stderr, "dir.type : %.*s\n", 4, (unsigned char *) &dir->type); | |
fprintf(stderr, "dir.num_frames : %u\n", dir->num_frames); | |
fprintf(stderr, "dir.default_image_is_first_frame : %s\n", | |
dir->default_image_is_first_frame ? "yes" : "no"); | |
fprintf(stderr, "dir.num_plays : %u\n", dir->num_plays); | |
for (i = 0; i < dir->num_frames; ++i) { | |
stbi__apng_frame_directory_entry *frame = &dir->frames[i]; | |
fprintf(stderr, "frame : %u\n", i); | |
fprintf(stderr, " width : %u\n", frame->width); | |
fprintf(stderr, " height : %u\n", frame->height); | |
fprintf(stderr, " x_offset : %u\n", frame->x_offset); | |
fprintf(stderr, " y_offset : %u\n", frame->y_offset); | |
fprintf(stderr, " delay_num : %u\n", frame->delay_num); | |
fprintf(stderr, " delay_den : %u\n", frame->delay_den); | |
fprintf(stderr, " dispose_op : %s\n", dispose_ops[frame->dispose_op]); | |
fprintf(stderr, " blend_op : %s\n", blend_ops[frame->blend_op]); | |
} | |
} | |
// For simplicity, this sample only supports APNG files where the default image is also the first frame. | |
assert(dir->default_image_is_first_frame == 1); | |
} | |
fclose(f); f = NULL; | |
if (data == NULL) { | |
fprintf(stderr, "%s\n", stbi_failure_reason()); | |
goto out; | |
} | |
if (print_dbginfo) { | |
fprintf(stderr, "%s: width=%d height=%d orig_format=%d req_format=%d\n", | |
argv[1], width, height, orig_format, req_format); | |
} | |
if (SDL_Init(SDL_INIT_VIDEO) < 0) | |
goto free_image; | |
win = SDL_CreateWindow("stb_image APNG test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, | |
screen_width, screen_height, SDL_WINDOW_ALLOW_HIGHDPI); | |
if (win == NULL) { | |
fprintf(stderr, "failed to create window: %s\n", SDL_GetError()); | |
goto free_image; | |
} | |
renderer = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED); | |
if (renderer == NULL) { | |
fprintf(stderr, "failed to create renderer: %s\n", SDL_GetError()); | |
goto destroy_window; | |
} | |
// Get the actual screen resolution (to support HiDPI displays). | |
SDL_GL_GetDrawableSize(win, &actual_screen_width, &actual_screen_height); | |
SDL_RenderSetScale(renderer, (float) actual_screen_width / width, (float) actual_screen_height / height); | |
if (req_format == STBI_rgb) { | |
depth = 24; | |
pitch_coeff = 3; | |
} else { | |
depth = 32; | |
pitch_coeff = 4; | |
} | |
num_frames = dir ? dir->num_frames : 1; | |
surfaces = malloc(num_frames * sizeof(surfaces[0])); | |
if (surfaces == NULL) { | |
fprintf(stderr, "out of memory\n"); | |
goto destroy_window; | |
} | |
if (dir) { | |
size_t frame_offset = 0; | |
for (i = 0; i < num_frames; ++i) { | |
size_t frame_size = dir->frames[i].width * dir->frames[i].height * pitch_coeff; | |
int pitch = pitch_coeff * dir->frames[i].width; | |
surfaces[i] = SDL_CreateRGBSurfaceFrom((void *) (data + frame_offset), | |
dir->frames[i].width, dir->frames[i].height, depth, | |
pitch, rmask, gmask, bmask, amask); | |
frame_offset += frame_size; | |
if (surfaces[i] == NULL) { | |
fprintf(stderr, "failed to create surface: %s\n", SDL_GetError()); | |
goto free_surfaces; | |
} | |
} | |
} else { | |
int pitch = pitch_coeff * width; | |
surfaces[0] = SDL_CreateRGBSurfaceFrom((void *) data, width, height, depth, pitch, | |
rmask, gmask, bmask, amask); | |
if (surfaces[0] == NULL) { | |
fprintf(stderr, "failed to create surface: %s\n", SDL_GetError()); | |
goto free_surfaces; | |
} | |
} | |
textures = malloc(num_frames * sizeof(textures[0])); | |
if (textures == NULL) { | |
fprintf(stderr, "out of memory\n"); | |
goto free_surfaces; | |
} | |
for (i = 0; i < num_frames; ++i) { | |
textures[i] = SDL_CreateTextureFromSurface(renderer, surfaces[i]); | |
if (textures[i] == NULL) { | |
fprintf(stderr, "failed to create texture: %s\n", SDL_GetError()); | |
goto free_textures; | |
} | |
} | |
while (1) { | |
stbi__apng_frame_directory_entry *render_frame = dir ? &dir->frames[current_frame] : NULL; | |
SDL_Event e; | |
SDL_Rect dstrect; | |
dstrect.x = 0; | |
dstrect.y = 0; | |
dstrect.w = width; | |
dstrect.h = height; | |
if (render_frame) { | |
dstrect.x = render_frame->x_offset; | |
dstrect.y = render_frame->y_offset; | |
dstrect.w = render_frame->width; | |
dstrect.h = render_frame->height; | |
if (render_frame->delay_den == 0) // delay_num specifies 1/100ths of a second | |
delay_ms = (Uint32) ((render_frame->delay_num / 100.0f) * 1000.0f); | |
else // delay_num / delay_den specifies seconds | |
delay_ms = (Uint32) (((float) render_frame->delay_num / render_frame->delay_den) * 1000.0f); | |
if (render_frame->delay_num == 0) // Render ASAP | |
delay_ms = 0; | |
} | |
if (forced_delay_ms) | |
delay_ms = forced_delay_ms; | |
if (render_frame && last_render_ms != 0 && SDL_GetTicks() - last_render_ms > delay_ms) { | |
// For simplicity, this sample only implements STBI_APNG_dispose_op_none, | |
// STBI_APNG_dispose_op_background, and STBI_APNG_blend_op_source. | |
assert(render_frame->dispose_op == STBI_APNG_dispose_op_none || | |
render_frame->dispose_op == STBI_APNG_dispose_op_background); | |
assert(render_frame->blend_op == STBI_APNG_blend_op_source); | |
if (render_frame->dispose_op == STBI_APNG_dispose_op_background) { | |
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_TRANSPARENT); | |
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); | |
SDL_RenderFillRect(renderer, &dstrect); | |
} | |
current_frame = (current_frame + 1) % num_frames; | |
render_frame = &dir->frames[current_frame]; | |
dstrect.x = render_frame->x_offset; | |
dstrect.y = render_frame->y_offset; | |
dstrect.w = render_frame->width; | |
dstrect.h = render_frame->height; | |
last_render_ms = SDL_GetTicks(); | |
if (current_frame == 0) | |
++num_plays; | |
// If the frame counter wraps around and the number of plays should be limited, simply set | |
// dir to NULL to switch off playback and "stick" the first frame. | |
if (dir->num_plays > 0 && num_plays == dir->num_plays) | |
dir = NULL; | |
} | |
if (SDL_PollEvent(&e)) { | |
if (e.type == SDL_QUIT) | |
break; | |
else if (e.type == SDL_KEYUP && e.key.keysym.sym == SDLK_ESCAPE) | |
break; | |
} | |
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); | |
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); | |
SDL_RenderClear(renderer); | |
SDL_RenderCopy(renderer, textures[current_frame], NULL, &dstrect); | |
if (draw_bounds) { | |
SDL_SetRenderDrawColor(renderer, 0, 255, 0, SDL_ALPHA_OPAQUE); | |
SDL_RenderDrawRect(renderer, &dstrect); | |
} | |
SDL_RenderPresent(renderer); | |
if (last_render_ms == 0) | |
last_render_ms = SDL_GetTicks(); | |
} | |
ec = 0; | |
free_textures: | |
for (i = 0; i < num_frames; ++i) | |
SDL_DestroyTexture(textures[i]); | |
free(textures); | |
free_surfaces: | |
for (i = 0; i < num_frames; ++i) | |
SDL_FreeSurface(surfaces[i]); | |
free(surfaces); | |
SDL_DestroyRenderer(renderer); | |
destroy_window: | |
SDL_DestroyWindow(win); | |
free_image: | |
stbi_image_free(data); | |
out: | |
return ec; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment