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.
509 lines
15 KiB
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);
|
|
}
|