/* screenshot.c: Routines for handling .png and .scr screenshots
Copyright (c) 2002-2018 Philip Kendall, Fredrick Meunier
This program 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; either version 2 of the License, or
(at your option) any later version.
This program 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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Author contact information:
E-mail: philip-fuse@shadowmagic.org.uk
*/
#include "config.h"
#include <errno.h>
#include <limits.h>
#include <string.h>
#include "libspectrum.h"
#include "display.h"
#include "infrastructure/startup_manager.h"
#include "machine.h"
#include "peripherals/scld.h"
#include "screenshot.h"
#include "settings.h"
#include "ui/scaler/scaler.h"
#include "ui/ui.h"
#include "utils.h"
#define MONO_BITMAP_SIZE 6144
#define HICOLOUR_SCR_SIZE (2 * MONO_BITMAP_SIZE)
#define MLT_SIZE (2 * MONO_BITMAP_SIZE)
#define HIRES_ATTR HICOLOUR_SCR_SIZE
#define HIRES_SCR_SIZE (HICOLOUR_SCR_SIZE + 1)
#ifdef USE_LIBPNG
#include <png.h>
#ifdef HAVE_ZLIB_H
#define ZLIB_CONST
#include <zlib.h>
#endif /* #ifdef HAVE_ZLIB_H */
static int get_rgb32_data( libspectrum_byte *rgb32_data, size_t stride,
size_t height, size_t width );
static int rgb32_to_rgb24( libspectrum_byte *rgb24_data, size_t rgb24_stride,
libspectrum_byte *rgb32_data, size_t rgb32_stride,
size_t height, size_t width );
static void fill_rgb32_margin( libspectrum_byte *rgb32_data, size_t stride,
size_t height, size_t width );
/* The biggest size screen (in units of DISPLAY_ASPECT_WIDTH x
DISPLAY_SCREEN_HEIGHT ie a Timex screen is size 2) we will be
creating via the scalers */
#define MAX_SIZE 4
/* Pixels arround the RGB image for scalers that "smear" the screen */
#define K_MARGIN 2
/* The space used for drawing the screen image on */
static libspectrum_byte *rgb_data = NULL;
static libspectrum_byte *scaled_data;
static libspectrum_byte *png_data = NULL;
int
screenshot_write( const char *filename, scaler_type scaler )
{
FILE *f;
png_structp png_ptr;
png_infop info_ptr;
libspectrum_byte *row_pointers[ MAX_SIZE * DISPLAY_SCREEN_HEIGHT ];
libspectrum_byte *rgb_data_centered;
size_t rgb_stride = ( DISPLAY_ASPECT_WIDTH + K_MARGIN * 2 ) * 4,
scaled_stride = MAX_SIZE * DISPLAY_ASPECT_WIDTH * 4,
png_stride = MAX_SIZE * DISPLAY_ASPECT_WIDTH * 3;
size_t y, base_height, base_width, height, width;
int error;
if( machine_current->timex ) {
base_height = 2 * DISPLAY_SCREEN_HEIGHT;
base_width = DISPLAY_SCREEN_WIDTH;
} else {
base_height = DISPLAY_SCREEN_HEIGHT;
base_width = DISPLAY_ASPECT_WIDTH;
}
/* Allocate buffers on demand */
if( !rgb_data )
rgb_data =
libspectrum_new( libspectrum_byte,
( DISPLAY_SCREEN_HEIGHT + K_MARGIN * 2 ) * rgb_stride );
if( !scaled_data )
scaled_data = libspectrum_new( libspectrum_byte,
MAX_SIZE * DISPLAY_SCREEN_HEIGHT *
scaled_stride );
if( !png_data )
png_data = libspectrum_new( libspectrum_byte,
MAX_SIZE * DISPLAY_SCREEN_HEIGHT * png_stride );
/* first pixel of rgb_data (without margin) */
rgb_data_centered = rgb_data + K_MARGIN * rgb_stride + K_MARGIN * 4;
/* Change from paletted data to RGB data */
error = get_rgb32_data( rgb_data_centered, rgb_stride, base_height,
base_width );
if( error ) return error;
/* Initialise margin for scalers that "smear" the screen */
if( scaler_get_flags( scaler ) & SCALER_FLAGS_EXPAND )
fill_rgb32_margin( rgb_data, rgb_stride, base_height, base_width );
/* Actually scale the data here */
scaler_get_proc32( scaler )( rgb_data_centered, rgb_stride, scaled_data,
scaled_stride, base_width, base_height );
height = base_height * scaler_get_scaling_factor( scaler );
width = base_width * scaler_get_scaling_factor( scaler );
/* Reduce from RGB(padding byte) to just RGB */
error = rgb32_to_rgb24( png_data, png_stride, scaled_data, scaled_stride,
height, width );
if( error ) return error;
for( y = 0; y < height; y++ )
row_pointers[y] = &png_data[ y * png_stride ];
f = fopen( filename, "wb" );
if( !f ) {
ui_error( UI_ERROR_ERROR, "Couldn't open `%s': %s", filename,
strerror( errno ) );
return 1;
}
png_ptr = png_create_write_struct( PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL );
if( !png_ptr ) {
ui_error( UI_ERROR_ERROR, "Couldn't allocate png_ptr" );
fclose( f );
return 1;
}
info_ptr = png_create_info_struct( png_ptr );
if( !info_ptr ) {
ui_error( UI_ERROR_ERROR, "Couldn't allocate info_ptr" );
png_destroy_write_struct( &png_ptr, NULL );
fclose( f );
return 1;
}
/* Set up the error handling; libpng will return to here if it
encounters an error */
if( setjmp( png_jmpbuf( png_ptr ) ) ) {
ui_error( UI_ERROR_ERROR, "Error from libpng" );
png_destroy_write_struct( &png_ptr, &info_ptr );
fclose( f );
return 1;
}
png_init_io( png_ptr, f );
/* Make files as small as possible */
png_set_compression_level( png_ptr, Z_BEST_COMPRESSION );
png_set_IHDR( png_ptr, info_ptr,
width, height, 8,
PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT );
png_set_rows( png_ptr, info_ptr, row_pointers );
png_write_png( png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL );
png_destroy_write_struct( &png_ptr, &info_ptr );
if( fclose( f ) ) {
ui_error( UI_ERROR_ERROR, "Couldn't close `%s': %s", filename,
strerror( errno ) );
return 1;
}
return 0;
}
static int
get_rgb32_data( libspectrum_byte *rgb32_data, size_t stride,
size_t height, size_t width )
{
size_t i, x, y;
static const /* R G B */
libspectrum_byte palette[16][3] = { { 0, 0, 0 },
{ 0, 0, 192 },
{ 192, 0, 0 },
{ 192, 0, 192 },
{ 0, 192, 0 },
{ 0, 192, 192 },
{ 192, 192, 0 },
{ 192, 192, 192 },
{ 0, 0, 0 },
{ 0, 0, 255 },
{ 255, 0, 0 },
{ 255, 0, 255 },
{ 0, 255, 0 },
{ 0, 255, 255 },
{ 255, 255, 0 },
{ 255, 255, 255 } };
libspectrum_byte grey_palette[16];
/* Addition of 0.5 is to avoid rounding errors */
for( i = 0; i < 16; i++ )
grey_palette[i] = ( 0.299 * palette[i][0] +
0.587 * palette[i][1] +
0.114 * palette[i][2] ) + 0.5;
for( y = 0; y < height; y++ ) {
for( x = 0; x < width; x++ ) {
size_t colour;
libspectrum_byte red, green, blue;
colour = display_getpixel( x, y );
if( settings_current.bw_tv ) {
red = green = blue = grey_palette[colour];
} else {
red = palette[colour][0];
green = palette[colour][1];
blue = palette[colour][2];
}
rgb32_data[ y * stride + 4 * x ] = red;
rgb32_data[ y * stride + 4 * x + 1 ] = green;
rgb32_data[ y * stride + 4 * x + 2 ] = blue;
rgb32_data[ y * stride + 4 * x + 3 ] = 0; /* padding */
}
}
return 0;
}
static void
fill_rgb32_margin( libspectrum_byte *rgb32_data, size_t stride,
size_t height, size_t width )
{
size_t x_dest, y_dest, x_src, y_src;
libspectrum_byte *dest_data, *src_data;
/* We need to initialise the extra space available for scalers that "smear"
the screen by copying the nearest neighbour, so we avoid any blending with
black colour or memory garbage at the outer border */
/* Fill top margin */
y_src = K_MARGIN;
for( y_dest = 0; y_dest < K_MARGIN; y_dest++ ) {
for( x_dest = 0; x_dest < width + K_MARGIN * 2; x_dest++ ) {
if( x_dest < K_MARGIN )
x_src = K_MARGIN;
else if( x_dest >= K_MARGIN + width )
x_src = K_MARGIN + width - 1;
else
x_src = x_dest;
dest_data = rgb32_data + y_dest * stride + 4 * x_dest;
src_data = rgb32_data + y_src * stride + 4 * x_src;
*(libspectrum_dword*) dest_data = *(libspectrum_dword*) src_data;
}
}
/* Fill left margin */
x_src = K_MARGIN;
for( y_dest = K_MARGIN; y_dest < K_MARGIN + height; y_dest++ ) {
for( x_dest = 0; x_dest < K_MARGIN; x_dest++ ) {
y_src = y_dest;
dest_data = rgb32_data + y_dest * stride + 4 * x_dest;
src_data = rgb32_data + y_src * stride + 4 * x_src;
*(libspectrum_dword*) dest_data = *(libspectrum_dword*) src_data;
}
}
/* Fill right margin */
x_src = K_MARGIN + width -1;
for( y_dest = K_MARGIN; y_dest < K_MARGIN + height; y_dest++ ) {
for( x_dest = K_MARGIN + width; x_dest < K_MARGIN * 2 + width; x_dest++ ) {
y_src = y_dest;
dest_data = rgb32_data + y_dest * stride + 4 * x_dest;
src_data = rgb32_data + y_src * stride + 4 * x_src;
*(libspectrum_dword*) dest_data = *(libspectrum_dword*) src_data;
}
}
/* Fill bottom margin */
y_src = K_MARGIN + height - 1;
for( y_dest = K_MARGIN + height; y_dest < height + K_MARGIN * 2; y_dest++ ) {
for( x_dest = 0; x_dest < width + K_MARGIN * 2; x_dest++ ) {
if( x_dest < K_MARGIN )
x_src = K_MARGIN;
else if( x_dest >= K_MARGIN + width )
x_src = K_MARGIN + width - 1;
else
x_src = x_dest;
dest_data = rgb32_data + y_dest * stride + 4 * x_dest;
src_data = rgb32_data + y_src * stride + 4 * x_src;
*(libspectrum_dword*) dest_data = *(libspectrum_dword*) src_data;
}
}
}
static int
rgb32_to_rgb24( libspectrum_byte *rgb24_data, size_t rgb24_stride,
libspectrum_byte *rgb32_data, size_t rgb32_stride,
size_t height, size_t width )
{
size_t x, y;
for( y = 0; y < height; y++ ) {
for( x = 0; x < width; x++ ) {
rgb24_data[y*rgb24_stride +3*x ] = rgb32_data[y*rgb32_stride +4*x ];
rgb24_data[y*rgb24_stride +3*x +1] = rgb32_data[y*rgb32_stride +4*x +1];
rgb24_data[y*rgb24_stride +3*x +2] = rgb32_data[y*rgb32_stride +4*x +2];
}
}
return 0;
}
int
screenshot_available_scalers( scaler_type scaler )
{
if( machine_current->timex ) {
switch( scaler ) {
case SCALER_HALF: case SCALER_HALFSKIP: case SCALER_NORMAL:
case SCALER_TIMEX1_5X: case SCALER_TIMEX2X:
case SCALER_TIMEXTV: case SCALER_PALTV:
return 1;
default:
return 0;
}
} else {
switch( scaler ) {
case SCALER_NORMAL: case SCALER_DOUBLESIZE: case SCALER_TRIPLESIZE: case SCALER_QUADSIZE:
case SCALER_2XSAI: case SCALER_SUPER2XSAI: case SCALER_SUPEREAGLE:
case SCALER_ADVMAME2X: case SCALER_ADVMAME3X:
case SCALER_TV2X: case SCALER_TV3X: case SCALER_TV4X: case SCALER_DOTMATRIX:
case SCALER_PALTV: case SCALER_PALTV2X: case SCALER_PALTV3X: case SCALER_PALTV4X:
case SCALER_HQ2X: case SCALER_HQ3X: case SCALER_HQ4X:
return 1;
default:
return 0;
}
}
}
#endif /* #ifdef USE_LIBPNG */
static void
screenshot_end( void )
{
#ifdef USE_LIBPNG
libspectrum_free( rgb_data ); rgb_data = NULL;
libspectrum_free( scaled_data ); scaled_data = NULL;
libspectrum_free( png_data ); png_data = NULL;
#endif
}
void
screenshot_register_startup( void )
{
startup_manager_module dependencies[] = { STARTUP_MANAGER_MODULE_SETUID };
startup_manager_register( STARTUP_MANAGER_MODULE_SCREENSHOT, dependencies,
ARRAY_SIZE( dependencies ), NULL, NULL,
screenshot_end );
}
static int
screenshot_scr_hires_write( const char *filename )
{
libspectrum_byte scr_data[ HIRES_SCR_SIZE ];
memset( scr_data, 0, HIRES_SCR_SIZE );
memcpy( scr_data,
&RAM[ memory_current_screen ][ display_get_addr(0,0) ],
MONO_BITMAP_SIZE );
memcpy( scr_data + MONO_BITMAP_SIZE,
&RAM[ memory_current_screen ][ display_get_addr(0,0) +
ALTDFILE_OFFSET], MONO_BITMAP_SIZE );
scr_data[ HIRES_ATTR ] = ( scld_last_dec.byte & HIRESCOLMASK ) |
scld_last_dec.mask.scrnmode;
return utils_write_file( filename, scr_data, HIRES_SCR_SIZE );
}
static int
get_display_last_screen_index( int x, int y )
{
int beam_x = x + DISPLAY_BORDER_WIDTH_COLS;
int beam_y = y + DISPLAY_BORDER_HEIGHT;
return beam_x + beam_y * DISPLAY_SCREEN_WIDTH_COLS;
}
static void
set_hicolor_pixels_and_attribute( int x, int y, libspectrum_byte* scr_data )
{
int index = get_display_last_screen_index( x, y );
libspectrum_byte pixel_data = display_last_screen[ index ] & 0xff;
libspectrum_byte attribute_data = (display_last_screen[ index ] & 0xff00)>>8;
libspectrum_word offset = display_get_offset( x, y );
scr_data[ offset ] = pixel_data;
/* write attribute into bitmap order buffer after bitmap */
scr_data[ MONO_BITMAP_SIZE + offset ] = attribute_data;
}
typedef void (*data_write_fn)( int x, int y, libspectrum_byte* scr_data );
static int
scr_write( const char *filename, const int data_size,
data_write_fn scr_data_write )
{
libspectrum_byte scr_data[ data_size ];
int x, y;
memset( scr_data, 0, data_size );
for( y = 0; y < DISPLAY_HEIGHT; y++ ) {
for( x = 0; x < DISPLAY_WIDTH_COLS; x++ ) {
scr_data_write( x, y, scr_data );
}
}
return utils_write_file( filename, scr_data, data_size );
}
static void
set_standard_pixels_and_attribute( int x, int y, libspectrum_byte* scr_data )
{
int index = get_display_last_screen_index( x, y );
libspectrum_byte pixel_data = display_last_screen[ index ] & 0xff;
libspectrum_byte attribute_data = (display_last_screen[ index ] & 0xff00)>>8;
int attribute_offset;
scr_data[ display_get_offset( x, y ) ] = pixel_data;
if( y%8 == 0 ) {
/* write attribute into standard attribute order buffer after bitmap */
attribute_offset = x + (y/8 * DISPLAY_WIDTH_COLS);
scr_data[ MONO_BITMAP_SIZE + attribute_offset ] = attribute_data;
}
}
int
screenshot_scr_write( const char *filename )
{
if( scld_last_dec.name.hires ) {
return screenshot_scr_hires_write( filename );
}
else if( scld_last_dec.name.b1 ) {
return scr_write( filename, HICOLOUR_SCR_SIZE,
&set_hicolor_pixels_and_attribute );
}
else { /* ALTDFILE and default */
return scr_write( filename, STANDARD_SCR_SIZE,
&set_standard_pixels_and_attribute );
}
}
static void
set_mlt_pixels_and_attribute( int x, int y, libspectrum_byte* mlt_data )
{
int index = get_display_last_screen_index( x, y );
libspectrum_byte pixel_data = display_last_screen[ index ] & 0xff;
libspectrum_byte attribute_data = (display_last_screen[ index ] & 0xff00)>>8;
mlt_data[ display_get_offset( x, y ) ] = pixel_data;
/* write attribute into linear buffer following bitmap */
mlt_data[ MONO_BITMAP_SIZE + x + y * DISPLAY_WIDTH_COLS ] = attribute_data;
}
int
screenshot_mlt_write( const char *filename )
{
if( machine_current->timex && scld_last_dec.name.hires ) {
ui_error( UI_ERROR_ERROR,
"MLT format not supported for Timex hi-res screen mode" );
return 1;
}
return scr_write( filename, MLT_SIZE, &set_mlt_pixels_and_attribute );
}
#ifdef WORDS_BIGENDIAN
typedef struct {
unsigned b0 : 1;
unsigned b1 : 1;
unsigned b2 : 1;
unsigned b3 : 1;
unsigned b4 : 1;
unsigned b5 : 1;
unsigned b6 : 1;
unsigned b7 : 1;
} byte_field_type;
#else /* #ifdef WORDS_BIGENDIAN */
typedef struct {
unsigned b7 : 1;
unsigned b6 : 1;
unsigned b5 : 1;
unsigned b4 : 1;
unsigned b3 : 1;
unsigned b2 : 1;
unsigned b1 : 1;
unsigned b0 : 1;
} byte_field_type;
#endif /* #ifdef WORDS_BIGENDIAN */
typedef union {
libspectrum_byte byte;
byte_field_type bit;
} bft_union;
static libspectrum_byte
convert_hires_to_lores( libspectrum_byte high, libspectrum_byte low )
{
bft_union ret, h, l;
h.byte = high;
l.byte = low;
ret.bit.b0 = l.bit.b0;
ret.bit.b1 = l.bit.b2;
ret.bit.b2 = l.bit.b4;
ret.bit.b3 = l.bit.b6;
ret.bit.b4 = h.bit.b0;
ret.bit.b5 = h.bit.b2;
ret.bit.b6 = h.bit.b4;
ret.bit.b7 = h.bit.b6;
return ret.byte;
}
int
screenshot_scr_read( const char *filename )
{
int error = 0;
int i;
utils_file screen;
error = utils_read_file( filename, &screen );
if( error ) return error;
switch( screen.length ) {
case STANDARD_SCR_SIZE:
memcpy( &RAM[ memory_current_screen ][display_get_addr(0,0)],
screen.buffer, screen.length );
/* If it is a Timex and it is in hi colour or hires mode, switch out of
hires or hicolour mode */
if( scld_last_dec.name.b1 || scld_last_dec.name.hires )
scld_dec_write( 0xff, scld_last_dec.byte & ~HIRES );
break;
case HICOLOUR_SCR_SIZE:
/* If it is a Timex and it is not in hi colour mode, copy screen and switch
mode if neccesary */
/* If it is not a Timex copy the mono bitmap and raise an error */
if( machine_current->timex ) {
if( !scld_last_dec.name.b1 )
scld_dec_write( 0xff, ( scld_last_dec.byte & ~HIRESATTR ) | EXTCOLOUR );
memcpy( &RAM[ memory_current_screen ][display_get_addr(0,0) +
ALTDFILE_OFFSET], screen.buffer + MONO_BITMAP_SIZE,
MONO_BITMAP_SIZE );
} else
ui_error( UI_ERROR_INFO,
"The file contained a TC2048 high-colour screen, loaded as mono");
memcpy( &RAM[ memory_current_screen ][display_get_addr(0,0)],
screen.buffer, MONO_BITMAP_SIZE );
break;
case HIRES_SCR_SIZE:
/* If it is a Timex and it is not in hi res mode, copy screen and switch
mode if neccesary */
/* If it is not a Timex scale the bitmap and raise an error */
if( machine_current->timex ) {
memcpy( &RAM[ memory_current_screen ][display_get_addr(0,0)],
screen.buffer, MONO_BITMAP_SIZE );
memcpy( &RAM[ memory_current_screen ][display_get_addr(0,0)] +
ALTDFILE_OFFSET, screen.buffer + MONO_BITMAP_SIZE,
MONO_BITMAP_SIZE );
if( !scld_last_dec.name.hires )
scld_dec_write( 0xff,
( scld_last_dec.byte & ~( HIRESCOLMASK | HIRES ) ) |
( *(screen.buffer + HIRES_ATTR) & ( HIRESCOLMASK | HIRES ) ) );
} else {
libspectrum_byte attr = hires_convert_dec( *(screen.buffer + HIRES_ATTR) );
for( i = 0; i < MONO_BITMAP_SIZE; i++ )
RAM[ memory_current_screen ][display_get_addr(0,0) + i] =
convert_hires_to_lores( *(screen.buffer + MONO_BITMAP_SIZE + i),
*(screen.buffer + i) );
/* set attributes based on hires attribute byte */
for( i = 0; i < 768; i++ )
RAM[ memory_current_screen ][display_get_addr(0,0) +
MONO_BITMAP_SIZE + i] = attr;
ui_error( UI_ERROR_INFO,
"The file contained a TC2048 high-res screen, converted to lores");
}
break;
default:
ui_error( UI_ERROR_ERROR, "'%s' is not a valid scr file", filename );
error = 1;
}
utils_close_file( &screen );
display_refresh_all();
return error;
}
int
screenshot_mlt_read( const char *filename )
{
int error = 0;
int x, y;
utils_file screen;
error = utils_read_file( filename, &screen );
if( error ) return error;
if( screen.length != MLT_SIZE ) {
ui_error( UI_ERROR_ERROR, "MLT picture ('%s') is not %d bytes long",
filename, MLT_SIZE );
return 1;
}
/* If it is a Timex and it is not in hi colour mode, copy screen and switch
mode if neccesary */
/* If it is not a Timex copy the mono bitmap and raise an error */
if( machine_current->timex ) {
if( !scld_last_dec.name.b1 )
scld_dec_write( 0xff, ( scld_last_dec.byte & ~HIRESATTR ) | EXTCOLOUR );
for( y = 0; y < DISPLAY_HEIGHT; y++ ) {
for( x = 0; x < DISPLAY_WIDTH_COLS; x++ ) {
RAM[ memory_current_screen ][display_get_addr(x,y) + ALTDFILE_OFFSET] =
*(screen.buffer + MONO_BITMAP_SIZE + x + y * DISPLAY_WIDTH_COLS);
}
}
} else
ui_error( UI_ERROR_INFO,
"The file contained a MLT high-colour screen, loaded as mono");
memcpy( &RAM[ memory_current_screen ][display_get_addr(0,0)],
screen.buffer, MONO_BITMAP_SIZE );
utils_close_file( &screen );
display_refresh_all();
return error;
}