// -*- mode: c++; c-set-style: "stroustrup"; tab-width: 4; -*- // // CAppFV.c // // Copyright (C) 2004 Koji Nakamaru // // 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // #include "common.h" #include #include #include #include "CFile.h" #include "CImage.h" #include "CReaderHDR.h" #include "CReaderPFM.h" #include "CAppFV.h" #define k_app_name "fv" #define k_version "1.02" #define k_title_length (36) #define k_wait (1000000) #define k_tbl_size (65536) static void *worker(void *); static void initializeTable(); static void initializeTextures(bool is_full); static void saveAsPNG(); static void resetOrigin(); static void moveOrigin(int d); static CAppFV _app; static float _gamma = 2.2; static char *_title = "fv"; static GLubyte _tbl[k_tbl_size]; static CFile _file; static bool _is_stdin; static CReader *_readers[] = { new CReaderHDR(), new CReaderPFM(), NULL, }; static CReader *_reader; static char _magic[8]; static CImage *_image; static pthread_t _thread; static int _mx, _my; static float _x, _y; static int _zoom; static float _stops; static bool _is_flip_h, _is_flip_v; static GLint _tex_dim0; static GLint _tex_dim; static GLuint *_tex; static int _iw, _ih; static GLubyte *_img; // public functions CAppFV::CAppFV() { } CAppFV::~CAppFV() { } // protected functions static struct option _options[] = { { "gamma", 1, NULL, 'g' }, { "title", 1, NULL, 't' }, { "help", 0, NULL, 'h' }, { "version", 0, NULL, 'v' }, { NULL, 0, NULL, 0 }, }; void CAppFV::initialize( int argc, char *argv[]) { opterr = 0; optind = 1; int c; while ((c = getopt_long(argc, argv, "g:t:hv", _options, NULL)) != -1) { switch (c) { case 'g': if (sscanf(optarg, "%f", &_gamma) != 1 || _gamma < 0.05 || _gamma > 3.0) { error("the gamma value should be in [0.05, 3.0]"); } break; case 't': _title = optarg; break; case 'v': fprintf( stderr, "%s %s\n" "Copyright (C) 2004 Koji Nakamaru.\n" "This is free software; see the source for copying conditions. There is NO\n" "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n", k_app_name, k_version); exit(0); break; case 'h': default: fprintf( stderr, "%s %s\n" "Usage: %s [OPTION]... [FILE]...\n" "displays hdr/pfm images.\n" " -g, --gamma=GAMMA use GAMMA as the gamma value (default %f).\n" " -t, --title=TITLE use TITLE as the string displayed in the titlebar (default %s).\n" " -h, --help display this help and exit.\n" " -v, --version output version information and exit.\n", k_app_name, k_version, progname(), _gamma, _title); exit(1); break; } } switch (argc - optind) { case 0: _file.open(NULL); _is_stdin = true; break; case 1: if (_title == k_app_name) { _title = argv[optind]; } if (! _file.open(argv[optind])) { error("cannot open %s", argv[optind]); } _is_stdin = false; break; default: for (int i = optind; i < argc; i++) { if (access(argv[i], R_OK) == -1) { message("cannot open %s", argv[i]); continue; } char buf[64]; sprintf(buf, "%f", _gamma); run((string(argv[0]) + " -g " + buf + " -t " + argv[i] + " " + argv[i]).c_str()); } exit(0); break; } { int c; for (int i = 0; (c = _file.getc()) != EOF && i < 2; i++) { _magic[i] = c; for (int j = 0; _readers[j] != NULL; j++) { for (int k = 0; _readers[j]->magic[k] != NULL; k++) { if (strcmp(_magic, _readers[j]->magic[k]) == 0) { _reader = _readers[j]; goto end; } } } } end: if (_reader == NULL) { error("found unknown format"); } } _image = new CImage(); if (_is_stdin) { if (! _reader->initialize(&_file, _magic)) { error("failed to read data"); } } else { size_t pos = _file.tell(); while (! _reader->initialize(&_file, _magic)) { usleep(k_wait); _file.seek(pos); } } if (! _image->initialize(_reader->w(), _reader->h())) { error("cannot allocate memory"); } pthread_create(&_thread, NULL, worker, NULL); // adjust the tilte { int n0 = mbstowcs(NULL, _title, strlen(_title)); if (n0 > k_title_length) { wchar_t bufw[n0 + 1]; memset(bufw, 0, n0 + 1); mbstowcs(bufw, _title, strlen(_title)); bufw[n0 - (k_title_length - 0)] = L'.'; bufw[n0 - (k_title_length - 1)] = L'.'; bufw[n0 - (k_title_length - 2)] = L'.'; int n1 = wcstombs(NULL, &bufw[n0 - k_title_length], k_title_length); _title = new char[n1 + 1]; memset(_title, 0, n1 + 1); wcstombs(_title, &bufw[n0 - k_title_length], n1); } } glutInitWindowSize(_image->w() + 20, _image->h() + 20); // glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH); glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); glutCreateWindow(_title); // glutSetCursor(GLUT_CURSOR_CROSSHAIR); glutAddMenuEntry("Zoom In (E/I)", 'I'); glutAddMenuEntry("Zoom Out (W/O)", 'O'); glutAddMenuEntry("Reset Zoom (Q/P)", 'P'); glutAddMenuEntry("Expose With the Current Pixel (F/J)", 'J'); glutAddMenuEntry("Expose Automatically (B)", 'B'); glutAddMenuEntry("Expose Up (D/K)", 'K'); glutAddMenuEntry("Expose Down (S/L)", 'L'); glutAddMenuEntry("Reset Exposure Scale (A/;)", ';'); glutAddMenuEntry("Flip Horizontally (C/M)", 'M'); glutAddMenuEntry("Flip Vertically (V/N)", 'N'); glutAddMenuEntry("Reset Origin (Space)", ' '); glutAddMenuEntry("Save as PNG (Enter)", '\r'); glHint(GL_POLYGON_SMOOTH_HINT, GL_FASTEST); glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST); glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 0.02); glDisable(GL_LIGHTING); glDisable(GL_LIGHT0); GLfloat background[] = { 0.75f, 0.75f, 0.75f, 1.0f }; glClearColor(background[0], background[1], background[2], background[3]); glGetIntegerv(GL_MAX_TEXTURE_SIZE, &_tex_dim0); if (256 < _tex_dim0) { _tex_dim0 = 256; } _tex_dim = _tex_dim0 - 2; initializeTable(); _img = new GLubyte[_tex_dim0 * _tex_dim0 * 4]; memset(_img, 255, _tex_dim0 * _tex_dim0 * 4); _iw = (_image->w() + (_tex_dim - 1)) / _tex_dim; _ih = (_image->h() + (_tex_dim - 1)) / _tex_dim; _tex = new GLuint[_iw * _ih]; glGenTextures(_iw * _ih, _tex); initializeTextures(true); startTimer(k_wait / 1000); } void CAppFV::finalize() { CApp::finalize(); } void CAppFV::display() { glViewport(0, 0, _view.w, _view.h); // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); float zoom = powf(2.0, _zoom); // NOTE: we limit all coordinates to be intergers for avoiding // numerical errors in texture mapping. glTranslatef((int)(_x * zoom), (int)(_y * zoom), 0.0f); glScalef((_is_flip_h) ? -1.0f : 1.0f, (_is_flip_v) ? -1.0f : 1.0f, 1.0f); const float k_min_xy = 1.0f / _tex_dim0; const float k_max_xy = 1.0f - 1.0f / _tex_dim0; int w2 = _image->w() / 2; int h2 = _image->h() / 2; for (int y = 0; y < _image->h(); y += _tex_dim) { for (int x = 0; x < _image->w(); x += _tex_dim) { glBindTexture(GL_TEXTURE_2D, _tex[y / _tex_dim * _iw + x / _tex_dim]); glBegin(GL_QUADS); glTexCoord3f(k_min_xy, k_min_xy, 0.0f); glVertex3f( (int)((x - w2) * zoom), (int)((y - h2) * -zoom), 0.0f); glTexCoord3f(k_min_xy, k_max_xy, 0.0f); glVertex3f( (int)((x - w2) * zoom), (int)((y - h2 + _tex_dim) * -zoom), 0.0f); glTexCoord3f(k_max_xy, k_max_xy, 0.0f); glVertex3f( (int)((x - w2 + _tex_dim) * zoom), (int)((y - h2 + _tex_dim) * -zoom), 0.0f); glTexCoord3f(k_max_xy, k_min_xy, 0.0f); glVertex3f( (int)((x - w2 + _tex_dim) * zoom), (int)((y - h2) * -zoom), 0.0f); glEnd(); } } glutSwapBuffers(); } void CAppFV::reshape( int width, int height) { // NOTE: we limit all coordinates to be integers for avoiding // numerical errors in texture mapping. glMatrixMode(GL_PROJECTION); glLoadIdentity(); int w = width / 2; int h = height / 2; gluOrtho2D(-w, width - w, -h, height - h); glMatrixMode(GL_MODELVIEW); glutPostRedisplay(); } void CAppFV::menu( int value) { key((unsigned char)value, _mx, _my); } void CAppFV::key( unsigned char key, int x, int y) { switch (toupper(key)) { case 'E': case 'I': if (_zoom < 7) { _zoom++; } break; case 'W': case 'O': if (_zoom > -7) { _zoom--; } break; case 'Q': case 'P': _zoom = 0; break; case 'F': case 'J': { getPixelPosition(&x, &y); float *pixel = _image->pixel(x, y); double gray = pixel[0] * 0.30 + pixel[1] * 0.59 + pixel[2] * 0.11; if (gray < 1e-3) { gray = 1e-3; } _stops = log(0.5 / gray) / log(1.2); initializeTextures(false); } break; case 'B': { // NOTE: we avoid "fully zero" pixels in determining the // log-average luminance. this approach is better than // adding a small delta. float *pixel = _image->pixel(0, 0); double avg = 0.0; int count = 0; for (int y = 0; y < _image->h(); y++) { for (int x = 0; x < _image->w(); x++) { double gray = pixel[0] * 0.30 + pixel[1] * 0.59 + pixel[2] * 0.11; if (gray > 1e-6) { avg += log(gray); count++; } pixel += 4; } } if (count > 0) { if ((avg = exp(avg / count)) < 1e-3) { avg = 1e-3; } _stops = log(0.18 / avg) / log(1.2); initializeTextures(false); } } break; case 'D': case 'K': _stops += 1.0; initializeTextures(false); break; case 'S': case 'L': _stops -= 1.0; initializeTextures(false); break; case 'A': case ';': _stops = 0.0; initializeTextures(false); break; case 'C': case 'M': _is_flip_h = ! _is_flip_h; break; case 'V': case 'N': _is_flip_v = ! _is_flip_v; break; case ' ': resetOrigin(); break; case '\r': saveAsPNG(); break; case 'Z': default: CApp::key(key, x, y); return; } passive(_mx, _my); glutPostRedisplay(); } void CAppFV::special( int key, int x, int y) { CApp::special(key, x, y); switch (key) { case GLUT_KEY_LEFT: moveOrigin(0); break; case GLUT_KEY_RIGHT: moveOrigin(1); break; case GLUT_KEY_DOWN: moveOrigin(2); break; case GLUT_KEY_UP: moveOrigin(3); break; } passive(_mx, _my); glutPostRedisplay(); } void CAppFV::mouse( int button, int state, int x, int y) { CApp::mouse(button, state, x, y); _mx = x; _my = y; } void CAppFV::drag( int x, int y) { CApp::drag(x, y); float zoom = powf(2.0, _zoom); _x += (x - _mx) / zoom; _y -= (y - _my) / zoom; _mx = x; _my = y; glutPostRedisplay(); } void CAppFV::passive( int x, int y) { CApp::passive(x, y); _mx = x; _my = y; getPixelPosition(&x, &y); float zoom = powf(2.0, _zoom); float scale = powf(1.2f, _stops); float *pixel = _image->pixel(x, y); char buf[1024]; sprintf( buf, "%s P(%5d, %5d) C(%6.3f, %6.3f, %6.3f, %6.3f) Z(%.3f) S(%.4f)", _title, x, y, pixel[0], pixel[1], pixel[2], pixel[3], zoom, scale); glutSetWindowTitle(buf); } void CAppFV::timer( unsigned dmillis) { _image->lock(); if (_image->isChanged()) { _image->setChanged(false); initializeTextures(false); glutPostRedisplay(); } _image->unlock(); } void CAppFV::getPixelPosition( int *x, int *y) { float zoom = powf(2.0, _zoom); int w = _view.w / 2; int h = _view.h / 2; int x0 = (int)(_x * zoom) + (int)(-(_image->w() / 2) * zoom) + w; int y0 = (int)(-_y * zoom) + (int)(-(_image->h() / 2) * zoom) + h; *x = clamp((int)((*x - x0) / zoom), 0, _image->w() - 1); *y = clamp((int)((*y - y0) / zoom), 0, _image->h() - 1); if (_is_flip_h) { *x = _image->w() - 1 - *x; } if (_is_flip_v) { *y = _image->h() - 1 - *y; } } // private functions // local functions static void *worker( void *) { if (_is_stdin) { _image->clear(); _reader->read(&_file, _image); } else { size_t pos = _file.tell(); struct stat stbuf0, stbuf; if (! _file.stat(&stbuf0)) { error("failed to read data"); } _image->clear(); _file.seek(pos); _reader->read(&_file, _image); for (;;) { usleep(k_wait); if (! _file.stat(&stbuf)) { error("failed to read data"); } if (stbuf0.st_mtime != stbuf.st_mtime) { _image->clear(); _file.seek(pos); _reader->read(&_file, _image); } stbuf0 = stbuf; } } return NULL; } static void initializeTable() { double inv_gamma = 1.0 / _gamma; for (int i = 0; i < 65536; i++) { _tbl[i] = (GLubyte)(pow(i / 65536.0, inv_gamma) * 255.0); } } static void initializeTextures( bool is_full) { float scale = powf(1.2f, _stops) * (k_tbl_size - 1); for (int y = 0; y < _image->h(); y += _tex_dim) { int h = _image->h() - y; if (h > _tex_dim) { h = _tex_dim; } for (int x = 0; x < _image->w(); x += _tex_dim) { int w = _image->w() - x; if (w > _tex_dim) { w = _tex_dim; } for (int j = 0; j < h; j++) { float *o = _image->pixel(x, y + j); GLubyte *p = &_img[((j + 1) * _tex_dim0 + 1) * 4]; for (int i = 0; i < w; i++) { *p++ = _tbl[(int)clamp(*o++ * scale, 0.0f, (float)(k_tbl_size - 1))]; *p++ = _tbl[(int)clamp(*o++ * scale, 0.0f, (float)(k_tbl_size - 1))]; *p++ = _tbl[(int)clamp(*o++ * scale, 0.0f, (float)(k_tbl_size - 1))]; *p++ = (GLubyte)(*o++ * 255.0f); } for (int i = w; i < _tex_dim; i++) { *p++ = 255; *p++ = 255; *p++ = 255; *p++ = 0; } } for (int j = h; j < _tex_dim; j++) { GLubyte *p = &_img[((j + 1) * _tex_dim0 + 1) * 4]; for (int i = 0; i < _tex_dim; i++) { *p++ = 255; *p++ = 255; *p++ = 255; *p++ = 0; } } // fill edges: top and its corners { GLuint *p = (GLuint *)_img + (1 * _tex_dim0 + 1); GLuint *q = p - _tex_dim0 - 1; *q++ = *p; for (int i = 0; i < _tex_dim; i++) { *q++ = *p++; } *q++ = *p; } // fill edges: bottom and its corners { GLuint *p = (GLuint *)_img + ((_tex_dim0 - 2) * _tex_dim0 + 1); GLuint *q = p + _tex_dim0 - 1; *q++ = *p; for (int i = 0; i < _tex_dim; i++) { *q++ = *p++; } *q++ = *p; } // fill edges: left { GLuint *p = (GLuint *)_img + _tex_dim0 + 1; GLuint *q = p - 1; for (int j = 0; j < _tex_dim; j++) { *q = *p; p += _tex_dim0; q += _tex_dim0; } } // fill edges: right { GLuint *p = (GLuint *)_img + _tex_dim0 + _tex_dim0 - 2; GLuint *q = p + 1; for (int j = 0; j < _tex_dim; j++) { *q = *p; p += _tex_dim0; q += _tex_dim0; } } glBindTexture(GL_TEXTURE_2D, _tex[y / _tex_dim * _iw + x / _tex_dim]); if (is_full) { glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D( GL_TEXTURE_2D, 0, 4, _tex_dim0, _tex_dim0, 0, GL_RGBA, GL_UNSIGNED_BYTE, _img); } else { glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, _tex_dim0, _tex_dim0, GL_RGBA, GL_UNSIGNED_BYTE, _img); } } } } static void saveAsPNG() { char name[128]; { time_t t = time(NULL); struct tm tl = *localtime(&t); sprintf( name, "%04d%02d%02d-%02d%02d%02d-" k_app_name ".png", tl.tm_year + 1900, tl.tm_mon + 1, tl.tm_mday, tl.tm_hour, tl.tm_min, tl.tm_sec); } png_structp png_ptr = NULL; png_infop info_ptr = NULL; png_bytep row = NULL; FILE *fp = NULL; if ((fp = fopen(name, "w")) == NULL || (png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL || (info_ptr = png_create_info_struct(png_ptr)) == NULL || (row = new png_byte[sizeof(png_byte) * 4 * _image->w()]) == NULL) { goto err; } if (setjmp(png_ptr->jmpbuf) != 0) { goto err; } png_init_io(png_ptr, fp); png_set_IHDR( png_ptr, info_ptr, _image->w(), _image->h(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(png_ptr, info_ptr); { float scale = powf(1.2f, _stops) * (k_tbl_size - 1); for (int y = 0; y < _image->h(); y++) { png_byte *p = row; for (int x = 0; x < _image->w(); x++) { int ix, iy; ix = (_is_flip_h) ? _image->w() - 1 - x : x; iy = (_is_flip_v) ? _image->h() - 1 - y : y; float *o = _image->pixel(ix, iy); *p++ = _tbl[(int)clamp(*o++ * scale, 0.0f, (float)(k_tbl_size - 1))]; *p++ = _tbl[(int)clamp(*o++ * scale, 0.0f, (float)(k_tbl_size - 1))]; *p++ = _tbl[(int)clamp(*o++ * scale, 0.0f, (float)(k_tbl_size - 1))]; *p++ = (GLubyte)(*o++ * 255.0f); } png_write_row(png_ptr, row); } } png_write_end(png_ptr, info_ptr); png_destroy_write_struct(&png_ptr, &info_ptr); forgetArray(&row); forgetFILE(&fp); return; err: png_destroy_write_struct(&png_ptr, &info_ptr); forgetArray(&row); forgetFILE(&fp); return; } static void resetOrigin() { _x = 0.0f; _y = 0.0f; } static void moveOrigin( int d) { float zoom = powf(2.0, _zoom); float offset = 16.0f / zoom; switch (d) { case 0: _x -= offset; break; case 1: _x += offset; break; case 2: _y -= offset; break; case 3: _y += offset; break; } }