blob: 9e334741990f6e4c205e9a8ea5097cf860edbcb6 [file] [log] [blame]
// Copyright 2020 The Wuffs Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------
/*
imageviewer is a simple GUI program for viewing images. On Linux, GUI means
X11. To run:
$CXX imageviewer.cc -lxcb -lxcb-image -lxcb-render -lxcb-render-util && \
./a.out ../../test/data/bricks-*.gif; rm -f a.out
for a C++ compiler $CXX, such as clang++ or g++.
The Space and BackSpace keys cycle through the files, if more than one was
given as command line arguments. If none were given, the program reads from
stdin.
The Return key is equivalent to the Space key.
The ',' Comma and '.' Period keys cycle through background colors, which
matters if the image has fully or partially transparent pixels.
The '1' to '8' keys change the magnification zoom (or minification zoom with
the shift key). The '0' key toggles nearest neighbor and bilinear filtering.
The Escape key quits.
*/
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
// Wuffs ships as a "single file C library" or "header file library" as per
// https://github.com/nothings/stb/blob/master/docs/stb_howto.txt
//
// To use that single file as a "foo.c"-like implementation, instead of a
// "foo.h"-like header, #define WUFFS_IMPLEMENTATION before #include'ing or
// compiling it.
#define WUFFS_IMPLEMENTATION
// Defining the WUFFS_CONFIG__STATIC_FUNCTIONS macro is optional, but when
// combined with WUFFS_IMPLEMENTATION, it demonstrates making all of Wuffs'
// functions have static storage.
//
// This can help the compiler ignore or discard unused code, which can produce
// faster compiles and smaller binaries. Other motivations are discussed in the
// "ALLOW STATIC IMPLEMENTATION" section of
// https://raw.githubusercontent.com/nothings/stb/master/docs/stb_howto.txt
#define WUFFS_CONFIG__STATIC_FUNCTIONS
// Defining the WUFFS_CONFIG__MODULE* macros are optional, but it lets users of
// release/c/etc.c choose which parts of Wuffs to build. That file contains the
// entire Wuffs standard library, implementing a variety of codecs and file
// formats. Without this macro definition, an optimizing compiler or linker may
// very well discard Wuffs code for unused codecs, but listing the Wuffs
// modules we use makes that process explicit. Preprocessing means that such
// code simply isn't compiled.
#define WUFFS_CONFIG__MODULES
#define WUFFS_CONFIG__MODULE__ADLER32
#define WUFFS_CONFIG__MODULE__AUX__BASE
#define WUFFS_CONFIG__MODULE__AUX__IMAGE
#define WUFFS_CONFIG__MODULE__BASE
#define WUFFS_CONFIG__MODULE__BMP
#define WUFFS_CONFIG__MODULE__CRC32
#define WUFFS_CONFIG__MODULE__DEFLATE
#define WUFFS_CONFIG__MODULE__GIF
#define WUFFS_CONFIG__MODULE__LZW
#define WUFFS_CONFIG__MODULE__NIE
#define WUFFS_CONFIG__MODULE__PNG
#define WUFFS_CONFIG__MODULE__TGA
#define WUFFS_CONFIG__MODULE__WBMP
#define WUFFS_CONFIG__MODULE__ZLIB
// If building this program in an environment that doesn't easily accommodate
// relative includes, you can use the script/inline-c-relative-includes.go
// program to generate a stand-alone C file.
#include "../../release/c/wuffs-unsupported-snapshot.c"
// X11 limits its image dimensions to uint16_t and some coordinates to int16_t.
#define MAX_INCL_DIMENSION 32767
#define NUM_BACKGROUND_COLORS 3
#define NUM_ZOOMS 8
#define SRC_BUFFER_ARRAY_SIZE (64 * 1024)
wuffs_base__color_u32_argb_premul g_background_colors[NUM_BACKGROUND_COLORS] = {
0xFF000000,
0xFFFFFFFF,
0xFFA9009A,
};
uint32_t g_width = 0;
uint32_t g_height = 0;
wuffs_aux::MemOwner g_pixbuf_mem_owner(nullptr, &free);
wuffs_base__pixel_buffer g_pixbuf = {0};
uint32_t g_background_color_index = 0;
int32_t g_zoom = 0;
bool g_filter = false;
struct {
int remaining_argc;
char** remaining_argv;
double screen_gamma;
} g_flags = {0};
static const char* g_usage =
"Usage: imageviewer -flags input0.gif input1.png\n"
"\n"
"Flags:\n"
" -screen_gamma=N.N (default 2.2; 0 disables gamma correction)";
const char* //
parse_flags(int argc, char** argv) {
g_flags.screen_gamma = 2.2;
int c = (argc > 0) ? 1 : 0; // Skip argv[0], the program name.
for (; c < argc; c++) {
char* arg = argv[c];
if (*arg++ != '-') {
break;
}
// A double-dash "--foo" is equivalent to a single-dash "-foo". As special
// cases, a bare "-" is not a flag (some programs may interpret it as
// stdin) and a bare "--" means to stop parsing flags.
if (*arg == '\x00') {
break;
} else if (*arg == '-') {
arg++;
if (*arg == '\x00') {
c++;
break;
}
}
if (!strncmp(arg, "screen_gamma=", 13)) {
g_flags.screen_gamma = atof(arg + 13);
continue;
}
return g_usage;
}
g_flags.remaining_argc = argc - c;
g_flags.remaining_argv = argv + c;
return NULL;
}
class MyDecodeImageCallbacks : public wuffs_aux::DecodeImageCallbacks {
public:
MyDecodeImageCallbacks() : m_combined_gamma(1.0) {}
private:
std::string //
HandleMetadata(const wuffs_base__more_information& minfo,
wuffs_base__slice_u8 raw) override {
if (minfo.flavor == WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED) {
switch (minfo.metadata__fourcc()) {
case WUFFS_BASE__FOURCC__GAMA:
// metadata_parsed__gama returns the inverse gamma scaled by 1e5.
m_combined_gamma =
1e5 / (g_flags.screen_gamma * minfo.metadata_parsed__gama());
break;
}
}
return wuffs_aux::DecodeImageCallbacks::HandleMetadata(minfo, raw);
}
void //
Done(wuffs_aux::DecodeImageResult& result,
wuffs_aux::sync_io::Input& input,
wuffs_aux::IOBuffer& buffer,
wuffs_base__image_decoder::unique_ptr image_decoder) override {
// Apply basic color correction - gamma correction. Proper color correction
// should also involve considering any CHRM, ICCP and SRGB metadata but
// that requires a non-trivial amount of code (such as skcms, Skia's Color
// Management System). To keep this example program simple, we only
// consider GAMA metadata (and 8-bit color channels) here.
//
// This code also assumes that wuffs_aux::DecodeImageCallbacks defaults to
// producing a WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL pixel buffer.
if ((result.pixbuf.pixel_format().repr ==
WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL) &&
((m_combined_gamma < 0.9999) || (1.0001 < m_combined_gamma))) {
uint8_t lut[256];
lut[0x00] = 0x00;
lut[0xFF] = 0xFF;
for (uint32_t i = 1; i < 0xFF; i++) {
lut[i] =
(uint8_t)(floor(255.0 * pow(i / 255.0, m_combined_gamma) + 0.5));
}
wuffs_base__table_u8 t = result.pixbuf.plane(0);
size_t w4 = t.width / 4;
for (size_t y = 0; y < t.height; y++) {
uint8_t* ptr = t.ptr + (y * t.stride);
for (size_t x = 0; x < w4; x++) {
ptr[0] = lut[ptr[0]];
ptr[1] = lut[ptr[1]];
ptr[2] = lut[ptr[2]];
ptr += 4;
}
}
}
}
// m_combined_gamma holds the product of the screen gamma and the image
// file's inverse gamma.
double m_combined_gamma;
};
bool //
load_image(const char* filename) {
FILE* file = stdin;
const char* adj_filename = "<stdin>";
if (filename) {
FILE* f = fopen(filename, "rb");
if (f == NULL) {
printf("%s: could not open file\n", filename);
return false;
}
file = f;
adj_filename = filename;
}
g_width = 0;
g_height = 0;
g_pixbuf_mem_owner.reset();
g_pixbuf = wuffs_base__null_pixel_buffer();
uint64_t dia_flags = 0;
if (g_flags.screen_gamma > 0) {
dia_flags |= wuffs_aux::DecodeImageArgFlags::REPORT_METADATA_GAMA;
}
MyDecodeImageCallbacks callbacks;
wuffs_aux::sync_io::FileInput input(file);
wuffs_aux::DecodeImageResult res = wuffs_aux::DecodeImage(
callbacks, input, wuffs_aux::DecodeImageArgQuirks::DefaultValue(),
wuffs_aux::DecodeImageArgFlags(dia_flags),
// Use PIXEL_BLEND__SRC_OVER, not the default PIXEL_BLEND__SRC, because
// we also pass a background color.
wuffs_aux::DecodeImageArgPixelBlend(WUFFS_BASE__PIXEL_BLEND__SRC_OVER),
wuffs_aux::DecodeImageArgBackgroundColor(
g_background_colors[g_background_color_index]),
wuffs_aux::DecodeImageArgMaxInclDimension(MAX_INCL_DIMENSION));
if (filename) {
fclose(file);
}
// wuffs_aux::DecodeImageCallbacks's default implementation should give us an
// interleaved (not multi-planar) pixel buffer, so that all of the pixel data
// is in a single 2-dimensional table (plane 0). Later on, we re-interpret
// that table as XCB image data, which isn't something we could do if we had
// e.g. multi-planar YCbCr.
if (!res.pixbuf.pixcfg.pixel_format().is_interleaved()) {
printf("%s: non-interleaved pixbuf\n", adj_filename);
return false;
}
wuffs_base__table_u8 tab = res.pixbuf.plane(0);
if (tab.width != tab.stride) {
// The xcb_image_create_native call, later on, assumes that (tab.height *
// tab.stride) bytes are readable, which isn't quite the same as what
// wuffs_base__table__flattened_length(tab.width, tab.height, tab.stride)
// returns unless the table is tight (its width equals its stride).
printf("%s: could not allocate tight pixbuf\n", adj_filename);
return false;
}
g_width = res.pixbuf.pixcfg.width();
g_height = res.pixbuf.pixcfg.height();
g_pixbuf_mem_owner = std::move(res.pixbuf_mem_owner);
g_pixbuf = res.pixbuf;
if (res.error_message.empty()) {
printf("%s: ok (%" PRIu32 " x %" PRIu32 ")\n", adj_filename, g_width,
g_height);
} else {
printf("%s: %s\n", adj_filename, res.error_message.c_str());
}
return res.pixbuf.pixcfg.is_valid();
}
// ---------------------------------------------------------------------
#if defined(__linux__)
#define SUPPORTED_OPERATING_SYSTEM
#include <xcb/render.h>
#include <xcb/xcb.h>
#include <xcb/xcb_image.h>
#include <xcb/xcb_renderutil.h>
#define XK_BackSpace 0xFF08
#define XK_Escape 0xFF1B
#define XK_Return 0xFF0D
uint32_t g_maximum_request_length = 0; // Measured in 4-byte units.
xcb_atom_t g_atom_net_wm_name = XCB_NONE;
xcb_atom_t g_atom_utf8_string = XCB_NONE;
xcb_atom_t g_atom_wm_protocols = XCB_NONE;
xcb_atom_t g_atom_wm_delete_window = XCB_NONE;
xcb_pixmap_t g_pixmap = XCB_NONE;
xcb_gcontext_t g_pixmap_gc = XCB_NONE;
xcb_render_picture_t g_pixmap_picture = XCB_NONE;
xcb_render_pictforminfo_t* g_pictforminfo = NULL;
xcb_keysym_t* g_keysyms = NULL;
xcb_get_keyboard_mapping_reply_t* g_keyboard_mapping = NULL;
void //
init_keymap(xcb_connection_t* c, const xcb_setup_t* z) {
xcb_get_keyboard_mapping_cookie_t cookie = xcb_get_keyboard_mapping(
c, z->min_keycode, z->max_keycode - z->min_keycode + 1);
g_keyboard_mapping = xcb_get_keyboard_mapping_reply(c, cookie, NULL);
g_keysyms = (xcb_keysym_t*)(g_keyboard_mapping + 1);
}
xcb_window_t //
make_window(xcb_connection_t* c, xcb_screen_t* s) {
xcb_window_t w = xcb_generate_id(c);
uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
uint32_t value_list[2];
value_list[0] = s->black_pixel;
value_list[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS;
xcb_create_window(c, 0, w, s->root, 0, 0, 1024, 768, 0,
XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, value_mask,
value_list);
xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, g_atom_net_wm_name,
g_atom_utf8_string, 8, 12, "Image Viewer");
xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, g_atom_wm_protocols,
XCB_ATOM_ATOM, 32, 1, &g_atom_wm_delete_window);
xcb_map_window(c, w);
return w;
}
void //
apply_zoom_and_filter(xcb_connection_t* c) {
static const xcb_render_fixed_t neg_zooms[NUM_ZOOMS] = {
0x00010000, // 1/1 as 16.16 fixed point
0x00020000, // 2/1
0x00040000, // 4/1
0x00080000, // 8/1
0x00100000, // 16/1
0x00200000, // 32/1
0x00400000, // 64/1
0x00800000, // 128/1
};
static const xcb_render_fixed_t pos_zooms[NUM_ZOOMS] = {
0x00010000, // 1/1 as 16.16 fixed point
0x00008000, // 1/2
0x00004000, // 1/4
0x00002000, // 1/8
0x00001000, // 1/16
0x00000800, // 1/32
0x00000400, // 1/64
0x00000200, // 1/128
};
xcb_render_fixed_t z = g_zoom < 0
? neg_zooms[((uint32_t)(-g_zoom)) % NUM_ZOOMS]
: pos_zooms[((uint32_t)(+g_zoom)) % NUM_ZOOMS];
xcb_render_set_picture_transform(c, g_pixmap_picture,
((xcb_render_transform_t){
z, 0, 0, //
0, z, 0, //
0, 0, 0x10000, //
}));
uint16_t f_len = 7;
const char* f_ptr = "nearest";
if (g_filter && (g_zoom != 0)) {
f_len = 8;
f_ptr = "bilinear";
}
xcb_render_set_picture_filter(c, g_pixmap_picture, f_len, f_ptr, 0, NULL);
}
// zoom_shift returns (a << g_zoom), roughly speaking, but saturates at an
// arbitrary value called M.
//
// The final two arguments to xcb_render_composite have uint16_t type (and
// UINT16_MAX is 65535), but in practice, values above M sometimes don't work
// in that the xcb_render_composite call has no visible effect.
//
// Some xrender debugging could potentially derive a more accurate maximum but
// for now, the M = 30000 round number will do.
uint16_t //
zoom_shift(uint32_t a) {
uint16_t M = 30000;
uint64_t b = g_zoom < 0
? (((uint64_t)a) >> (((uint32_t)(-g_zoom)) % NUM_ZOOMS))
: (((uint64_t)a) << (((uint32_t)(+g_zoom)) % NUM_ZOOMS));
return (b < M) ? b : M;
}
bool //
load(xcb_connection_t* c, xcb_window_t w, const char* filename) {
if (g_pixmap != XCB_NONE) {
xcb_render_free_picture(c, g_pixmap_picture);
xcb_free_gc(c, g_pixmap_gc);
xcb_free_pixmap(c, g_pixmap);
}
if (!load_image(filename)) {
return false;
}
wuffs_base__table_u8 tab = g_pixbuf.plane(0);
xcb_create_pixmap(c, g_pictforminfo->depth, g_pixmap, w, g_width, g_height);
xcb_create_gc(c, g_pixmap_gc, g_pixmap, 0, NULL);
xcb_render_create_picture(c, g_pixmap_picture, g_pixmap, g_pictforminfo->id,
0, NULL);
apply_zoom_and_filter(c);
// Copy the pixels from the X11 client process (this process) to the X11
// server process. For large images, this may involve multiple xcb_image_put
// calls, each copying part of the pixels (a strip that has the same width
// but smaller height), to avoid XCB_CONN_CLOSED_REQ_LEN_EXCEED.
if (g_width > 0) {
uint32_t max_strip_height = g_maximum_request_length / g_width;
for (uint32_t y = 0; y < g_height;) {
uint32_t h = g_height - y;
if (h > max_strip_height) {
h = max_strip_height;
}
// Make libxcb-image interpret WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL as
// XCB_PICT_STANDARD_ARGB_32 with byte_order XCB_IMAGE_ORDER_LSB_FIRST.
xcb_image_t* unconverted =
xcb_image_create(g_width, // width
h, // height
XCB_IMAGE_FORMAT_Z_PIXMAP, // format
32, // xpad
g_pictforminfo->depth, // depth
32, // bpp
32, // unit
XCB_IMAGE_ORDER_LSB_FIRST, // byte_order
XCB_IMAGE_ORDER_MSB_FIRST, // bit_order
NULL, // base
h * tab.stride, // bytes
tab.ptr + (y * tab.stride)); // data
xcb_image_t* converted =
xcb_image_native(c, unconverted, true); // true means to convert.
if (converted != unconverted) {
xcb_image_destroy(unconverted);
}
xcb_image_put(c, g_pixmap, g_pixmap_gc, converted, 0, y, 0);
xcb_image_destroy(converted);
y += h;
}
}
return true;
}
int //
main(int argc, char** argv) {
const char* status = parse_flags(argc, argv);
if (status) {
fprintf(stderr, "%s\n", status);
return 1;
}
xcb_connection_t* c = xcb_connect(NULL, NULL);
g_maximum_request_length = xcb_get_maximum_request_length(c);
// Our X11 requests (especially xcb_image_put) also need a header, in terms
// of wire format. 256 4-byte units should be big enough.
const uint32_t max_req_len_adjustment = 256;
if (g_maximum_request_length < max_req_len_adjustment) {
printf("XCB failure (maximum request length is too short)\n");
exit(EXIT_FAILURE);
}
g_maximum_request_length -= max_req_len_adjustment;
const xcb_setup_t* z = xcb_get_setup(c);
xcb_screen_t* s = xcb_setup_roots_iterator(z).data;
const xcb_render_query_pict_formats_reply_t* pict_formats =
xcb_render_util_query_formats(c);
g_pictforminfo = xcb_render_util_find_standard_format(
pict_formats, XCB_PICT_STANDARD_ARGB_32);
{
xcb_intern_atom_cookie_t cookie0 =
xcb_intern_atom(c, 1, 12, "_NET_WM_NAME");
xcb_intern_atom_cookie_t cookie1 = xcb_intern_atom(c, 1, 11, "UTF8_STRING");
xcb_intern_atom_cookie_t cookie2 =
xcb_intern_atom(c, 1, 12, "WM_PROTOCOLS");
xcb_intern_atom_cookie_t cookie3 =
xcb_intern_atom(c, 1, 16, "WM_DELETE_WINDOW");
xcb_intern_atom_reply_t* reply0 = xcb_intern_atom_reply(c, cookie0, NULL);
xcb_intern_atom_reply_t* reply1 = xcb_intern_atom_reply(c, cookie1, NULL);
xcb_intern_atom_reply_t* reply2 = xcb_intern_atom_reply(c, cookie2, NULL);
xcb_intern_atom_reply_t* reply3 = xcb_intern_atom_reply(c, cookie3, NULL);
g_atom_net_wm_name = reply0->atom;
g_atom_utf8_string = reply1->atom;
g_atom_wm_protocols = reply2->atom;
g_atom_wm_delete_window = reply3->atom;
free(reply0);
free(reply1);
free(reply2);
free(reply3);
}
xcb_window_t w = make_window(c, s);
xcb_render_picture_t p = xcb_generate_id(c);
xcb_render_create_picture(
c, p, w,
xcb_render_util_find_visual_format(pict_formats, s->root_visual)->format,
0, NULL);
init_keymap(c, z);
xcb_flush(c);
g_pixmap = xcb_generate_id(c);
g_pixmap_gc = xcb_generate_id(c);
g_pixmap_picture = xcb_generate_id(c);
bool loaded = load(
c, w, (g_flags.remaining_argc > 0) ? g_flags.remaining_argv[0] : NULL);
int arg = 0;
while (true) {
xcb_generic_event_t* event = xcb_wait_for_event(c);
if (!event) {
printf("XCB failure (error code %d)\n", xcb_connection_has_error(c));
exit(EXIT_FAILURE);
}
bool reload = false;
switch (event->response_type & 0x7F) {
case XCB_EXPOSE: {
xcb_expose_event_t* e = (xcb_expose_event_t*)event;
if (loaded && (e->count == 0)) {
xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, g_pixmap_picture,
XCB_NONE, p, 0, 0, 0, 0, 0, 0,
zoom_shift(g_width), zoom_shift(g_height));
xcb_flush(c);
}
break;
}
case XCB_KEY_PRESS: {
xcb_key_press_event_t* e = (xcb_key_press_event_t*)event;
uint32_t i = e->detail;
if ((z->min_keycode <= i) && (i <= z->max_keycode)) {
i = g_keysyms[(i - z->min_keycode) *
g_keyboard_mapping->keysyms_per_keycode];
switch (i) {
case XK_Escape:
return 0;
case ' ':
case XK_BackSpace:
case XK_Return:
if (g_flags.remaining_argc <= 1) {
break;
}
arg += (i != XK_BackSpace) ? +1 : -1;
if (arg < 0) {
arg = g_flags.remaining_argc - 1;
} else if (arg == g_flags.remaining_argc) {
arg = 0;
}
reload = true;
break;
case ',':
case '.':
g_background_color_index +=
(i == ',') ? (NUM_BACKGROUND_COLORS - 1) : 1;
g_background_color_index %= NUM_BACKGROUND_COLORS;
reload = true;
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
if (i == '0') {
g_filter = !g_filter;
} else {
int32_t z = i - '1';
if (e->state & XCB_MOD_MASK_SHIFT) {
z = -z;
}
if (g_zoom == z) {
break;
}
g_zoom = z;
}
apply_zoom_and_filter(c);
xcb_clear_area(c, 1, w, 0, 0, 0xFFFF, 0xFFFF);
xcb_flush(c);
break;
}
}
break;
}
case XCB_CLIENT_MESSAGE: {
xcb_client_message_event_t* e = (xcb_client_message_event_t*)event;
if (e->data.data32[0] == g_atom_wm_delete_window) {
return 0;
}
break;
}
}
free(event);
if (reload) {
loaded = load(c, w, g_flags.remaining_argv[arg]);
xcb_clear_area(c, 1, w, 0, 0, 0xFFFF, 0xFFFF);
xcb_flush(c);
}
}
return 0;
}
#endif // defined(__linux__)
// ---------------------------------------------------------------------
#if !defined(SUPPORTED_OPERATING_SYSTEM)
int //
main(int argc, char** argv) {
printf("unsupported operating system\n");
return 1;
}
#endif // !defined(SUPPORTED_OPERATING_SYSTEM)