You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
PDCursesMod/demos/picsview.c

509 lines
15 KiB
C

#include <stdlib.h>
#include <locale.h>
#include <stdio.h>
#include <string.h>
#include "../demos/snprintf.c"
#define PDC_NCMOUSE
#include "curses.h"
#include <math.h>
#include <assert.h>
#ifndef _WIN32
#define CONVERT_IMAGES
#include <unistd.h>
#endif
#ifdef WACS_S1
# define HAVE_WIDE
#endif
/* This is inspired by, but not really based on, Thomas E. Dickey's
'picsmap' program for ncurses. Its real purpose is to demonstrate the
ability to use large palettes (basically, full RGB) and large numbers
of color pairs (up to 2^20 = 1048576 pairs). That ability is currently
available only in the VT and WinGUI flavors of PDCurses, but will be
expanded to X11, SDLx, and probably WinCon.
Run with an image file as a command-line argument. The image will
be displayed using PDCurses' extended color palette and color pairs.
You can pan around the image with the mouse and keyboard, and zoom
in/out using the mouse wheel, and hit 'r' to rotate the image 90 degrees.
If you don't specify an image file, a fake image showing color
gradients is generated. On Linux, it'll use 'convert' (ImageMagick)
to turn JPEGs or other image files into PNMs or PGMs. On DOS/Windows,
you have to give it a PNM or PGM file.
This program relies on the availability of a 2^24+256=16777472-color
palette and 2^20=1048576 color pairs. Thus, any color can be specified,
and up to a million combinations of foreground and background color.
In each cell, an ACS_BBLOCK ('bottom half block') is shown; the
bottom half then appears in the foreground color for that character
cell, and the upper half in the background color for that cell. Thus,
the 'pixels' making up our image are one character wide, but only
half a character high.
We start out with the image scaled to fit entirely within the window.
The loop below goes through 2 * LINES iterations. On even passes, we
compute RGB values for the pixels that will go into the top half of each
character cell. On odd passes, we're computing the RGB for the bottom
halves, and each time one is computed, we spit out another character.
(And, if the colors have changed, we allocate another color pair.
So this can chew through as many color pairs as there are character
cells on the current screen.) */
double aspect = 1.1; /* The 'bottom block' (half-height cell character) */
/* is _almost_ square, but is about 10% wider than it is tall */
/* PNM files give each pixel as an RGB triplet of bytes. A width by height
image consumes exactly width * height * 3 bytes. */
#define UNICODE_BBLOCK 0x2584
#ifndef ACS_BBLOCK
#define ACS_BBLOCK 0xdc
#endif
static int32_t get_rgb_value( const char *iptr)
{
unsigned char red = (unsigned char)iptr[0];
unsigned char grn = (unsigned char)iptr[1];
unsigned char blu = (unsigned char)iptr[2];
return( (int32_t)red | ((int32_t)grn << 8) | ((int32_t)blu << 16));
}
/* Image rotation isn't done fancily here. We allocate another array of equal
size, copy pixels into it rearranged to their rotated positions, and copy
that new array back into the original. */
static void rotate_pixels_ninety_degrees( const int xsize, const int ysize, char *pixels)
{
int i, j;
char *tmp = (char *)malloc( xsize * ysize * 3);
assert( tmp);
for( i = 0; i < ysize; i++)
{
char *tptr = tmp + (ysize - 1 - i) * 3;
for( j = xsize; j; j--)
{
*tptr++ = *pixels++;
*tptr++ = *pixels++;
*tptr++ = *pixels++;
tptr += (ysize - 1) * 3;
}
}
pixels -= xsize * ysize * 3;
memcpy( pixels, tmp, xsize * ysize * 3);
free( tmp);
}
static void invert_pixels( const int xsize, const int ysize, char *pixels)
{
char *endpix = pixels + (xsize * ysize - 1) * 3;
while( endpix > pixels)
{
char tbuff[3];
tbuff[0] = pixels[0]; pixels[0] = endpix[0]; endpix[0] = tbuff[0];
tbuff[1] = pixels[1]; pixels[1] = endpix[1]; endpix[1] = tbuff[1];
tbuff[2] = pixels[2]; pixels[2] = endpix[2]; endpix[2] = tbuff[2];
pixels += 3;
endpix -= 3;
}
}
static void mirror_pixels_top_bottom( const int xsize, const int ysize, char *pixels)
{
char *tbuff = (char *)malloc( xsize * 3);
char *tptr1 = pixels, *tptr2 = pixels + (ysize - 1) * xsize * 3;
while( tptr2 > tptr1)
{
memcpy( tbuff, tptr1, 3 * xsize);
memcpy( tptr1, tptr2, 3 * xsize);
memcpy( tptr2, tbuff, 3 * xsize);
tptr1 += xsize * 3;
tptr2 -= xsize * 3;
}
free( tbuff);
}
static void mirror_pixels_left_right( const int xsize, int ysize, char *pixels)
{
mirror_pixels_top_bottom( xsize, ysize, pixels);
invert_pixels( xsize, ysize, pixels);
}
/* Make a reasonably interesting image with gradients, circles,
and hyperbolas */
static void make_fake_image( const char *filename)
{
FILE *ofile = fopen( filename, "wb");
int i, j, xsize = 640, ysize = 480, r = 150, r1 = 200;
unsigned char buff[3];
fprintf( ofile, "P6\n%d %d\n255\n", xsize, ysize);
for( i = 0; i < ysize; i++)
for( j = 0; j < xsize; j++)
{
const int dx = j - xsize / 2;
const int dy = i - ysize / 2;
buff[0] = (unsigned char)( i * 255 / ysize);
buff[1] = (unsigned char)( j * 255 / xsize);
if( dx * dx + dy * dy < r1 * r1)
buff[1] ^= 0xff;
if( abs( dx * dx - dy * dy) > r * r)
buff[2] = 0xc0;
else
buff[2] = 0;
fwrite( buff, 3, 1, ofile);
}
fclose( ofile);
}
/* Compute dither offset. See https://en.wikipedia.org/wiki/Ordered_dithering
for explanation. This would correspond to a 16x16 'threshold map'. */
static int calc_dither( int x, int y)
{
int rval = 1;
while( !(rval & 0x100))
{
rval <<= 2;
rval |= (x & 1) | ((y & 1) << 1);
x >>= 1;
y >>= 1;
}
return( rval & 0xff);
}
/* For 'full-color' palettes, the mapping from RGB to default palette
entry is a simple shift by 256. For 'traditional' 256-color systems
with a 6x6x6 color cube, the math is slightly harder. Dithering is
used to make the results marginally less ugly. */
static int find_in_palette( const int32_t rgb, const int dither)
{
#ifndef CHTYPE_32
if( COLORS > 0x100000)
return( rgb + 256);
else
#endif
{ /* find entry in 6x6x6 color cube */
const int red = (rgb & 0xff);
const int grn = ((rgb >> 8) & 0xff);
const int blu = ((rgb >> 16) & 0xff);
return( 16 + ((blu * 5 + dither) / 256)
+ ((grn * 5 + dither) / 256) * 6
+ ((red * 5 + dither) / 256) * 36);
}
}
#if defined( __linux)
const char *temp_image_name = "/tmp/ickywax.pnm";
#else
const char *temp_image_name = "ickywax.pnm";
#endif
int main( const int argc, const char *argv[])
{
FILE *ifile;
int xsize, ysize;
char *pixels, buff[100];
const char *filename = temp_image_name;
const char *filename_to_show = temp_image_name;
int c = 0, i, bytes_per_pixel = 3;
double scale = 0., xpix = 0., ypix = 0.;
bool show_help = TRUE;
SCREEN *screen_pointer;
setlocale(LC_ALL, ".utf8");
for( i = 1; i < argc; i++)
if( argv[i][0] == '-')
switch( argv[i][1])
{
case 'a':
aspect = atof( argv[i] + 2);
break;
}
if( argc > 1 && argv[1][0] != '-')
{
filename_to_show = argv[1];
#ifdef CONVERT_IMAGES
snprintf( buff, sizeof( buff), "convert %s %s", argv[1], temp_image_name);
if( system( buff))
{
printf( "Couldn't convert %s\n", filename);
return( -1);
}
#else
filename = argv[1];
temp_image_name = NULL;
#endif
}
else
make_fake_image( temp_image_name);
ifile = fopen( filename, "rb");
if( !ifile)
{
printf( "'%s' not opened\n", filename_to_show);
return( -2);
}
if( !fgets( buff, sizeof( buff), ifile) || *buff != 'P'
|| buff[1] > '6' || buff[1] < '5')
{
printf( "'%s' was not a .pnm file\n", filename_to_show);
return( -3);
}
if( buff[1] == '5')
bytes_per_pixel = 1;
if( fgets( buff, sizeof( buff), ifile))
sscanf( buff, "%d %d", &xsize, &ysize);
else
{
fprintf( stderr, "Unable to read '%s'\n", filename_to_show);
return( -9);
}
if( !fgets( buff, sizeof( buff), ifile))
return( -4);
assert( xsize < 300000 && ysize < 300000);
pixels = (char *)calloc( xsize, ysize * 3);
assert( pixels);
if( !fread( pixels, xsize * ysize, bytes_per_pixel, ifile))
{
printf( "%d x %d pixels not read\n", xsize, ysize);
return( -5);
}
fclose( ifile);
if( temp_image_name)
#ifdef _WIN32
_unlink( temp_image_name);
#else
unlink( temp_image_name);
#endif
if( bytes_per_pixel == 1) /* expand grayscale to RGB */
for( i = xsize * ysize - 1; i >= 0; i--)
pixels[i * 3] = pixels[i * 3 + 1] = pixels[i * 3 + 2] = pixels[i];
screen_pointer = newterm(NULL, stdout, stdin);
start_color( );
cbreak( );
noecho();
keypad( stdscr, 1);
mousemask( ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
while( c != 27 && c != 'q')
{
int *xloc = (int *)calloc( 2 * COLS, sizeof( int));
int *idxs = xloc + COLS;
int prev_idx = -1, prev_low_idx = -1;
int j, pair_num = 16;
MEVENT mouse_event;
double xpix1, ypix1;
assert( xloc);
memset( &mouse_event, 0, sizeof( MEVENT));
if( !scale) /* recompute scale */
{
double scale1 = (double)xsize / (double)COLS;
scale = (double)ysize / ((double)LINES * 2. * aspect);
if( scale < scale1)
scale = scale1;
xpix = (double)xsize / 2.;
ypix = (double)ysize / 2.;
}
for( i = 0; i < COLS; i++)
xloc[i] = (int)( xpix + (double)i * scale) - (int)( COLS * scale) / 2;
erase( );
for( j = 0; j < LINES * 2; j++)
{
const int yloc = (int)( ypix + (double)j * scale * aspect)
- (int)( LINES * scale * aspect);
const char *pptr = pixels + yloc * 3 * xsize;
if( yloc < 0 || yloc >= ysize)
pptr = NULL;
for( i = 0; i < COLS && xloc[i] < 0; i++)
;
if( !(j % 2))
{
memset( idxs, 0, COLS * sizeof( int));
if( pptr)
for( ; i < COLS && xloc[i] < xsize; i++)
idxs[i] = find_in_palette( get_rgb_value( pptr + xloc[i] * 3),
calc_dither( i, j));
}
else
{
move( j / 2, i);
for( ; i < COLS && xloc[i] < xsize; i++)
{
int low_rgb = (pptr ? get_rgb_value( pptr + xloc[i] * 3) : 0);
int low_idx = find_in_palette( low_rgb, calc_dither( i, j));
#ifdef HAVE_WIDE
wchar_t bblock_char = (wchar_t)UNICODE_BBLOCK;
#endif
if( low_idx != prev_low_idx || idxs[i] != prev_idx)
{
#ifdef NCURSES_VERSION
init_pair( pair_num, low_idx, idxs[i]);
#else
init_extended_pair( pair_num, low_idx, idxs[i]);
#endif
attrset( COLOR_PAIR( pair_num));
pair_num++;
prev_low_idx = low_idx;
prev_idx = idxs[i];
}
#ifdef HAVE_WIDE
addnwstr( &bblock_char, 1);
#else
addch( ACS_BBLOCK);
#endif
}
}
}
free( xloc);
attrset( 0);
mvaddstr( LINES - 1, 0, filename_to_show);
snprintf( buff, sizeof( buff), " %d x %d pixels", xsize, ysize);
addstr( buff);
snprintf( buff, sizeof( buff), " %d color pairs used", pair_num);
addstr( buff);
if( show_help)
{
mvaddstr( LINES - 8, 0, "Home key sets default view (image fit to screen)");
mvaddstr( LINES - 7, 0, "Click mouse to pan; scroll wheel to zoom in/out");
mvaddstr( LINES - 6, 0, "Cursor keys also pan; * zooms in, / zooms out");
mvaddstr( LINES - 5, 0, "'r' rotates clockwise, 'R' CCW, 'i' inverts image");
mvaddstr( LINES - 4, 0, "'m' mirrors image left/right, 'f' flips top/bottom");
mvaddstr( LINES - 3, 0, "Any other key causes this help to appear");
show_help = FALSE;
}
refresh();
xpix1 = xpix;
ypix1 = ypix;
do
{
c = getch( );
if( c == KEY_MOUSE)
{
getmouse( &mouse_event);
xpix1 = xpix + (double)(mouse_event.x - COLS / 2) * scale;
ypix1 = ypix + (double)(mouse_event.y - LINES / 2) * scale * 2. * aspect;
if( mouse_event.bstate & REPORT_MOUSE_POSITION)
{
c = -1;
snprintf( buff, sizeof( buff), "x=%5d y=%5d", (int)xpix1, (int)ypix1);
mvaddstr( LINES - 2, 0, buff);
}
}
}
while( c == -1);
switch( c)
{
case KEY_MOUSE:
{
double rescale = 1.;
if( mouse_event.bstate & BUTTON4_PRESSED)
rescale = 1. / 1.2;
#ifdef __PDCURSES__
else if( mouse_event.bstate & BUTTON5_PRESSED)
rescale = 1.2;
#endif
else
{
xpix = xpix1;
ypix = ypix1;
}
scale *= rescale;
xpix += (xpix1 - xpix) * (1. - rescale);
ypix += (ypix1 - ypix) * (1. - rescale);
}
break;
case '*':
scale /= 1.2;
break;
case '/':
scale *= 1.2;
break;
case KEY_A1:
case KEY_HOME:
scale = 0.; /* recompute/recenter */
break;
case KEY_UP:
#ifdef KEY_A2
case KEY_A2:
#endif
ypix -= LINES * scale / 2.;
break;
case KEY_LEFT:
#ifdef KEY_B1
case KEY_B1:
#endif
xpix -= COLS * scale / 4.;
break;
case KEY_DOWN:
#ifdef KEY_C2
case KEY_C2:
#endif
ypix += LINES * scale / 2.;
break;
case KEY_RIGHT:
#ifdef KEY_B3
case KEY_B3:
#endif
xpix += COLS * scale / 4.;
break;
#ifdef KEY_RESIZE
case KEY_RESIZE:
resize_term( 0, 0);
break;
#endif
case 'r':
case 'R':
{
const int tval = xsize;
rotate_pixels_ninety_degrees( xsize, ysize, pixels);
if( c == 'R')
invert_pixels( xsize, ysize, pixels);
xsize = ysize;
ysize = tval;
scale = 0.;
}
break;
case 'i':
invert_pixels( xsize, ysize, pixels);
break;
case 'f':
mirror_pixels_top_bottom( xsize, ysize, pixels);
ypix = ysize - ypix;
break;
case 'm':
mirror_pixels_left_right( xsize, ysize, pixels);
xpix = xsize - xpix;
break;
default:
show_help = TRUE;
break;
}
}
free( pixels);
endwin();
/* Not really needed, but ensures Valgrind */
delscreen( screen_pointer); /* says all memory was freed */
return( 0);
}