blob: 5fc8b043b08d7b8f463ed5fbb9136af5bef06ca8 [file] [log] [blame]
Nigel Tao8f1fe432020-01-15 14:22:48 +11001// Copyright 2020 The Wuffs Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// ----------------
16
17/*
18imageviewer is a simple GUI program for viewing images. On Linux, GUI means
19X11. To run:
20
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +020021$CXX imageviewer.cc -lxcb -lxcb-image -lxcb-render -lxcb-render-util && \
Nigel Tao8f1fe432020-01-15 14:22:48 +110022 ./a.out ../../test/data/bricks-*.gif; rm -f a.out
23
Nigel Taoccee2072021-02-27 22:40:32 +110024for a C++ compiler $CXX, such as clang++ or g++.
Nigel Tao8f1fe432020-01-15 14:22:48 +110025
26The Space and BackSpace keys cycle through the files, if more than one was
27given as command line arguments. If none were given, the program reads from
28stdin.
29
30The Return key is equivalent to the Space key.
31
Nigel Tao8eaa2222021-10-03 16:42:58 +110032The ',' Comma and '.' Period keys cycle through background colors, which
33matters if the image has fully or partially transparent pixels.
34
Nigel Tao90d32322021-10-07 12:59:38 +110035The '1' to '8' keys change the magnification zoom (or minification zoom with
36the shift key). The '0' key toggles nearest neighbor and bilinear filtering.
Nigel Tao8eaa2222021-10-03 16:42:58 +110037
Nigel Tao4c5d3132022-05-31 12:25:08 +100038The arrow keys (or hjkl keys) scroll the image. The '`' key resets.
39
Nigel Tao8f1fe432020-01-15 14:22:48 +110040The Escape key quits.
41*/
42
43#include <inttypes.h>
Nigel Taob003f9f2021-10-19 22:09:21 +110044#include <math.h>
Nigel Tao8f1fe432020-01-15 14:22:48 +110045#include <stdio.h>
46#include <stdlib.h>
47
48// Wuffs ships as a "single file C library" or "header file library" as per
49// https://github.com/nothings/stb/blob/master/docs/stb_howto.txt
50//
51// To use that single file as a "foo.c"-like implementation, instead of a
52// "foo.h"-like header, #define WUFFS_IMPLEMENTATION before #include'ing or
53// compiling it.
54#define WUFFS_IMPLEMENTATION
55
Nigel Tao7f9f37c2021-10-04 12:35:32 +110056// Defining the WUFFS_CONFIG__STATIC_FUNCTIONS macro is optional, but when
57// combined with WUFFS_IMPLEMENTATION, it demonstrates making all of Wuffs'
58// functions have static storage.
59//
60// This can help the compiler ignore or discard unused code, which can produce
61// faster compiles and smaller binaries. Other motivations are discussed in the
62// "ALLOW STATIC IMPLEMENTATION" section of
63// https://raw.githubusercontent.com/nothings/stb/master/docs/stb_howto.txt
64#define WUFFS_CONFIG__STATIC_FUNCTIONS
65
Nigel Tao8f1fe432020-01-15 14:22:48 +110066// Defining the WUFFS_CONFIG__MODULE* macros are optional, but it lets users of
Nigel Tao2f788042021-01-23 19:29:19 +110067// release/c/etc.c choose which parts of Wuffs to build. That file contains the
68// entire Wuffs standard library, implementing a variety of codecs and file
Nigel Tao8f1fe432020-01-15 14:22:48 +110069// formats. Without this macro definition, an optimizing compiler or linker may
70// very well discard Wuffs code for unused codecs, but listing the Wuffs
71// modules we use makes that process explicit. Preprocessing means that such
72// code simply isn't compiled.
73#define WUFFS_CONFIG__MODULES
Nigel Tao58c30e92020-12-23 22:11:04 +110074#define WUFFS_CONFIG__MODULE__ADLER32
Nigel Tao04f86862021-03-01 23:01:51 +110075#define WUFFS_CONFIG__MODULE__AUX__BASE
76#define WUFFS_CONFIG__MODULE__AUX__IMAGE
Nigel Tao8f1fe432020-01-15 14:22:48 +110077#define WUFFS_CONFIG__MODULE__BASE
Nigel Taoe6cc2fe2020-02-06 14:36:29 +110078#define WUFFS_CONFIG__MODULE__BMP
Nigel Tao58c30e92020-12-23 22:11:04 +110079#define WUFFS_CONFIG__MODULE__CRC32
80#define WUFFS_CONFIG__MODULE__DEFLATE
Nigel Tao8f1fe432020-01-15 14:22:48 +110081#define WUFFS_CONFIG__MODULE__GIF
82#define WUFFS_CONFIG__MODULE__LZW
Nigel Tao1197e862020-12-22 15:29:35 +110083#define WUFFS_CONFIG__MODULE__NIE
Nigel Tao58c30e92020-12-23 22:11:04 +110084#define WUFFS_CONFIG__MODULE__PNG
Nigel Tao4c09c772022-01-14 22:46:57 +110085#define WUFFS_CONFIG__MODULE__TGA
Nigel Tao4feb6412020-01-16 15:02:32 +110086#define WUFFS_CONFIG__MODULE__WBMP
Nigel Tao58c30e92020-12-23 22:11:04 +110087#define WUFFS_CONFIG__MODULE__ZLIB
Nigel Tao8f1fe432020-01-15 14:22:48 +110088
89// If building this program in an environment that doesn't easily accommodate
90// relative includes, you can use the script/inline-c-relative-includes.go
91// program to generate a stand-alone C file.
92#include "../../release/c/wuffs-unsupported-snapshot.c"
93
Nigel Tao2945ef22021-10-07 12:35:12 +110094// X11 limits its image dimensions to uint16_t and some coordinates to int16_t.
95#define MAX_INCL_DIMENSION 32767
Nigel Tao8f1fe432020-01-15 14:22:48 +110096
Nigel Taod5959802020-01-24 11:51:54 +110097#define NUM_BACKGROUND_COLORS 3
Nigel Tao8eaa2222021-10-03 16:42:58 +110098#define NUM_ZOOMS 8
Nigel Taofdac24a2020-03-06 21:53:08 +110099#define SRC_BUFFER_ARRAY_SIZE (64 * 1024)
Nigel Tao8f1fe432020-01-15 14:22:48 +1100100
Nigel Taod5959802020-01-24 11:51:54 +1100101wuffs_base__color_u32_argb_premul g_background_colors[NUM_BACKGROUND_COLORS] = {
102 0xFF000000,
103 0xFFFFFFFF,
104 0xFFA9009A,
105};
106
Nigel Tao8f1fe432020-01-15 14:22:48 +1100107uint32_t g_width = 0;
108uint32_t g_height = 0;
Nigel Tao04f86862021-03-01 23:01:51 +1100109wuffs_aux::MemOwner g_pixbuf_mem_owner(nullptr, &free);
Nigel Tao314d2652021-04-02 23:57:18 +1100110wuffs_base__pixel_buffer g_pixbuf = {0};
Nigel Taod5959802020-01-24 11:51:54 +1100111uint32_t g_background_color_index = 0;
Nigel Tao90d32322021-10-07 12:59:38 +1100112int32_t g_zoom = 0;
Nigel Tao4c5d3132022-05-31 12:25:08 +1000113int32_t g_pos_x = 0;
114int32_t g_pos_y = 0;
Nigel Tao8eaa2222021-10-03 16:42:58 +1100115bool g_filter = false;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100116
Nigel Tao15b62852021-10-20 15:36:19 +1100117struct {
118 int remaining_argc;
119 char** remaining_argv;
120
121 double screen_gamma;
122} g_flags = {0};
123
124static const char* g_usage =
125 "Usage: imageviewer -flags input0.gif input1.png\n"
126 "\n"
127 "Flags:\n"
128 " -screen_gamma=N.N (default 2.2; 0 disables gamma correction)";
129
130const char* //
131parse_flags(int argc, char** argv) {
132 g_flags.screen_gamma = 2.2;
133
134 int c = (argc > 0) ? 1 : 0; // Skip argv[0], the program name.
135 for (; c < argc; c++) {
136 char* arg = argv[c];
137 if (*arg++ != '-') {
138 break;
139 }
140
141 // A double-dash "--foo" is equivalent to a single-dash "-foo". As special
142 // cases, a bare "-" is not a flag (some programs may interpret it as
143 // stdin) and a bare "--" means to stop parsing flags.
144 if (*arg == '\x00') {
145 break;
146 } else if (*arg == '-') {
147 arg++;
148 if (*arg == '\x00') {
149 c++;
150 break;
151 }
152 }
153
154 if (!strncmp(arg, "screen_gamma=", 13)) {
155 g_flags.screen_gamma = atof(arg + 13);
156 continue;
157 }
158
159 return g_usage;
160 }
161
162 g_flags.remaining_argc = argc - c;
163 g_flags.remaining_argv = argv + c;
164 return NULL;
165}
166
Nigel Taob003f9f2021-10-19 22:09:21 +1100167class MyDecodeImageCallbacks : public wuffs_aux::DecodeImageCallbacks {
168 public:
169 MyDecodeImageCallbacks() : m_combined_gamma(1.0) {}
170
171 private:
172 std::string //
173 HandleMetadata(const wuffs_base__more_information& minfo,
Nigel Taoc754f812021-10-31 15:03:06 +1100174 wuffs_base__slice_u8 raw) override {
Nigel Taob003f9f2021-10-19 22:09:21 +1100175 if (minfo.flavor == WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED) {
176 switch (minfo.metadata__fourcc()) {
177 case WUFFS_BASE__FOURCC__GAMA:
178 // metadata_parsed__gama returns the inverse gamma scaled by 1e5.
179 m_combined_gamma =
Nigel Tao15b62852021-10-20 15:36:19 +1100180 1e5 / (g_flags.screen_gamma * minfo.metadata_parsed__gama());
Nigel Taob003f9f2021-10-19 22:09:21 +1100181 break;
182 }
183 }
Nigel Taoc754f812021-10-31 15:03:06 +1100184 return wuffs_aux::DecodeImageCallbacks::HandleMetadata(minfo, raw);
Nigel Taob003f9f2021-10-19 22:09:21 +1100185 }
186
187 void //
188 Done(wuffs_aux::DecodeImageResult& result,
189 wuffs_aux::sync_io::Input& input,
190 wuffs_aux::IOBuffer& buffer,
191 wuffs_base__image_decoder::unique_ptr image_decoder) override {
192 // Apply basic color correction - gamma correction. Proper color correction
193 // should also involve considering any CHRM, ICCP and SRGB metadata but
194 // that requires a non-trivial amount of code (such as skcms, Skia's Color
195 // Management System). To keep this example program simple, we only
196 // consider GAMA metadata (and 8-bit color channels) here.
197 //
198 // This code also assumes that wuffs_aux::DecodeImageCallbacks defaults to
199 // producing a WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL pixel buffer.
200 if ((result.pixbuf.pixel_format().repr ==
201 WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL) &&
202 ((m_combined_gamma < 0.9999) || (1.0001 < m_combined_gamma))) {
203 uint8_t lut[256];
204 lut[0x00] = 0x00;
205 lut[0xFF] = 0xFF;
206 for (uint32_t i = 1; i < 0xFF; i++) {
207 lut[i] =
208 (uint8_t)(floor(255.0 * pow(i / 255.0, m_combined_gamma) + 0.5));
209 }
210
211 wuffs_base__table_u8 t = result.pixbuf.plane(0);
212 size_t w4 = t.width / 4;
213 for (size_t y = 0; y < t.height; y++) {
214 uint8_t* ptr = t.ptr + (y * t.stride);
215 for (size_t x = 0; x < w4; x++) {
216 ptr[0] = lut[ptr[0]];
217 ptr[1] = lut[ptr[1]];
218 ptr[2] = lut[ptr[2]];
219 ptr += 4;
220 }
221 }
222 }
223 }
224
225 // m_combined_gamma holds the product of the screen gamma and the image
226 // file's inverse gamma.
227 double m_combined_gamma;
228};
229
Nigel Tao2914bae2020-02-26 09:40:30 +1100230bool //
231load_image(const char* filename) {
Nigel Tao04f86862021-03-01 23:01:51 +1100232 FILE* file = stdin;
233 const char* adj_filename = "<stdin>";
Nigel Tao8f1fe432020-01-15 14:22:48 +1100234 if (filename) {
Nigel Tao816475b2021-07-07 20:28:35 +1000235 FILE* f = fopen(filename, "rb");
Nigel Tao8f1fe432020-01-15 14:22:48 +1100236 if (f == NULL) {
237 printf("%s: could not open file\n", filename);
238 return false;
239 }
Nigel Tao04f86862021-03-01 23:01:51 +1100240 file = f;
241 adj_filename = filename;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100242 }
243
Nigel Tao04f86862021-03-01 23:01:51 +1100244 g_width = 0;
245 g_height = 0;
246 g_pixbuf_mem_owner.reset();
Nigel Tao314d2652021-04-02 23:57:18 +1100247 g_pixbuf = wuffs_base__null_pixel_buffer();
Nigel Tao04f86862021-03-01 23:01:51 +1100248
Nigel Tao15b62852021-10-20 15:36:19 +1100249 uint64_t dia_flags = 0;
250 if (g_flags.screen_gamma > 0) {
251 dia_flags |= wuffs_aux::DecodeImageArgFlags::REPORT_METADATA_GAMA;
252 }
253
Nigel Taob003f9f2021-10-19 22:09:21 +1100254 MyDecodeImageCallbacks callbacks;
Nigel Tao04f86862021-03-01 23:01:51 +1100255 wuffs_aux::sync_io::FileInput input(file);
256 wuffs_aux::DecodeImageResult res = wuffs_aux::DecodeImage(
Nigel Tao2b7b6e12021-10-18 00:40:11 +1100257 callbacks, input, wuffs_aux::DecodeImageArgQuirks::DefaultValue(),
Nigel Tao15b62852021-10-20 15:36:19 +1100258 wuffs_aux::DecodeImageArgFlags(dia_flags),
Nigel Tao04f86862021-03-01 23:01:51 +1100259 // Use PIXEL_BLEND__SRC_OVER, not the default PIXEL_BLEND__SRC, because
Nigel Tao441e96a2021-03-11 15:28:20 +1100260 // we also pass a background color.
Nigel Taoca335062021-10-18 00:25:13 +1100261 wuffs_aux::DecodeImageArgPixelBlend(WUFFS_BASE__PIXEL_BLEND__SRC_OVER),
262 wuffs_aux::DecodeImageArgBackgroundColor(
263 g_background_colors[g_background_color_index]),
264 wuffs_aux::DecodeImageArgMaxInclDimension(MAX_INCL_DIMENSION));
Nigel Tao8f1fe432020-01-15 14:22:48 +1100265 if (filename) {
Nigel Tao04f86862021-03-01 23:01:51 +1100266 fclose(file);
Nigel Tao8f1fe432020-01-15 14:22:48 +1100267 }
Nigel Tao04f86862021-03-01 23:01:51 +1100268
Nigel Taoc86ac5b2021-04-03 10:59:33 +1100269 // wuffs_aux::DecodeImageCallbacks's default implementation should give us an
270 // interleaved (not multi-planar) pixel buffer, so that all of the pixel data
271 // is in a single 2-dimensional table (plane 0). Later on, we re-interpret
272 // that table as XCB image data, which isn't something we could do if we had
273 // e.g. multi-planar YCbCr.
274 if (!res.pixbuf.pixcfg.pixel_format().is_interleaved()) {
275 printf("%s: non-interleaved pixbuf\n", adj_filename);
276 return false;
277 }
278 wuffs_base__table_u8 tab = res.pixbuf.plane(0);
279 if (tab.width != tab.stride) {
280 // The xcb_image_create_native call, later on, assumes that (tab.height *
281 // tab.stride) bytes are readable, which isn't quite the same as what
282 // wuffs_base__table__flattened_length(tab.width, tab.height, tab.stride)
283 // returns unless the table is tight (its width equals its stride).
284 printf("%s: could not allocate tight pixbuf\n", adj_filename);
285 return false;
286 }
287
Nigel Tao04f86862021-03-01 23:01:51 +1100288 g_width = res.pixbuf.pixcfg.width();
289 g_height = res.pixbuf.pixcfg.height();
290 g_pixbuf_mem_owner = std::move(res.pixbuf_mem_owner);
Nigel Tao314d2652021-04-02 23:57:18 +1100291 g_pixbuf = res.pixbuf;
Nigel Tao04f86862021-03-01 23:01:51 +1100292
293 if (res.error_message.empty()) {
294 printf("%s: ok (%" PRIu32 " x %" PRIu32 ")\n", adj_filename, g_width,
295 g_height);
296 } else {
297 printf("%s: %s\n", adj_filename, res.error_message.c_str());
298 }
299 return res.pixbuf.pixcfg.is_valid();
Nigel Tao8f1fe432020-01-15 14:22:48 +1100300}
301
Nigel Taof45f01f2020-06-05 14:06:56 +1000302// ---------------------------------------------------------------------
Nigel Tao8f1fe432020-01-15 14:22:48 +1100303
304#if defined(__linux__)
305#define SUPPORTED_OPERATING_SYSTEM
306
Nigel Tao2453a8d2021-10-03 13:53:27 +1100307#include <xcb/render.h>
Nigel Tao8f1fe432020-01-15 14:22:48 +1100308#include <xcb/xcb.h>
309#include <xcb/xcb_image.h>
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200310#include <xcb/xcb_renderutil.h>
Nigel Tao8f1fe432020-01-15 14:22:48 +1100311
312#define XK_BackSpace 0xFF08
313#define XK_Escape 0xFF1B
314#define XK_Return 0xFF0D
Nigel Tao4c5d3132022-05-31 12:25:08 +1000315#define XK_Left 0xFF51
316#define XK_Up 0xFF52
317#define XK_Right 0xFF53
318#define XK_Down 0xFF54
Nigel Tao8f1fe432020-01-15 14:22:48 +1100319
Nigel Tao2945ef22021-10-07 12:35:12 +1100320uint32_t g_maximum_request_length = 0; // Measured in 4-byte units.
Nigel Tao8f1fe432020-01-15 14:22:48 +1100321xcb_atom_t g_atom_net_wm_name = XCB_NONE;
322xcb_atom_t g_atom_utf8_string = XCB_NONE;
323xcb_atom_t g_atom_wm_protocols = XCB_NONE;
324xcb_atom_t g_atom_wm_delete_window = XCB_NONE;
325xcb_pixmap_t g_pixmap = XCB_NONE;
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200326xcb_gcontext_t g_pixmap_gc = XCB_NONE;
327xcb_render_picture_t g_pixmap_picture = XCB_NONE;
Nigel Tao1cf0df92021-10-03 15:30:04 +1100328xcb_render_pictforminfo_t* g_pictforminfo = NULL;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100329xcb_keysym_t* g_keysyms = NULL;
330xcb_get_keyboard_mapping_reply_t* g_keyboard_mapping = NULL;
331
Nigel Tao2914bae2020-02-26 09:40:30 +1100332void //
333init_keymap(xcb_connection_t* c, const xcb_setup_t* z) {
Nigel Tao8f1fe432020-01-15 14:22:48 +1100334 xcb_get_keyboard_mapping_cookie_t cookie = xcb_get_keyboard_mapping(
335 c, z->min_keycode, z->max_keycode - z->min_keycode + 1);
336 g_keyboard_mapping = xcb_get_keyboard_mapping_reply(c, cookie, NULL);
337 g_keysyms = (xcb_keysym_t*)(g_keyboard_mapping + 1);
338}
339
Nigel Tao2914bae2020-02-26 09:40:30 +1100340xcb_window_t //
341make_window(xcb_connection_t* c, xcb_screen_t* s) {
Nigel Tao8f1fe432020-01-15 14:22:48 +1100342 xcb_window_t w = xcb_generate_id(c);
343 uint32_t value_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
344 uint32_t value_list[2];
345 value_list[0] = s->black_pixel;
Nigel Tao4c5d3132022-05-31 12:25:08 +1000346 value_list[1] = XCB_EVENT_MASK_KEY_PRESS | //
347 XCB_EVENT_MASK_BUTTON_PRESS | //
348 XCB_EVENT_MASK_BUTTON_1_MOTION | //
349 XCB_EVENT_MASK_EXPOSURE;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100350 xcb_create_window(c, 0, w, s->root, 0, 0, 1024, 768, 0,
351 XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, value_mask,
352 value_list);
353 xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, g_atom_net_wm_name,
354 g_atom_utf8_string, 8, 12, "Image Viewer");
355 xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, g_atom_wm_protocols,
356 XCB_ATOM_ATOM, 32, 1, &g_atom_wm_delete_window);
357 xcb_map_window(c, w);
358 return w;
359}
360
Nigel Tao8eaa2222021-10-03 16:42:58 +1100361void //
362apply_zoom_and_filter(xcb_connection_t* c) {
Nigel Tao90d32322021-10-07 12:59:38 +1100363 static const xcb_render_fixed_t neg_zooms[NUM_ZOOMS] = {
364 0x00010000, // 1/1 as 16.16 fixed point
365 0x00020000, // 2/1
366 0x00040000, // 4/1
367 0x00080000, // 8/1
368 0x00100000, // 16/1
369 0x00200000, // 32/1
370 0x00400000, // 64/1
371 0x00800000, // 128/1
372 };
373 static const xcb_render_fixed_t pos_zooms[NUM_ZOOMS] = {
374 0x00010000, // 1/1 as 16.16 fixed point
375 0x00008000, // 1/2
376 0x00004000, // 1/4
377 0x00002000, // 1/8
378 0x00001000, // 1/16
379 0x00000800, // 1/32
380 0x00000400, // 1/64
381 0x00000200, // 1/128
Nigel Tao8eaa2222021-10-03 16:42:58 +1100382 };
383
Nigel Tao90d32322021-10-07 12:59:38 +1100384 xcb_render_fixed_t z = g_zoom < 0
385 ? neg_zooms[((uint32_t)(-g_zoom)) % NUM_ZOOMS]
386 : pos_zooms[((uint32_t)(+g_zoom)) % NUM_ZOOMS];
Nigel Tao8eaa2222021-10-03 16:42:58 +1100387 xcb_render_set_picture_transform(c, g_pixmap_picture,
388 ((xcb_render_transform_t){
389 z, 0, 0, //
390 0, z, 0, //
391 0, 0, 0x10000, //
392 }));
393
394 uint16_t f_len = 7;
395 const char* f_ptr = "nearest";
396 if (g_filter && (g_zoom != 0)) {
397 f_len = 8;
398 f_ptr = "bilinear";
399 }
400 xcb_render_set_picture_filter(c, g_pixmap_picture, f_len, f_ptr, 0, NULL);
401}
402
403// zoom_shift returns (a << g_zoom), roughly speaking, but saturates at an
404// arbitrary value called M.
405//
406// The final two arguments to xcb_render_composite have uint16_t type (and
407// UINT16_MAX is 65535), but in practice, values above M sometimes don't work
408// in that the xcb_render_composite call has no visible effect.
409//
410// Some xrender debugging could potentially derive a more accurate maximum but
411// for now, the M = 30000 round number will do.
412uint16_t //
413zoom_shift(uint32_t a) {
414 uint16_t M = 30000;
Nigel Tao90d32322021-10-07 12:59:38 +1100415 uint64_t b = g_zoom < 0
416 ? (((uint64_t)a) >> (((uint32_t)(-g_zoom)) % NUM_ZOOMS))
417 : (((uint64_t)a) << (((uint32_t)(+g_zoom)) % NUM_ZOOMS));
Nigel Tao8eaa2222021-10-03 16:42:58 +1100418 return (b < M) ? b : M;
419}
420
Nigel Tao4c5d3132022-05-31 12:25:08 +1000421int32_t //
422calculate_delta(uint16_t state) {
423 if (state & XCB_MOD_MASK_SHIFT) {
424 return 256;
425 } else if (state & XCB_MOD_MASK_CONTROL) {
426 return 1;
427 }
428 return 16;
429}
430
Nigel Tao2914bae2020-02-26 09:40:30 +1100431bool //
Nigel Tao2453a8d2021-10-03 13:53:27 +1100432load(xcb_connection_t* c, xcb_window_t w, const char* filename) {
Nigel Tao8f1fe432020-01-15 14:22:48 +1100433 if (g_pixmap != XCB_NONE) {
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200434 xcb_render_free_picture(c, g_pixmap_picture);
Nigel Tao1cf0df92021-10-03 15:30:04 +1100435 xcb_free_gc(c, g_pixmap_gc);
Nigel Tao8f1fe432020-01-15 14:22:48 +1100436 xcb_free_pixmap(c, g_pixmap);
437 }
438
439 if (!load_image(filename)) {
440 return false;
441 }
Nigel Tao314d2652021-04-02 23:57:18 +1100442 wuffs_base__table_u8 tab = g_pixbuf.plane(0);
Nigel Tao8f1fe432020-01-15 14:22:48 +1100443
Nigel Tao1cf0df92021-10-03 15:30:04 +1100444 xcb_create_pixmap(c, g_pictforminfo->depth, g_pixmap, w, g_width, g_height);
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200445 xcb_create_gc(c, g_pixmap_gc, g_pixmap, 0, NULL);
Nigel Tao1cf0df92021-10-03 15:30:04 +1100446 xcb_render_create_picture(c, g_pixmap_picture, g_pixmap, g_pictforminfo->id,
447 0, NULL);
Nigel Tao8eaa2222021-10-03 16:42:58 +1100448 apply_zoom_and_filter(c);
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200449
Nigel Tao2945ef22021-10-07 12:35:12 +1100450 // Copy the pixels from the X11 client process (this process) to the X11
451 // server process. For large images, this may involve multiple xcb_image_put
452 // calls, each copying part of the pixels (a strip that has the same width
453 // but smaller height), to avoid XCB_CONN_CLOSED_REQ_LEN_EXCEED.
454 if (g_width > 0) {
455 uint32_t max_strip_height = g_maximum_request_length / g_width;
456 for (uint32_t y = 0; y < g_height;) {
457 uint32_t h = g_height - y;
458 if (h > max_strip_height) {
459 h = max_strip_height;
460 }
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200461
Nigel Tao2945ef22021-10-07 12:35:12 +1100462 // Make libxcb-image interpret WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL as
463 // XCB_PICT_STANDARD_ARGB_32 with byte_order XCB_IMAGE_ORDER_LSB_FIRST.
464 xcb_image_t* unconverted =
465 xcb_image_create(g_width, // width
466 h, // height
467 XCB_IMAGE_FORMAT_Z_PIXMAP, // format
468 32, // xpad
469 g_pictforminfo->depth, // depth
470 32, // bpp
471 32, // unit
472 XCB_IMAGE_ORDER_LSB_FIRST, // byte_order
473 XCB_IMAGE_ORDER_MSB_FIRST, // bit_order
474 NULL, // base
475 h * tab.stride, // bytes
476 tab.ptr + (y * tab.stride)); // data
477
478 xcb_image_t* converted =
479 xcb_image_native(c, unconverted, true); // true means to convert.
480 if (converted != unconverted) {
481 xcb_image_destroy(unconverted);
482 }
483 xcb_image_put(c, g_pixmap, g_pixmap_gc, converted, 0, y, 0);
484 xcb_image_destroy(converted);
485
486 y += h;
487 }
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200488 }
489
Nigel Tao8f1fe432020-01-15 14:22:48 +1100490 return true;
491}
492
Nigel Tao5d7b1132022-06-08 17:22:29 +1000493bool //
494intersects(int32_t old_x,
495 int32_t old_y,
496 int32_t width,
497 int32_t height,
498 int32_t new_x,
499 int32_t new_y) {
500 if ((width <= 0) || (height <= 0)) {
501 return false;
502 } else if ((old_x > new_x) && ((old_x - new_x) >= width)) {
503 return false;
504 } else if ((new_x > old_x) && ((new_x - old_x) >= width)) {
505 return false;
506 } else if ((old_y > new_y) && ((old_y - new_y) >= height)) {
507 return false;
508 } else if ((new_y > old_y) && ((new_y - old_y) >= height)) {
509 return false;
510 }
511 return true;
512}
513
Nigel Tao4c5d3132022-05-31 12:25:08 +1000514// clear_area clears the L-shaped difference between old and new rectangles (of
515// equal width and height). It does this in up to two xcb_clear_area calls,
516// labeled A and B in the example below (with old_x=0, old_y=0, width=5,
517// height=4, new_x=2, new_y=2).
518//
Nigel Tao5d7b1132022-06-08 17:22:29 +1000519// If the old and new rectangles do not intersect then the old rectangle (not
520// an L-shape) will be cleared.
521//
Nigel Tao4c5d3132022-05-31 12:25:08 +1000522// AAAAA
523// AAAAA
524// BB+---+
525// BB| |
526// | |
527// +---+
528void //
529clear_area(xcb_connection_t* c,
530 xcb_window_t w,
531 int32_t old_x,
532 int32_t old_y,
533 int32_t width,
534 int32_t height,
535 int32_t new_x,
536 int32_t new_y) {
Nigel Tao5d7b1132022-06-08 17:22:29 +1000537 if ((width <= 0) || (height <= 0)) {
538 return;
539 } else if (!intersects(old_x, old_y, width, height, new_x, new_y)) {
540 xcb_clear_area(c, 1, w, old_x, old_y, width, height);
541 return;
542 }
543
Nigel Tao4c5d3132022-05-31 12:25:08 +1000544 int32_t dy = new_y - old_y;
545 if (dy < 0) {
546 xcb_clear_area(c, 1, w, old_x, old_y + height + dy, width, -dy);
547 } else if (dy > 0) {
548 xcb_clear_area(c, 1, w, old_x, old_y, width, dy);
549 }
550
Nigel Tao8feb2702022-06-03 11:28:39 +1000551 int32_t y0 = wuffs_base__i32__max(old_y, new_y);
552 int32_t y1 = wuffs_base__i32__min(old_y + height, new_y + height);
Nigel Tao4c5d3132022-05-31 12:25:08 +1000553
554 int32_t dx = new_x - old_x;
555 if (dx < 0) {
556 xcb_clear_area(c, 1, w, old_x + width + dx, y0, -dx, y1 - y0);
557 } else if (dx > 0) {
558 xcb_clear_area(c, 1, w, old_x, y0, dx, y1 - y0);
559 }
560}
561
Nigel Tao2914bae2020-02-26 09:40:30 +1100562int //
563main(int argc, char** argv) {
Nigel Tao15b62852021-10-20 15:36:19 +1100564 const char* status = parse_flags(argc, argv);
565 if (status) {
566 fprintf(stderr, "%s\n", status);
567 return 1;
568 }
569
Nigel Tao8f1fe432020-01-15 14:22:48 +1100570 xcb_connection_t* c = xcb_connect(NULL, NULL);
Nigel Tao2945ef22021-10-07 12:35:12 +1100571
572 g_maximum_request_length = xcb_get_maximum_request_length(c);
573 // Our X11 requests (especially xcb_image_put) also need a header, in terms
574 // of wire format. 256 4-byte units should be big enough.
575 const uint32_t max_req_len_adjustment = 256;
576 if (g_maximum_request_length < max_req_len_adjustment) {
577 printf("XCB failure (maximum request length is too short)\n");
578 exit(EXIT_FAILURE);
579 }
580 g_maximum_request_length -= max_req_len_adjustment;
581
Nigel Tao8f1fe432020-01-15 14:22:48 +1100582 const xcb_setup_t* z = xcb_get_setup(c);
583 xcb_screen_t* s = xcb_setup_roots_iterator(z).data;
Nigel Tao1cf0df92021-10-03 15:30:04 +1100584
585 const xcb_render_query_pict_formats_reply_t* pict_formats =
586 xcb_render_util_query_formats(c);
587 g_pictforminfo = xcb_render_util_find_standard_format(
588 pict_formats, XCB_PICT_STANDARD_ARGB_32);
Nigel Tao8f1fe432020-01-15 14:22:48 +1100589
590 {
591 xcb_intern_atom_cookie_t cookie0 =
592 xcb_intern_atom(c, 1, 12, "_NET_WM_NAME");
593 xcb_intern_atom_cookie_t cookie1 = xcb_intern_atom(c, 1, 11, "UTF8_STRING");
594 xcb_intern_atom_cookie_t cookie2 =
595 xcb_intern_atom(c, 1, 12, "WM_PROTOCOLS");
596 xcb_intern_atom_cookie_t cookie3 =
597 xcb_intern_atom(c, 1, 16, "WM_DELETE_WINDOW");
598 xcb_intern_atom_reply_t* reply0 = xcb_intern_atom_reply(c, cookie0, NULL);
599 xcb_intern_atom_reply_t* reply1 = xcb_intern_atom_reply(c, cookie1, NULL);
600 xcb_intern_atom_reply_t* reply2 = xcb_intern_atom_reply(c, cookie2, NULL);
601 xcb_intern_atom_reply_t* reply3 = xcb_intern_atom_reply(c, cookie3, NULL);
602 g_atom_net_wm_name = reply0->atom;
603 g_atom_utf8_string = reply1->atom;
604 g_atom_wm_protocols = reply2->atom;
605 g_atom_wm_delete_window = reply3->atom;
606 free(reply0);
607 free(reply1);
608 free(reply2);
609 free(reply3);
610 }
611
612 xcb_window_t w = make_window(c, s);
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200613 xcb_render_picture_t p = xcb_generate_id(c);
Nigel Tao2453a8d2021-10-03 13:53:27 +1100614 xcb_render_create_picture(
615 c, p, w,
Nigel Tao1cf0df92021-10-03 15:30:04 +1100616 xcb_render_util_find_visual_format(pict_formats, s->root_visual)->format,
Nigel Tao2453a8d2021-10-03 13:53:27 +1100617 0, NULL);
Nigel Tao8f1fe432020-01-15 14:22:48 +1100618 init_keymap(c, z);
619 xcb_flush(c);
Nigel Tao8f1fe432020-01-15 14:22:48 +1100620
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200621 g_pixmap = xcb_generate_id(c);
622 g_pixmap_gc = xcb_generate_id(c);
623 g_pixmap_picture = xcb_generate_id(c);
624
Nigel Tao4c5d3132022-05-31 12:25:08 +1000625 int32_t button_x = 0;
626 int32_t button_y = 0;
627
Nigel Tao15b62852021-10-20 15:36:19 +1100628 bool loaded = load(
629 c, w, (g_flags.remaining_argc > 0) ? g_flags.remaining_argv[0] : NULL);
630 int arg = 0;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100631
632 while (true) {
633 xcb_generic_event_t* event = xcb_wait_for_event(c);
Přemysl Eric Janoucha1d9c6a2021-09-30 07:31:45 +0200634 if (!event) {
Nigel Tao2453a8d2021-10-03 13:53:27 +1100635 printf("XCB failure (error code %d)\n", xcb_connection_has_error(c));
636 exit(EXIT_FAILURE);
Přemysl Eric Janoucha1d9c6a2021-09-30 07:31:45 +0200637 }
Nigel Tao8f1fe432020-01-15 14:22:48 +1100638
Přemysl Eric Janoucha1d9c6a2021-09-30 07:31:45 +0200639 bool reload = false;
Nigel Tao4c5d3132022-05-31 12:25:08 +1000640 int32_t old_pos_x = g_pos_x;
641 int32_t old_pos_y = g_pos_y;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100642 switch (event->response_type & 0x7F) {
643 case XCB_EXPOSE: {
Nigel Tao731350f2020-01-24 11:36:44 +1100644 xcb_expose_event_t* e = (xcb_expose_event_t*)event;
645 if (loaded && (e->count == 0)) {
Přemysl Eric Janouch550fabd2021-09-30 12:14:51 +0200646 xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, g_pixmap_picture,
Nigel Tao4c5d3132022-05-31 12:25:08 +1000647 XCB_NONE, p, 0, 0, 0, 0, g_pos_x, g_pos_y,
Nigel Tao8eaa2222021-10-03 16:42:58 +1100648 zoom_shift(g_width), zoom_shift(g_height));
Nigel Tao8f1fe432020-01-15 14:22:48 +1100649 xcb_flush(c);
650 }
651 break;
652 }
653
654 case XCB_KEY_PRESS: {
655 xcb_key_press_event_t* e = (xcb_key_press_event_t*)event;
656 uint32_t i = e->detail;
657 if ((z->min_keycode <= i) && (i <= z->max_keycode)) {
658 i = g_keysyms[(i - z->min_keycode) *
659 g_keyboard_mapping->keysyms_per_keycode];
660 switch (i) {
661 case XK_Escape:
662 return 0;
Nigel Taod5959802020-01-24 11:51:54 +1100663
Nigel Tao8f1fe432020-01-15 14:22:48 +1100664 case ' ':
665 case XK_BackSpace:
666 case XK_Return:
Nigel Tao15b62852021-10-20 15:36:19 +1100667 if (g_flags.remaining_argc <= 1) {
Nigel Tao8f1fe432020-01-15 14:22:48 +1100668 break;
669 }
670 arg += (i != XK_BackSpace) ? +1 : -1;
Nigel Tao15b62852021-10-20 15:36:19 +1100671 if (arg < 0) {
672 arg = g_flags.remaining_argc - 1;
673 } else if (arg == g_flags.remaining_argc) {
674 arg = 0;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100675 }
Nigel Taod5959802020-01-24 11:51:54 +1100676 reload = true;
677 break;
678
679 case ',':
680 case '.':
681 g_background_color_index +=
682 (i == ',') ? (NUM_BACKGROUND_COLORS - 1) : 1;
683 g_background_color_index %= NUM_BACKGROUND_COLORS;
684 reload = true;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100685 break;
Nigel Tao8eaa2222021-10-03 16:42:58 +1100686
Nigel Tao4c5d3132022-05-31 12:25:08 +1000687 case '`':
688 g_pos_x = 0;
689 g_pos_y = 0;
690 break;
691
692 case XK_Left:
693 case 'h':
694 g_pos_x += calculate_delta(e->state);
695 break;
696
697 case XK_Down:
698 case 'j':
699 g_pos_y -= calculate_delta(e->state);
700 break;
701
702 case XK_Up:
703 case 'k':
704 g_pos_y += calculate_delta(e->state);
705 break;
706
707 case XK_Right:
708 case 'l':
709 g_pos_x -= calculate_delta(e->state);
710 break;
711
Nigel Tao8eaa2222021-10-03 16:42:58 +1100712 case '0':
713 case '1':
714 case '2':
715 case '3':
716 case '4':
717 case '5':
718 case '6':
719 case '7':
720 case '8':
721 if (i == '0') {
722 g_filter = !g_filter;
723 } else {
Nigel Tao90d32322021-10-07 12:59:38 +1100724 int32_t z = i - '1';
725 if (e->state & XCB_MOD_MASK_SHIFT) {
726 z = -z;
727 }
728 if (g_zoom == z) {
729 break;
730 }
731 g_zoom = z;
Nigel Tao8eaa2222021-10-03 16:42:58 +1100732 }
733 apply_zoom_and_filter(c);
734 xcb_clear_area(c, 1, w, 0, 0, 0xFFFF, 0xFFFF);
735 xcb_flush(c);
736 break;
Nigel Tao8f1fe432020-01-15 14:22:48 +1100737 }
738 }
739 break;
740 }
741
Nigel Tao4c5d3132022-05-31 12:25:08 +1000742 case XCB_BUTTON_PRESS: {
743 xcb_button_press_event_t* e = (xcb_button_press_event_t*)event;
744 switch (e->detail) {
745 case 1:
746 button_x = e->event_x;
747 button_y = e->event_y;
748 break;
749 case 4:
750 g_pos_y += calculate_delta(e->state);
751 break;
752 case 5:
753 g_pos_y -= calculate_delta(e->state);
754 break;
755 case 6:
756 g_pos_x += calculate_delta(e->state);
757 break;
758 case 7:
759 g_pos_x -= calculate_delta(e->state);
760 break;
761 }
762 break;
763 }
764
765 case XCB_MOTION_NOTIFY: {
766 xcb_motion_notify_event_t* e = (xcb_motion_notify_event_t*)event;
767 g_pos_x += e->event_x - button_x;
768 g_pos_y += e->event_y - button_y;
769 button_x = e->event_x;
770 button_y = e->event_y;
771 break;
772 }
773
Nigel Tao8f1fe432020-01-15 14:22:48 +1100774 case XCB_CLIENT_MESSAGE: {
775 xcb_client_message_event_t* e = (xcb_client_message_event_t*)event;
776 if (e->data.data32[0] == g_atom_wm_delete_window) {
777 return 0;
778 }
779 break;
780 }
781 }
782
783 free(event);
Nigel Taod5959802020-01-24 11:51:54 +1100784
785 if (reload) {
Nigel Tao15b62852021-10-20 15:36:19 +1100786 loaded = load(c, w, g_flags.remaining_argv[arg]);
Nigel Taod5959802020-01-24 11:51:54 +1100787 xcb_clear_area(c, 1, w, 0, 0, 0xFFFF, 0xFFFF);
788 xcb_flush(c);
Nigel Tao4c5d3132022-05-31 12:25:08 +1000789 } else if (loaded && //
790 ((old_pos_x != g_pos_x) || (old_pos_y != g_pos_y))) {
791 clear_area(c, w, old_pos_x, old_pos_y, zoom_shift(g_width),
792 zoom_shift(g_height), g_pos_x, g_pos_y);
793 xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, g_pixmap_picture,
794 XCB_NONE, p, 0, 0, 0, 0, g_pos_x, g_pos_y,
795 zoom_shift(g_width), zoom_shift(g_height));
796 xcb_flush(c);
Nigel Taod5959802020-01-24 11:51:54 +1100797 }
Nigel Tao8f1fe432020-01-15 14:22:48 +1100798 }
799 return 0;
800}
801
802#endif // defined(__linux__)
803
Nigel Taof45f01f2020-06-05 14:06:56 +1000804// ---------------------------------------------------------------------
Nigel Tao8f1fe432020-01-15 14:22:48 +1100805
806#if !defined(SUPPORTED_OPERATING_SYSTEM)
807
Nigel Tao2914bae2020-02-26 09:40:30 +1100808int //
809main(int argc, char** argv) {
Nigel Tao8f1fe432020-01-15 14:22:48 +1100810 printf("unsupported operating system\n");
811 return 1;
812}
813
814#endif // !defined(SUPPORTED_OPERATING_SYSTEM)