0,0 → 1,832 |
/* |
* Copyright 2005 Richard Wilson <info@tinct.net> |
* |
* This file is part of NetSurf, http://www.netsurf-browser.org/ |
* |
* NetSurf is free software; you can redistribute it and/or modify |
* it under the terms of the GNU General Public License as published by |
* the Free Software Foundation; version 2 of the License. |
* |
* NetSurf is distributed in the hope that it will be useful, |
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
* GNU General Public License for more details. |
* |
* You should have received a copy of the GNU General Public License |
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
*/ |
|
/** \file |
* Content for image/mng, image/png, and image/jng (implementation). |
*/ |
|
#include <assert.h> |
#include <stdbool.h> |
#include <string.h> |
#include <stdlib.h> |
#include <sys/time.h> |
#include <time.h> |
#include <libmng.h> |
#include "content/content_protected.h" |
#include "desktop/options.h" |
#include "desktop/plotters.h" |
#include "image/bitmap.h" |
#include "image/mng.h" |
#include "utils/log.h" |
#include "utils/messages.h" |
#include "utils/schedule.h" |
#include "utils/utils.h" |
|
/* This implementation does not currently support dynamic MNGs or any |
* form of colour/gamma correction, |
*/ |
|
typedef struct nsmng_content |
{ |
struct content base; |
|
bool opaque_test_pending; |
bool read_start; |
bool read_resume; |
int read_size; |
bool waiting; |
bool displayed; |
void *handle; |
|
struct bitmap *bitmap; /**< Created NetSurf bitmap */ |
} nsmng_content; |
|
|
#ifndef MNG_INTERNAL_MEMMNGMT |
|
/** |
* Memory allocation callback for libmng. |
*/ |
|
static mng_ptr nsmng_alloc(mng_size_t n) |
{ |
return calloc(1, n); |
} |
|
|
/** |
* Memory free callback for libmng. |
*/ |
|
static void nsmng_free(mng_ptr p, mng_size_t n) |
{ |
free(p); |
} |
|
#endif |
|
/** |
* Broadcasts an error message and returns false |
* |
* \param c the content to broadcast for |
* \return Appropriate error |
*/ |
static nserror nsmng_broadcast_error(nsmng_content *c, mng_retcode code) |
{ |
union content_msg_data msg_data; |
char error[100]; |
|
assert(c != NULL); |
|
if (code == MNG_OUTOFMEMORY) { |
msg_data.error = messages_get("NoMemory"); |
content_broadcast(&c->base, CONTENT_MSG_ERROR, msg_data); |
return NSERROR_NOMEM; |
} |
|
snprintf(error, sizeof error, messages_get("MNGError"), code); |
msg_data.error = error; |
content_broadcast(&c->base, CONTENT_MSG_ERROR, msg_data); |
return NSERROR_MNG_ERROR; |
} |
|
/* CALLBACKS REQUIRED FOR libmng READING */ |
|
static mng_bool nsmng_openstream(mng_handle mng) |
{ |
assert(mng != NULL); |
return MNG_TRUE; |
} |
|
static mng_bool nsmng_readdata(mng_handle mng, mng_ptr buffer, mng_uint32 size, |
mng_uint32 *bytesread) |
{ |
nsmng_content *c; |
const char *data; |
unsigned long data_size; |
|
assert(mng != NULL); |
assert(buffer != NULL); |
assert(bytesread != NULL); |
|
/* Get our content back |
*/ |
c = (nsmng_content *) mng_get_userdata(mng); |
assert(c != NULL); |
|
/* Copy any data we have (maximum of 'size') |
*/ |
data = content__get_source_data(&c->base, &data_size); |
|
*bytesread = ((data_size - c->read_size) < size) ? |
(data_size - c->read_size) : size; |
|
if ((*bytesread) > 0) { |
memcpy(buffer, data + c->read_size, *bytesread); |
c->read_size += *bytesread; |
} |
|
/* Return success |
*/ |
return MNG_TRUE; |
} |
|
static mng_bool nsmng_closestream(mng_handle mng) |
{ |
assert(mng != NULL); |
return MNG_TRUE; |
} |
|
static mng_bool nsmng_processheader(mng_handle mng, mng_uint32 width, |
mng_uint32 height) |
{ |
nsmng_content *c; |
union content_msg_data msg_data; |
uint8_t *buffer; |
|
assert(mng != NULL); |
|
/* This function is called when the header has been read and we |
know the dimensions of the canvas. |
*/ |
c = (nsmng_content *) mng_get_userdata(mng); |
assert(c != NULL); |
|
c->bitmap = bitmap_create(width, height, BITMAP_NEW); |
if (c->bitmap == NULL) { |
msg_data.error = messages_get("NoMemory"); |
content_broadcast(&c->base, CONTENT_MSG_ERROR, msg_data); |
LOG(("Insufficient memory to create canvas.")); |
return MNG_FALSE; |
} |
|
/* Get the buffer to ensure that it is allocated and the calls in |
* nsmng_getcanvasline() succeed. */ |
buffer = bitmap_get_buffer(c->bitmap); |
if (buffer == NULL) { |
msg_data.error = messages_get("NoMemory"); |
content_broadcast(&c->base, CONTENT_MSG_ERROR, msg_data); |
LOG(("Insufficient memory to create canvas.")); |
return MNG_FALSE; |
} |
|
/* Initialise the content size |
*/ |
c->base.width = width; |
c->base.height = height; |
|
/* Set the canvas style |
*/ |
if (mng_set_canvasstyle(mng, MNG_CANVAS_RGBA8) != MNG_NOERROR) { |
LOG(("Error setting canvas style.")); |
} |
|
/* Return success |
*/ |
return MNG_TRUE; |
} |
|
|
/* END OF CALLBACKS REQUIRED FOR READING |
*/ |
|
|
static bool nsmng_process_data(struct content *c, const char *data, unsigned int size) |
{ |
nsmng_content *mng = (nsmng_content *) c; |
mng_retcode status; |
|
assert(c != NULL); |
assert(data != NULL); |
|
/* We only need to do any processing if we're starting/resuming reading. |
*/ |
if ((!mng->read_resume) && (!mng->read_start)) |
return true; |
|
/* Try to start processing, or process some more data |
*/ |
if (mng->read_start) { |
status = mng_read(mng->handle); |
mng->read_start = false; |
} else { |
status = mng_read_resume(mng->handle); |
} |
mng->read_resume = (status == MNG_NEEDMOREDATA); |
if ((status != MNG_NOERROR) && (status != MNG_NEEDMOREDATA)) { |
LOG(("Failed to start/continue reading (%i).", status)); |
return nsmng_broadcast_error(mng, status) == NSERROR_OK; |
} |
|
/* Continue onwards |
*/ |
return true; |
} |
|
/* START OF CALLBACKS REQUIRED FOR DISPLAYING |
*/ |
|
|
static mng_ptr nsmng_getcanvasline(mng_handle mng, mng_uint32 line) |
{ |
nsmng_content *c; |
|
assert(mng != NULL); |
|
/* Get our content back |
*/ |
c = (nsmng_content *) mng_get_userdata(mng); |
assert(c != NULL); |
|
/* Calculate the address |
*/ |
return bitmap_get_buffer(c->bitmap) + |
bitmap_get_rowstride(c->bitmap) * line; |
} |
|
static mng_bool nsmng_refresh(mng_handle mng, mng_uint32 x, mng_uint32 y, |
mng_uint32 w, mng_uint32 h) |
{ |
union content_msg_data data; |
nsmng_content *c; |
|
assert(mng != NULL); |
|
/* Get our content back |
*/ |
c = (nsmng_content *) mng_get_userdata(mng); |
assert(c != NULL); |
|
/* Set the minimum redraw area |
*/ |
data.redraw.x = x; |
data.redraw.y = y; |
data.redraw.width = w; |
data.redraw.height = h; |
|
/* Set the redraw area to the whole canvas to ensure that if |
we can redraw something to trigger animation later then we do |
*/ |
/* data.redraw.x = 0; |
data.redraw.y = 0; |
data.redraw.width = c->width; |
data.redraw.height = c->height; |
*/ |
/* Always redraw everything |
*/ |
data.redraw.full_redraw = true; |
|
/* Set the object characteristics |
*/ |
data.redraw.object = &c->base; |
data.redraw.object_x = 0; |
data.redraw.object_y = 0; |
data.redraw.object_width = c->base.width; |
data.redraw.object_height = c->base.height; |
|
/* Only attempt to force the redraw if we've been requested to |
* display the image in the first place (i.e. nsmng_redraw has |
* been called). This avoids the situation of forcibly redrawing |
* an image that shouldn't be shown (e.g. if the image is a fallback |
* for an object that can't be rendered) |
*/ |
if (c->displayed) |
content_broadcast(&c->base, CONTENT_MSG_REDRAW, data); |
|
return MNG_TRUE; |
} |
|
/** |
* Animates to the next frame |
*/ |
static void nsmng_animate(void *p) |
{ |
nsmng_content *c; |
|
assert(p != NULL); |
|
c = (nsmng_content *) p; |
|
/* If we used the last animation we advance, if not we try again later |
*/ |
if (c->base.user_list->next == NULL) { |
c->waiting = true; |
} else { |
c->waiting = false; |
mng_display_resume(c->handle); |
c->opaque_test_pending = true; |
if (c->bitmap) |
bitmap_modified(c->bitmap); |
} |
} |
|
static mng_bool nsmng_settimer(mng_handle mng, mng_uint32 msecs) |
{ |
nsmng_content *c; |
|
assert(mng != NULL); |
|
/* Get our content back |
*/ |
c = (nsmng_content *) mng_get_userdata(mng); |
assert(c != NULL); |
|
/* Perform the scheduling |
*/ |
schedule(msecs / 10, nsmng_animate, c); |
return MNG_TRUE; |
} |
|
/** |
* Get the wall-clock time in milliseconds since some fixed time. |
*/ |
|
static mng_uint32 nsmng_gettickcount(mng_handle mng) |
{ |
static bool start = true; |
static time_t t0; |
struct timeval tv; |
#if defined(__SVR4) && defined(__sun) || defined(__NetBSD__) || \ |
defined(__APPLE__) |
/* Solaris, NetBSD, and OS X don't have this structure, and ignore the |
* second parameter to gettimeofday() |
*/ |
int tz; |
#else |
struct timezone tz; |
#endif |
assert(mng != NULL); |
|
gettimeofday(&tv, &tz); |
if (start) { |
t0 = tv.tv_sec; |
start = false; |
} |
|
return (tv.tv_sec - t0) * 1000 + tv.tv_usec / 1000; |
} |
|
/* END OF CALLBACKS REQUIRED FOR DISPLAYING |
*/ |
|
static mng_bool nsmng_errorproc(mng_handle mng, mng_int32 code, |
mng_int8 severity, mng_chunkid chunktype, mng_uint32 chunkseq, |
mng_int32 extra1, mng_int32 extra2, mng_pchar text) |
{ |
nsmng_content *c; |
char chunk[5]; |
|
assert(mng != NULL); |
|
c = (nsmng_content *) mng_get_userdata(mng); |
assert(c != NULL); |
|
chunk[0] = (char)((chunktype >> 24) & 0xFF); |
chunk[1] = (char)((chunktype >> 16) & 0xFF); |
chunk[2] = (char)((chunktype >> 8) & 0xFF); |
chunk[3] = (char)((chunktype ) & 0xFF); |
chunk[4] = '\0'; |
|
LOG(("error playing '%s' chunk %s (%d):", |
content_get_url(&c->base), chunk, chunkseq)); |
LOG(("code %d severity %d extra1 %d extra2 %d text:'%s'", code, |
severity, extra1, extra2, text)); |
|
return (0); |
} |
|
static nserror nsmng_create_mng_data(nsmng_content *c) |
{ |
mng_retcode code; |
union content_msg_data msg_data; |
|
assert(c != NULL); |
|
/* Initialise the library |
*/ |
#ifdef MNG_INTERNAL_MEMMNGMT |
c->handle = mng_initialize(c, MNG_NULL, MNG_NULL, MNG_NULL); |
#else |
c->handle = mng_initialize(c, nsmng_alloc, nsmng_free, MNG_NULL); |
#endif |
if (c->handle == MNG_NULL) { |
LOG(("Unable to initialise MNG library.")); |
msg_data.error = messages_get("NoMemory"); |
content_broadcast(&c->base, CONTENT_MSG_ERROR, msg_data); |
return NSERROR_NOMEM; |
} |
|
/* We need to decode in suspension mode |
*/ |
code = mng_set_suspensionmode(c->handle, MNG_TRUE); |
if (code) { |
LOG(("Unable to set suspension mode.")); |
return nsmng_broadcast_error(c, code); |
} |
|
/* We need to register our callbacks |
*/ |
code = mng_setcb_openstream(c->handle, nsmng_openstream); |
if (code) { |
LOG(("Unable to set openstream callback.")); |
return nsmng_broadcast_error(c, code); |
} |
code = mng_setcb_readdata(c->handle, nsmng_readdata); |
if (code) { |
LOG(("Unable to set readdata callback.")); |
return nsmng_broadcast_error(c, code); |
} |
code = mng_setcb_closestream(c->handle, nsmng_closestream); |
if (code) { |
LOG(("Unable to set closestream callback.")); |
return nsmng_broadcast_error(c, code); |
} |
code = mng_setcb_processheader(c->handle, nsmng_processheader); |
if (code) { |
LOG(("Unable to set processheader callback.")); |
return nsmng_broadcast_error(c, code); |
} |
|
/* Register our callbacks for displaying |
*/ |
code = mng_setcb_getcanvasline(c->handle, nsmng_getcanvasline); |
if (code) { |
LOG(("Unable to set getcanvasline callback.")); |
return nsmng_broadcast_error(c, code); |
} |
code = mng_setcb_refresh(c->handle, nsmng_refresh); |
if (code) { |
LOG(("Unable to set refresh callback.")); |
return nsmng_broadcast_error(c, code); |
} |
code = mng_setcb_gettickcount(c->handle, nsmng_gettickcount); |
if (code) { |
LOG(("Unable to set gettickcount callback.")); |
return nsmng_broadcast_error(c, code); |
} |
code = mng_setcb_settimer(c->handle, nsmng_settimer); |
if (code) { |
LOG(("Unable to set settimer callback.")); |
return nsmng_broadcast_error(c, code); |
} |
|
/* register error handling function */ |
code = mng_setcb_errorproc(c->handle, nsmng_errorproc); |
if (code) { |
LOG(("Unable to set errorproc")); |
return nsmng_broadcast_error(c, code); |
} |
|
/* Initialise the reading |
*/ |
c->read_start = true; |
c->read_resume = false; |
c->read_size = 0; |
c->waiting = false; |
|
c->displayed = false; |
|
return NSERROR_OK; |
} |
|
static nserror nsmng_create(const content_handler *handler, |
lwc_string *imime_type, const struct http_parameter *params, |
llcache_handle *llcache, const char *fallback_charset, |
bool quirks, struct content **c) |
{ |
nsmng_content *mng; |
nserror error; |
|
mng = calloc(1, sizeof(nsmng_content)); |
if (mng == NULL) |
return NSERROR_NOMEM; |
|
error = content__init(&mng->base, handler, imime_type, params, |
llcache, fallback_charset, quirks); |
if (error != NSERROR_OK) { |
free(mng); |
return error; |
} |
|
error = nsmng_create_mng_data(mng); |
if (error != NSERROR_OK) { |
free(mng); |
return error; |
} |
|
*c = (struct content *) mng; |
|
return NSERROR_OK; |
} |
|
|
|
static bool nsmng_convert(struct content *c) |
{ |
nsmng_content *mng = (nsmng_content *) c; |
mng_retcode status; |
unsigned long size; |
char *title; |
|
assert(c != NULL); |
|
content__get_source_data(c, &size); |
|
/* by this point, the png should have been parsed |
* and the bitmap created, so ensure that's the case |
*/ |
if (mng->bitmap == NULL) { |
return nsmng_broadcast_error(mng, -1) == NSERROR_OK; |
} |
|
|
/* set title text */ |
title = messages_get_buff("MNGTitle", |
nsurl_access_leaf(llcache_handle_get_url(c->llcache)), |
c->width, c->height); |
if (title != NULL) { |
content__set_title(c, title); |
free(title); |
} |
|
c->size += c->width * c->height * 4; |
content_set_ready(c); |
content_set_done(c); |
/* Done: update status bar */ |
content_set_status(c, ""); |
|
/* jmb: I'm really not sure that this should be here. |
* The *_convert functions are for converting a content into a |
* displayable format. They should not, however, do anything which |
* could cause the content to be displayed; the content may have |
* hidden visibility or be a fallback for an object; this |
* information is not available here (nor is there any need for it |
* to be). |
* The specific issue here is that mng_display calls the display |
* callbacks, which include nsmng_refresh. nsmng_refresh forces |
* a content to be redrawn regardless of whether it should be |
* displayed or not. |
*/ |
/* Start displaying |
*/ |
status = mng_display(mng->handle); |
if ((status != MNG_NOERROR) && (status != MNG_NEEDTIMERWAIT)) { |
LOG(("Unable to start display (%i)", status)); |
return nsmng_broadcast_error(mng, status) == NSERROR_OK; |
} |
bitmap_modified(mng->bitmap); |
|
/* Optimise the plotting of MNG */ |
mng->opaque_test_pending = false; |
|
return true; |
} |
|
static bool nsjpng_convert(struct content *c) |
{ |
nsmng_content *mng = (nsmng_content *) c; |
mng_retcode status; |
unsigned long size; |
char *title; |
mng_handle handle; |
|
assert(c != NULL); |
|
content__get_source_data(c, &size); |
|
/* by this point, the png should have been parsed |
* and the bitmap created, so ensure that's the case |
*/ |
if (mng->bitmap == NULL) { |
return nsmng_broadcast_error(mng, -1) == NSERROR_OK; |
} |
|
/* set title text */ |
title = messages_get_buff("PNGTitle", |
nsurl_access_leaf(llcache_handle_get_url(c->llcache)), |
c->width, c->height); |
if (title != NULL) { |
content__set_title(c, title); |
free(title); |
} |
|
c->size += c->width * c->height * 4; |
content_set_ready(c); |
content_set_done(c); |
/* Done: update status bar */ |
content_set_status(c, ""); |
|
/* jmb: I'm really not sure that this should be here. |
* The *_convert functions are for converting a content into a |
* displayable format. They should not, however, do anything which |
* could cause the content to be displayed; the content may have |
* hidden visibility or be a fallback for an object; this |
* information is not available here (nor is there any need for it |
* to be). |
* The specific issue here is that mng_display calls the display |
* callbacks, which include nsmng_refresh. nsmng_refresh forces |
* a content to be redrawn regardless of whether it should be |
* displayed or not. |
*/ |
/* Start displaying |
*/ |
status = mng_display(mng->handle); |
if ((status != MNG_NOERROR) && (status != MNG_NEEDTIMERWAIT)) { |
LOG(("Unable to start display (%i)", status)); |
return nsmng_broadcast_error(mng, status) == NSERROR_OK; |
} |
bitmap_modified(mng->bitmap); |
|
/* Optimise the plotting of JNG/PNGs |
*/ |
mng->opaque_test_pending = true; |
bitmap_set_opaque(mng->bitmap, false); |
|
/* free associated memory */ |
|
handle = mng->handle; |
|
mng_cleanup(&handle); |
|
mng->handle = NULL; |
|
return true; |
} |
|
static void nsmng_destroy(struct content *c) |
{ |
nsmng_content *mng = (nsmng_content *) c; |
|
assert (c != NULL); |
|
/* Cleanup the MNG structure and release the canvas memory |
*/ |
schedule_remove(nsmng_animate, c); |
|
if (mng->handle != NULL) { |
mng_handle handle = mng->handle; |
|
mng_cleanup(&handle); |
|
mng->handle = NULL; |
} |
|
if (mng->bitmap) { |
bitmap_destroy(mng->bitmap); |
} |
} |
|
|
static bool nsmng_redraw(struct content *c, struct content_redraw_data *data, |
const struct rect *clip, const struct redraw_context *ctx) |
{ |
nsmng_content *mng = (nsmng_content *) c; |
bool ret; |
bitmap_flags_t flags = BITMAPF_NONE; |
|
/* mark image as having been requested to display */ |
mng->displayed = true; |
|
if ((mng->bitmap) && |
(mng->opaque_test_pending)) { |
bitmap_set_opaque(mng->bitmap, bitmap_test_opaque(mng->bitmap)); |
mng->opaque_test_pending = false; |
} |
|
if (data->repeat_x) |
flags |= BITMAPF_REPEAT_X; |
if (data->repeat_y) |
flags |= BITMAPF_REPEAT_Y; |
|
ret = ctx->plot->bitmap(data->x, data->y, data->width, data->height, |
mng->bitmap, data->background_colour, flags); |
|
/* Check if we need to restart the animation */ |
if ((mng->waiting) && |
(nsoption_bool(animate_images))) { |
nsmng_animate(c); |
} |
|
return ret; |
} |
|
|
static nserror nsmng_clone(const struct content *old, struct content **newc) |
{ |
nsmng_content *mng; |
nserror error; |
const char *data; |
unsigned long size; |
|
mng = calloc(1, sizeof(nsmng_content)); |
if (mng == NULL) |
return NSERROR_NOMEM; |
|
error = content__clone(old, &mng->base); |
if (error != NSERROR_OK) { |
content_destroy(&mng->base); |
return error; |
} |
|
/* Simply replay create/process/convert */ |
error = nsmng_create_mng_data(mng); |
if (error != NSERROR_OK) { |
content_destroy(&mng->base); |
return error; |
} |
|
data = content__get_source_data(&mng->base, &size); |
if (size > 0) { |
if (nsmng_process_data(&mng->base, data, size) == false) { |
content_destroy(&mng->base); |
return NSERROR_CLONE_FAILED; |
} |
} |
|
if (old->status == CONTENT_STATUS_READY || |
old->status == CONTENT_STATUS_DONE) { |
if (nsmng_convert(&mng->base) == false) { |
content_destroy(&mng->base); |
return NSERROR_CLONE_FAILED; |
} |
} |
|
*newc = (struct content *) mng; |
|
return NSERROR_OK; |
} |
|
static void *nsmng_get_internal(const struct content *c, void *context) |
{ |
nsmng_content *mng = (nsmng_content *)c; |
|
return mng->bitmap; |
} |
|
static content_type nsmng_content_type(void) |
{ |
return CONTENT_IMAGE; |
} |
|
/* register handler for mng types */ |
static const content_handler nsmng_content_handler = { |
.create = nsmng_create, |
.process_data = nsmng_process_data, |
.data_complete = nsmng_convert, |
.destroy = nsmng_destroy, |
.redraw = nsmng_redraw, |
.clone = nsmng_clone, |
.get_internal = nsmng_get_internal, |
.type = nsmng_content_type, |
.no_share = false, |
}; |
|
static const char *nsmng_types[] = { |
/* MNG types*/ |
"image/mng", |
"image/x-mng", |
"video/mng", |
"video/x-mng", |
}; |
|
CONTENT_FACTORY_REGISTER_TYPES(nsmng, nsmng_types, nsmng_content_handler); |
|
/* register handler for jng and png types */ |
static const content_handler nsjpng_content_handler = { |
.create = nsmng_create, |
.process_data = nsmng_process_data, |
.data_complete = nsjpng_convert, |
.destroy = nsmng_destroy, |
.redraw = nsmng_redraw, |
.clone = nsmng_clone, |
.get_internal = nsmng_get_internal, |
.type = nsmng_content_type, |
.no_share = false, |
}; |
|
|
static const char *nsjpng_types[] = { |
/* JNG types*/ |
"image/jng", |
"image/x-jng", |
/* PNG types*/ |
"image/png", |
"image/x-png" |
}; |
|
CONTENT_FACTORY_REGISTER_TYPES(nsjpng, nsjpng_types, nsjpng_content_handler); |