diff mupdf-source/platform/x11/x11_main.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/platform/x11/x11_main.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1203 @@
+// Copyright (C) 2004-2025 Artifex Software, Inc.
+//
+// This file is part of MuPDF.
+//
+// MuPDF is free software: you can redistribute it and/or modify it under the
+// terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// MuPDF 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 Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
+//
+// Alternative licensing terms are available from the licensor.
+// For commercial licensing, see <https://www.artifex.com/> or contact
+// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
+// CA 94129, USA, for further information.
+
+#include "pdfapp.h"
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#include <X11/cursorfont.h>
+#include <X11/keysym.h>
+#include <X11/XF86keysym.h>
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <sys/select.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <signal.h>
+#include <limits.h>
+
+#define mupdf_icon_bitmap_16_width 16
+#define mupdf_icon_bitmap_16_height 16
+static unsigned char mupdf_icon_bitmap_16_bits[] = {
+	0x00, 0x00, 0x00, 0x1e, 0x00, 0x2b, 0x80, 0x55, 0x8c, 0x62, 0x8c, 0x51,
+	0x9c, 0x61, 0x1c, 0x35, 0x3c, 0x1f, 0x3c, 0x0f, 0xfc, 0x0f, 0xec, 0x0d,
+	0xec, 0x0d, 0xcc, 0x0c, 0xcc, 0x0c, 0x00, 0x00 };
+
+#define mupdf_icon_bitmap_16_mask_width 16
+#define mupdf_icon_bitmap_16_mask_height 16
+static unsigned char mupdf_icon_bitmap_16_mask_bits[] = {
+	0x00, 0x1e, 0x00, 0x3f, 0x80, 0x7f, 0xce, 0xff, 0xde, 0xff, 0xde, 0xff,
+	0xfe, 0xff, 0xfe, 0x7f, 0xfe, 0x3f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
+	0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xce, 0x1c };
+
+#ifndef timeradd
+#define timeradd(a, b, result) \
+	do { \
+		(result)->tv_sec = (a)->tv_sec + (b)->tv_sec; \
+		(result)->tv_usec = (a)->tv_usec + (b)->tv_usec; \
+		if ((result)->tv_usec >= 1000000) \
+		{ \
+			++(result)->tv_sec; \
+			(result)->tv_usec -= 1000000; \
+		} \
+	} while (0)
+#endif
+
+#ifndef timersub
+#define timersub(a, b, result) \
+	do { \
+		(result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
+		(result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
+		if ((result)->tv_usec < 0) { \
+			--(result)->tv_sec; \
+			(result)->tv_usec += 1000000; \
+		} \
+	} while (0)
+#endif
+
+extern int ximage_init(Display *display, int screen, Visual *visual);
+extern int ximage_get_depth(void);
+extern Visual *ximage_get_visual(void);
+extern Colormap ximage_get_colormap(void);
+extern void ximage_blit(Drawable d, GC gc, int dstx, int dsty,
+	unsigned char *srcdata,
+	int srcx, int srcy, int srcw, int srch, int srcstride);
+
+static void windrawstringxor(pdfapp_t *app, int x, int y, char *s);
+static void cleanup(pdfapp_t *app);
+
+static Display *xdpy;
+static Atom XA_CLIPBOARD;
+static Atom XA_TARGETS;
+static Atom XA_TIMESTAMP;
+static Atom XA_UTF8_STRING;
+static Atom WM_DELETE_WINDOW;
+static Atom NET_WM_NAME;
+static Atom NET_WM_STATE;
+static Atom NET_WM_STATE_FULLSCREEN;
+static Atom WM_RELOAD_PAGE;
+static int x11fd;
+static int xscr;
+static Window xwin;
+static Pixmap xicon, xmask;
+static GC xgc;
+static XEvent xevt;
+static int mapped = 0;
+static Cursor xcarrow, xchand, xcwait, xccaret;
+static int justcopied = 0;
+static int dirty = 0;
+static int transition_dirty = 0;
+static int dirtysearch = 0;
+static char *password = "";
+static XColor xbgcolor;
+static int reqw = 0;
+static int reqh = 0;
+static char copylatin1[1024 * 16] = "";
+static char copyutf8[1024 * 48] = "";
+static Time copytime;
+static char *filename;
+static char message[1024] = "";
+
+static pdfapp_t gapp;
+static int closing = 0;
+static int reloading = 0;
+static int showingpage = 0;
+static int showingmessage = 0;
+
+static int advance_scheduled = 0;
+static struct timeval tmo;
+static struct timeval tmo_advance;
+static struct timeval tmo_at;
+
+/*
+ * Dialog boxes
+ */
+static void showmessage(pdfapp_t *app, int timeout, char *msg)
+{
+	struct timeval now;
+
+	showingmessage = 1;
+	showingpage = 0;
+
+	fz_strlcpy(message, msg, sizeof message);
+
+	if ((!tmo_at.tv_sec && !tmo_at.tv_usec) || tmo.tv_sec < timeout)
+	{
+		tmo.tv_sec = timeout;
+		tmo.tv_usec = 0;
+		gettimeofday(&now, NULL);
+		timeradd(&now, &tmo, &tmo_at);
+	}
+}
+
+void winerror(pdfapp_t *app, char *msg)
+{
+	fprintf(stderr, "mupdf: error: %s\n", msg);
+	cleanup(app);
+	exit(1);
+}
+
+void winwarn(pdfapp_t *app, char *msg)
+{
+	char buf[1024];
+	snprintf(buf, sizeof buf, "warning: %s", msg);
+	showmessage(app, 10, buf);
+	fprintf(stderr, "mupdf: %s\n", buf);
+}
+
+void winalert(pdfapp_t *app, pdf_alert_event *alert)
+{
+	char buf[1024];
+	snprintf(buf, sizeof buf, "Alert %s: %s", alert->title, alert->message);
+	fprintf(stderr, "%s\n", buf);
+	switch (alert->button_group_type)
+	{
+	case PDF_ALERT_BUTTON_GROUP_OK:
+	case PDF_ALERT_BUTTON_GROUP_OK_CANCEL:
+		alert->button_pressed = PDF_ALERT_BUTTON_OK;
+		break;
+	case PDF_ALERT_BUTTON_GROUP_YES_NO:
+	case PDF_ALERT_BUTTON_GROUP_YES_NO_CANCEL:
+		alert->button_pressed = PDF_ALERT_BUTTON_YES;
+		break;
+	}
+}
+
+void winprint(pdfapp_t *app)
+{
+	fprintf(stderr, "The MuPDF library supports printing, but this application currently does not\n");
+}
+
+char *winpassword(pdfapp_t *app, char *fname)
+{
+	char *r = password;
+	password = NULL;
+	return r;
+}
+
+char *wintextinput(pdfapp_t *app, char *inittext, int retry)
+{
+	/* We don't support text input on the x11 viewer */
+	return NULL;
+}
+
+int winchoiceinput(pdfapp_t *app, int nopts, const char *opts[], int *nvals, const char *vals[])
+{
+	/* FIXME: temporary dummy implementation */
+	return 0;
+}
+
+/*
+ * X11 magic
+ */
+
+static void winopen(void)
+{
+	XWMHints *wmhints;
+	XClassHint *classhint;
+
+#ifdef HAVE_CURL
+	if (!XInitThreads())
+		fz_throw(gapp.ctx, FZ_ERROR_GENERIC, "cannot initialize X11 for multi-threading");
+#endif
+
+	xdpy = XOpenDisplay(NULL);
+	if (!xdpy)
+		fz_throw(gapp.ctx, FZ_ERROR_GENERIC, "cannot open display");
+
+	XA_CLIPBOARD = XInternAtom(xdpy, "CLIPBOARD", False);
+	XA_TARGETS = XInternAtom(xdpy, "TARGETS", False);
+	XA_TIMESTAMP = XInternAtom(xdpy, "TIMESTAMP", False);
+	XA_UTF8_STRING = XInternAtom(xdpy, "UTF8_STRING", False);
+	WM_DELETE_WINDOW = XInternAtom(xdpy, "WM_DELETE_WINDOW", False);
+	NET_WM_NAME = XInternAtom(xdpy, "_NET_WM_NAME", False);
+	NET_WM_STATE = XInternAtom(xdpy, "_NET_WM_STATE", False);
+	NET_WM_STATE_FULLSCREEN = XInternAtom(xdpy, "_NET_WM_STATE_FULLSCREEN", False);
+	WM_RELOAD_PAGE = XInternAtom(xdpy, "_WM_RELOAD_PAGE", False);
+
+	xscr = DefaultScreen(xdpy);
+
+	ximage_init(xdpy, xscr, DefaultVisual(xdpy, xscr));
+
+	xcarrow = XCreateFontCursor(xdpy, XC_left_ptr);
+	xchand = XCreateFontCursor(xdpy, XC_hand2);
+	xcwait = XCreateFontCursor(xdpy, XC_watch);
+	xccaret = XCreateFontCursor(xdpy, XC_xterm);
+
+	xbgcolor.red = 0x7000;
+	xbgcolor.green = 0x7000;
+	xbgcolor.blue = 0x7000;
+
+	XAllocColor(xdpy, DefaultColormap(xdpy, xscr), &xbgcolor);
+
+	xwin = XCreateWindow(xdpy, DefaultRootWindow(xdpy),
+		10, 10, 200, 100, 0,
+		ximage_get_depth(),
+		InputOutput,
+		ximage_get_visual(),
+		0,
+		NULL);
+	if (xwin == None)
+		fz_throw(gapp.ctx, FZ_ERROR_GENERIC, "cannot create window");
+
+	XSetWindowColormap(xdpy, xwin, ximage_get_colormap());
+	XSelectInput(xdpy, xwin,
+		StructureNotifyMask | ExposureMask | KeyPressMask |
+		PointerMotionMask | ButtonPressMask | ButtonReleaseMask);
+
+	mapped = 0;
+
+	xgc = XCreateGC(xdpy, xwin, 0, NULL);
+
+	XDefineCursor(xdpy, xwin, xcarrow);
+
+	wmhints = XAllocWMHints();
+	if (wmhints)
+	{
+		wmhints->flags = IconPixmapHint | IconMaskHint;
+		xicon = XCreateBitmapFromData(xdpy, xwin,
+			(char*)mupdf_icon_bitmap_16_bits,
+			mupdf_icon_bitmap_16_width,
+			mupdf_icon_bitmap_16_height);
+		xmask = XCreateBitmapFromData(xdpy, xwin,
+			(char*)mupdf_icon_bitmap_16_mask_bits,
+			mupdf_icon_bitmap_16_mask_width,
+			mupdf_icon_bitmap_16_mask_height);
+		if (xicon && xmask)
+		{
+			wmhints->icon_pixmap = xicon;
+			wmhints->icon_mask = xmask;
+			XSetWMHints(xdpy, xwin, wmhints);
+		}
+		XFree(wmhints);
+	}
+
+	classhint = XAllocClassHint();
+	if (classhint)
+	{
+		classhint->res_name = "mupdf";
+		classhint->res_class = "MuPDF";
+		XSetClassHint(xdpy, xwin, classhint);
+		XFree(classhint);
+	}
+
+	XSetWMProtocols(xdpy, xwin, &WM_DELETE_WINDOW, 1);
+
+	x11fd = ConnectionNumber(xdpy);
+}
+
+void winclose(pdfapp_t *app)
+{
+	if (pdfapp_preclose(app))
+	{
+		closing = 1;
+	}
+}
+
+int winsavequery(pdfapp_t *app)
+{
+	fprintf(stderr, "mupdf: discarded changes to document\n");
+	/* FIXME: temporary dummy implementation */
+	return DISCARD;
+}
+
+int wingetsavepath(pdfapp_t *app, char *buf, int len)
+{
+	/* FIXME: temporary dummy implementation */
+	return 0;
+}
+
+void winreplacefile(pdfapp_t *app, char *source, char *target)
+{
+	if (rename(source, target) == -1)
+		pdfapp_warn(app, "unable to rename file");
+}
+
+void wincopyfile(pdfapp_t *app, char *source, char *target)
+{
+	FILE *in, *out;
+	char buf[32 << 10];
+	size_t n;
+
+	in = fopen(source, "rb");
+	if (!in)
+	{
+		pdfapp_error(app, "cannot open source file for copying");
+		return;
+	}
+	out = fopen(target, "wb");
+	if (!out)
+	{
+		pdfapp_error(app, "cannot open target file for copying");
+		fclose(in);
+		return;
+	}
+
+	for (;;)
+	{
+		n = fread(buf, 1, sizeof buf, in);
+		fwrite(buf, 1, n, out);
+		if (n < sizeof buf)
+		{
+			if (ferror(in))
+				pdfapp_error(app, "cannot read data from source file");
+			break;
+		}
+	}
+
+	fclose(out);
+	fclose(in);
+}
+
+static void cleanup(pdfapp_t *app)
+{
+	fz_context *ctx = app->ctx;
+
+	pdfapp_close(app);
+
+	XDestroyWindow(xdpy, xwin);
+
+	XFreePixmap(xdpy, xicon);
+
+	XFreeCursor(xdpy, xccaret);
+	XFreeCursor(xdpy, xcwait);
+	XFreeCursor(xdpy, xchand);
+	XFreeCursor(xdpy, xcarrow);
+
+	XFreeGC(xdpy, xgc);
+
+	XCloseDisplay(xdpy);
+
+	fz_drop_context(ctx);
+}
+
+static int winresolution(void)
+{
+	return DisplayWidth(xdpy, xscr) * 25.4f /
+		DisplayWidthMM(xdpy, xscr) + 0.5f;
+}
+
+void wincursor(pdfapp_t *app, int curs)
+{
+	if (curs == ARROW)
+		XDefineCursor(xdpy, xwin, xcarrow);
+	if (curs == HAND)
+		XDefineCursor(xdpy, xwin, xchand);
+	if (curs == WAIT)
+		XDefineCursor(xdpy, xwin, xcwait);
+	if (curs == CARET)
+		XDefineCursor(xdpy, xwin, xccaret);
+	XFlush(xdpy);
+}
+
+void wintitle(pdfapp_t *app, char *s)
+{
+	XStoreName(xdpy, xwin, s);
+#ifdef X_HAVE_UTF8_STRING
+	Xutf8SetWMProperties(xdpy, xwin, s, s, NULL, 0, NULL, NULL, NULL);
+#else
+	XmbSetWMProperties(xdpy, xwin, s, s, NULL, 0, NULL, NULL, NULL);
+#endif
+	XChangeProperty(xdpy, xwin, NET_WM_NAME, XA_UTF8_STRING, 8,
+			PropModeReplace, (unsigned char *)s, strlen(s));
+}
+
+void winhelp(pdfapp_t *app)
+{
+	fprintf(stderr, "%s\n%s", pdfapp_version(app), pdfapp_usage(app));
+}
+
+void winresize(pdfapp_t *app, int w, int h)
+{
+	int image_w = gapp.layout_w;
+	int image_h = gapp.layout_h;
+	XWindowChanges values;
+	int mask, width, height;
+
+	if (gapp.image)
+	{
+		image_w = fz_pixmap_width(gapp.ctx, gapp.image);
+		image_h = fz_pixmap_height(gapp.ctx, gapp.image);
+	}
+
+	mask = CWWidth | CWHeight;
+	values.width = w;
+	values.height = h;
+	XConfigureWindow(xdpy, xwin, mask, &values);
+
+	reqw = w;
+	reqh = h;
+
+	if (!mapped)
+	{
+		gapp.winw = w;
+		gapp.winh = h;
+		width = -1;
+		height = -1;
+
+		XMapWindow(xdpy, xwin);
+		XFlush(xdpy);
+
+		while (1)
+		{
+			XNextEvent(xdpy, &xevt);
+			if (xevt.type == ConfigureNotify)
+			{
+				width = xevt.xconfigure.width;
+				height = xevt.xconfigure.height;
+			}
+			if (xevt.type == MapNotify)
+				break;
+		}
+
+		XSetForeground(xdpy, xgc, WhitePixel(xdpy, xscr));
+		XFillRectangle(xdpy, xwin, xgc, 0, 0, image_w, image_h);
+		XFlush(xdpy);
+
+		if (width != reqw || height != reqh)
+		{
+			gapp.shrinkwrap = 0;
+			dirty = 1;
+			pdfapp_onresize(&gapp, width, height);
+		}
+
+		mapped = 1;
+	}
+}
+
+void winfullscreen(pdfapp_t *app, int state)
+{
+	XEvent xev;
+	xev.xclient.type = ClientMessage;
+	xev.xclient.serial = 0;
+	xev.xclient.send_event = True;
+	xev.xclient.window = xwin;
+	xev.xclient.message_type = NET_WM_STATE;
+	xev.xclient.format = 32;
+	xev.xclient.data.l[0] = state;
+	xev.xclient.data.l[1] = NET_WM_STATE_FULLSCREEN;
+	xev.xclient.data.l[2] = 0;
+	XSendEvent(xdpy, DefaultRootWindow(xdpy), False,
+		SubstructureRedirectMask | SubstructureNotifyMask,
+		&xev);
+}
+
+static void fillrect(int x, int y, int w, int h)
+{
+	if (w > 0 && h > 0)
+		XFillRectangle(xdpy, xwin, xgc, x, y, w, h);
+}
+
+static void winblitstatusbar(pdfapp_t *app)
+{
+	if (gapp.issearching)
+	{
+		char buf[sizeof(gapp.search) + 50];
+		sprintf(buf, "Search: %s", gapp.search);
+		XSetForeground(xdpy, xgc, WhitePixel(xdpy, xscr));
+		fillrect(0, 0, gapp.winw, 30);
+		windrawstring(&gapp, 10, 20, buf);
+	}
+	else if (showingmessage)
+	{
+		XSetForeground(xdpy, xgc, WhitePixel(xdpy, xscr));
+		fillrect(0, 0, gapp.winw, 30);
+		windrawstring(&gapp, 10, 20, message);
+	}
+	else if (showingpage)
+	{
+		char buf[42];
+		snprintf(buf, sizeof buf, "Page %d/%d", gapp.pageno, gapp.pagecount);
+		windrawstringxor(&gapp, 10, 20, buf);
+	}
+}
+
+int winisresolutionacceptable(pdfapp_t *app, fz_matrix ctm)
+{
+	fz_rect bounds;
+	fz_irect ibounds;
+	int w, h;
+
+	bounds = fz_transform_rect(app->page_bbox, ctm);
+	ibounds = fz_round_rect(bounds);
+	w = ibounds.x1 - ibounds.x0;
+	h = ibounds.y1 - ibounds.y0;
+
+	return w < (INT_MAX / 4) / h;
+}
+
+static void winblit(pdfapp_t *app)
+{
+	if (gapp.image)
+	{
+		int image_w = fz_pixmap_width(gapp.ctx, gapp.image);
+		int image_h = fz_pixmap_height(gapp.ctx, gapp.image);
+		int image_n = fz_pixmap_components(gapp.ctx, gapp.image);
+		unsigned char *image_samples = fz_pixmap_samples(gapp.ctx, gapp.image);
+		int x0 = gapp.panx;
+		int y0 = gapp.pany;
+		int x1 = gapp.panx + image_w;
+		int y1 = gapp.pany + image_h;
+
+		if (app->invert)
+			XSetForeground(xdpy, xgc, BlackPixel(xdpy, DefaultScreen(xdpy)));
+		else
+			XSetForeground(xdpy, xgc, xbgcolor.pixel);
+		fillrect(0, 0, x0, gapp.winh);
+		fillrect(x1, 0, gapp.winw - x1, gapp.winh);
+		fillrect(0, 0, gapp.winw, y0);
+		fillrect(0, y1, gapp.winw, gapp.winh - y1);
+
+		if (gapp.iscopying || justcopied)
+		{
+			pdfapp_invert(&gapp, gapp.selr);
+			justcopied = 1;
+		}
+
+		pdfapp_inverthit(&gapp);
+
+		if (image_n == 4)
+			ximage_blit(xwin, xgc,
+				x0, y0,
+				image_samples,
+				0, 0,
+				image_w,
+				image_h,
+				image_w * image_n);
+		else if (image_n == 2)
+		{
+			int i = image_w*image_h;
+			unsigned char *color = malloc(i*4);
+			if (color)
+			{
+				unsigned char *s = image_samples;
+				unsigned char *d = color;
+				for (; i > 0 ; i--)
+				{
+					d[2] = d[1] = d[0] = *s++;
+					d[3] = *s++;
+					d += 4;
+				}
+				ximage_blit(xwin, xgc,
+					x0, y0,
+					color,
+					0, 0,
+					image_w,
+					image_h,
+					image_w * 4);
+				free(color);
+			}
+		}
+
+		pdfapp_inverthit(&gapp);
+
+		if (gapp.iscopying || justcopied)
+		{
+			pdfapp_invert(&gapp, gapp.selr);
+			justcopied = 1;
+		}
+	}
+	else
+	{
+		XSetForeground(xdpy, xgc, xbgcolor.pixel);
+		fillrect(0, 0, gapp.winw, gapp.winh);
+	}
+
+	winblitstatusbar(app);
+}
+
+void winrepaint(pdfapp_t *app)
+{
+	dirty = 1;
+	if (app->in_transit)
+		transition_dirty = 1;
+}
+
+void winrepaintsearch(pdfapp_t *app)
+{
+	dirtysearch = 1;
+}
+
+void winadvancetimer(pdfapp_t *app, float duration)
+{
+	struct timeval now;
+
+	gettimeofday(&now, NULL);
+	memset(&tmo_advance, 0, sizeof(tmo_advance));
+	tmo_advance.tv_sec = (int)duration;
+	tmo_advance.tv_usec = 1000000 * (duration - tmo_advance.tv_sec);
+	timeradd(&tmo_advance, &now, &tmo_advance);
+	advance_scheduled = 1;
+}
+
+static void windrawstringxor(pdfapp_t *app, int x, int y, char *s)
+{
+	int prevfunction;
+	XGCValues xgcv;
+
+	XGetGCValues(xdpy, xgc, GCFunction, &xgcv);
+	prevfunction = xgcv.function;
+	xgcv.function = GXxor;
+	XChangeGC(xdpy, xgc, GCFunction, &xgcv);
+
+	XSetForeground(xdpy, xgc, WhitePixel(xdpy, DefaultScreen(xdpy)));
+
+	XDrawString(xdpy, xwin, xgc, x, y, s, strlen(s));
+	XFlush(xdpy);
+
+	XGetGCValues(xdpy, xgc, GCFunction, &xgcv);
+	xgcv.function = prevfunction;
+	XChangeGC(xdpy, xgc, GCFunction, &xgcv);
+}
+
+void windrawstring(pdfapp_t *app, int x, int y, char *s)
+{
+	XSetForeground(xdpy, xgc, BlackPixel(xdpy, DefaultScreen(xdpy)));
+	XDrawString(xdpy, xwin, xgc, x, y, s, strlen(s));
+}
+
+static void docopy(pdfapp_t *app, Atom copy_target)
+{
+	unsigned short copyucs2[16 * 1024];
+	char *latin1 = copylatin1;
+	char *utf8 = copyutf8;
+	unsigned short *ucs2;
+	int ucs;
+
+	pdfapp_oncopy(&gapp, copyucs2, 16 * 1024);
+
+	for (ucs2 = copyucs2; ucs2[0] != 0; ucs2++)
+	{
+		ucs = ucs2[0];
+
+		utf8 += fz_runetochar(utf8, ucs);
+
+		if (ucs < 256)
+			*latin1++ = ucs;
+		else
+			*latin1++ = '?';
+	}
+
+	*utf8 = 0;
+	*latin1 = 0;
+
+	XSetSelectionOwner(xdpy, copy_target, xwin, copytime);
+
+	justcopied = 1;
+}
+
+void windocopy(pdfapp_t *app)
+{
+	docopy(app, XA_PRIMARY);
+}
+
+static void onselreq(Window requestor, Atom selection, Atom target, Atom property, Time time)
+{
+	XEvent nevt;
+
+	advance_scheduled = 0;
+
+	if (property == None)
+		property = target;
+
+	nevt.xselection.type = SelectionNotify;
+	nevt.xselection.send_event = True;
+	nevt.xselection.display = xdpy;
+	nevt.xselection.requestor = requestor;
+	nevt.xselection.selection = selection;
+	nevt.xselection.target = target;
+	nevt.xselection.property = property;
+	nevt.xselection.time = time;
+
+	if (target == XA_TARGETS)
+	{
+		Atom atomlist[4];
+		atomlist[0] = XA_TARGETS;
+		atomlist[1] = XA_TIMESTAMP;
+		atomlist[2] = XA_STRING;
+		atomlist[3] = XA_UTF8_STRING;
+		XChangeProperty(xdpy, requestor, property, target,
+			32, PropModeReplace,
+			(unsigned char *)atomlist, sizeof(atomlist)/sizeof(Atom));
+	}
+
+	else if (target == XA_STRING)
+	{
+		XChangeProperty(xdpy, requestor, property, target,
+			8, PropModeReplace,
+			(unsigned char *)copylatin1, strlen(copylatin1));
+	}
+
+	else if (target == XA_UTF8_STRING)
+	{
+		XChangeProperty(xdpy, requestor, property, target,
+			8, PropModeReplace,
+			(unsigned char *)copyutf8, strlen(copyutf8));
+	}
+
+	else
+	{
+		nevt.xselection.property = None;
+	}
+
+	XSendEvent(xdpy, requestor, False, 0, &nevt);
+}
+
+void winreloadpage(pdfapp_t *app)
+{
+	XEvent xev;
+	Display *dpy = XOpenDisplay(NULL);
+
+	xev.xclient.type = ClientMessage;
+	xev.xclient.serial = 0;
+	xev.xclient.send_event = True;
+	xev.xclient.window = xwin;
+	xev.xclient.message_type = WM_RELOAD_PAGE;
+	xev.xclient.format = 32;
+	xev.xclient.data.l[0] = 0;
+	xev.xclient.data.l[1] = 0;
+	xev.xclient.data.l[2] = 0;
+	XSendEvent(dpy, xwin, 0, 0, &xev);
+	XCloseDisplay(dpy);
+}
+
+void winopenuri(pdfapp_t *app, char *buf)
+{
+	char *browser = getenv("BROWSER");
+	pid_t pid;
+	if (!browser)
+	{
+#ifdef __APPLE__
+		browser = "open";
+#else
+		browser = "xdg-open";
+#endif
+	}
+	/* Fork once to start a child process that we wait on. This
+	 * child process forks again and immediately exits. The
+	 * grandchild process continues in the background. The purpose
+	 * of this strange two-step is to avoid zombie processes. See
+	 * bug 695701 for an explanation. */
+	pid = fork();
+	if (pid == 0)
+	{
+		if (fork() == 0)
+		{
+			execlp(browser, browser, buf, (char*)0);
+			fprintf(stderr, "cannot exec '%s'\n", browser);
+		}
+		_exit(0);
+	}
+	waitpid(pid, NULL, 0);
+}
+
+int winquery(pdfapp_t *app, const char *query)
+{
+	return QUERY_NO;
+}
+
+int wingetcertpath(pdfapp_t *app, char *buf, int len)
+{
+	return 0;
+}
+
+static void onkey(int c, int modifiers)
+{
+	advance_scheduled = 0;
+
+	if (justcopied)
+	{
+		justcopied = 0;
+		winrepaint(&gapp);
+	}
+
+	if (!gapp.issearching && c == 'P')
+	{
+		struct timeval now;
+		struct timeval t;
+		t.tv_sec = 2;
+		t.tv_usec = 0;
+		gettimeofday(&now, NULL);
+		timeradd(&now, &t, &tmo_at);
+		showingpage = 1;
+		winrepaint(&gapp);
+		return;
+	}
+
+	pdfapp_onkey(&gapp, c, modifiers);
+
+	if (gapp.issearching)
+	{
+		showingpage = 0;
+		showingmessage = 0;
+	}
+}
+
+static void onmouse(int x, int y, int btn, int modifiers, int state)
+{
+	if (state != 0)
+		advance_scheduled = 0;
+
+	if (state != 0 && justcopied)
+	{
+		justcopied = 0;
+		winrepaint(&gapp);
+	}
+
+	pdfapp_onmouse(&gapp, x, y, btn, modifiers, state);
+}
+
+static void signal_handler(int signal)
+{
+	if (signal == SIGHUP)
+		reloading = 1;
+}
+
+static void usage(const char *argv0)
+{
+	fprintf(stderr, "usage: %s [options] file.pdf [page]\n", argv0);
+	fprintf(stderr, "\t-p -\tpassword\n");
+	fprintf(stderr, "\t-r -\tresolution\n");
+	fprintf(stderr, "\t-A -\tset anti-aliasing quality in bits (0=off, 8=best)\n");
+	fprintf(stderr, "\t-C -\tRRGGBB (tint color in hexadecimal syntax)\n");
+	fprintf(stderr, "\t-W -\tpage width for EPUB layout\n");
+	fprintf(stderr, "\t-H -\tpage height for EPUB layout\n");
+	fprintf(stderr, "\t-I -\tinvert colors\n");
+	fprintf(stderr, "\t-S -\tfont size for EPUB layout\n");
+	fprintf(stderr, "\t-U -\tuser style sheet for EPUB layout\n");
+	fprintf(stderr, "\t-X\tdisable document styles for EPUB layout\n");
+	exit(1);
+}
+
+int main(int argc, char **argv)
+{
+	int c;
+	int len;
+	char buf[128];
+	KeySym keysym;
+	int oldx = 0;
+	int oldy = 0;
+	int resolution = -1;
+	int pageno = 1;
+	fd_set fds;
+	int width = -1;
+	int height = -1;
+	fz_context *ctx;
+	struct timeval now;
+	struct timeval *timeout;
+	struct timeval tmo_advance_delay;
+	char *profile_name = NULL;
+	int kbps = 0;
+
+	ctx = fz_new_context(NULL, NULL, FZ_STORE_DEFAULT);
+	if (!ctx)
+	{
+		fprintf(stderr, "cannot initialise context\n");
+		exit(1);
+	}
+
+	pdfapp_init(ctx, &gapp);
+
+	while ((c = fz_getopt(argc, argv, "Ip:r:A:C:W:H:S:U:Xb:c:")) != -1)
+	{
+		switch (c)
+		{
+		case 'C':
+			c = strtol(fz_optarg, NULL, 16);
+			gapp.tint = 1;
+			gapp.tint_white = c;
+			break;
+		case 'p': password = fz_optarg; break;
+		case 'r': resolution = atoi(fz_optarg); break;
+		case 'I': gapp.invert = 1; break;
+		case 'A': fz_set_aa_level(ctx, atoi(fz_optarg)); break;
+		case 'c': profile_name = fz_optarg; break;
+		case 'W': gapp.layout_w = fz_atof(fz_optarg); break;
+		case 'H': gapp.layout_h = fz_atof(fz_optarg); break;
+		case 'S': gapp.layout_em = fz_atof(fz_optarg); break;
+		case 'U': gapp.layout_css = fz_optarg; break;
+		case 'X': gapp.layout_use_doc_css = 0; break;
+		case 'b': kbps = fz_atoi(fz_optarg); break;
+		default: usage(argv[0]);
+		}
+	}
+
+	if (argc - fz_optind == 0)
+		usage(argv[0]);
+
+	filename = argv[fz_optind++];
+
+	if (argc - fz_optind == 1)
+		pageno = atoi(argv[fz_optind++]);
+
+	winopen();
+
+	if (resolution == -1)
+		resolution = winresolution();
+	if (resolution < MINRES)
+		resolution = MINRES;
+	if (resolution > MAXRES)
+		resolution = MAXRES;
+
+	gapp.transitions_enabled = 1;
+	gapp.scrw = DisplayWidth(xdpy, xscr);
+	gapp.scrh = DisplayHeight(xdpy, xscr);
+	gapp.default_resolution = resolution;
+	gapp.resolution = resolution;
+	gapp.pageno = pageno;
+
+	if (profile_name)
+		pdfapp_load_profile(&gapp, profile_name);
+
+	tmo_at.tv_sec = 0;
+	tmo_at.tv_usec = 0;
+	timeout = NULL;
+
+	if (kbps)
+		pdfapp_open_progressive(&gapp, filename, 0, kbps);
+	else
+		pdfapp_open(&gapp, filename, 0);
+
+	FD_ZERO(&fds);
+
+	signal(SIGHUP, signal_handler);
+
+	while (!closing)
+	{
+		while (!closing && XPending(xdpy) && !transition_dirty)
+		{
+			XNextEvent(xdpy, &xevt);
+
+			switch (xevt.type)
+			{
+			case Expose:
+				dirty = 1;
+				break;
+
+			case ConfigureNotify:
+				if (gapp.image)
+				{
+					if (xevt.xconfigure.width != reqw ||
+						xevt.xconfigure.height != reqh)
+						gapp.shrinkwrap = 0;
+				}
+				width = xevt.xconfigure.width;
+				height = xevt.xconfigure.height;
+
+				break;
+
+			case KeyPress:
+				len = XLookupString(&xevt.xkey, buf, sizeof buf, &keysym, NULL);
+
+				if (!gapp.issearching)
+					switch (keysym)
+					{
+					case XK_Escape:
+						len = 1; buf[0] = '\033';
+						break;
+
+					case XK_Up:
+					case XK_KP_Up:
+						len = 1; buf[0] = 'k';
+						break;
+					case XK_Down:
+					case XK_KP_Down:
+						len = 1; buf[0] = 'j';
+						break;
+
+					case XK_Left:
+					case XK_KP_Left:
+						len = 1; buf[0] = 'h';
+						break;
+					case XK_Right:
+					case XK_KP_Right:
+						len = 1; buf[0] = 'l';
+						break;
+
+					case XK_Page_Up:
+					case XK_KP_Page_Up:
+					case XF86XK_Back:
+						len = 1; buf[0] = ',';
+						break;
+					case XK_Page_Down:
+					case XK_KP_Page_Down:
+					case XF86XK_Forward:
+						len = 1; buf[0] = '.';
+						break;
+					}
+				if (xevt.xkey.state & ControlMask && keysym == XK_c)
+					docopy(&gapp, XA_CLIPBOARD);
+				else if (len)
+					onkey(buf[0], xevt.xkey.state);
+
+				onmouse(oldx, oldy, 0, 0, 0);
+
+				break;
+
+			case MotionNotify:
+				oldx = xevt.xmotion.x;
+				oldy = xevt.xmotion.y;
+				onmouse(xevt.xmotion.x, xevt.xmotion.y, 0, xevt.xmotion.state, 0);
+				break;
+
+			case ButtonPress:
+				onmouse(xevt.xbutton.x, xevt.xbutton.y, xevt.xbutton.button, xevt.xbutton.state, 1);
+				break;
+
+			case ButtonRelease:
+				copytime = xevt.xbutton.time;
+				onmouse(xevt.xbutton.x, xevt.xbutton.y, xevt.xbutton.button, xevt.xbutton.state, -1);
+				break;
+
+			case SelectionRequest:
+				onselreq(xevt.xselectionrequest.requestor,
+					xevt.xselectionrequest.selection,
+					xevt.xselectionrequest.target,
+					xevt.xselectionrequest.property,
+					xevt.xselectionrequest.time);
+				break;
+
+			case ClientMessage:
+				if (xevt.xclient.message_type == WM_RELOAD_PAGE)
+					pdfapp_reloadpage(&gapp);
+				else if (xevt.xclient.format == 32 && ((Atom) xevt.xclient.data.l[0]) == WM_DELETE_WINDOW)
+					closing = 1;
+				break;
+			}
+		}
+
+		if (closing)
+			continue;
+
+		if (width != -1 || height != -1)
+		{
+			pdfapp_onresize(&gapp, width, height);
+			width = -1;
+			height = -1;
+		}
+
+		if (dirty || dirtysearch)
+		{
+			if (dirty)
+				winblit(&gapp);
+			else if (dirtysearch)
+				winblitstatusbar(&gapp);
+			dirty = 0;
+			transition_dirty = 0;
+			dirtysearch = 0;
+			pdfapp_postblit(&gapp);
+		}
+
+		if (!showingpage && !showingmessage && (tmo_at.tv_sec || tmo_at.tv_usec))
+		{
+			tmo_at.tv_sec = 0;
+			tmo_at.tv_usec = 0;
+			timeout = NULL;
+		}
+
+		if (XPending(xdpy) || transition_dirty)
+			continue;
+
+		timeout = NULL;
+
+		if (tmo_at.tv_sec || tmo_at.tv_usec)
+		{
+			gettimeofday(&now, NULL);
+			timersub(&tmo_at, &now, &tmo);
+			if (tmo.tv_sec <= 0)
+			{
+				tmo_at.tv_sec = 0;
+				tmo_at.tv_usec = 0;
+				timeout = NULL;
+				showingpage = 0;
+				showingmessage = 0;
+				winrepaint(&gapp);
+			}
+			else
+				timeout = &tmo;
+		}
+
+		if (advance_scheduled)
+		{
+			gettimeofday(&now, NULL);
+			timersub(&tmo_advance, &now, &tmo_advance_delay);
+			if (tmo_advance_delay.tv_sec <= 0)
+			{
+				/* Too late already */
+				onkey(' ', 0);
+				onmouse(oldx, oldy, 0, 0, 0);
+				advance_scheduled = 0;
+			}
+			else if (timeout == NULL)
+			{
+				timeout = &tmo_advance_delay;
+			}
+			else
+			{
+				struct timeval tmp;
+				timersub(&tmo_advance_delay, timeout, &tmp);
+				if (tmp.tv_sec < 0)
+				{
+					timeout = &tmo_advance_delay;
+				}
+			}
+		}
+
+		FD_SET(x11fd, &fds);
+		if (select(x11fd + 1, &fds, NULL, NULL, timeout) < 0)
+		{
+			if (reloading)
+			{
+				pdfapp_reloadfile(&gapp);
+				reloading = 0;
+			}
+		}
+		if (!FD_ISSET(x11fd, &fds))
+		{
+			if (timeout == &tmo_advance_delay)
+			{
+				onkey(' ', 0);
+				onmouse(oldx, oldy, 0, 0, 0);
+				advance_scheduled = 0;
+			}
+			else
+			{
+				tmo_at.tv_sec = 0;
+				tmo_at.tv_usec = 0;
+				timeout = NULL;
+				showingpage = 0;
+				showingmessage = 0;
+				winrepaint(&gapp);
+			}
+		}
+	}
+
+	cleanup(&gapp);
+
+	return 0;
+}