diff mupdf-source/source/pdf/pdf-appearance.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/source/pdf/pdf-appearance.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,3912 @@
+// 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 "mupdf/fitz.h"
+#include "pdf-annot-imp.h"
+#include "mupdf/ucdn.h"
+
+#include <float.h>
+#include <limits.h>
+#include <string.h>
+#include <time.h>
+
+#include <stdio.h>
+
+#include "annotation-icons.h"
+
+/* #define PDF_DEBUG_APPEARANCE_SYNTHESIS */
+
+#define REPLACEMENT 0xB7
+#define CIRCLE_MAGIC 0.551915f
+
+static fz_point rect_center(const fz_rect rect)
+{
+	fz_point c;
+	c.x = (rect.x0 + rect.x1) / 2.0f;
+	c.y = (rect.y0 + rect.y1) / 2.0f;
+	return c;
+}
+
+static fz_matrix center_rect_within_rect(const fz_rect tofit, const fz_rect within)
+{
+	float xscale = (within.x1 - within.x0) / (tofit.x1 - tofit.x0);
+	float yscale = (within.y1 - within.y0) / (tofit.y1 - tofit.y0);
+	float scale = fz_min(xscale, yscale);
+	fz_point tofit_center;
+	fz_point within_center;
+
+	within_center = rect_center(within);
+	tofit_center = rect_center(tofit);
+
+	/* Translate "tofit" to be centered on the origin
+	 * Scale "tofit" to a size that fits within "within"
+	 * Translate "tofit" to "within's" center
+	 * Do all the above in reverse order so that we can use the fz_pre_xx functions */
+	return fz_pre_translate(fz_pre_scale(fz_translate(within_center.x, within_center.y), scale, -scale), -tofit_center.x, -tofit_center.y);
+}
+
+static void
+draw_circle(fz_context *ctx, fz_buffer *buf, float rx, float ry, float cx, float cy)
+{
+	float mx = rx * CIRCLE_MAGIC;
+	float my = ry * CIRCLE_MAGIC;
+	fz_append_printf(ctx, buf, "%g %g m\n", cx, cy+ry);
+	fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", cx+mx, cy+ry, cx+rx, cy+my, cx+rx, cy);
+	fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", cx+rx, cy-my, cx+mx, cy-ry, cx, cy-ry);
+	fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", cx-mx, cy-ry, cx-rx, cy-my, cx-rx, cy);
+	fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", cx-rx, cy+my, cx-mx, cy+ry, cx, cy+ry);
+}
+
+static void
+draw_circle_in_box(fz_context *ctx, fz_buffer *buf, float lw, float x0, float y0, float x1, float y1)
+{
+	float rx = (x1 - x0) / 2 - lw/2;
+	float ry = (y1 - y0) / 2 - lw/2;
+	float cx = x0 + lw/2 + rx;
+	float cy = y0 + lw/2 + ry;
+	draw_circle(ctx, buf, rx, ry, cx, cy);
+}
+
+static void
+draw_arc_seg(fz_context *ctx, fz_buffer *buf, float r, float xc, float yc, float th0, float th1, int move)
+{
+	float x1 = xc + r * cosf(th0);
+	float y1 = yc + r * sinf(th0);
+	float x4 = xc + r * cosf(th1);
+	float y4 = yc + r * sinf(th1);
+
+	float ax = x1 - xc;
+	float ay = y1 - yc;
+	float bx = x4 - xc;
+	float by = y4 - yc;
+	float q1 = ax * ax + ay * ay;
+	float q2 = q1 + ax * bx + ay * by;
+	float k2 = (4.0f/3.0f) * (sqrtf(2 * q1 * q2) - q2) / (ax * by - ay * bx);
+
+	float x2 = xc + ax - k2 * ay;
+	float y2 = yc + ay + k2 * ax;
+	float x3 = xc + bx + k2 * by;
+	float y3 = yc + by - k2 * bx;
+
+	if (move)
+		fz_append_printf(ctx, buf, "%g %g m\n", x1, y1);
+	fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", x2, y2, x3, y3, x4, y4);
+}
+
+static void
+draw_arc(fz_context *ctx, fz_buffer *buf, float r, float xc, float yc, float th0, float th1, int move)
+{
+	float d = th0 - th1;
+	if (d > FZ_PI / 4)
+	{
+		draw_arc(ctx, buf, r, xc, yc, th0, th0 - d / 2, move);
+		draw_arc(ctx, buf, r, xc, yc, th0 - d / 2, th1, 0);
+	}
+	else
+	{
+		draw_arc_seg(ctx, buf, r, xc, yc, th0, th1, move);
+	}
+}
+
+static void
+draw_arc_tail(fz_context *ctx, fz_buffer *buf, float r, float xc, float yc, float th0, float th1, int fill)
+{
+	draw_arc_seg(ctx, buf, r, xc, yc, th0, th1, 0);
+	if (fill)
+		draw_arc_seg(ctx, buf, r, xc, yc, th1, th0, 0);
+}
+
+static void
+pdf_write_opacity_blend_mode(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, pdf_obj **res, int bm)
+{
+	pdf_obj *res_egs, *res_egs_h;
+	float opacity = pdf_annot_opacity(ctx, annot);
+
+	if (bm == FZ_BLEND_NORMAL && opacity == 1)
+		return;
+
+	/* /Resources << /ExtGState << /H << /Type/ExtGState /BM/Multiply /CA %g /ca %g >> >> >> */
+
+	if (!*res)
+		*res = pdf_new_dict(ctx, annot->page->doc, 1);
+
+	res_egs = pdf_dict_put_dict(ctx, *res, PDF_NAME(ExtGState), 1);
+	res_egs_h = pdf_dict_put_dict(ctx, res_egs, PDF_NAME(H), 2);
+	pdf_dict_put(ctx, res_egs_h, PDF_NAME(Type), PDF_NAME(ExtGState));
+
+	if (bm == FZ_BLEND_MULTIPLY)
+	{
+		pdf_dict_put(ctx, res_egs_h, PDF_NAME(BM), PDF_NAME(Multiply));
+	}
+
+	if (opacity < 1)
+	{
+		pdf_dict_put_real(ctx, res_egs_h, PDF_NAME(CA), opacity);
+		pdf_dict_put_real(ctx, res_egs_h, PDF_NAME(ca), opacity);
+	}
+
+	fz_append_printf(ctx, buf, "/H gs\n");
+}
+
+static void
+pdf_write_opacity(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, pdf_obj **res)
+{
+	pdf_write_opacity_blend_mode(ctx, annot, buf, res, FZ_BLEND_NORMAL);
+}
+
+static void
+pdf_write_dash_pattern(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, pdf_obj **res)
+{
+	int count = pdf_annot_border_dash_count(ctx, annot);
+	int i;
+
+	if (count == 0)
+		return;
+
+	fz_append_printf(ctx, buf, "[");
+	for (i = 0; i < count; ++i)
+	{
+		float length = pdf_annot_border_dash_item(ctx, annot, i);
+		if (i == 0)
+			fz_append_printf(ctx, buf, "%g", length);
+		else
+			fz_append_printf(ctx, buf, " %g", length);
+	}
+	fz_append_printf(ctx, buf, "]0 d\n");
+}
+
+static float pdf_write_border_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf)
+{
+	float w = pdf_annot_border_width(ctx, annot);
+	fz_append_printf(ctx, buf, "%g w\n", w);
+	return w;
+}
+
+static int
+write_color(fz_context *ctx, fz_buffer *buf, int n, float *color, int stroke)
+{
+	if (n == 4)
+		fz_append_printf(ctx, buf, "%g %g %g %g %c\n", color[0], color[1], color[2], color[3], stroke ? 'K' : 'k');
+	else if (n == 3)
+		fz_append_printf(ctx, buf, "%g %g %g %s\n", color[0], color[1], color[2], stroke ? "RG" : "rg");
+	else if (n == 1)
+		fz_append_printf(ctx, buf, "%g %c\n", color[0], stroke ? 'G' : 'g');
+	else
+		return 0;
+	return 1;
+}
+
+static void
+write_color0(fz_context *ctx, fz_buffer *buf, int n, float *color, int stroke)
+{
+	if (n == 0)
+		fz_append_printf(ctx, buf, "0 %c\n", stroke ? 'G' : 'g');
+	else
+		write_color(ctx, buf, n, color, stroke);
+}
+
+
+static int pdf_write_stroke_color_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf)
+{
+	float color[4];
+	int n;
+	pdf_annot_color(ctx, annot, &n, color);
+	return write_color(ctx, buf, n, color, 1);
+}
+
+static int pdf_is_dark_fill_color(fz_context *ctx, pdf_annot *annot)
+{
+	float color[4], gray;
+	int n;
+	pdf_annot_color(ctx, annot, &n, color);
+	switch (n)
+	{
+	default:
+		gray = 1;
+		break;
+	case 1:
+		gray = color[0];
+		break;
+	case 3:
+		gray = color[0] * 0.3f + color[1] * 0.59f + color[2] * 0.11f;
+		break;
+	case 4:
+		gray = color[0] * 0.3f + color[1] * 0.59f + color[2] * 0.11f + color[3];
+		gray = 1 - fz_min(gray, 1);
+		break;
+	}
+	return gray < 0.25f;
+}
+
+static int pdf_write_fill_color_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf)
+{
+	float color[4];
+	int n;
+	pdf_annot_color(ctx, annot, &n, color);
+	return write_color(ctx, buf, n, color, 0);
+}
+
+static int pdf_write_interior_fill_color_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf)
+{
+	float color[4];
+	int n;
+	pdf_annot_interior_color(ctx, annot, &n, color);
+	return write_color(ctx, buf, n, color, 0);
+}
+
+static int pdf_write_MK_BG_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf)
+{
+	float color[4];
+	int n;
+	pdf_annot_MK_BG(ctx, annot, &n, color);
+	return write_color(ctx, buf, n, color, 0);
+}
+
+static int pdf_write_MK_BC_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf)
+{
+	float color[4];
+	int n;
+	pdf_annot_MK_BC(ctx, annot, &n, color);
+	return write_color(ctx, buf, n, color, 1);
+}
+
+static void maybe_stroke_and_fill(fz_context *ctx, fz_buffer *buf, int sc, int ic)
+{
+	if (sc)
+		fz_append_string(ctx, buf, ic ? "b\n" : "S\n");
+	else
+		fz_append_string(ctx, buf, ic ? "f\n" : "n\n");
+}
+
+static void maybe_stroke(fz_context *ctx, fz_buffer *buf, int sc)
+{
+	fz_append_string(ctx, buf, sc ? "S\n" : "n\n");
+}
+
+static fz_point rotate_vector(float angle, float x, float y)
+{
+	float ca = cosf(angle);
+	float sa = sinf(angle);
+	return fz_make_point(x*ca - y*sa, x*sa + y*ca);
+}
+
+static void pdf_write_arrow_appearance(fz_context *ctx, fz_buffer *buf, fz_rect *rect, float x, float y, float dx, float dy, float w, int close)
+{
+	float r = fz_max(1, w);
+	float angle = atan2f(dy, dx);
+	fz_point v, a, b;
+
+	v = rotate_vector(angle, 8.8f*r, 4.5f*r);
+	a = fz_make_point(x + v.x, y + v.y);
+	v = rotate_vector(angle, 8.8f*r, -4.5f*r);
+	b = fz_make_point(x + v.x, y + v.y);
+
+	*rect = fz_include_point_in_rect(*rect, a);
+	*rect = fz_include_point_in_rect(*rect, b);
+	*rect = fz_expand_rect(*rect, w);
+
+	fz_append_printf(ctx, buf, "%g %g m\n", a.x, a.y);
+	fz_append_printf(ctx, buf, "%g %g l\n", x, y);
+	fz_append_printf(ctx, buf, "%g %g l\n", b.x, b.y);
+	if (close)
+		fz_append_printf(ctx, buf, "h\n");
+}
+
+static void include_cap(fz_rect *rect, float x, float y, float r)
+{
+	rect->x0 = fz_min(rect->x0, x-r);
+	rect->y0 = fz_min(rect->y0, y-r);
+	rect->x1 = fz_max(rect->x1, x+r);
+	rect->y1 = fz_max(rect->y1, y+r);
+}
+
+static void
+pdf_write_line_cap_appearance(fz_context *ctx, fz_buffer *buf, fz_rect *rect,
+		float x, float y, float dx, float dy, float w,
+		int sc, int ic, pdf_obj *cap)
+{
+	float l = sqrtf(dx*dx + dy*dy);
+	dx = dx / l;
+	dy = dy / l;
+
+	if (cap == PDF_NAME(Square))
+	{
+		float r = fz_max(3.0f, w * 3.0f);
+		fz_append_printf(ctx, buf, "%g %g %g %g re\n", x-r, y-r, r*2, r*2);
+		maybe_stroke_and_fill(ctx, buf, sc, ic);
+		include_cap(rect, x, y, r + w/2);
+	}
+	else if (cap == PDF_NAME(Circle))
+	{
+		float r = fz_max(3.0f, w * 3.0f);
+		float m = r * CIRCLE_MAGIC;
+		fz_append_printf(ctx, buf, "%g %g m\n", x, y+r);
+		fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", x+m, y+r, x+r, y+m, x+r, y);
+		fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", x+r, y-m, x+m, y-r, x, y-r);
+		fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", x-m, y-r, x-r, y-m, x-r, y);
+		fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n", x-r, y+m, x-m, y+r, x, y+r);
+		maybe_stroke_and_fill(ctx, buf, sc, ic);
+		include_cap(rect, x, y, r + w/2);
+	}
+	else if (cap == PDF_NAME(Diamond))
+	{
+		float r = fz_max(3.0f, w * 3.0f);
+		fz_append_printf(ctx, buf, "%g %g m\n", x, y+r);
+		fz_append_printf(ctx, buf, "%g %g l\n", x+r, y);
+		fz_append_printf(ctx, buf, "%g %g l\n", x, y-r);
+		fz_append_printf(ctx, buf, "%g %g l\n", x-r, y);
+		fz_append_printf(ctx, buf, "h\n");
+		maybe_stroke_and_fill(ctx, buf, sc, ic);
+		include_cap(rect, x, y, r + w/sqrtf(2));
+	}
+	else if (cap == PDF_NAME(OpenArrow))
+	{
+		pdf_write_arrow_appearance(ctx, buf, rect, x, y, dx, dy, w, 0);
+		maybe_stroke(ctx, buf, sc);
+	}
+	else if (cap == PDF_NAME(ClosedArrow))
+	{
+		pdf_write_arrow_appearance(ctx, buf, rect, x, y, dx, dy, w, 1);
+		maybe_stroke_and_fill(ctx, buf, sc, ic);
+	}
+	/* PDF 1.5 */
+	else if (cap == PDF_NAME(Butt))
+	{
+		float r = fz_max(3, w * 3);
+		fz_point a = { x-dy*r, y+dx*r };
+		fz_point b = { x+dy*r, y-dx*r };
+		fz_append_printf(ctx, buf, "%g %g m\n", a.x, a.y);
+		fz_append_printf(ctx, buf, "%g %g l\n", b.x, b.y);
+		maybe_stroke(ctx, buf, sc);
+		*rect = fz_include_point_in_rect(*rect, a);
+		*rect = fz_include_point_in_rect(*rect, b);
+		*rect = fz_expand_rect(*rect, w);
+	}
+	else if (cap == PDF_NAME(ROpenArrow))
+	{
+		pdf_write_arrow_appearance(ctx, buf, rect, x, y, -dx, -dy, w, 0);
+		maybe_stroke(ctx, buf, sc);
+	}
+	else if (cap == PDF_NAME(RClosedArrow))
+	{
+		pdf_write_arrow_appearance(ctx, buf, rect, x, y, -dx, -dy, w, 1);
+		maybe_stroke_and_fill(ctx, buf, sc, ic);
+	}
+	/* PDF 1.6 */
+	else if (cap == PDF_NAME(Slash))
+	{
+		float r = fz_max(5, w * 5);
+		float angle = atan2f(dy, dx) - (30 * FZ_PI / 180);
+		fz_point a, b, v;
+		v = rotate_vector(angle, 0, r);
+		a = fz_make_point(x + v.x, y + v.y);
+		v = rotate_vector(angle, 0, -r);
+		b = fz_make_point(x + v.x, y + v.y);
+		fz_append_printf(ctx, buf, "%g %g m\n", a.x, a.y);
+		fz_append_printf(ctx, buf, "%g %g l\n", b.x, b.y);
+		maybe_stroke(ctx, buf, sc);
+		*rect = fz_include_point_in_rect(*rect, a);
+		*rect = fz_include_point_in_rect(*rect, b);
+		*rect = fz_expand_rect(*rect, w);
+	}
+}
+
+static float pdf_write_line_caption(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res, fz_point a, fz_point b);
+
+static void
+pdf_write_line_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	pdf_obj *line, *le;
+	float w;
+	fz_point a, b, aa, ab, ba, bb;
+	float dx, dy, line_length, ll, lle, llo;
+	int sc;
+	int ic;
+
+	// The start and end point of the actual line (a, b)
+	// The start and end points of the leader line (aa, ab, ba, bb)
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_dash_pattern(ctx, annot, buf, res);
+	w = pdf_write_border_appearance(ctx, annot, buf);
+	sc = pdf_write_stroke_color_appearance(ctx, annot, buf);
+	ic = pdf_write_interior_fill_color_appearance(ctx, annot, buf);
+
+	line = pdf_dict_get(ctx, annot->obj, PDF_NAME(L));
+	a.x = pdf_array_get_real(ctx, line, 0);
+	a.y = pdf_array_get_real(ctx, line, 1);
+	b.x = pdf_array_get_real(ctx, line, 2);
+	b.y = pdf_array_get_real(ctx, line, 3);
+
+	/* vector of line */
+	dx = b.x - a.x;
+	dy = b.y - a.y;
+	line_length = hypotf(dx, dy);
+	dx /= line_length;
+	dy /= line_length;
+
+	rect->x0 = fz_min(a.x, b.x);
+	rect->y0 = fz_min(a.y, b.y);
+	rect->x1 = fz_max(a.x, b.x);
+	rect->y1 = fz_max(a.y, b.y);
+
+	ll = pdf_dict_get_real(ctx, annot->obj, PDF_NAME(LL));
+
+	if (ll != 0)
+	{
+		lle = pdf_dict_get_real(ctx, annot->obj, PDF_NAME(LLE));
+		llo = pdf_dict_get_real(ctx, annot->obj, PDF_NAME(LLO));
+		if (ll < 0) {
+			lle = -lle;
+			llo = -llo;
+		}
+
+		aa = fz_make_point(a.x - dy * (llo), a.y + dx * (llo));
+		ba = fz_make_point(b.x - dy * (llo), b.y + dx * (llo));
+		ab = fz_make_point(a.x - dy * (llo + ll + lle), a.y + dx * (llo + ll + lle));
+		bb = fz_make_point(b.x - dy * (llo + ll + lle), b.y + dx * (llo + ll + lle));
+		a = fz_make_point(a.x - dy * (llo + ll), a.y + dx * (llo + ll));
+		b = fz_make_point(b.x - dy * (llo + ll), b.y + dx * (llo + ll));
+
+		fz_append_printf(ctx, buf, "%g %g m\n%g %g l\n", aa.x, aa.y, ab.x, ab.y);
+		fz_append_printf(ctx, buf, "%g %g m\n%g %g l\n", ba.x, ba.y, bb.x, bb.y);
+
+		*rect = fz_include_point_in_rect(*rect, a);
+		*rect = fz_include_point_in_rect(*rect, b);
+		*rect = fz_include_point_in_rect(*rect, ab);
+		*rect = fz_include_point_in_rect(*rect, bb);
+	}
+
+	if (pdf_dict_get_bool(ctx, annot->obj, PDF_NAME(Cap)))
+	{
+		float gap = pdf_write_line_caption(ctx, annot, buf, rect, res, a, b);
+		if (gap > 0)
+		{
+			fz_point m = fz_make_point((a.x + b.x) / 2, (a.y + b.y) / 2);
+			fz_point ca = fz_make_point(m.x - dx * gap, m.y - dy * gap);
+			fz_point cb = fz_make_point(m.x + dx * gap, m.y + dy * gap);
+			fz_append_printf(ctx, buf, "%g %g m\n%g %g l\n", a.x, a.y, ca.x, ca.y);
+			fz_append_printf(ctx, buf, "%g %g m\n%g %g l\n", cb.x, cb.y, b.x, b.y);
+		}
+		else
+		{
+			fz_append_printf(ctx, buf, "%g %g m\n%g %g l\n", a.x, a.y, b.x, b.y);
+		}
+	}
+	else
+	{
+		fz_append_printf(ctx, buf, "%g %g m\n%g %g l\n", a.x, a.y, b.x, b.y);
+	}
+
+	maybe_stroke(ctx, buf, sc);
+
+	le = pdf_dict_get(ctx, annot->obj, PDF_NAME(LE));
+	if (pdf_array_len(ctx, le) == 2)
+	{
+		dx = b.x - a.x;
+		dy = b.y - a.y;
+		pdf_write_line_cap_appearance(ctx, buf, rect, a.x, a.y, dx, dy, w, sc, ic, pdf_array_get(ctx, le, 0));
+		pdf_write_line_cap_appearance(ctx, buf, rect, b.x, b.y, -dx, -dy, w, sc, ic, pdf_array_get(ctx, le, 1));
+	}
+	*rect = fz_expand_rect(*rect, fz_max(1, w));
+}
+
+/* The rect diff is NOT an fz_rect. It's differences between
+ * 2 rects. We return it as a rect for convenience. */
+fz_rect
+pdf_annot_rect_diff(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_obj *rd_obj = pdf_dict_get(ctx, annot->obj, PDF_NAME(RD));
+	fz_rect rd;
+
+	if (!pdf_is_array(ctx, rd_obj))
+		return fz_make_rect(0, 0, 0, 0);
+
+	rd.x0 = pdf_array_get_real(ctx, rd_obj, 0);
+	rd.y0 = pdf_array_get_real(ctx, rd_obj, 1);
+	rd.x1 = pdf_array_get_real(ctx, rd_obj, 2);
+	rd.y1 = pdf_array_get_real(ctx, rd_obj, 3);
+
+	return rd;
+}
+
+static float
+cloud_intensity(fz_context *ctx, pdf_annot *annot)
+{
+	if (pdf_annot_border_effect(ctx, annot) == PDF_BORDER_EFFECT_CLOUDY)
+		return pdf_annot_border_effect_intensity(ctx, annot);
+	return 0;
+}
+
+struct cloud_list {
+	// store first 2 and latest 3 points
+	fz_point data[5];
+	int len;
+	int first;
+	int fill;
+	float spacing, radius, phase;
+};
+
+static float intersect_cloud(fz_point p0, fz_point p1, float r, int sel)
+{
+	float dx = p1.x - p0.x;
+	float dy = p1.y - p0.y;
+	float d = sqrtf(dx * dx + dy * dy);
+	float x2, y2, x3, y3;
+	float a, h;
+
+	if (d >= r + r) return 0;
+	if (d <= 0) return 0;
+
+	a = d / 2;
+	h = sqrtf(r * r - a * a);
+
+	x2 = (p0.x + p1.x) / 2;
+	y2 = (p0.y + p1.y) / 2;
+
+	x3 = x2 - h * (p1.y - p0.y) / d;
+	y3 = y2 + h * (p1.x - p0.x) / d;
+
+	if (sel == 0)
+		return atan2(y3 - p1.y, x3 - p1.x);
+	else
+		return atan2(y3 - p0.y, x3 - p0.x);
+}
+
+static void start_cloud(fz_context *ctx, struct cloud_list *list, fz_buffer *buf, float border, float intensity, int fill)
+{
+	// Constants measured from Acrobat Reader
+	list->spacing = intensity * 6.666667f + border * 0.8333333f;
+	list->radius = intensity * 4.0f + border * 0.5f;
+	list->phase = 0;
+	list->len = 0;
+	list->first = 1;
+	list->fill = fill;
+	fz_append_string(ctx, buf, "2 j\n"); // bevel join
+}
+
+static void emit_cloud(fz_context *ctx, struct cloud_list *list, fz_buffer *buf, fz_point a, fz_point b, fz_point c)
+{
+	float th0 = intersect_cloud(a, b, list->radius, 0);
+	float th1 = intersect_cloud(b, c, list->radius, 1);
+	while (th1 > th0)
+		th1 -= FZ_PI * 2;
+	draw_arc(ctx, buf, list->radius, b.x, b.y, th0, th1, list->first || !list->fill);
+	draw_arc_tail(ctx, buf, list->radius, b.x, b.y, th1, th1 - FZ_PI / 8, list->fill);
+	list->first = 0;
+}
+
+static void add_cloud(fz_context *ctx, struct cloud_list *list, fz_buffer *buf, float x, float y)
+{
+	if (list->len < 5)
+	{
+		list->data[list->len].x = x;
+		list->data[list->len].y = y;
+		list->len++;
+	}
+	else
+	{
+		list->data[2] = list->data[3];
+		list->data[3] = list->data[4];
+		list->data[4].x = x;
+		list->data[4].y = y;
+	}
+	if (list->len >= 3)
+	{
+		emit_cloud(ctx, list, buf, list->data[list->len-3], list->data[list->len-2], list->data[list->len-1]);
+	}
+}
+
+static void add_cloud_line(fz_context *ctx, struct cloud_list *list, fz_buffer *buf, float x0, float y0, float x1, float y1)
+{
+	float dx = x1 - x0;
+	float dy = y1 - y0;
+	float total = hypotf(dx, dy);
+	float used = 0;
+	float t;
+
+	if (list->phase == 0)
+		add_cloud(ctx, list, buf, x0, y0);
+
+	while (total - used > list->spacing - list->phase)
+	{
+		used += list->spacing - list->phase;
+		t = used / total;
+		add_cloud(ctx, list, buf, x0 + dx * t, y0 + dy * t);
+		list->phase = 0;
+	}
+
+	list->phase += total - used;
+}
+
+static void add_cloud_circle(fz_context *ctx, struct cloud_list *list, fz_buffer *buf, float x0, float y0, float x1, float y1)
+{
+	float cx = (x0 + x1) / 2;
+	float cy = (y0 + y1) / 2;
+	float rx = (x1 - x0) / 2;
+	float ry = (y1 - y0) / 2;
+	float da = -FZ_PI * 2 / 32;
+	int i;
+	for (i = 1; i <= 32; ++i) {
+		float ax = cx + cosf((i-1) * da) * rx;
+		float ay = cy + sinf((i-1) * da) * ry;
+		float bx = cx + cosf(i * da) * rx;
+		float by = cy + sinf(i * da) * ry;
+		add_cloud_line(ctx, list, buf, ax, ay, bx, by);
+	}
+}
+
+static void end_cloud(fz_context *ctx, struct cloud_list *list, fz_buffer *buf)
+{
+	// Join up with last and first circles.
+	switch (list->len)
+	{
+	case 0:
+		break;
+	case 1:
+		draw_circle(ctx, buf, list->radius, list->radius, list->data[0].x, list->data[0].y);
+		break;
+	case 2:
+		emit_cloud(ctx, list, buf, list->data[0], list->data[1], list->data[0]);
+		emit_cloud(ctx, list, buf, list->data[1], list->data[0], list->data[1]);
+		break;
+	case 3:
+		emit_cloud(ctx, list, buf, list->data[1], list->data[2], list->data[0]);
+		emit_cloud(ctx, list, buf, list->data[2], list->data[0], list->data[1]);
+		break;
+	case 4:
+		emit_cloud(ctx, list, buf, list->data[2], list->data[3], list->data[0]);
+		emit_cloud(ctx, list, buf, list->data[3], list->data[0], list->data[1]);
+		break;
+	case 5:
+		emit_cloud(ctx, list, buf, list->data[3], list->data[4], list->data[0]);
+		emit_cloud(ctx, list, buf, list->data[4], list->data[0], list->data[1]);
+		break;
+	}
+}
+
+static void
+pdf_write_square_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	struct cloud_list cloud_list;
+	fz_rect orect, rd;
+	float x, y, w, h;
+	float lw;
+	int sc;
+	int ic;
+	float cloud;
+	float exp;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_dash_pattern(ctx, annot, buf, res);
+	lw = pdf_write_border_appearance(ctx, annot, buf);
+	sc = pdf_write_stroke_color_appearance(ctx, annot, buf);
+	ic = pdf_write_interior_fill_color_appearance(ctx, annot, buf);
+	orect = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect));
+	rd = pdf_annot_rect_diff(ctx, annot);
+
+	/* We have various rules that we need to follow here:
+	 * 1) No part of what we draw should extend outside of 'Rect'.
+	 * 2) The 'centre' of the border should be on 'Rect+RD'.
+	 * 3) RD and linewidth will therefore have problems if they are too large.
+	 * We do our best to cope with all of these.
+	 */
+
+	exp = lw/2;
+	if (rd.x0 < exp)
+		rd.x0 = exp;
+	if (rd.x1 < exp)
+		rd.x1 = exp;
+	if (rd.y0 < exp)
+		rd.y0 = exp;
+	if (rd.y1 < exp)
+		rd.y1 = exp;
+
+	x = orect.x0 + rd.x0;
+	y = orect.y0 + rd.y0;
+	w = orect.x1 - orect.x0 - rd.x0 - rd.x1;
+	h = orect.y1 - orect.y0 - rd.y0 - rd.y1;
+
+	if (w < 1) w = 1;
+	if (h < 1) h = 1;
+
+	cloud = cloud_intensity(ctx, annot);
+	if (cloud > 0)
+	{
+		start_cloud(ctx, &cloud_list, buf, lw, cloud, ic);
+
+		add_cloud_line(ctx, &cloud_list, buf, x, y, x, y+h);
+		cloud_list.phase = 0;
+		add_cloud_line(ctx, &cloud_list, buf, x, y+h, x+w, y+h);
+		cloud_list.phase = 0;
+		add_cloud_line(ctx, &cloud_list, buf, x+w, y+h, x+w, y);
+		cloud_list.phase = 0;
+		add_cloud_line(ctx, &cloud_list, buf, x+w, y, x, y);
+
+		end_cloud(ctx, &cloud_list, buf);
+		exp += cloud_list.radius;
+
+		if (rd.x0 < exp)
+			rd.x0 = exp;
+		if (rd.x1 < exp)
+			rd.x1 = exp;
+		if (rd.y0 < exp)
+			rd.y0 = exp;
+		if (rd.y1 < exp)
+			rd.y1 = exp;
+	}
+	else
+	{
+		fz_append_printf(ctx, buf, "%g %g %g %g re\n", x, y, w, h);
+	}
+	maybe_stroke_and_fill(ctx, buf, sc, ic);
+
+	pdf_dict_put_rect(ctx, annot->obj, PDF_NAME(RD), rd);
+	rect->x0 = x - rd.x0;
+	rect->y0 = y - rd.y0;
+	rect->x1 = x + w + rd.x1;
+	rect->y1 = y + h + rd.y1;
+}
+
+static void
+pdf_write_circle_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	struct cloud_list cloud_list;
+	fz_rect orect, rd;
+	float x, y, w, h;
+	float lw;
+	int sc;
+	int ic;
+	float cloud;
+	float exp;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_dash_pattern(ctx, annot, buf, res);
+	lw = pdf_write_border_appearance(ctx, annot, buf);
+	sc = pdf_write_stroke_color_appearance(ctx, annot, buf);
+	ic = pdf_write_interior_fill_color_appearance(ctx, annot, buf);
+	orect = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect));
+	rd = pdf_annot_rect_diff(ctx, annot);
+
+	/* We have various rules that we need to follow here:
+	 * 1) No part of what we draw should extend outside of 'Rect'.
+	 * 2) The 'centre' of the border should be on 'Rect+RD'.
+	 * 3) RD and linewidth will therefore have problems if they are too large.
+	 * We do our best to cope with all of these.
+	 */
+
+	exp = lw/2;
+	if (rd.x0 < exp)
+		rd.x0 = exp;
+	if (rd.x1 < exp)
+		rd.x1 = exp;
+	if (rd.y0 < exp)
+		rd.y0 = exp;
+	if (rd.y1 < exp)
+		rd.y1 = exp;
+
+	x = orect.x0 + rd.x0;
+	y = orect.y0 + rd.y0;
+	w = orect.x1 - orect.x0 - rd.x0 - rd.x1;
+	h = orect.y1 - orect.y0 - rd.y0 - rd.y1;
+
+	if (w < 1) w = 1;
+	if (h < 1) h = 1;
+
+	cloud = cloud_intensity(ctx, annot);
+	if (cloud > 0)
+	{
+		start_cloud(ctx, &cloud_list, buf, lw, cloud, ic);
+		add_cloud_circle(ctx, &cloud_list, buf, x, y, x+w, y+h);
+		end_cloud(ctx, &cloud_list, buf);
+		exp += cloud_list.radius;
+
+		if (rd.x0 < exp)
+			rd.x0 = exp;
+		if (rd.x1 < exp)
+			rd.x1 = exp;
+		if (rd.y0 < exp)
+			rd.y0 = exp;
+		if (rd.y1 < exp)
+			rd.y1 = exp;
+	}
+	else
+	{
+		draw_circle(ctx, buf, w / 2, h / 2, x + w / 2, y + h / 2);
+	}
+	maybe_stroke_and_fill(ctx, buf, sc, ic);
+
+	pdf_dict_put_rect(ctx, annot->obj, PDF_NAME(RD), rd);
+	rect->x0 = x - rd.x0;
+	rect->y0 = y - rd.x1;
+	rect->x1 = x + w + rd.x1;
+	rect->y1 = y + h + rd.y1;
+}
+
+/*
+ * This doesn't quite match Acrobat, but I haven't really got a clue
+ * what magic algorithm Acrobat is using. It'll match for all 'simple'
+ * polygons, and makes a reasonable stab for complex ones.
+ *
+ * Find the y position of the centre of the polygon. Sum the
+ * area * winding_number across that line. (Effectively an integration?)
+ * Then pick cw or ccw according to what will be leave the largest
+ * area correct.
+ */
+static int
+polygon_winding(fz_context *ctx, pdf_obj *v, int n)
+{
+	int i;
+	float mid_y = 0;
+	float min_x = 0;
+	fz_point q, r;
+	float area = 0;
+
+	/* Find the centre for y, and min x. */
+	for (i = 0; i < n; i++)
+	{
+		float x = pdf_array_get_real(ctx, v, 2*i);
+		if (x < min_x || i == 0)
+			min_x = x;
+		mid_y += pdf_array_get_real(ctx, v, 2*i+1);
+	}
+	mid_y /= n;
+
+	/* Now run through finding the weighted area across that middle line. */
+	q.x = pdf_array_get_real(ctx, v, 0);
+	q.y = pdf_array_get_real(ctx, v, 1);
+	for (i = 1; i < n; i++)
+	{
+		r.x = pdf_array_get_real(ctx, v, 2*i);
+		r.y = pdf_array_get_real(ctx, v, 2*i+1);
+		if ((r.y > mid_y && mid_y > q.y) || (r.y < mid_y && mid_y < q.y))
+		{
+			int w = (r.y > mid_y) ? 1 : -1;
+			float x = r.x + (q.x - r.x) * (r.y - mid_y) / (r.y - q.y);
+
+			area += (x - min_x) * w;
+		}
+		q = r;
+	}
+
+	return area < 0;
+}
+
+static void
+pdf_write_polygon_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res, int close)
+{
+	struct cloud_list cloud_list;
+	float cloud = 0;
+	pdf_obj *verts, *le;
+	fz_point p, first = {0,0}, last = {0,0};
+	int i, n;
+	float lw;
+	int sc, ic;
+	int i0, i1, is;
+	float exp = 0;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_dash_pattern(ctx, annot, buf, res);
+	lw = pdf_write_border_appearance(ctx, annot, buf);
+	sc = pdf_write_stroke_color_appearance(ctx, annot, buf);
+	ic = pdf_write_interior_fill_color_appearance(ctx, annot, buf);
+
+	*rect = fz_empty_rect;
+
+	if (close)
+		cloud = cloud_intensity(ctx, annot);
+
+	verts = pdf_dict_get(ctx, annot->obj, PDF_NAME(Vertices));
+	n = pdf_array_len(ctx, verts) / 2;
+	if (n > 0)
+	{
+		if (polygon_winding(ctx, verts, n))
+			i0 = 0, i1 = n, is = 1;
+		else
+			i0 = n-1, i1 = -1, is = -1;
+
+		if (cloud > 0)
+		{
+			start_cloud(ctx, &cloud_list, buf, lw, cloud, ic);
+		}
+
+		for (i = i0; i != i1; i += is)
+		{
+			p.x = pdf_array_get_real(ctx, verts, i*2+0);
+			p.y = pdf_array_get_real(ctx, verts, i*2+1);
+			if (i == i0)
+			{
+				rect->x0 = rect->x1 = p.x;
+				rect->y0 = rect->y1 = p.y;
+			}
+			else
+			{
+				*rect = fz_include_point_in_rect(*rect, p);
+			}
+			if (cloud > 0)
+			{
+				if (i == i0)
+					first = p;
+				else
+					add_cloud_line(ctx, &cloud_list, buf, last.x, last.y, p.x, p.y);
+				last = p;
+			}
+			else
+			{
+				if (i == i0)
+					fz_append_printf(ctx, buf, "%g %g m\n", p.x, p.y);
+				else
+					fz_append_printf(ctx, buf, "%g %g l\n", p.x, p.y);
+			}
+		}
+
+		if (cloud > 0)
+		{
+			add_cloud_line(ctx, &cloud_list, buf, last.x, last.y, first.x, first.y);
+			end_cloud(ctx, &cloud_list, buf);
+		}
+		else
+		{
+			if (close)
+				fz_append_string(ctx, buf, "h\n");
+		}
+
+		if (close)
+			maybe_stroke_and_fill(ctx, buf, sc, ic);
+		else
+			maybe_stroke(ctx, buf, sc);
+
+		exp = lw;
+		if (cloud > 0)
+			exp += cloud_list.radius;
+
+		*rect = fz_expand_rect(*rect, exp);
+	}
+
+	le = pdf_dict_get(ctx, annot->obj, PDF_NAME(LE));
+	if (!close && n >= 2 && pdf_array_len(ctx, le) == 2)
+	{
+		float dx, dy;
+		fz_point a, b;
+
+		a.x = pdf_array_get_real(ctx, verts, 0*2+0);
+		a.y = pdf_array_get_real(ctx, verts, 0*2+1);
+		b.x = pdf_array_get_real(ctx, verts, 1*2+0);
+		b.y = pdf_array_get_real(ctx, verts, 1*2+1);
+
+		dx = b.x - a.x;
+		dy = b.y - a.y;
+
+		pdf_write_line_cap_appearance(ctx, buf, rect, a.x, a.y, dx, dy, lw, sc, ic, pdf_array_get(ctx, le, 0));
+
+		a.x = pdf_array_get_real(ctx, verts, (n-1)*2+0);
+		a.y = pdf_array_get_real(ctx, verts, (n-1)*2+1);
+		b.x = pdf_array_get_real(ctx, verts, (n-2)*2+0);
+		b.y = pdf_array_get_real(ctx, verts, (n-2)*2+1);
+
+		dx = b.x - a.x;
+		dy = b.y - a.y;
+
+		pdf_write_line_cap_appearance(ctx, buf, rect, a.x, a.y, dx, dy, lw, sc, ic, pdf_array_get(ctx, le, 1));
+	}
+
+	if (exp == 0)
+		pdf_dict_del(ctx, annot->obj, PDF_NAME(RD));
+	else
+		pdf_dict_put_rect(ctx, annot->obj, PDF_NAME(RD), fz_make_rect(exp, exp, exp, exp));
+}
+
+static void
+pdf_write_ink_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	pdf_obj *ink_list, *stroke;
+	int i, n, k, m;
+	float lw;
+	fz_point p;
+	int sc;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_dash_pattern(ctx, annot, buf, res);
+	lw = pdf_write_border_appearance(ctx, annot, buf);
+	sc = pdf_write_stroke_color_appearance(ctx, annot, buf);
+
+	*rect = fz_empty_rect;
+
+	fz_append_printf(ctx, buf, "1 J\n1 j\n");
+
+	ink_list = pdf_dict_get(ctx, annot->obj, PDF_NAME(InkList));
+	n = pdf_array_len(ctx, ink_list);
+	for (i = 0; i < n; ++i)
+	{
+		stroke = pdf_array_get(ctx, ink_list, i);
+		m = pdf_array_len(ctx, stroke) / 2;
+		for (k = 0; k < m; ++k)
+		{
+			p.x = pdf_array_get_real(ctx, stroke, k*2+0);
+			p.y = pdf_array_get_real(ctx, stroke, k*2+1);
+			if (i == 0 && k == 0)
+			{
+				rect->x0 = rect->x1 = p.x;
+				rect->y0 = rect->y1 = p.y;
+			}
+			else
+				*rect = fz_include_point_in_rect(*rect, p);
+			fz_append_printf(ctx, buf, "%g %g %c\n", p.x, p.y, k == 0 ? 'm' : 'l');
+		}
+
+		if (m == 1)
+			fz_append_printf(ctx, buf, "%g %g %c\n", p.x, p.y, 'l');
+	}
+	maybe_stroke(ctx, buf, sc);
+
+	/* Account for line width and add an extra 6 points of whitespace padding.
+	 * In case the annotation is a dot or a perfectly horizontal/vertical line,
+	 * we need some extra size to allow selecting it easily.
+	 */
+	*rect = fz_expand_rect(*rect, lw + 6);
+
+	pdf_dict_put_rect(ctx, annot->obj, PDF_NAME(RD), fz_make_rect(lw + 6, lw + 6, lw + 6, lw + 6));
+}
+
+/* Contrary to the specification, the points within a QuadPoint are NOT
+ * ordered in a counter-clockwise fashion starting with the lower left.
+ * Experiments with Adobe's implementation indicates a cross-wise
+ * ordering is intended: ul, ur, ll, lr.
+ */
+enum { UL, UR, LL, LR };
+
+static float
+extract_quad(fz_context *ctx, fz_point *quad, pdf_obj *obj, int i)
+{
+	float dx, dy;
+	quad[0].x = pdf_array_get_real(ctx, obj, i+0);
+	quad[0].y = pdf_array_get_real(ctx, obj, i+1);
+	quad[1].x = pdf_array_get_real(ctx, obj, i+2);
+	quad[1].y = pdf_array_get_real(ctx, obj, i+3);
+	quad[2].x = pdf_array_get_real(ctx, obj, i+4);
+	quad[2].y = pdf_array_get_real(ctx, obj, i+5);
+	quad[3].x = pdf_array_get_real(ctx, obj, i+6);
+	quad[3].y = pdf_array_get_real(ctx, obj, i+7);
+	dx = quad[UL].x - quad[LL].x;
+	dy = quad[UL].y - quad[LL].y;
+	return sqrtf(dx * dx + dy * dy);
+}
+
+static void
+union_quad(fz_rect *rect, const fz_point quad[4], float lw)
+{
+	fz_rect qbox;
+	qbox.x0 = fz_min(fz_min(quad[0].x, quad[1].x), fz_min(quad[2].x, quad[3].x));
+	qbox.y0 = fz_min(fz_min(quad[0].y, quad[1].y), fz_min(quad[2].y, quad[3].y));
+	qbox.x1 = fz_max(fz_max(quad[0].x, quad[1].x), fz_max(quad[2].x, quad[3].x));
+	qbox.y1 = fz_max(fz_max(quad[0].y, quad[1].y), fz_max(quad[2].y, quad[3].y));
+	*rect = fz_union_rect(*rect, fz_expand_rect(qbox, lw));
+}
+
+static fz_point
+lerp_point(fz_point a, fz_point b, float t)
+{
+	return fz_make_point(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));
+}
+
+static void
+pdf_write_highlight_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	pdf_obj *qp;
+	fz_point quad[4], mquad[4], v;
+	float h, m, dx, dy, vn;
+	int i, n;
+
+	*rect = fz_empty_rect;
+
+	pdf_write_opacity_blend_mode(ctx, annot, buf, res, FZ_BLEND_MULTIPLY);
+	pdf_write_fill_color_appearance(ctx, annot, buf);
+
+	qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints));
+	n = pdf_array_len(ctx, qp);
+	if (n > 0)
+	{
+		for (i = 0; i < n; i += 8)
+		{
+			h = extract_quad(ctx, quad, qp, i);
+			m = h / 4.2425f; /* magic number that matches adobe's appearance */
+			dx = quad[LR].x - quad[LL].x;
+			dy = quad[LR].y - quad[LL].y;
+			vn = sqrtf(dx * dx + dy * dy);
+			v = fz_make_point(dx * m / vn, dy * m / vn);
+
+			mquad[LL].x = quad[LL].x - v.x - v.y;
+			mquad[LL].y = quad[LL].y - v.y + v.x;
+			mquad[UL].x = quad[UL].x - v.x + v.y;
+			mquad[UL].y = quad[UL].y - v.y - v.x;
+			mquad[LR].x = quad[LR].x + v.x - v.y;
+			mquad[LR].y = quad[LR].y + v.y + v.x;
+			mquad[UR].x = quad[UR].x + v.x + v.y;
+			mquad[UR].y = quad[UR].y + v.y - v.x;
+
+			fz_append_printf(ctx, buf, "%g %g m\n", quad[LL].x, quad[LL].y);
+			fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n",
+				mquad[LL].x, mquad[LL].y,
+				mquad[UL].x, mquad[UL].y,
+				quad[UL].x, quad[UL].y);
+			fz_append_printf(ctx, buf, "%g %g l\n", quad[UR].x, quad[UR].y);
+			fz_append_printf(ctx, buf, "%g %g %g %g %g %g c\n",
+				mquad[UR].x, mquad[UR].y,
+				mquad[LR].x, mquad[LR].y,
+				quad[LR].x, quad[LR].y);
+			fz_append_printf(ctx, buf, "f\n");
+
+			union_quad(rect, quad, h/16);
+			union_quad(rect, mquad, 0);
+		}
+	}
+}
+
+static void
+pdf_write_underline_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	fz_point quad[4], a, b;
+	float h;
+	pdf_obj *qp;
+	int i, n;
+
+	*rect = fz_empty_rect;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_stroke_color_appearance(ctx, annot, buf);
+
+	qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints));
+	n = pdf_array_len(ctx, qp);
+	if (n > 0)
+	{
+		for (i = 0; i < n; i += 8)
+		{
+			/* Acrobat draws the line at 1/7 of the box width from the bottom
+			 * of the box and 1/16 thick of the box width. */
+
+			h = extract_quad(ctx, quad, qp, i);
+			a = lerp_point(quad[LL], quad[UL], 1/7.0f);
+			b = lerp_point(quad[LR], quad[UR], 1/7.0f);
+
+			fz_append_printf(ctx, buf, "%g w\n", h/16);
+			fz_append_printf(ctx, buf, "%g %g m\n", a.x, a.y);
+			fz_append_printf(ctx, buf, "%g %g l\n", b.x, b.y);
+			fz_append_printf(ctx, buf, "S\n");
+
+			union_quad(rect, quad, h/16);
+		}
+	}
+}
+
+static void
+pdf_write_strike_out_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	fz_point quad[4], a, b;
+	float h;
+	pdf_obj *qp;
+	int i, n;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_stroke_color_appearance(ctx, annot, buf);
+
+	qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints));
+	n = pdf_array_len(ctx, qp);
+	if (n > 0)
+	{
+		*rect = fz_empty_rect;
+		for (i = 0; i < n; i += 8)
+		{
+			/* Acrobat draws the line at 3/7 of the box width from the bottom
+			 * of the box and 1/16 thick of the box width. */
+
+			h = extract_quad(ctx, quad, qp, i);
+			a = lerp_point(quad[LL], quad[UL], 3/7.0f);
+			b = lerp_point(quad[LR], quad[UR], 3/7.0f);
+
+			fz_append_printf(ctx, buf, "%g w\n", h/16);
+			fz_append_printf(ctx, buf, "%g %g m\n", a.x, a.y);
+			fz_append_printf(ctx, buf, "%g %g l\n", b.x, b.y);
+			fz_append_printf(ctx, buf, "S\n");
+
+			union_quad(rect, quad, h/16);
+		}
+	}
+}
+
+static void
+pdf_write_squiggly_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	fz_point quad[4], a, b, c, v;
+	float h, x, w;
+	pdf_obj *qp;
+	int i, n;
+
+	*rect = fz_empty_rect;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_stroke_color_appearance(ctx, annot, buf);
+
+	qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints));
+	n = pdf_array_len(ctx, qp);
+	if (n > 0)
+	{
+		for (i = 0; i < n; i += 8)
+		{
+			int up = 1;
+			h = extract_quad(ctx, quad, qp, i);
+			v = fz_make_point(quad[LR].x - quad[LL].x, quad[LR].y - quad[LL].y);
+			w = sqrtf(v.x * v.x + v.y * v.y);
+			x = 0;
+
+			fz_append_printf(ctx, buf, "%g w\n", h/16);
+			fz_append_printf(ctx, buf, "%g %g m\n", quad[LL].x, quad[LL].y);
+			while (x < w)
+			{
+				x += h/7;
+				a = lerp_point(quad[LL], quad[LR], x/w);
+				if (up)
+				{
+					b = lerp_point(quad[UL], quad[UR], x/w);
+					c = lerp_point(a, b, 1/7.0f);
+					fz_append_printf(ctx, buf, "%g %g l\n", c.x, c.y);
+				}
+				else
+					fz_append_printf(ctx, buf, "%g %g l\n", a.x, a.y);
+				up = !up;
+			}
+			fz_append_printf(ctx, buf, "S\n");
+
+			union_quad(rect, quad, h/16);
+		}
+	}
+}
+
+static void
+pdf_write_redact_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res)
+{
+	fz_point quad[4];
+	pdf_obj *qp;
+	int i, n;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+
+	fz_append_printf(ctx, buf, "1 0 0 RG\n");
+
+	qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints));
+	n = pdf_array_len(ctx, qp);
+	if (n > 0)
+	{
+		*rect = fz_empty_rect;
+		for (i = 0; i < n; i += 8)
+		{
+			extract_quad(ctx, quad, qp, i);
+			fz_append_printf(ctx, buf, "%g %g m\n", quad[LL].x, quad[LL].y);
+			fz_append_printf(ctx, buf, "%g %g l\n", quad[LR].x, quad[LR].y);
+			fz_append_printf(ctx, buf, "%g %g l\n", quad[UR].x, quad[UR].y);
+			fz_append_printf(ctx, buf, "%g %g l\n", quad[UL].x, quad[UL].y);
+			fz_append_printf(ctx, buf, "s\n");
+			union_quad(rect, quad, 1);
+		}
+	}
+	else
+	{
+		fz_append_printf(ctx, buf, "%g %g m\n", rect->x0+1, rect->y0+1);
+		fz_append_printf(ctx, buf, "%g %g l\n", rect->x1-1, rect->y0+1);
+		fz_append_printf(ctx, buf, "%g %g l\n", rect->x1-1, rect->y1-1);
+		fz_append_printf(ctx, buf, "%g %g l\n", rect->x0+1, rect->y1-1);
+		fz_append_printf(ctx, buf, "s\n");
+	}
+}
+
+static void
+pdf_write_caret_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, fz_rect *bbox, pdf_obj **res)
+{
+	float xc = (rect->x0 + rect->x1) / 2;
+	float yc = (rect->y0 + rect->y1) / 2;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_fill_color_appearance(ctx, annot, buf);
+
+	fz_append_string(ctx, buf, "0 0 m\n");
+	fz_append_string(ctx, buf, "10 0 10 7 10 14 c\n");
+	fz_append_string(ctx, buf, "10 7 10 0 20 0 c\n");
+	fz_append_string(ctx, buf, "f\n");
+
+	*rect = fz_make_rect(xc - 10, yc - 7, xc + 10, yc + 7);
+	*bbox = fz_make_rect(0, 0, 20, 14);
+}
+
+static void
+pdf_write_icon_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, fz_rect *bbox, pdf_obj **res)
+{
+	const char *name;
+	float xc = (rect->x0 + rect->x1) / 2;
+	float yc = (rect->y0 + rect->y1) / 2;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+
+	if (!pdf_write_fill_color_appearance(ctx, annot, buf))
+		fz_append_string(ctx, buf, "1 g\n");
+
+	fz_append_string(ctx, buf, "1 w\n0.5 0.5 15 15 re\nb\n");
+	fz_append_string(ctx, buf, "1 0 0 -1 4 12 cm\n");
+
+	if (pdf_is_dark_fill_color(ctx, annot))
+		fz_append_string(ctx, buf, "1 g\n");
+	else
+		fz_append_string(ctx, buf, "0 g\n");
+
+	name = pdf_annot_icon_name(ctx, annot);
+
+	/* Text names */
+	if (!strcmp(name, "Comment"))
+		fz_append_string(ctx, buf, icon_comment);
+	else if (!strcmp(name, "Key"))
+		fz_append_string(ctx, buf, icon_key);
+	else if (!strcmp(name, "Note"))
+		fz_append_string(ctx, buf, icon_note);
+	else if (!strcmp(name, "Help"))
+		fz_append_string(ctx, buf, icon_help);
+	else if (!strcmp(name, "NewParagraph"))
+		fz_append_string(ctx, buf, icon_new_paragraph);
+	else if (!strcmp(name, "Paragraph"))
+		fz_append_string(ctx, buf, icon_paragraph);
+	else if (!strcmp(name, "Insert"))
+		fz_append_string(ctx, buf, icon_insert);
+
+	/* FileAttachment names */
+	else if (!strcmp(name, "Graph"))
+		fz_append_string(ctx, buf, icon_graph);
+	else if (!strcmp(name, "PushPin"))
+		fz_append_string(ctx, buf, icon_push_pin);
+	else if (!strcmp(name, "Paperclip"))
+		fz_append_string(ctx, buf, icon_paperclip);
+	else if (!strcmp(name, "Tag"))
+		fz_append_string(ctx, buf, icon_tag);
+
+	/* Sound names */
+	else if (!strcmp(name, "Speaker"))
+		fz_append_string(ctx, buf, icon_speaker);
+	else if (!strcmp(name, "Mic"))
+		fz_append_string(ctx, buf, icon_mic);
+
+	/* Unknown */
+	else
+		fz_append_string(ctx, buf, icon_star);
+
+	*rect = fz_make_rect(xc - 9, yc - 9, xc + 9, yc + 9);
+	*bbox = fz_make_rect(0, 0, 16, 16);
+}
+
+static float
+measure_stamp_string(fz_context *ctx, fz_font *font, const char *text)
+{
+	float w = 0;
+	while (*text)
+	{
+		int c, g;
+		text += fz_chartorune(&c, text);
+		if (fz_windows_1252_from_unicode(c) < 0)
+			c = REPLACEMENT;
+		g = fz_encode_character(ctx, font, c);
+		w += fz_advance_glyph(ctx, font, g, 0);
+	}
+	return w;
+}
+
+static void
+write_stamp_string(fz_context *ctx, fz_buffer *buf, fz_font *font, const char *text)
+{
+	fz_append_byte(ctx, buf, '(');
+	while (*text)
+	{
+		int c;
+		text += fz_chartorune(&c, text);
+		c = fz_windows_1252_from_unicode(c);
+		if (c < 0) c = REPLACEMENT;
+		if (c == '(' || c == ')' || c == '\\')
+			fz_append_byte(ctx, buf, '\\');
+		fz_append_byte(ctx, buf, c);
+	}
+	fz_append_byte(ctx, buf, ')');
+}
+
+static void
+write_stamp(fz_context *ctx, fz_buffer *buf, fz_font *font, const char *text, float y, float h)
+{
+	float tw = measure_stamp_string(ctx, font, text) * h;
+	fz_append_string(ctx, buf, "BT\n");
+	fz_append_printf(ctx, buf, "/Times %g Tf\n", h);
+	fz_append_printf(ctx, buf, "%g %g Td\n", (190-tw)/2, y);
+	write_stamp_string(ctx, buf, font, text);
+	fz_append_string(ctx, buf, " Tj\n");
+	fz_append_string(ctx, buf, "ET\n");
+}
+
+static void
+pdf_write_stamp_appearance_rubber(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res)
+{
+	fz_font *font;
+	pdf_obj *res_font;
+	pdf_obj *name;
+	float w, h, xs, ys;
+	fz_matrix rotate;
+
+	name = pdf_dict_get(ctx, annot->obj, PDF_NAME(Name));
+	if (!name)
+		name = PDF_NAME(Draft);
+
+	h = rect->y1 - rect->y0;
+	w = rect->x1 - rect->x0;
+	xs = w / 190;
+	ys = h / 50;
+
+	font = fz_new_base14_font(ctx, "Times-Bold");
+	fz_try(ctx)
+	{
+		/* /Resources << /Font << /Times %d 0 R >> >> */
+		if (!*res)
+			*res = pdf_new_dict(ctx, annot->page->doc, 1);
+		res_font = pdf_dict_put_dict(ctx, *res, PDF_NAME(Font), 1);
+		pdf_dict_put_drop(ctx, res_font, PDF_NAME(Times), pdf_add_simple_font(ctx, annot->page->doc, font, 0));
+
+		pdf_write_opacity(ctx, annot, buf, res);
+		pdf_write_fill_color_appearance(ctx, annot, buf);
+		pdf_write_stroke_color_appearance(ctx, annot, buf);
+		rotate = fz_rotate(0.6f);
+		fz_append_printf(ctx, buf, "%M cm\n", &rotate);
+		fz_append_string(ctx, buf, "2 w\n2 2 186 44 re\nS\n");
+
+		if (name == PDF_NAME(Approved))
+			write_stamp(ctx, buf, font, "APPROVED", 13, 30);
+		else if (name == PDF_NAME(AsIs))
+			write_stamp(ctx, buf, font, "AS IS", 13, 30);
+		else if (name == PDF_NAME(Confidential))
+			write_stamp(ctx, buf, font, "CONFIDENTIAL", 17, 20);
+		else if (name == PDF_NAME(Departmental))
+			write_stamp(ctx, buf, font, "DEPARTMENTAL", 17, 20);
+		else if (name == PDF_NAME(Experimental))
+			write_stamp(ctx, buf, font, "EXPERIMENTAL", 17, 20);
+		else if (name == PDF_NAME(Expired))
+			write_stamp(ctx, buf, font, "EXPIRED", 13, 30);
+		else if (name == PDF_NAME(Final))
+			write_stamp(ctx, buf, font, "FINAL", 13, 30);
+		else if (name == PDF_NAME(ForComment))
+			write_stamp(ctx, buf, font, "FOR COMMENT", 17, 20);
+		else if (name == PDF_NAME(ForPublicRelease))
+		{
+			write_stamp(ctx, buf, font, "FOR PUBLIC", 26, 18);
+			write_stamp(ctx, buf, font, "RELEASE", 8.5f, 18);
+		}
+		else if (name == PDF_NAME(NotApproved))
+			write_stamp(ctx, buf, font, "NOT APPROVED", 17, 20);
+		else if (name == PDF_NAME(NotForPublicRelease))
+		{
+			write_stamp(ctx, buf, font, "NOT FOR", 26, 18);
+			write_stamp(ctx, buf, font, "PUBLIC RELEASE", 8.5, 18);
+		}
+		else if (name == PDF_NAME(Sold))
+			write_stamp(ctx, buf, font, "SOLD", 13, 30);
+		else if (name == PDF_NAME(TopSecret))
+			write_stamp(ctx, buf, font, "TOP SECRET", 14, 26);
+		else if (name == PDF_NAME(Draft))
+			write_stamp(ctx, buf, font, "DRAFT", 13, 30);
+		else
+			write_stamp(ctx, buf, font, pdf_to_name(ctx, name), 17, 20);
+	}
+	fz_always(ctx)
+		fz_drop_font(ctx, font);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	*bbox = fz_make_rect(0, 0, 190, 50);
+	if (xs > ys)
+	{
+		float xc = (rect->x1+rect->x0) / 2;
+		rect->x0 = xc - 95 * ys;
+		rect->x1 = xc + 95 * ys;
+	}
+	else
+	{
+		float yc = (rect->y1+rect->y0) / 2;
+		rect->y0 = yc - 25 * xs;
+		rect->y1 = yc + 25 * xs;
+	}
+
+	*matrix = fz_identity;
+}
+
+static void
+pdf_write_stamp_appearance_image(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res, pdf_obj *img)
+{
+	pdf_obj *res_xobj;
+
+	if (!*res)
+		*res = pdf_new_dict(ctx, annot->page->doc, 1);
+	res_xobj = pdf_dict_put_dict(ctx, *res, PDF_NAME(XObject), 1);
+	pdf_dict_put(ctx, res_xobj, PDF_NAME(I), img);
+
+	pdf_write_opacity(ctx, annot, buf, res);
+
+	fz_append_string(ctx, buf, "/I Do\n");
+
+	*bbox = fz_unit_rect;
+	*matrix = fz_identity;
+}
+
+static void
+pdf_write_stamp_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res)
+{
+	pdf_obj *img = pdf_annot_stamp_image_obj(ctx, annot);
+	if (img)
+		pdf_write_stamp_appearance_image(ctx, annot, buf, rect, bbox, matrix, res, img);
+	else
+		pdf_write_stamp_appearance_rubber(ctx, annot, buf, rect, bbox, matrix, res);
+}
+
+static void
+add_required_fonts(fz_context *ctx, pdf_document *doc, pdf_obj *res_font,
+	fz_text_language lang, fz_font *font, const char *fontname, const char *text)
+{
+	fz_font *cjk_font;
+	char buf[40];
+
+	int add_latin = 0;
+	int add_greek = 0;
+	int add_cyrillic = 0;
+	int add_korean = 0;
+	int add_japanese = 0;
+	int add_bopomofo = 0;
+	int add_han = 0;
+	int add_hans = 0;
+	int add_hant = 0;
+
+	while (*text)
+	{
+		int c;
+		text += fz_chartorune(&c, text);
+		switch (ucdn_get_script(c))
+		{
+		default: add_latin = 1; /* for fallback bullet character */ break;
+		case UCDN_SCRIPT_COMMON: break;
+		case UCDN_SCRIPT_INHERITED: break;
+		case UCDN_SCRIPT_LATIN: add_latin = 1; break;
+		case UCDN_SCRIPT_GREEK: add_greek = 1; break;
+		case UCDN_SCRIPT_CYRILLIC: add_cyrillic = 1; break;
+		case UCDN_SCRIPT_HANGUL: add_korean = 1; break;
+		case UCDN_SCRIPT_HIRAGANA: add_japanese = 1; break;
+		case UCDN_SCRIPT_KATAKANA: add_japanese = 1; break;
+		case UCDN_SCRIPT_BOPOMOFO: add_bopomofo = 1; break;
+		case UCDN_SCRIPT_HAN: add_han = 1; break;
+		}
+	}
+
+	if (add_han)
+	{
+		switch (lang)
+		{
+		case FZ_LANG_ko: add_korean = 1; break;
+		default: /* fall through */
+		case FZ_LANG_ja: add_japanese = 1; break;
+		case FZ_LANG_zh: /* fall through */
+		case FZ_LANG_zh_Hant: add_hant = 1; break;
+		case FZ_LANG_zh_Hans: add_hans = 1; break;
+		}
+	}
+
+	if (add_bopomofo)
+	{
+		if (lang == FZ_LANG_zh_Hans)
+			add_hans = 1;
+		else
+			add_hant = 1;
+	}
+
+	if (!add_greek && !add_cyrillic && !add_korean && !add_japanese && !add_hant && !add_hans)
+		add_latin = 1;
+
+	if (add_latin)
+	{
+		if (!pdf_dict_gets(ctx, res_font, fontname))
+			pdf_dict_puts_drop(ctx, res_font, fontname,
+				pdf_add_simple_font(ctx, doc, font, PDF_SIMPLE_ENCODING_LATIN));
+	}
+	if (add_greek)
+	{
+		fz_snprintf(buf, sizeof buf, "%sGRK", fontname);
+		if (!pdf_dict_gets(ctx, res_font, buf))
+			pdf_dict_puts_drop(ctx, res_font, buf,
+				pdf_add_simple_font(ctx, doc, font, PDF_SIMPLE_ENCODING_GREEK));
+	}
+	if (add_cyrillic)
+	{
+		fz_snprintf(buf, sizeof buf, "%sCYR", fontname);
+		if (!pdf_dict_gets(ctx, res_font, buf))
+			pdf_dict_puts_drop(ctx, res_font, buf,
+				pdf_add_simple_font(ctx, doc, font, PDF_SIMPLE_ENCODING_CYRILLIC));
+	}
+	if (add_korean && !pdf_dict_gets(ctx, res_font, "Batang"))
+	{
+		cjk_font = fz_new_cjk_font(ctx, FZ_ADOBE_KOREA);
+		pdf_dict_puts_drop(ctx, res_font, "Batang",
+			pdf_add_cjk_font(ctx, doc, font, FZ_ADOBE_KOREA, 0, 1));
+		fz_drop_font(ctx, cjk_font);
+	}
+	if (add_japanese && !pdf_dict_gets(ctx, res_font, "Mincho"))
+	{
+		cjk_font = fz_new_cjk_font(ctx, FZ_ADOBE_JAPAN);
+		pdf_dict_puts_drop(ctx, res_font, "Mincho",
+			pdf_add_cjk_font(ctx, doc, font, FZ_ADOBE_JAPAN, 0, 1));
+		fz_drop_font(ctx, cjk_font);
+	}
+	if (add_hant && !pdf_dict_gets(ctx, res_font, "Ming"))
+	{
+		cjk_font = fz_new_cjk_font(ctx, FZ_ADOBE_CNS);
+		pdf_dict_puts_drop(ctx, res_font, "Ming",
+			pdf_add_cjk_font(ctx, doc, font, FZ_ADOBE_CNS, 0, 1));
+		fz_drop_font(ctx, cjk_font);
+	}
+	if (add_hans && !pdf_dict_gets(ctx, res_font, "Song"))
+	{
+		cjk_font = fz_new_cjk_font(ctx, FZ_ADOBE_GB);
+		pdf_dict_puts_drop(ctx, res_font, "Song",
+			pdf_add_cjk_font(ctx, doc, font, FZ_ADOBE_GB, 0, 1));
+		fz_drop_font(ctx, cjk_font);
+	}
+}
+
+static int find_initial_script(const char *text)
+{
+	int script = UCDN_SCRIPT_COMMON;
+	int c;
+	while (*text)
+	{
+		text += fz_chartorune(&c, text);
+		script = ucdn_get_script(c);
+		if (script != UCDN_SCRIPT_COMMON && script != UCDN_SCRIPT_INHERITED)
+			break;
+	}
+	if (script == UCDN_SCRIPT_COMMON || script == UCDN_SCRIPT_INHERITED)
+		script = UCDN_SCRIPT_LATIN;
+	return script;
+}
+
+enum { ENC_LATIN = 1, ENC_GREEK, ENC_CYRILLIC, ENC_KOREAN, ENC_JAPANESE, ENC_HANT, ENC_HANS };
+
+struct text_walk_state
+{
+	const char *text, *end;
+	fz_font *font;
+	fz_text_language lang;
+	int enc, u, c, n, last_script;
+	float w;
+};
+
+static void init_text_walk(fz_context *ctx, struct text_walk_state *state, fz_text_language lang, fz_font *font, const char *text, const char *end)
+{
+	state->text = text;
+	state->end = end ? end : text + strlen(text);
+	state->lang = lang;
+	state->font = font;
+	state->last_script = find_initial_script(text);
+	state->n = 0;
+}
+
+static int next_text_walk(fz_context *ctx, struct text_walk_state *state)
+{
+	int script, g;
+
+	state->text += state->n;
+	if (state->text >= state->end)
+	{
+		state->n = 0;
+		return 0;
+	}
+
+	state->n = fz_chartorune(&state->u, state->text);
+	script = ucdn_get_script(state->u);
+	if (script == UCDN_SCRIPT_COMMON || script == UCDN_SCRIPT_INHERITED)
+		script = state->last_script;
+	state->last_script = script;
+
+	switch (script)
+	{
+	default:
+		state->enc = ENC_LATIN;
+		state->c = REPLACEMENT;
+		break;
+	case UCDN_SCRIPT_LATIN:
+		state->enc = ENC_LATIN;
+		state->c = fz_windows_1252_from_unicode(state->u);
+		break;
+	case UCDN_SCRIPT_GREEK:
+		state->enc = ENC_GREEK;
+		state->c = fz_iso8859_7_from_unicode(state->u);
+		break;
+	case UCDN_SCRIPT_CYRILLIC:
+		state->enc = ENC_CYRILLIC;
+		state->c = fz_koi8u_from_unicode(state->u);
+		break;
+	case UCDN_SCRIPT_HANGUL:
+		state->enc = ENC_KOREAN;
+		state->c = state->u;
+		break;
+	case UCDN_SCRIPT_HIRAGANA:
+	case UCDN_SCRIPT_KATAKANA:
+		state->enc = ENC_JAPANESE;
+		state->c = state->u;
+		break;
+	case UCDN_SCRIPT_BOPOMOFO:
+		state->enc = (state->lang == FZ_LANG_zh_Hans) ? ENC_HANS : ENC_HANT;
+		state->c = state->u;
+		break;
+	case UCDN_SCRIPT_HAN:
+		switch (state->lang)
+		{
+		case FZ_LANG_ko: state->enc = ENC_KOREAN; break;
+		default: /* fall through */
+		case FZ_LANG_ja: state->enc = ENC_JAPANESE; break;
+		case FZ_LANG_zh: /* fall through */
+		case FZ_LANG_zh_Hant: state->enc = ENC_HANT; break;
+		case FZ_LANG_zh_Hans: state->enc = ENC_HANS; break;
+		}
+		state->c = state->u;
+		break;
+	}
+
+	/* TODO: check that character is encodable with ENC_KOREAN/etc */
+	if (state->c < 0)
+	{
+		state->enc = ENC_LATIN;
+		state->c = REPLACEMENT;
+	}
+
+	if (state->enc >= ENC_KOREAN)
+	{
+		state->w = 1;
+	}
+	else
+	{
+		if (state->font != NULL)
+		{
+			g = fz_encode_character(ctx, state->font, state->u);
+			state->w = fz_advance_glyph(ctx, state->font, g, 0);
+		}
+	}
+
+	return 1;
+}
+
+static float
+measure_string(fz_context *ctx, fz_text_language lang, fz_font *font, const char *a)
+{
+	struct text_walk_state state;
+	float w = 0;
+	init_text_walk(ctx, &state, lang, font, a, NULL);
+	while (next_text_walk(ctx, &state))
+		w += state.w;
+	return w;
+}
+
+
+static float
+break_string(fz_context *ctx, fz_text_language lang, fz_font *font, float size, const char *text, const char **endp, float maxw)
+{
+	struct text_walk_state state;
+	const char *space = NULL;
+	float space_x, x = 0;
+	init_text_walk(ctx, &state, lang, font, text, NULL);
+	while (next_text_walk(ctx, &state))
+	{
+		if (state.u == '\n' || state.u == '\r')
+			break;
+		if (state.u == ' ')
+		{
+			space = state.text + state.n;
+			space_x = x;
+		}
+		x += state.w * size;
+		if (space && x > maxw)
+			return *endp = space, space_x;
+	}
+	return *endp = state.text + state.n, x;
+}
+
+static void
+write_string(fz_context *ctx, fz_buffer *buf,
+	fz_text_language lang, fz_font *font, const char *fontname, float size, const char *text, const char *end)
+{
+	struct text_walk_state state;
+	int last_enc = 0;
+	init_text_walk(ctx, &state, lang, font, text, end);
+	while (next_text_walk(ctx, &state))
+	{
+		if (state.enc != last_enc)
+		{
+			if (last_enc)
+			{
+				if (last_enc < ENC_KOREAN)
+					fz_append_byte(ctx, buf, ')');
+				else
+					fz_append_byte(ctx, buf, '>');
+				fz_append_string(ctx, buf, " Tj\n");
+			}
+
+			switch (state.enc)
+			{
+			case ENC_LATIN: fz_append_printf(ctx, buf, "/%s %g Tf\n", fontname, size); break;
+			case ENC_GREEK: fz_append_printf(ctx, buf, "/%sGRK %g Tf\n", fontname, size); break;
+			case ENC_CYRILLIC: fz_append_printf(ctx, buf, "/%sCYR %g Tf\n", fontname, size); break;
+			case ENC_KOREAN: fz_append_printf(ctx, buf, "/Batang %g Tf\n", size); break;
+			case ENC_JAPANESE: fz_append_printf(ctx, buf, "/Mincho %g Tf\n", size); break;
+			case ENC_HANT: fz_append_printf(ctx, buf, "/Ming %g Tf\n", size); break;
+			case ENC_HANS: fz_append_printf(ctx, buf, "/Song %g Tf\n", size); break;
+			}
+
+			if (state.enc < ENC_KOREAN)
+				fz_append_byte(ctx, buf, '(');
+			else
+				fz_append_byte(ctx, buf, '<');
+
+			last_enc = state.enc;
+		}
+
+		if (state.enc < ENC_KOREAN)
+		{
+			if (state.c == '(' || state.c == ')' || state.c == '\\')
+				fz_append_byte(ctx, buf, '\\');
+			fz_append_byte(ctx, buf, state.c);
+		}
+		else
+		{
+			fz_append_printf(ctx, buf, "%04x", state.c);
+		}
+	}
+
+	if (last_enc)
+	{
+		if (last_enc < ENC_KOREAN)
+			fz_append_byte(ctx, buf, ')');
+		else
+			fz_append_byte(ctx, buf, '>');
+		fz_append_string(ctx, buf, " Tj\n");
+	}
+}
+
+static void
+write_string_with_quadding(fz_context *ctx, fz_buffer *buf,
+	fz_text_language lang, const char *fontname,
+	fz_font *font, float size, float lineheight,
+	const char *a, float maxw, int q)
+{
+	const char *b;
+	float px = 0, x = 0, w;
+	while (*a)
+	{
+		w = break_string(ctx, lang, font, size, a, &b, maxw);
+		if (b > a)
+		{
+			if (q == 0)
+				x = 0;
+			else if (q == 1)
+				x = (maxw - w) / 2;
+			else
+				x = (maxw - w);
+			fz_append_printf(ctx, buf, "%g %g Td\n", x - px, -lineheight);
+			if (b[-1] == '\n' || b[-1] == '\r')
+				write_string(ctx, buf, lang, font, fontname, size, a, b-1);
+			else
+				write_string(ctx, buf, lang, font, fontname, size, a, b);
+			a = b;
+			px = x;
+		}
+	}
+}
+
+static void
+write_comb_string(fz_context *ctx, fz_buffer *buf,
+	fz_text_language lang, const char *fontname,
+	fz_font *font, float size, const char *text, float cell_w)
+{
+	struct text_walk_state state;
+	int last_enc = 0;
+	float pad, carry = 0;
+
+	init_text_walk(ctx, &state, lang, font, text, text + strlen(text));
+
+	while (next_text_walk(ctx, &state))
+	{
+		if (state.enc != last_enc)
+		{
+			if (last_enc)
+				fz_append_string(ctx, buf, "] TJ\n");
+
+			switch (state.enc)
+			{
+			case ENC_LATIN: fz_append_printf(ctx, buf, "/%s %g Tf\n", fontname, size); break;
+			case ENC_GREEK: fz_append_printf(ctx, buf, "/%sGRK %g Tf\n", fontname, size); break;
+			case ENC_CYRILLIC: fz_append_printf(ctx, buf, "/%sCYR %g Tf\n", fontname, size); break;
+			case ENC_KOREAN: fz_append_printf(ctx, buf, "/Batang %g Tf\n", size); break;
+			case ENC_JAPANESE: fz_append_printf(ctx, buf, "/Mincho %g Tf\n", size); break;
+			case ENC_HANT: fz_append_printf(ctx, buf, "/Ming %g Tf\n", size); break;
+			case ENC_HANS: fz_append_printf(ctx, buf, "/Song %g Tf\n", size); break;
+			}
+
+			fz_append_byte(ctx, buf, '[');
+
+			last_enc = state.enc;
+		}
+
+		pad = (cell_w - state.w * 1000) / 2;
+		fz_append_printf(ctx, buf, "%g", -(carry + pad));
+		carry = pad;
+
+		if (state.enc < ENC_KOREAN)
+		{
+			fz_append_byte(ctx, buf, '(');
+			if (state.c == '(' || state.c == ')' || state.c == '\\')
+				fz_append_byte(ctx, buf, '\\');
+			fz_append_byte(ctx, buf, state.c);
+			fz_append_byte(ctx, buf, ')');
+		}
+		else
+		{
+			fz_append_printf(ctx, buf, "<%04x>", state.c);
+		}
+	}
+	if (last_enc)
+		fz_append_string(ctx, buf, "] TJ\n");
+}
+
+static void
+layout_comb_string(fz_context *ctx, fz_layout_block *out, float x, float y,
+	const char *a, const char *b, fz_font *font, float size, float cell_w)
+{
+	int n, c, g;
+	int first = 1;
+	float w;
+	if (a == b)
+		fz_add_layout_line(ctx, out, x + cell_w / 2, y, size, a);
+	while (a < b)
+	{
+		n = fz_chartorune(&c, a);
+		c = fz_windows_1252_from_unicode(c);
+		if (c < 0) c = REPLACEMENT;
+		g = fz_encode_character(ctx, font, c);
+		w = fz_advance_glyph(ctx, font, g, 0) * size;
+		if (first)
+		{
+			fz_add_layout_line(ctx, out, x + (cell_w - w) / 2, y, size, a);
+			first = 0;
+		}
+		fz_add_layout_char(ctx, out, x + (cell_w - w) / 2, w, a);
+		a += n;
+		x += cell_w;
+	}
+}
+
+static void
+layout_string(fz_context *ctx, fz_layout_block *out,
+	fz_text_language lang, fz_font *font, float size,
+	float x, float y, const char *a, const char *b)
+{
+	struct text_walk_state state;
+	fz_add_layout_line(ctx, out, x, y, size, a);
+	init_text_walk(ctx, &state, lang, font, a, b);
+	while (next_text_walk(ctx, &state))
+	{
+		fz_add_layout_char(ctx, out, x, state.w * size, state.text);
+		x += state.w * size;
+	}
+}
+
+static void
+layout_string_with_quadding(fz_context *ctx, fz_layout_block *out,
+	fz_text_language lang, fz_font *font, float size, float lineheight,
+	float xorig, float y, const char *a, float maxw, int q)
+{
+	const char *b;
+	float x = 0, w;
+	int add_line_at_end = 0;
+
+	if (!*a)
+		add_line_at_end = 1;
+
+	while (*a)
+	{
+		w = break_string(ctx, lang, font, size, a, &b, maxw);
+		if (b > a)
+		{
+			if (q > 0)
+			{
+				if (q == 1)
+					x = (maxw - w) / 2;
+				else
+					x = (maxw - w);
+			}
+			if (b[-1] == '\n' || b[-1] == '\r')
+			{
+				layout_string(ctx, out, lang, font, size, xorig+x, y, a, b-1);
+				add_line_at_end = 1;
+			}
+			else
+			{
+				layout_string(ctx, out, lang, font, size, xorig+x, y, a, b);
+				add_line_at_end = 0;
+			}
+			a = b;
+			y -= lineheight;
+		}
+	}
+	if (add_line_at_end)
+		fz_add_layout_line(ctx, out, xorig, y, size, a);
+}
+
+static const char *full_font_name(const char **name)
+{
+	if (!strcmp(*name, "Cour")) return "Courier";
+	if (!strcmp(*name, "Helv")) return "Helvetica";
+	if (!strcmp(*name, "TiRo")) return "Times-Roman";
+	if (!strcmp(*name, "Symb")) return "Symbol";
+	if (!strcmp(*name, "ZaDb")) return "ZapfDingbats";
+	return *name = "Helv", "Helvetica";
+}
+
+static void
+write_variable_text(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, pdf_obj **res,
+	fz_text_language lang, const char *text,
+	const char *fontname, float size, int n, float *color, int q,
+	float w, float h, float padding, float baseline, float lineheight,
+	int multiline, int comb, int adjust_baseline)
+{
+	fz_font *font;
+	pdf_obj *res_font;
+
+	w -= padding * 2;
+	h -= padding * 2;
+
+	font = fz_new_base14_font(ctx, full_font_name(&fontname));
+	fz_try(ctx)
+	{
+		if (!*res)
+			*res = pdf_new_dict(ctx, annot->page->doc, 1);
+		res_font = pdf_dict_put_dict(ctx, *res, PDF_NAME(Font), 1);
+		add_required_fonts(ctx, annot->page->doc, res_font, lang, font, fontname, text);
+
+		if (size == 0)
+		{
+			if (multiline)
+				size = 12;
+			else
+			{
+				size = w / measure_string(ctx, lang, font, text);
+				if (size > h)
+					size = h;
+			}
+		}
+
+		lineheight = size * lineheight;
+		baseline = size * baseline;
+
+		if (adjust_baseline)
+		{
+			/* Make sure baseline is inside rectangle */
+			if (baseline + 0.2f * size > h)
+				baseline = h - 0.2f * size;
+		}
+
+		fz_append_string(ctx, buf, "BT\n");
+		write_color0(ctx, buf, n, color, 0);
+		if (multiline)
+		{
+			fz_append_printf(ctx, buf, "%g %g Td\n", padding, padding+h-baseline+lineheight);
+			write_string_with_quadding(ctx, buf, lang, fontname, font, size, lineheight, text, w, q);
+		}
+		else if (comb > 0)
+		{
+			float ty = (h - size) / 2;
+			fz_append_printf(ctx, buf, "%g %g Td\n", padding, padding+h-baseline-ty);
+			write_comb_string(ctx, buf, lang, fontname, font, size, text, (w * 1000 / size) / comb);
+		}
+		else
+		{
+			float tx = 0, ty = (h - size) / 2;
+			if (q > 0)
+			{
+				float tw = measure_string(ctx, lang, font, text) * size;
+				if (q == 1)
+					tx = (w - tw) / 2;
+				else
+					tx = (w - tw);
+			}
+			fz_append_printf(ctx, buf, "%g %g Td\n", padding+tx, padding+h-baseline-ty);
+			write_string(ctx, buf, lang, font, fontname, size, text, text + strlen(text));
+		}
+		fz_append_string(ctx, buf, "ET\n");
+	}
+	fz_always(ctx)
+		fz_drop_font(ctx, font);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+layout_variable_text(fz_context *ctx, fz_layout_block *out,
+	const char *text, fz_text_language lang, const char *fontname, float size, int q,
+	float x, float y, float w, float h, float padding, float baseline, float lineheight,
+	int multiline, int comb, int adjust_baseline)
+{
+	fz_font *font;
+
+	w -= padding * 2;
+	h -= padding * 2;
+
+	font = fz_new_base14_font(ctx, full_font_name(&fontname));
+	fz_try(ctx)
+	{
+		if (size == 0)
+		{
+			if (multiline)
+				size = 12;
+			else
+			{
+				size = w / measure_string(ctx, lang, font, text);
+				if (size > h)
+					size = h;
+			}
+		}
+
+		lineheight = size * lineheight;
+		baseline = size * baseline;
+
+		if (adjust_baseline)
+		{
+			/* Make sure baseline is inside rectangle */
+			if (baseline + 0.2f * size > h)
+				baseline = h - 0.2f * size;
+		}
+
+		if (multiline)
+		{
+			x += padding;
+			y += padding + h - baseline;
+			layout_string_with_quadding(ctx, out, lang, font, size, lineheight, x, y, text, w, q);
+		}
+		else if (comb > 0)
+		{
+			float ty = (h - size) / 2;
+			x += padding;
+			y += padding + h - baseline - ty;
+			layout_comb_string(ctx, out, x, y, text, text + strlen(text), font, size, w / comb);
+		}
+		else
+		{
+			float tx = 0, ty = (h - size) / 2;
+			if (q > 0)
+			{
+				float tw = measure_string(ctx, lang, font, text) * size;
+				if (q == 1)
+					tx = (w - tw) / 2;
+				else
+					tx = (w - tw);
+			}
+			x += padding + tx;
+			y += padding + h - baseline - ty;
+			layout_string(ctx, out, lang, font, size, x, y, text, text + strlen(text));
+		}
+	}
+	fz_always(ctx)
+		fz_drop_font(ctx, font);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+#if FZ_ENABLE_HTML_ENGINE
+static void
+write_rich_content(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, pdf_obj **res, const char *rc, const char *ds, float size, float w, float h, float b, int multiline)
+{
+	/* border_box is the actual rectangle that we are writing into.
+	 * We use this to feed to the pdfwriting device. */
+	fz_rect border_box = { 0, 0, w, h };
+
+	/* content_box is adjusted for padding and has added height.
+	 * We know a clipping rectangle will have been set to the proper rectangle
+	 * so we can allow text to flow out the bottom of the rectangle rather than
+	 * just missing it out. This matches adobe. */
+	fz_rect content_box = fz_make_rect(b, b, w - b*2, h + size * 2);
+
+	fz_buffer *inbuf = fz_new_buffer_from_copied_data(ctx, (const unsigned char *)rc, strlen(rc)+1);
+	fz_story *story = NULL;
+	fz_device *dev = NULL;
+	fz_buffer *buf2 = NULL;
+	const char *default_css = "@page{margin:0} body{margin:0;line-height:1.2;white-space:pre-wrap;} p{margin:0}";
+	char *css = NULL;
+
+	fz_var(story);
+	fz_var(dev);
+	fz_var(res);
+	fz_var(buf2);
+	fz_var(css);
+
+	// single-line should be centered in the box. adjust content box accordingly.
+	// this matches the math in write_variable_text.
+	if (!multiline)
+	{
+		float ty = ((h - b * 2) - size) / 2;
+		content_box.y0 = h - b - 0.8f * size + ty;
+		content_box.y1 = content_box.y0 + size * 2;
+	}
+
+	fz_try(ctx)
+	{
+		if (ds)
+			css = fz_asprintf(ctx, "%s body{%s}", default_css, ds);
+		story = fz_new_story(ctx, inbuf, css ? css : default_css, size, NULL);
+		dev = pdf_page_write(ctx, annot->page->doc, border_box, res, &buf2);
+		fz_place_story(ctx, story, content_box, NULL);
+		fz_draw_story(ctx, story, dev, fz_identity);
+		fz_close_device(ctx, dev);
+
+		fz_append_buffer(ctx, buf, buf2);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_device(ctx, dev);
+		fz_drop_buffer(ctx, buf2);
+		fz_drop_story(ctx, story);
+		fz_drop_buffer(ctx, inbuf);
+		fz_free(ctx, css);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+#endif
+
+#if FZ_ENABLE_HTML_ENGINE
+
+static char *
+escape_text(fz_context *ctx, const char *s)
+{
+	size_t len = 1;
+	char c;
+	const char *s2;
+	char *d, *d2;
+
+	for (s2 = s; (c = *s2++) != 0; len++)
+	{
+		if (c == '<')
+			len += 3; /* &lt; */
+		else if (c == '>')
+			len += 3; /* &gt; */
+		else if (c == '&')
+			len += 4; /* &amp; */
+	}
+
+	d = d2 = fz_malloc(ctx, len);
+
+	for (s2 = s; (c = *s2++) != 0; )
+	{
+		if (c == '<')
+		{
+			*d++ = '&';
+			*d++ = 'l';
+			*d++ = 't';
+			*d++ = ';';
+		}
+		else if (c == '>')
+		{
+			*d++ = '&';
+			*d++ = 'g';
+			*d++ = 't';
+			*d++ = ';';
+		}
+		else if (c == '&')
+		{
+			*d++ = '&';
+			*d++ = 'a';
+			*d++ = 'm';
+			*d++ = 'p';
+			*d++ = ';';
+		}
+		else
+			*d++ = c;
+	}
+	*d++ = 0;
+
+	return d2;
+}
+
+int text_needs_rich_layout(fz_context *ctx, const char *s)
+{
+	int c, script;
+	while (*s)
+	{
+		s += fz_chartorune(&c, s);
+
+		// base 14 fonts
+		if (fz_windows_1252_from_unicode(c) > 0)
+			continue;
+		if (fz_iso8859_7_from_unicode(c) > 0)
+			continue;
+		if (fz_koi8u_from_unicode(c) > 0)
+			continue;
+
+		// cjk fonts
+		script = ucdn_get_script(c);
+		if (
+			script == UCDN_SCRIPT_HANGUL ||
+			script == UCDN_SCRIPT_HIRAGANA ||
+			script == UCDN_SCRIPT_KATAKANA ||
+			script == UCDN_SCRIPT_BOPOMOFO ||
+			script == UCDN_SCRIPT_HAN
+		)
+			continue;
+
+		return 1;
+	}
+	return 0;
+}
+
+static unsigned int hex_from_color(fz_context *ctx, int n, float color[4])
+{
+	float rgb[4];
+	int r, g, b;
+	switch (n)
+	{
+	default:
+		r = g = b = 0;
+		break;
+	case 1:
+		r = g = b = color[0] * 255;
+		break;
+	case 3:
+		r = color[0] * 255;
+		g = color[1] * 255;
+		b = color[2] * 255;
+		break;
+	case 4:
+		fz_convert_color(ctx, fz_device_cmyk(ctx), color, fz_device_rgb(ctx), rgb, NULL, fz_default_color_params);
+		r = rgb[0] * 255;
+		g = rgb[1] * 255;
+		b = rgb[2] * 255;
+		break;
+	}
+	return (r<<16) | (g<<8) | b;
+}
+
+#endif
+
+static float
+pdf_write_line_caption(fz_context *ctx, pdf_annot *annot, fz_buffer *buf, fz_rect *rect, pdf_obj **res, fz_point a, fz_point b)
+{
+	float dx, dy, line_length;
+	fz_point co;
+	float size;
+	const char *text;
+	pdf_obj *res_font;
+	fz_font *font = NULL;
+	fz_matrix tm;
+	int lang;
+	int top;
+	float tw;
+
+	fz_var(font);
+
+	fz_try(ctx)
+	{
+		// vector of line
+		dx = b.x - a.x;
+		dy = b.y - a.y;
+		line_length = hypotf(dx, dy);
+		dx /= line_length;
+		dy /= line_length;
+
+		text = pdf_annot_contents(ctx, annot);
+		lang = pdf_annot_language(ctx, annot);
+		co = pdf_dict_get_point(ctx, annot->obj, PDF_NAME(CO));
+
+		font = fz_new_base14_font(ctx, "Helvetica");
+		if (!*res)
+			*res = pdf_new_dict(ctx, annot->page->doc, 1);
+		res_font = pdf_dict_put_dict(ctx, *res, PDF_NAME(Font), 1);
+		add_required_fonts(ctx, annot->page->doc, res_font, lang, font, "Helv", text);
+		size = 12;
+
+		tw = measure_string(ctx, lang, font, text) * size;
+
+		// don't inline if CP says so
+		top = 0;
+		if (pdf_dict_get(ctx, annot->obj, PDF_NAME(CP)) == PDF_NAME(Top))
+			top = 1;
+
+		// don't inline if caption wouldn't fit
+		if (tw + size > line_length)
+			top = 1;
+
+		tm = fz_rotate(atan2(dy, dx) * 180 / M_PI);
+		tm.e = (a.x + b.x) / 2 - dx * (tw / 2);
+		tm.f = (a.y + b.y) / 2 - dy * (tw / 2);
+
+		// caption offset
+		if (co.x || co.y)
+		{
+			// don't write text inline
+			top = 1;
+
+			if (co.y < 0)
+				co.y -= size;
+
+			tm.e += co.x * dx - co.y * dy;
+			tm.f += co.x * dy + co.y * dx;
+		}
+		else if (top)
+		{
+			tm.e -= dy * size * 0.2f;
+			tm.f += dx * size * 0.2f;
+		}
+		else
+		{
+			tm.e += dy * size * 0.3f;
+			tm.f -= dx * size * 0.3f;
+		}
+
+		fz_append_printf(ctx, buf, "q\n%M cm\n", &tm);
+		fz_append_string(ctx, buf, "0 g\n"); // Acrobat always draws captions in black
+		fz_append_printf(ctx, buf, "BT\n");
+		write_string(ctx, buf, lang, font, "Helv", size, text, text + strlen(text));
+		fz_append_printf(ctx, buf, "ET\n");
+		fz_append_printf(ctx, buf, "Q\n");
+
+		*rect = fz_include_point_in_rect(*rect, fz_make_point(tm.e - dx * tw/2, tm.f - dy * tw/2));
+		*rect = fz_include_point_in_rect(*rect, fz_make_point(tm.e + dx * tw/2, tm.f + dy * tw/2));
+		*rect = fz_expand_rect(*rect, size);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_font(ctx, font);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	if (!top)
+		return (tw + size / 2) / 2;
+	return 0;
+}
+
+static void
+pdf_write_free_text_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf,
+	fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res)
+{
+	const char *font;
+	float size, color[4];
+	const char *text;
+	float w, h, b;
+	int q, r, n;
+	int lang;
+	fz_rect rd;
+	pdf_obj *le;
+	int ic;
+	fz_rect text_box;
+	fz_matrix tfm;
+#if FZ_ENABLE_HTML_ENGINE
+	const char *rc, *ds;
+	char *free_rc = NULL;
+	char ds_buf[400];
+#endif
+
+	text = pdf_annot_contents(ctx, annot);
+	q = pdf_annot_quadding(ctx, annot);
+	pdf_annot_default_appearance(ctx, annot, &font, &size, &n, color);
+	lang = pdf_annot_language(ctx, annot);
+	rd = pdf_annot_rect_diff(ctx, annot);
+
+	/* /Rotate is an undocumented annotation property supported by Adobe.
+	 * When Rotate is used, neither the box, nor the arrow move at all.
+	 * Only the position of the text moves within the box. Thus we don't
+	 * need to adjust rd at all! */
+	r = pdf_dict_get_int(ctx, annot->obj, PDF_NAME(Rotate));
+
+	// Adjust input Rect for RD to get the bounds of the text box area
+	text_box = *rect;
+	text_box.x0 += rd.x0;
+	text_box.y0 += rd.y0;
+	text_box.x1 -= rd.x1;
+	text_box.y1 -= rd.y1;
+
+	*rect = text_box;
+
+	// Size of text box area (including padding and border)
+	w = text_box.x1 - text_box.x0;
+	h = text_box.y1 - text_box.y0;
+
+	pdf_write_opacity(ctx, annot, buf, res);
+	pdf_write_dash_pattern(ctx, annot, buf, res);
+
+	// Set stroke and fill colors for box and callout line
+	ic = pdf_write_fill_color_appearance(ctx, annot, buf);
+	write_color0(ctx, buf, n, color, 1);
+	b = pdf_write_border_appearance(ctx, annot, buf);
+
+	// Draw Callout line
+	if (pdf_name_eq(ctx, pdf_dict_get(ctx, annot->obj, PDF_NAME(IT)), PDF_NAME(FreeTextCallout)))
+	{
+		pdf_obj *cl = pdf_dict_get(ctx, annot->obj, PDF_NAME(CL));
+		int i, len = pdf_array_len(ctx, cl);
+		if (len == 4 || len == 6)
+		{
+			float xy[6];
+
+			for (i = 0; i < len; i += 2)
+			{
+				float x = xy[i+0] = pdf_array_get_real(ctx, cl, i+0);
+				float y = xy[i+1] = pdf_array_get_real(ctx, cl, i+1);
+				if (x < rect->x0) rect->x0 = x;
+				if (y < rect->y0) rect->y0 = y;
+				if (x > rect->x1) rect->x1 = x;
+				if (y > rect->y1) rect->y1 = y;
+			}
+
+			fz_append_printf(ctx, buf, "%g %g m\n", xy[0], xy[1]);
+			for (i = 2; i < len; i += 2)
+				fz_append_printf(ctx, buf, "%g %g l\n", xy[i+0], xy[i+1]);
+			fz_append_printf(ctx, buf, "S\n");
+
+			le = pdf_dict_get(ctx, annot->obj, PDF_NAME(LE));
+			pdf_write_line_cap_appearance(ctx, buf, rect,
+				xy[0], xy[1],
+				xy[2] - xy[0], xy[3] - xy[1], b,
+				1, ic, le);
+		}
+	}
+
+	// Draw text box background
+	if (ic)
+		fz_append_printf(ctx, buf, "%g %g %g %g re\nf\n", text_box.x0, text_box.y0, w, h);
+
+	// Draw text box border
+	if (b > 0)
+		fz_append_printf(ctx, buf, "%g %g %g %g re\nS\n", text_box.x0 + b/2, text_box.y0 + b/2, w - b, h - b);
+
+	// Clip text to box
+	fz_append_printf(ctx, buf, "%g %g %g %g re\nW\nn\n", text_box.x0 + b, text_box.y0 + b, w - b * 2, h - b * 2);
+
+	// Recompute Rect and RD to account for Callout line
+	rd.x0 = text_box.x0 - rect->x0;
+	rd.y0 = text_box.y0 - rect->y0;
+	rd.x1 = rect->x1 - text_box.x1;
+	rd.y1 = rect->y1 - text_box.y1;
+
+	// Compute rotation (and offset) transform for text
+	if (r == 90 || r == 270)
+	{
+		float t = w; w = h; h = t;
+	}
+	tfm = fz_rotate(r);
+	if (r == 270)
+		tfm.e += 0, tfm.f += w;
+	else if (r == 90)
+		tfm.e += h, tfm.f -= 0;
+	else if (r == 180)
+		tfm.e += w, tfm.f += h;
+	tfm.e += text_box.x0;
+	tfm.f += text_box.y0;
+	fz_append_printf(ctx, buf, "q\n%g %g %g %g %g %g cm\n", tfm.a, tfm.b, tfm.c, tfm.d, tfm.e, tfm.f);
+
+#if FZ_ENABLE_HTML_ENGINE
+	ds = pdf_dict_get_text_string_opt(ctx, annot->obj, PDF_NAME(DS));
+	rc = pdf_dict_get_text_string_opt(ctx, annot->obj, PDF_NAME(RC));
+	if (!rc && (ds || text_needs_rich_layout(ctx, text)))
+	{
+		rc = free_rc = escape_text(ctx, text);
+		if (!ds)
+		{
+			fz_snprintf(ds_buf, sizeof ds_buf,
+				"font-family:%s;font-size:%gpt;color:#%06x;text-align:%s;",
+				full_font_name(&font),
+				size,
+				hex_from_color(ctx, n, color),
+				(q == 0 ? "left" : q == 1 ? "center" : "right")
+			);
+			ds = ds_buf;
+		}
+	}
+	if (rc)
+	{
+		fz_try(ctx)
+			write_rich_content(ctx, annot, buf, res, rc ? rc : text, ds, size, w, h, b * 2, 1);
+		fz_always(ctx)
+			fz_free(ctx, free_rc);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+	else
+#endif
+	{
+		write_variable_text(ctx, annot, buf, res, lang, text, font, size, n, color, q, w, h, b*2,
+			0.8f, 1.2f, 1, 0, 0);
+	}
+
+	fz_append_printf(ctx, buf, "Q\n");
+
+	pdf_dict_put_rect(ctx, annot->obj, PDF_NAME(RD), rd);
+
+	*matrix = fz_identity;
+	*bbox = *rect;
+}
+
+static void
+pdf_write_tx_widget_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf,
+	const fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res,
+	const char *text, int ff)
+{
+	fz_text_language lang;
+	const char *font;
+	float size, color[4];
+	float w, h, t, b;
+	int has_bc = 0;
+	int q, r, n;
+
+#if FZ_ENABLE_HTML_ENGINE
+	const char *rc, *ds;
+	char *free_rc = NULL;
+	char ds_buf[400];
+#endif
+
+	r = pdf_dict_get_int(ctx, pdf_dict_get(ctx, annot->obj, PDF_NAME(MK)), PDF_NAME(R));
+	q = pdf_annot_quadding(ctx, annot);
+	pdf_annot_default_appearance(ctx, annot, &font, &size, &n, color);
+	lang = pdf_annot_language(ctx, annot);
+
+	w = rect->x1 - rect->x0;
+	h = rect->y1 - rect->y0;
+	r = r % 360;
+	if (r == 90 || r == 270)
+		t = h, h = w, w = t;
+	*matrix = fz_rotate(r);
+	*bbox = fz_make_rect(0, 0, w, h);
+
+	fz_append_string(ctx, buf, "/Tx BMC\nq\n");
+
+	if (pdf_write_MK_BG_appearance(ctx, annot, buf))
+		fz_append_printf(ctx, buf, "0 0 %g %g re\nf\n", w, h);
+
+	b = pdf_write_border_appearance(ctx, annot, buf);
+	if (b > 0 && pdf_write_MK_BC_appearance(ctx, annot, buf))
+	{
+		fz_append_printf(ctx, buf, "%g %g %g %g re\ns\n", b/2, b/2, w-b, h-b);
+		has_bc = 1;
+	}
+
+	fz_append_printf(ctx, buf, "%g %g %g %g re\nW\nn\n", b, b, w-b*2, h-b*2);
+
+#if FZ_ENABLE_HTML_ENGINE
+	ds = pdf_dict_get_text_string_opt(ctx, annot->obj, PDF_NAME(DS));
+	rc = pdf_dict_get_text_string_opt(ctx, annot->obj, PDF_NAME(RV));
+	if (!rc && (ds || text_needs_rich_layout(ctx, text)))
+	{
+		rc = free_rc = escape_text(ctx, text);
+		if (!ds)
+		{
+			fz_snprintf(ds_buf, sizeof ds_buf,
+				"font-family:%s;font-size:%gpt;color:#%06x;text-align:%s",
+				full_font_name(&font),
+				size,
+				hex_from_color(ctx, n, color),
+				(q == 0 ? "left" : q == 1 ? "center" : "right")
+			);
+			ds = ds_buf;
+		}
+	}
+	if (rc)
+	{
+		fz_try(ctx)
+			write_rich_content(ctx, annot, buf, res, rc ? rc : text, ds, size, w, h, b * 2,
+				(ff & PDF_TX_FIELD_IS_MULTILINE));
+		fz_always(ctx)
+			fz_free(ctx, free_rc);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+	else
+#endif
+
+	if (ff & PDF_TX_FIELD_IS_MULTILINE)
+	{
+		write_variable_text(ctx, annot, buf, res, lang, text, font, size, n, color, q, w, h, b*2,
+			1.116f, 1.116f, 1, 0, 1);
+	}
+	else if (ff & PDF_TX_FIELD_IS_COMB)
+	{
+		int maxlen = pdf_dict_get_inheritable_int(ctx, annot->obj, PDF_NAME(MaxLen));
+		if (has_bc && maxlen > 1)
+		{
+			float cell_w = (w - 2 * b) / maxlen;
+			int i;
+			for (i = 1; i < maxlen; ++i)
+			{
+				float x = b + cell_w * i;
+				fz_append_printf(ctx, buf, "%g %g m %g %g l s\n", x, b, x, h-b);
+			}
+		}
+		write_variable_text(ctx, annot, buf, res, lang, text, font, size, n, color, q, w, h, 0,
+			0.8f, 1.2f, 0, maxlen, 0);
+	}
+	else
+	{
+		write_variable_text(ctx, annot, buf, res, lang, text, font, size, n, color, q, w, h, b*2,
+			0.8f, 1.2f, 0, 0, 0);
+	}
+
+	fz_append_string(ctx, buf, "Q\nEMC\n");
+}
+
+fz_layout_block *
+pdf_layout_text_widget(fz_context *ctx, pdf_annot *annot)
+{
+	fz_text_language lang;
+	fz_layout_block *out;
+	const char *font;
+	const char *text;
+	fz_rect rect;
+	float size, color[4];
+	float w, h, t, b, x, y;
+	int q, r, n;
+	int ff;
+
+	rect = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect));
+	text = pdf_field_value(ctx, annot->obj);
+	ff = pdf_field_flags(ctx, annot->obj);
+
+	b = pdf_annot_border_width(ctx, annot);
+	r = pdf_dict_get_int(ctx, pdf_dict_get(ctx, annot->obj, PDF_NAME(MK)), PDF_NAME(R));
+	q = pdf_annot_quadding(ctx, annot);
+	pdf_annot_default_appearance(ctx, annot, &font, &size, &n, color);
+	lang = pdf_annot_language(ctx, annot);
+
+	w = rect.x1 - rect.x0;
+	h = rect.y1 - rect.y0;
+	r = r % 360;
+	if (r == 90 || r == 270)
+		t = h, h = w, w = t;
+
+	x = rect.x0;
+	y = rect.y0;
+
+	out = fz_new_layout(ctx);
+	fz_try(ctx)
+	{
+		pdf_page_transform(ctx, annot->page, NULL, &out->matrix);
+		out->matrix = fz_concat(out->matrix, fz_rotate(r));
+		out->inv_matrix = fz_invert_matrix(out->matrix);
+
+		if (ff & PDF_TX_FIELD_IS_MULTILINE)
+		{
+			layout_variable_text(ctx, out, text, lang, font, size, q, x, y, w, h, b*2, 1.116f, 1.116f, 1, 0, 1);
+		}
+		else if (ff & PDF_TX_FIELD_IS_COMB)
+		{
+			int maxlen = pdf_dict_get_inheritable_int(ctx, annot->obj, PDF_NAME(MaxLen));
+			layout_variable_text(ctx, out, text, lang, font, size, q, x, y, w, h, 0, 0.8f, 1.2f, 0, maxlen, 0);
+		}
+		else
+		{
+			layout_variable_text(ctx, out, text, lang, font, size, q, x, y, w, h, b*2, 0.8f, 1.2f, 0, 0, 0);
+		}
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_layout(ctx, out);
+		fz_rethrow(ctx);
+	}
+	return out;
+}
+
+static void
+pdf_write_ch_widget_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf,
+	const fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res)
+{
+	int ff = pdf_field_flags(ctx, annot->obj);
+	if (ff & PDF_CH_FIELD_IS_COMBO)
+	{
+		/* TODO: Pop-down arrow */
+		pdf_write_tx_widget_appearance(ctx, annot, buf, rect, bbox, matrix, res,
+			pdf_field_value(ctx, annot->obj), 0);
+	}
+	else
+	{
+		fz_buffer *text = fz_new_buffer(ctx, 1024);
+		fz_try(ctx)
+		{
+			pdf_obj *opt = pdf_dict_get(ctx, annot->obj, PDF_NAME(Opt));
+			int i = pdf_dict_get_int(ctx, annot->obj, PDF_NAME(TI));
+			int n = pdf_array_len(ctx, opt);
+			/* TODO: Scrollbar */
+			/* TODO: Highlight selected items */
+			if (i < 0)
+				i = 0;
+			for (; i < n; ++i)
+			{
+				pdf_obj *val = pdf_array_get(ctx, opt, i);
+				if (pdf_is_array(ctx, val))
+					fz_append_string(ctx, text, pdf_array_get_text_string(ctx, val, 1));
+				else
+					fz_append_string(ctx, text, pdf_to_text_string(ctx, val));
+				fz_append_byte(ctx, text, '\n');
+			}
+			pdf_write_tx_widget_appearance(ctx, annot, buf, rect, bbox, matrix, res,
+				fz_string_from_buffer(ctx, text), PDF_TX_FIELD_IS_MULTILINE);
+		}
+		fz_always(ctx)
+			fz_drop_buffer(ctx, text);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+}
+
+static void
+pdf_write_sig_widget_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf,
+	const fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res)
+{
+	float x0 = rect->x0 + 1;
+	float y0 = rect->y0 + 1;
+	float x1 = rect->x1 - 1;
+	float y1 = rect->y1 - 1;
+	float w = x1 - x0;
+	float h = y1 - y0;
+	fz_append_printf(ctx, buf, "1 w\n0 G\n");
+	fz_append_printf(ctx, buf, "%g %g %g %g re\n", x0, y0, w, h);
+	fz_append_printf(ctx, buf, "%g %g m %g %g l\n", x0, y0, x1, y1);
+	fz_append_printf(ctx, buf, "%g %g m %g %g l\n", x1, y0, x0, y1);
+	fz_append_printf(ctx, buf, "s\n");
+	*bbox = *rect;
+	*matrix = fz_identity;
+}
+
+static void
+pdf_write_widget_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf,
+	fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res)
+{
+	pdf_obj *ft = pdf_dict_get_inheritable(ctx, annot->obj, PDF_NAME(FT));
+	if (pdf_name_eq(ctx, ft, PDF_NAME(Tx)))
+	{
+		int ff = pdf_field_flags(ctx, annot->obj);
+		char *format = NULL;
+		const char *text = NULL;
+		if (!annot->ignore_trigger_events)
+		{
+			format = pdf_field_event_format(ctx, annot->page->doc, annot->obj);
+			if (format)
+				text = format;
+			else
+				text = pdf_field_value(ctx, annot->obj);
+		}
+		else
+		{
+			text = pdf_field_value(ctx, annot->obj);
+		}
+		fz_try(ctx)
+			pdf_write_tx_widget_appearance(ctx, annot, buf, rect, bbox, matrix, res, text, ff);
+		fz_always(ctx)
+			fz_free(ctx, format);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+	else if (pdf_name_eq(ctx, ft, PDF_NAME(Ch)))
+	{
+		pdf_write_ch_widget_appearance(ctx, annot, buf, rect, bbox, matrix, res);
+	}
+	else if (pdf_name_eq(ctx, ft, PDF_NAME(Sig)))
+	{
+		pdf_write_sig_widget_appearance(ctx, annot, buf, rect, bbox, matrix, res);
+	}
+	else
+	{
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "cannot create appearance stream for %s widgets", pdf_to_name(ctx, ft));
+	}
+}
+
+static void
+pdf_write_appearance(fz_context *ctx, pdf_annot *annot, fz_buffer *buf,
+	fz_rect *rect, fz_rect *bbox, fz_matrix *matrix, pdf_obj **res)
+{
+	switch (pdf_annot_type(ctx, annot))
+	{
+	default:
+		fz_throw(ctx, FZ_ERROR_UNSUPPORTED, "cannot create appearance stream for %s annotations",
+			pdf_dict_get_name(ctx, annot->obj, PDF_NAME(Subtype)));
+	case PDF_ANNOT_WIDGET:
+		pdf_write_widget_appearance(ctx, annot, buf, rect, bbox, matrix, res);
+		break;
+	case PDF_ANNOT_INK:
+		pdf_write_ink_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_POLYGON:
+		pdf_write_polygon_appearance(ctx, annot, buf, rect, res, 1);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_POLY_LINE:
+		pdf_write_polygon_appearance(ctx, annot, buf, rect, res, 0);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_LINE:
+		pdf_write_line_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_SQUARE:
+		pdf_write_square_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_CIRCLE:
+		pdf_write_circle_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_CARET:
+		pdf_write_caret_appearance(ctx, annot, buf, rect, bbox, res);
+		*matrix = fz_identity;
+		break;
+	case PDF_ANNOT_TEXT:
+	case PDF_ANNOT_FILE_ATTACHMENT:
+	case PDF_ANNOT_SOUND:
+		pdf_write_icon_appearance(ctx, annot, buf, rect, bbox, res);
+		*matrix = fz_identity;
+		break;
+	case PDF_ANNOT_HIGHLIGHT:
+		pdf_write_highlight_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_UNDERLINE:
+		pdf_write_underline_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_STRIKE_OUT:
+		pdf_write_strike_out_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_SQUIGGLY:
+		pdf_write_squiggly_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_REDACT:
+		pdf_write_redact_appearance(ctx, annot, buf, rect, res);
+		*matrix = fz_identity;
+		*bbox = *rect;
+		break;
+	case PDF_ANNOT_STAMP:
+		pdf_write_stamp_appearance(ctx, annot, buf, rect, bbox, matrix, res);
+		break;
+	case PDF_ANNOT_FREE_TEXT:
+		pdf_write_free_text_appearance(ctx, annot, buf, rect, bbox, matrix, res);
+		break;
+	}
+}
+
+static pdf_obj *draw_push_button(fz_context *ctx, pdf_annot *annot, fz_rect bbox, fz_matrix matrix, float w, float h,
+	const char *caption, const char *font, float size, int n, float *color,
+	int down)
+{
+	pdf_obj *ap, *res = NULL;
+	fz_buffer *buf;
+	float bc[3] = { 0, 0, 0 };
+	float bg[3] = { 0.8f, 0.8f, 0.8f };
+	float hi[3], sh[3];
+	int has_bg, has_bc;
+	float b;
+	int i;
+
+	buf = fz_new_buffer(ctx, 1024);
+	fz_var(res);
+	fz_try(ctx)
+	{
+		b = pdf_annot_border_width(ctx, annot);
+		has_bc = pdf_annot_MK_BC_rgb(ctx, annot, bc);
+		has_bg = pdf_annot_MK_BG_rgb(ctx, annot, bg);
+
+		for (i = 0; i < 3; ++i)
+		{
+			if (down)
+			{
+				sh[i] = 1 - (1 - bg[i]) / 2;
+				hi[i] = bg[i] / 2;
+			}
+			else
+			{
+				hi[i] = 1 - (1 - bg[i]) / 2;
+				sh[i] = bg[i] / 2;
+			}
+		}
+
+		fz_append_string(ctx, buf, "q\n");
+		fz_append_printf(ctx, buf, "%g w\n", b);
+		if (has_bg)
+		{
+			fz_append_printf(ctx, buf, "%g %g %g rg\n", bg[0], bg[1], bg[2]);
+			fz_append_printf(ctx, buf, "0 0 %g %g re\nf\n", 0, 0, w, h);
+		}
+		if (has_bc && b > 0)
+		{
+			fz_append_printf(ctx, buf, "%g %g %g RG\n", bc[0], bc[1], bc[2]);
+			fz_append_printf(ctx, buf, "%g %g %g %g re\nS\n", b/2, b/2, w-b, h-b);
+		}
+		if (has_bg)
+		{
+			fz_append_printf(ctx, buf, "%g %g %g rg\n", hi[0], hi[1], hi[2]);
+			fz_append_printf(ctx, buf, "%g %g m %g %g l %g %g l %g %g l %g %g l %g %g l f\n",
+				b, b, b, h-b, w-b, h-b, w-b-2, h-b-2, b+2, h-b-2, b+2, b+2);
+			fz_append_printf(ctx, buf, "%g %g %g rg\n", sh[0], sh[1], sh[2]);
+			fz_append_printf(ctx, buf, "%g %g m %g %g l %g %g l %g %g l %g %g l %g %g l f\n",
+				b, b, b+2, b+2, w-b-2, b+2, w-b-2, h-b-2, w-b, h-b, w-b, b);
+		}
+		if (down)
+			fz_append_string(ctx, buf, "1 0 0 1 2 -2 cm\n");
+		write_variable_text(ctx, annot, buf, &res, FZ_LANG_UNSET, caption, font, size, n, color, 1, w, h, b+6, 0.8f, 1.2f, 0, 0, 0);
+		fz_append_string(ctx, buf, "Q\n");
+
+		ap = pdf_new_xobject(ctx, annot->page->doc, bbox, matrix, res, buf);
+	}
+	fz_always(ctx)
+	{
+		pdf_drop_obj(ctx, res);
+		fz_drop_buffer(ctx, buf);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+	return ap;
+}
+
+static pdf_obj *draw_radio_button(fz_context *ctx, pdf_annot *annot, fz_rect bbox, fz_matrix matrix, float w, float h, int yes)
+{
+	pdf_obj *ap;
+	fz_buffer *buf;
+	float b;
+
+	buf = fz_new_buffer(ctx, 1024);
+	fz_try(ctx)
+	{
+		fz_append_string(ctx, buf, "q\n");
+		if (pdf_write_MK_BG_appearance(ctx, annot, buf))
+		{
+			draw_circle_in_box(ctx, buf, 0, 0, 0, w, h);
+			fz_append_string(ctx, buf, "f\n");
+		}
+		b = pdf_write_border_appearance(ctx, annot, buf);
+		if (b > 0 && pdf_write_MK_BC_appearance(ctx, annot, buf))
+		{
+			draw_circle_in_box(ctx, buf, b, 0, 0, w, h);
+			fz_append_string(ctx, buf, "s\n");
+		}
+		if (yes)
+		{
+			fz_append_string(ctx, buf, "0 g\n");
+			draw_circle(ctx, buf, (w-b*2)/4, (h-b*2)/4, w/2, h/2);
+			fz_append_string(ctx, buf, "f\n");
+		}
+		fz_append_string(ctx, buf, "Q\n");
+		ap = pdf_new_xobject(ctx, annot->page->doc, bbox, matrix, NULL, buf);
+	}
+	fz_always(ctx)
+		fz_drop_buffer(ctx, buf);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+	return ap;
+}
+
+static pdf_obj *draw_check_button(fz_context *ctx, pdf_annot *annot, fz_rect bbox, fz_matrix matrix, float w, float h, int yes)
+{
+	float black[1] = { 0 };
+	pdf_obj *ap, *res = NULL;
+	fz_buffer *buf;
+	float b;
+
+	fz_var(res);
+
+	buf = fz_new_buffer(ctx, 1024);
+	fz_try(ctx)
+	{
+		fz_append_string(ctx, buf, "q\n");
+		if (pdf_write_MK_BG_appearance(ctx, annot, buf))
+			fz_append_printf(ctx, buf, "0 0 %g %g re\nf\n", w, h);
+		b = pdf_write_border_appearance(ctx, annot, buf);
+		if (b > 0 && pdf_write_MK_BC_appearance(ctx, annot, buf))
+			fz_append_printf(ctx, buf, "%g %g %g %g re\nS\n", b/2, b/2, w-b, h-b);
+		if (yes)
+			write_variable_text(ctx, annot, buf, &res, FZ_LANG_UNSET, "3", "ZaDb", h, nelem(black), black, 0, w, h, b+h/10, 0.8f, 1.2f, 0, 0, 0);
+		fz_append_string(ctx, buf, "Q\n");
+		ap = pdf_new_xobject(ctx, annot->page->doc, bbox, matrix, res, buf);
+	}
+	fz_always(ctx)
+	{
+		pdf_drop_obj(ctx, res);
+		fz_drop_buffer(ctx, buf);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+	return ap;
+}
+
+static void pdf_update_button_appearance(fz_context *ctx, pdf_annot *annot)
+{
+	int ff = pdf_field_flags(ctx, annot->obj);
+	fz_rect rect = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect));
+	fz_matrix matrix;
+	fz_rect bbox;
+	float w, h, t;
+	int r;
+
+	r = pdf_dict_get_int(ctx, pdf_dict_get(ctx, annot->obj, PDF_NAME(MK)), PDF_NAME(R));
+	w = rect.x1 - rect.x0;
+	h = rect.y1 - rect.y0;
+	r = r % 360;
+	if (r == 90 || r == 270)
+		t = h, h = w, w = t;
+	matrix = fz_rotate(r);
+	bbox = fz_make_rect(0, 0, w, h);
+
+
+	if (ff & PDF_BTN_FIELD_IS_PUSHBUTTON)
+	{
+		pdf_obj *ap_n = NULL;
+		pdf_obj *ap_d = NULL;
+		fz_var(ap_n);
+		fz_var(ap_d);
+		fz_try(ctx)
+		{
+			pdf_obj *ap, *MK, *CA, *AC;
+			const char *font;
+			const char *label;
+			float size, color[4];
+			int n;
+
+			pdf_annot_default_appearance(ctx, annot, &font, &size, &n, color);
+
+			MK = pdf_dict_get(ctx, annot->obj, PDF_NAME(MK));
+			CA = pdf_dict_get(ctx, MK, PDF_NAME(CA));
+			AC = pdf_dict_get(ctx, MK, PDF_NAME(AC));
+
+			label = pdf_to_text_string(ctx, CA);
+			ap_n = draw_push_button(ctx, annot, bbox, matrix, w, h, label, font, size, n, color, 0);
+
+			label = pdf_to_text_string(ctx, AC ? AC : CA);
+			ap_d = draw_push_button(ctx, annot, bbox, matrix, w, h, label, font, size, n, color, 1);
+
+			ap = pdf_dict_put_dict(ctx, annot->obj, PDF_NAME(AP), 2);
+			pdf_dict_put(ctx, ap, PDF_NAME(N), ap_n);
+			pdf_dict_put(ctx, ap, PDF_NAME(D), ap_d);
+		}
+		fz_always(ctx)
+		{
+			pdf_drop_obj(ctx, ap_n);
+			pdf_drop_obj(ctx, ap_d);
+		}
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+	else
+	{
+		pdf_obj *as_yes = NULL;
+		pdf_obj *ap_off = NULL;
+		pdf_obj *ap_yes = NULL;
+		fz_var(ap_off);
+		fz_var(ap_yes);
+		fz_var(as_yes);
+		fz_try(ctx)
+		{
+			pdf_obj *ap, *ap_n, *as;
+
+			if (w > h) w = h;
+			if (h > w) h = w;
+
+			if (ff & PDF_BTN_FIELD_IS_RADIO)
+			{
+				ap_off = draw_radio_button(ctx, annot, bbox, matrix, w, h, 0);
+				ap_yes = draw_radio_button(ctx, annot, bbox, matrix, w, h, 1);
+			}
+			else
+			{
+				ap_off = draw_check_button(ctx, annot, bbox, matrix, w, h, 0);
+				ap_yes = draw_check_button(ctx, annot, bbox, matrix, w, h, 1);
+			}
+
+			as = pdf_dict_get(ctx, annot->obj, PDF_NAME(AS));
+			if (!as)
+			{
+				pdf_dict_put(ctx, annot->obj, PDF_NAME(AS), PDF_NAME(Off));
+				as = PDF_NAME(Off);
+			}
+			else
+				as = pdf_resolve_indirect_chain(ctx, as);
+
+			if (as == PDF_NAME(Off))
+				as_yes = pdf_keep_obj(ctx, pdf_button_field_on_state(ctx, annot->obj));
+			else
+				as_yes = pdf_keep_obj(ctx, as);
+
+			ap = pdf_dict_put_dict(ctx, annot->obj, PDF_NAME(AP), 2);
+			ap_n = pdf_dict_put_dict(ctx, ap, PDF_NAME(N), 2);
+			pdf_dict_put(ctx, ap_n, PDF_NAME(Off), ap_off);
+			pdf_dict_put(ctx, ap_n, as_yes, ap_yes);
+		}
+		fz_always(ctx)
+		{
+			pdf_drop_obj(ctx, as_yes);
+			pdf_drop_obj(ctx, ap_yes);
+			pdf_drop_obj(ctx, ap_off);
+		}
+		fz_catch(ctx)
+		{
+			fz_rethrow(ctx);
+		}
+	}
+	pdf_set_annot_resynthesised(ctx, annot);
+}
+
+static void draw_logo(fz_context *ctx, fz_path *path)
+{
+	/* Use mupdf logo for signature appearance background. */
+	fz_moveto(ctx, path, 122.25f, 0.0f);
+	fz_lineto(ctx, path, 122.25f, 14.249f);
+	fz_curveto(ctx, path, 125.98f, 13.842f, 129.73f, 13.518f, 133.5f, 13.277f);
+	fz_lineto(ctx, path, 133.5f, 0.0f);
+	fz_lineto(ctx, path, 122.25f, 0.0f);
+	fz_closepath(ctx, path);
+	fz_moveto(ctx, path, 140.251f, 0.0f);
+	fz_lineto(ctx, path, 140.251f, 12.935f);
+	fz_curveto(ctx, path, 152.534f, 12.477f, 165.03f, 12.899f, 177.75f, 14.249f);
+	fz_lineto(ctx, path, 177.75f, 21.749f);
+	fz_curveto(ctx, path, 165.304f, 20.413f, 152.809f, 19.871f, 140.251f, 20.348f);
+	fz_lineto(ctx, path, 140.251f, 39.0f);
+	fz_lineto(ctx, path, 133.5f, 39.0f);
+	fz_lineto(ctx, path, 133.5f, 20.704f);
+	fz_curveto(ctx, path, 129.756f, 20.956f, 126.006f, 21.302f, 122.25f, 21.749f);
+	fz_lineto(ctx, path, 122.25f, 50.999f);
+	fz_lineto(ctx, path, 177.751f, 50.999f);
+	fz_lineto(ctx, path, 177.751f, 0.0f);
+	fz_lineto(ctx, path, 140.251f, 0.0f);
+	fz_closepath(ctx, path);
+	fz_moveto(ctx, path, 23.482f, 129.419f);
+	fz_curveto(ctx, path, -20.999f, 199.258f, -0.418f, 292.039f, 69.42f, 336.519f);
+	fz_curveto(ctx, path, 139.259f, 381.0f, 232.04f, 360.419f, 276.52f, 290.581f);
+	fz_curveto(ctx, path, 321.001f, 220.742f, 300.42f, 127.961f, 230.582f, 83.481f);
+	fz_curveto(ctx, path, 160.743f, 39.0f, 67.962f, 59.581f, 23.482f, 129.419f);
+	fz_closepath(ctx, path);
+	fz_moveto(ctx, path, 254.751f, 128.492f);
+	fz_curveto(ctx, path, 303.074f, 182.82f, 295.364f, 263.762f, 237.541f, 309.165f);
+	fz_curveto(ctx, path, 179.718f, 354.568f, 93.57f, 347.324f, 45.247f, 292.996f);
+	fz_curveto(ctx, path, -3.076f, 238.668f, 4.634f, 157.726f, 62.457f, 112.323f);
+	fz_curveto(ctx, path, 120.28f, 66.92f, 206.428f, 74.164f, 254.751f, 128.492f);
+	fz_closepath(ctx, path);
+	fz_moveto(ctx, path, 111.0f, 98.999f);
+	fz_curveto(ctx, path, 87.424f, 106.253f, 68.25f, 122.249f, 51.75f, 144.749f);
+	fz_lineto(ctx, path, 103.5f, 297.749f);
+	fz_lineto(ctx, path, 213.75f, 298.499f);
+	fz_curveto(ctx, path, 206.25f, 306.749f, 195.744f, 311.478f, 185.25f, 314.249f);
+	fz_curveto(ctx, path, 164.22f, 319.802f, 141.22f, 319.775f, 120.0f, 314.999f);
+	fz_curveto(ctx, path, 96.658f, 309.745f, 77.25f, 298.499f, 55.5f, 283.499f);
+	fz_curveto(ctx, path, 69.75f, 299.249f, 84.617f, 311.546f, 102.75f, 319.499f);
+	fz_curveto(ctx, path, 117.166f, 325.822f, 133.509f, 327.689f, 149.25f, 327.749f);
+	fz_curveto(ctx, path, 164.21f, 327.806f, 179.924f, 326.532f, 193.5f, 320.249f);
+	fz_curveto(ctx, path, 213.95f, 310.785f, 232.5f, 294.749f, 245.25f, 276.749f);
+	fz_lineto(ctx, path, 227.25f, 276.749f);
+	fz_curveto(ctx, path, 213.963f, 276.749f, 197.25f, 263.786f, 197.25f, 250.499f);
+	fz_lineto(ctx, path, 197.25f, 112.499f);
+	fz_curveto(ctx, path, 213.75f, 114.749f, 228.0f, 127.499f, 241.5f, 140.999f);
+	fz_curveto(ctx, path, 231.75f, 121.499f, 215.175f, 109.723f, 197.25f, 101.249f);
+	fz_curveto(ctx, path, 181.5f, 95.249f, 168.412f, 94.775f, 153.0f, 94.499f);
+	fz_curveto(ctx, path, 139.42f, 94.256f, 120.75f, 95.999f, 111.0f, 98.999f);
+	fz_closepath(ctx, path);
+	fz_moveto(ctx, path, 125.25f, 105.749f);
+	fz_lineto(ctx, path, 125.25f, 202.499f);
+	fz_lineto(ctx, path, 95.25f, 117.749f);
+	fz_curveto(ctx, path, 105.75f, 108.749f, 114.0f, 105.749f, 125.25f, 105.749f);
+	fz_closepath(ctx, path);
+};
+
+static float logo_color[3] = { (float)0xa4 / (float)0xFF, (float)0xca / (float)0xFF, (float)0xf5 / (float)0xFF };
+
+fz_display_list *
+pdf_signature_appearance_signed(fz_context *ctx, fz_rect rect, fz_text_language lang, fz_image *img, const char *left_text, const char *right_text, int include_logo)
+{
+	fz_display_list *dlist = NULL;
+	fz_device *dev = NULL;
+	fz_text *text = NULL;
+	fz_colorspace *cs = NULL;
+	fz_path *path = NULL;
+	fz_font *font = NULL;
+
+	fz_var(path);
+	fz_var(dlist);
+	fz_var(dev);
+	fz_var(text);
+	fz_var(font);
+	fz_try(ctx)
+	{
+		fz_rect prect;
+		fz_rect logo_bounds;
+		fz_matrix logo_tm;
+		float color[] = { 0.0, 0.0, 0.0 };
+
+		font = fz_new_base14_font(ctx, "Helvetica");
+
+		dlist = fz_new_display_list(ctx, rect);
+		dev = fz_new_list_device(ctx, dlist);
+		cs = fz_device_rgb(ctx);
+
+		if (include_logo)
+		{
+			path = fz_new_path(ctx);
+			draw_logo(ctx, path);
+			logo_bounds = fz_bound_path(ctx, path, NULL, fz_identity);
+			logo_tm = center_rect_within_rect(logo_bounds, rect);
+			fz_fill_path(ctx, dev, path, 0, logo_tm, cs, logo_color, 1.0f, fz_default_color_params);
+		}
+
+		prect = rect;
+		/* If there is to be info on the right then use only the left half of the rectangle for
+		 * what is intended for the left */
+		if (right_text)
+			prect.x1 = (prect.x0 + prect.x1) / 2.0f;
+
+		if (img)
+		{
+			float img_aspect = (float) img->w / img->h;
+			float rectw = prect.x1 - prect.x0;
+			float recth = prect.y1 - prect.y0;
+			float midx = (prect.x0 + prect.x1) / 2.0f;
+			float midy = (prect.y0 + prect.y1) / 2.0f;
+			float rect_aspect = rectw / recth;
+			float scale = img_aspect > rect_aspect ? rectw / img->w : recth / img->h;
+			fz_matrix ctm = fz_pre_translate(fz_pre_scale(fz_translate(midx, midy), scale * img->w, scale * img->h), -0.5, -0.5);
+			fz_fill_image(ctx, dev, img, ctm, 1.0f, fz_default_color_params);
+		}
+
+		if (left_text)
+		{
+			text = pdf_layout_fit_text(ctx, font, lang, left_text, prect);
+			fz_fill_text(ctx, dev, text, fz_identity, cs, color, 1.0f, fz_default_color_params);
+			fz_drop_text(ctx, text);
+			text = NULL;
+		}
+
+		prect = rect;
+		/* If there is to be info on the left then use only the right half of the rectangle for
+		 * what is intended for the right */
+		if (img || left_text)
+			prect.x0 = (prect.x0 + prect.x1) / 2.0f;
+
+		if (right_text)
+		{
+			text = pdf_layout_fit_text(ctx, font, lang, right_text, prect);
+			fz_fill_text(ctx, dev, text, fz_identity, cs, color, 1.0f, fz_default_color_params);
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_device(ctx, dev);
+		fz_drop_path(ctx, path);
+		fz_drop_text(ctx, text);
+		fz_drop_font(ctx, font);
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_display_list(ctx, dlist);
+		fz_rethrow(ctx);
+	}
+
+	return dlist;
+}
+
+fz_display_list *
+pdf_signature_appearance_unsigned(fz_context *ctx, fz_rect rect, fz_text_language lang)
+{
+	fz_display_list *dlist = NULL;
+	fz_device *dev = NULL;
+	fz_text *text = NULL;
+	fz_colorspace *cs = NULL;
+	fz_path *path = NULL;
+	fz_font *font = NULL;
+
+	fz_var(path);
+	fz_var(dlist);
+	fz_var(dev);
+	fz_var(text);
+	fz_var(font);
+	fz_try(ctx)
+	{
+		float text_color[] = { 1.0f, 1.0f, 1.0f };
+		float arrow_color[] = { 0.95f, 0.33f, 0.18f };
+
+		dlist = fz_new_display_list(ctx, rect);
+		dev = fz_new_list_device(ctx, dlist);
+
+		rect.y1 = rect.y0 + (rect.y1 - rect.y0) / 6;
+		rect.x1 = rect.x0 + (rect.y1 - rect.y0) * 4;
+		font = fz_new_base14_font(ctx, "Helvetica");
+
+		path = fz_new_path(ctx);
+		/* Draw a rectangle with a protrusion to the right [xxxxx> */
+		fz_moveto(ctx, path, rect.x0, rect.y0);
+		fz_lineto(ctx, path, rect.x1, rect.y0);
+		fz_lineto(ctx, path, rect.x1 + (rect.y1 - rect.y0) / 2.0f, (rect.y0 + rect.y1) / 2.0f);
+		fz_lineto(ctx, path, rect.x1, rect.y1);
+		fz_lineto(ctx, path, rect.x0, rect.y1);
+		fz_closepath(ctx, path);
+		cs = fz_device_rgb(ctx);
+		fz_fill_path(ctx, dev, path, 0, fz_identity, cs, arrow_color, 1.0f, fz_default_color_params);
+
+		text = pdf_layout_fit_text(ctx, font, lang, "SIGN", rect);
+		fz_fill_text(ctx, dev, text, fz_identity, cs, text_color, 1.0f, fz_default_color_params);
+		fz_drop_text(ctx, text);
+		text = NULL;
+	}
+	fz_always(ctx)
+	{
+		fz_drop_device(ctx, dev);
+		fz_drop_path(ctx, path);
+		fz_drop_text(ctx, text);
+		fz_drop_font(ctx, font);
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_display_list(ctx, dlist);
+		fz_rethrow(ctx);
+	}
+
+	return dlist;
+}
+
+char *
+pdf_signature_info(fz_context *ctx, const char *name, pdf_pkcs7_distinguished_name *dn, const char *reason, const char *location, int64_t date, int include_labels)
+{
+	fz_buffer *fzbuf = NULL;
+	char *dn_str = NULL;
+	char *full_str = NULL;
+	time_t tdate = (time_t)date;
+
+	fz_var(fzbuf);
+	fz_var(dn_str);
+	fz_try(ctx)
+	{
+#ifdef _POSIX_SOURCE
+		struct tm tmbuf, *tm = localtime_r(&tdate, &tmbuf);
+#else
+		struct tm *tm = localtime(&tdate);
+#endif
+		char now_str[40];
+		size_t len = 0;
+#ifdef CLUSTER
+		memset(&date, 0, sizeof(date));
+		memset(tm, 0, sizeof(*tm));
+#endif
+
+		fzbuf = fz_new_buffer(ctx, 256);
+		if (name && strlen(name) > 0)
+		{
+			if (include_labels)
+				fz_append_string(ctx, fzbuf, "Digitally signed by ");
+			fz_append_string(ctx, fzbuf, name);
+		}
+
+		if (dn)
+		{
+			fz_append_string(ctx, fzbuf, "\n");
+			if (include_labels)
+				fz_append_string(ctx, fzbuf, "DN: ");
+			dn_str = pdf_signature_format_distinguished_name(ctx, dn);
+			fz_append_string(ctx, fzbuf, dn_str);
+		}
+
+		if (reason && strlen(reason) > 0)
+		{
+			fz_append_string(ctx, fzbuf, "\n");
+			if (include_labels)
+				fz_append_string(ctx, fzbuf, "Reason: ");
+			fz_append_string(ctx, fzbuf, reason);
+		}
+
+		if (location && strlen(location) > 0)
+		{
+			fz_append_string(ctx, fzbuf, "\n");
+			if (include_labels)
+				fz_append_string(ctx, fzbuf, "Location: ");
+			fz_append_string(ctx, fzbuf, location);
+		}
+
+		if (date >= 0)
+		{
+			len = strftime(now_str, sizeof now_str, "%FT%T%z", tm);
+			if (len)
+			{
+				fz_append_string(ctx, fzbuf, "\n");
+				if (include_labels)
+					fz_append_string(ctx, fzbuf, "Date: ");
+				fz_append_string(ctx, fzbuf, now_str);
+			}
+		}
+
+		fz_terminate_buffer(ctx, fzbuf);
+		(void)fz_buffer_extract(ctx, fzbuf, (unsigned char **)&full_str);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_buffer(ctx, fzbuf);
+		fz_free(ctx, dn_str);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+
+	return full_str;
+}
+
+void
+pdf_annot_push_local_xref(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_document *doc;
+
+	if (!annot->page)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "annotation not bound to any page");
+
+	doc = annot->page->doc;
+
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+	if (doc->local_xref_nesting == 0 && doc->local_xref)
+		fz_write_printf(ctx, fz_stddbg(ctx), "push local_xref for annot\n");
+#endif
+	doc->local_xref_nesting++;
+}
+
+void
+pdf_annot_ensure_local_xref(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_document *doc = annot->page->doc;
+
+	if (doc->local_xref != NULL)
+		return;
+
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+	fz_write_printf(ctx, fz_stddbg(ctx), "creating local_xref\n");
+#endif
+
+	/* We have no local_xref, but we want to be using one. */
+	/* First off, create one. */
+	doc->local_xref = pdf_new_local_xref(ctx, doc);
+}
+
+void
+pdf_annot_pop_local_xref(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_document *doc = annot->page->doc;
+
+	--doc->local_xref_nesting;
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+	if (doc->local_xref_nesting == 0 && doc->local_xref)
+		fz_write_printf(ctx, fz_stddbg(ctx), "pop local_xref for annot\n");
+#endif
+}
+
+void pdf_annot_pop_and_discard_local_xref(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_document *doc = annot->page->doc;
+
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+	if (doc->local_xref)
+		fz_write_printf(ctx, fz_stddbg(ctx), "pop and discard local_xref for annot\n");
+#endif
+	--doc->local_xref_nesting;
+	assert(doc->local_xref_nesting == 0);
+	pdf_drop_local_xref_and_resources(ctx, doc);
+}
+
+static void pdf_update_appearance(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_obj *subtype;
+	pdf_obj *ft = NULL;
+	pdf_obj *ap_n;
+	int pop_local_xref = 1;
+
+retry_after_repair:
+	/* Must have any local xref in place in order to check if it's dirty. */
+	pdf_annot_push_local_xref(ctx, annot);
+
+	pdf_begin_implicit_operation(ctx, annot->page->doc);
+	fz_start_throw_on_repair(ctx);
+
+	fz_var(pop_local_xref);
+
+	fz_try(ctx)
+	{
+		int needs_resynth;
+		int local_synthesis = 0;
+
+		/* Never update Popup and Link annotations */
+		subtype = pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype));
+		if (subtype == PDF_NAME(Popup) || subtype == PDF_NAME(Link))
+		{
+			pdf_end_operation(ctx, annot->page->doc);
+			break;
+		}
+
+		/* Never update signed Signature widgets */
+		if (subtype == PDF_NAME(Widget))
+		{
+			ft = pdf_dict_get_inheritable(ctx, annot->obj, PDF_NAME(FT));
+			if (ft == PDF_NAME(Sig))
+			{
+				/* We cannot synthesise an appearance for a signed Sig, so don't even try. */
+				if (pdf_signature_is_signed(ctx, annot->page->doc, annot->obj))
+				{
+					pdf_end_operation(ctx, annot->page->doc);
+					break;
+				}
+			}
+		}
+
+		/* Check if the field is dirtied by JS events */
+		if (pdf_obj_is_dirty(ctx, annot->obj))
+			pdf_annot_request_resynthesis(ctx, annot);
+
+		/* Find the current appearance stream, if one exists. */
+		ap_n = pdf_annot_ap(ctx, annot);
+
+		/* If there is no appearance stream, we need to create a local one for display purposes. */
+		if (!ap_n)
+			local_synthesis = 1;
+
+		/* Ignore appearance streams not created by us (so not local)
+		 * for unsigned digital signature widgets. They are often blank
+		 * and we want the "sign" arrow to be visible. Never write back
+		 * the forced appearance stream for unsigned signatures. */
+		if (subtype == PDF_NAME(Widget) && ft == PDF_NAME(Sig))
+		{
+			if (ap_n && !pdf_is_local_object(ctx, annot->page->doc, ap_n))
+				local_synthesis = 1;
+		}
+
+		/* We need to put this appearance stream back into the document. */
+		needs_resynth = pdf_annot_needs_resynthesis(ctx, annot);
+		if (needs_resynth)
+			local_synthesis = 0;
+
+		/* Some appearances can NEVER be resynthesised. Spot those here. */
+		if (needs_resynth)
+		{
+			if (pdf_name_eq(ctx, pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)), PDF_NAME(Stamp)))
+			{
+				/* Don't resynthesize Stamps with non-standard names if
+				 * they already have an appearance. These usually have
+				 * custom images already set for their appearance.
+				 */
+				if (!pdf_annot_is_standard_stamp(ctx, annot) && ap_n)
+				{
+					/* However, we allow changing the Rect even if we don't
+					 * resynthesize the appearance. This should also count
+					 * as having a changed appearance. */
+					pdf_set_annot_resynthesised(ctx, annot);
+					needs_resynth = 0;
+				}
+			}
+		}
+
+		if (local_synthesis || needs_resynth)
+		{
+			fz_display_list *dlist;
+			fz_rect rect, bbox;
+			fz_buffer *buf;
+			pdf_obj *res = NULL;
+			pdf_obj *new_ap_n = NULL;
+			fz_var(res);
+			fz_var(new_ap_n);
+
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+			fz_write_printf(ctx, fz_stddbg(ctx), "Update Appearance: %d\n", pdf_to_num(ctx, annot->obj));
+			pdf_debug_obj(ctx, annot->obj);
+			fz_write_printf(ctx, fz_stddbg(ctx), "\n");
+#endif
+
+			if (local_synthesis)
+			{
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+				fz_write_printf(ctx, fz_stddbg(ctx), "Local synthesis\n");
+#endif
+				pdf_annot_ensure_local_xref(ctx, annot);
+			}
+			else
+			{
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+				fz_write_printf(ctx, fz_stddbg(ctx), "Non-Local synthesis\n");
+#endif
+				/* We don't want to be using any local xref, so
+				 * bin any that we have. */
+				pdf_annot_pop_and_discard_local_xref(ctx, annot);
+				/* Binning the xref may leave ap_n holding an invalid pointer.
+				 * We don't use it from this point onwards anymore, but beware
+				 * in future code changes. */
+				pop_local_xref = 0;
+			}
+
+			if (subtype == PDF_NAME(Widget) && ft == PDF_NAME(Btn))
+			{
+				/* Special case for Btn widgets that need multiple appearance streams. */
+				pdf_update_button_appearance(ctx, annot);
+			}
+			else if (subtype == PDF_NAME(Widget) && ft == PDF_NAME(Sig))
+			{
+				/* Special case for unsigned signature widgets,
+				 * which are most easily created via a display list. */
+				rect = pdf_annot_rect(ctx, annot);
+				dlist = pdf_signature_appearance_unsigned(ctx, rect, pdf_annot_language(ctx, annot));
+				fz_try(ctx)
+					pdf_set_annot_appearance_from_display_list(ctx, annot, "N", NULL, fz_identity, dlist);
+				fz_always(ctx)
+					fz_drop_display_list(ctx, dlist);
+				fz_catch(ctx)
+					fz_rethrow(ctx);
+			}
+			else
+			{
+				buf = fz_new_buffer(ctx, 1024);
+				fz_try(ctx)
+				{
+					fz_matrix matrix = fz_identity;
+					rect = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect));
+					pdf_write_appearance(ctx, annot, buf, &rect, &bbox, &matrix, &res);
+					pdf_dict_put_rect(ctx, annot->obj, PDF_NAME(Rect), rect);
+					pdf_set_annot_appearance(ctx, annot, "N", NULL, matrix, bbox, res, buf);
+				}
+				fz_always(ctx)
+				{
+					fz_drop_buffer(ctx, buf);
+					pdf_drop_obj(ctx, res);
+					pdf_drop_obj(ctx, new_ap_n);
+				}
+				fz_catch(ctx)
+				{
+					fz_rethrow_if(ctx, FZ_ERROR_REPAIRED);
+					fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
+					fz_report_error(ctx);
+					fz_warn(ctx, "cannot create appearance stream");
+				}
+			}
+
+#ifdef PDF_DEBUG_APPEARANCE_SYNTHESIS
+			fz_write_printf(ctx, fz_stddbg(ctx), "Annot obj after synthesis\n");
+			pdf_debug_obj(ctx, annot->obj);
+			fz_write_printf(ctx, fz_stddbg(ctx), "\nAppearance after synthesis\n");
+			pdf_debug_obj(ctx, pdf_dict_getp(ctx, annot->obj, "AP/N"));
+			fz_write_printf(ctx, fz_stddbg(ctx), "\n");
+#endif
+		}
+
+		pdf_clean_obj(ctx, annot->obj);
+		pdf_end_operation(ctx, annot->page->doc);
+	}
+	fz_always(ctx)
+	{
+		if (pop_local_xref)
+			pdf_annot_pop_local_xref(ctx, annot);
+		fz_end_throw_on_repair(ctx);
+	}
+	fz_catch(ctx)
+	{
+		pdf_abandon_operation(ctx, annot->page->doc);
+		/* If we hit a repair while synthesising, we need to give it another
+		 * go. Do that directly here, rather than waiting for the next time
+		 * we are called, because we don't want to risk discarding any
+		 * local_xrefs on the second pass through the list of annotations.
+		 * Repairs only ever happen once for a document, so no infinite
+		 * loop potential here. */
+		if (fz_caught(ctx) == FZ_ERROR_REPAIRED)
+		{
+			fz_report_error(ctx);
+			goto retry_after_repair;
+		}
+		fz_rethrow(ctx);
+	}
+}
+
+static void *
+update_appearances(fz_context *ctx, fz_page *page_, void *state)
+{
+	pdf_page *page = (pdf_page *)page_;
+	pdf_annot *annot;
+
+	for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
+		pdf_update_appearance(ctx, annot);
+	for (annot = pdf_first_widget(ctx, page); annot; annot = pdf_next_widget(ctx, annot))
+		pdf_update_appearance(ctx, annot);
+
+	return NULL;
+}
+
+static void
+update_all_appearances(fz_context *ctx, pdf_page *page)
+{
+	pdf_document *doc = page->doc;
+
+	/* Update all the annotations on all the pages open in the document.
+	 * At least one annotation should be resynthesised because we'll
+	 * only reach here if resynth_required was set. Any such resynthesis
+	 * that changes the document will throw away any local_xref. */
+	fz_process_opened_pages(ctx, &doc->super, update_appearances, NULL);
+	/* If the page isn't linked in yet (as is the case whilst loading
+	 * the annots for a page), process that too. */
+	if (page->super.prev == NULL && page->super.next == NULL)
+		update_appearances(ctx, &page->super, NULL);
+
+	/* Run it a second time, so that any annotations whose synthesised
+	 * appearances live in the local_xref (such as unsigned sigs) can
+	 * be regenerated too. Running this for annots which are up to date
+	 * should be fast. */
+	fz_process_opened_pages(ctx, &doc->super, update_appearances, NULL);
+	/* And cope with a non-linked in page again. */
+	if (page->super.prev == NULL && page->super.next == NULL)
+		update_appearances(ctx, &page->super, NULL);
+
+	doc->resynth_required = 0;
+}
+
+int
+pdf_update_annot(fz_context *ctx, pdf_annot *annot)
+{
+	int changed;
+
+	if (!annot->page)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "annotation not bound to any page");
+
+	if (annot->page->doc->resynth_required)
+		update_all_appearances(ctx, annot->page);
+
+	changed = annot->has_new_ap;
+	annot->has_new_ap = 0;
+	return changed;
+}