diff mupdf-source/source/pdf/pdf-link.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-link.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1500 @@
+// Copyright (C) 2004-2024 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 <string.h>
+#include <math.h>
+
+static pdf_obj *
+resolve_dest_rec(fz_context *ctx, pdf_document *doc, pdf_obj *dest, int depth)
+{
+	if (depth > 10) /* Arbitrary to avoid infinite recursion */
+		return NULL;
+
+	if (pdf_is_name(ctx, dest) || pdf_is_string(ctx, dest))
+	{
+		dest = pdf_lookup_dest(ctx, doc, dest);
+		dest = resolve_dest_rec(ctx, doc, dest, depth+1);
+		return dest;
+	}
+
+	else if (pdf_is_array(ctx, dest))
+	{
+		return dest;
+	}
+
+	else if (pdf_is_dict(ctx, dest))
+	{
+		dest = pdf_dict_get(ctx, dest, PDF_NAME(D));
+		return resolve_dest_rec(ctx, doc, dest, depth+1);
+	}
+
+	else if (pdf_is_indirect(ctx, dest))
+		return dest;
+
+	return NULL;
+}
+
+static pdf_obj *
+resolve_dest(fz_context *ctx, pdf_document *doc, pdf_obj *dest)
+{
+	return resolve_dest_rec(ctx, doc, dest, 0);
+}
+
+static void
+populate_destination(fz_context *ctx, pdf_document *doc, pdf_obj *dest, int is_remote, fz_link_dest *destination)
+{
+	pdf_obj *arg1 = pdf_array_get(ctx, dest, 2);
+	pdf_obj *arg2 = pdf_array_get(ctx, dest, 3);
+	pdf_obj *arg3 = pdf_array_get(ctx, dest, 4);
+	pdf_obj *arg4 = pdf_array_get(ctx, dest, 5);
+	float arg1v = pdf_to_real(ctx, arg1);
+	float arg2v = pdf_to_real(ctx, arg2);
+	float arg3v = pdf_to_real(ctx, arg3);
+	float arg4v = pdf_to_real(ctx, arg4);
+	pdf_obj *type, *page = NULL;
+	fz_matrix ctm = fz_identity;
+	fz_rect rect;
+	fz_point p;
+	int pageno;
+
+	if (is_remote)
+		pageno = pdf_array_get_int(ctx, dest, 0);
+	else
+	{
+		page = pdf_array_get(ctx, dest, 0);
+		if (pdf_is_int(ctx, page))
+		{
+			pageno = pdf_to_int(ctx, page);
+			page = pdf_lookup_page_obj(ctx, doc, pageno);
+		}
+		else
+			pageno = pdf_lookup_page_number(ctx, doc, page);
+		pageno = fz_clampi(pageno, 0, pdf_count_pages(ctx, doc) - 1);
+		if (pdf_is_dict(ctx, page))
+			pdf_page_obj_transform(ctx, page, NULL, &ctm);
+	}
+
+	destination->loc.page = pageno;
+
+	type = pdf_array_get(ctx, dest, 1);
+	if (type == PDF_NAME(XYZ))
+		destination->type = FZ_LINK_DEST_XYZ;
+	else if (type == PDF_NAME(Fit))
+		destination->type = FZ_LINK_DEST_FIT;
+	else if (type == PDF_NAME(FitH))
+		destination->type = FZ_LINK_DEST_FIT_H;
+	else if (type == PDF_NAME(FitV))
+		destination->type = FZ_LINK_DEST_FIT_V;
+	else if (type == PDF_NAME(FitR))
+		destination->type = FZ_LINK_DEST_FIT_R;
+	else if (type == PDF_NAME(FitB))
+		destination->type = FZ_LINK_DEST_FIT_B;
+	else if (type == PDF_NAME(FitBH))
+		destination->type = FZ_LINK_DEST_FIT_BH;
+	else if (type == PDF_NAME(FitBV))
+		destination->type = FZ_LINK_DEST_FIT_BV;
+	else
+		destination->type = FZ_LINK_DEST_XYZ;
+
+	switch (destination->type)
+	{
+	default:
+	case FZ_LINK_DEST_FIT:
+	case FZ_LINK_DEST_FIT_B:
+		break;
+	case FZ_LINK_DEST_FIT_H:
+	case FZ_LINK_DEST_FIT_BH:
+		p = fz_transform_point_xy(0, arg1v, ctm);
+		destination->y = arg1 ? p.y : NAN;
+		break;
+	case FZ_LINK_DEST_FIT_V:
+	case FZ_LINK_DEST_FIT_BV:
+		p = fz_transform_point_xy(arg1v, 0, ctm);
+		destination->x = arg1 ? p.x : NAN;
+		break;
+	case FZ_LINK_DEST_XYZ:
+		p = fz_transform_point_xy(arg1v, arg2v, ctm);
+		destination->x = arg1 ? p.x : NAN;
+		destination->y = arg2 ? p.y : NAN;
+		destination->zoom = arg3 ? (arg3v > 0 ? (arg3v * 100) : 100) : NAN;
+		break;
+	case FZ_LINK_DEST_FIT_R:
+		rect.x0 = arg1v;
+		rect.y0 = arg2v;
+		rect.x1 = arg3v;
+		rect.y1 = arg4v;
+		fz_transform_rect(rect, ctm);
+		destination->x = fz_min(rect.x0, rect.x1);
+		destination->y = fz_min(rect.y0, rect.y1);
+		destination->w = fz_abs(rect.x1 - rect.x0);
+		destination->h = fz_abs(rect.y1 - rect.y0);
+		break;
+	}
+}
+
+static char *
+pdf_parse_link_dest_to_file_with_uri(fz_context *ctx, pdf_document *doc, const char *uri, pdf_obj *dest)
+{
+	if (pdf_is_array(ctx, dest) && pdf_array_len(ctx, dest) >= 1)
+	{
+		fz_link_dest destination = fz_make_link_dest_none();
+		populate_destination(ctx, doc, dest, 1, &destination);
+		return pdf_append_explicit_dest_to_uri(ctx, uri, destination);
+	}
+	else if (pdf_is_name(ctx, dest))
+	{
+		const char *name = pdf_to_name(ctx, dest);
+		return pdf_append_named_dest_to_uri(ctx, uri, name);
+	}
+	else if (pdf_is_string(ctx, dest))
+	{
+		const char *name = pdf_to_text_string(ctx, dest);
+		return pdf_append_named_dest_to_uri(ctx, uri, name);
+	}
+	else
+	{
+		fz_warn(ctx, "invalid link destination");
+		return NULL;
+	}
+}
+
+static char *
+pdf_parse_link_dest_to_file_with_path(fz_context *ctx, pdf_document *doc, const char *path, pdf_obj *dest, int is_remote)
+{
+	if (pdf_is_array(ctx, dest) && pdf_array_len(ctx, dest) >= 1)
+	{
+		fz_link_dest destination = fz_make_link_dest_none();
+		if (!is_remote)
+			dest = resolve_dest(ctx, doc, dest);
+		populate_destination(ctx, doc, dest, is_remote, &destination);
+		return pdf_new_uri_from_path_and_explicit_dest(ctx, path, destination);
+	}
+	else if (pdf_is_name(ctx, dest))
+	{
+		const char *name = pdf_to_name(ctx, dest);
+		return pdf_new_uri_from_path_and_named_dest(ctx, path, name);
+	}
+	else if (pdf_is_string(ctx, dest))
+	{
+		const char *name = pdf_to_text_string(ctx, dest);
+		return pdf_new_uri_from_path_and_named_dest(ctx, path, name);
+	}
+	else if (path)
+	{
+		fz_link_dest destination = fz_make_link_dest_none();
+		return pdf_new_uri_from_path_and_explicit_dest(ctx, path, destination);
+	}
+	else
+	{
+		fz_warn(ctx, "invalid link destination");
+		return NULL;
+	}
+}
+
+/* Look at an FS object, and find a name. Find any embedded
+ * file stream object that corresponds to that and return it.
+ * Optionally return the name.
+ *
+ * Note that for NON-embedded files, this function will return
+ * NULL, but may still return a filename.
+ *
+ * We will never return a file unless we also found a name.
+ */
+static pdf_obj *
+get_file_stream_and_name(fz_context *ctx, pdf_obj *fs, pdf_obj **namep)
+{
+	pdf_obj *ef = pdf_dict_get(ctx, fs, PDF_NAME(EF));
+	pdf_obj *name = pdf_dict_get(ctx, fs, PDF_NAME(UF));
+	pdf_obj *file = pdf_dict_get(ctx, ef, PDF_NAME(UF));
+	pdf_obj *any_name = name;
+
+	if (!name && !file)
+	{
+		name = pdf_dict_get(ctx, fs, PDF_NAME(F));
+		if (any_name == NULL)
+			any_name = name;
+		file = pdf_dict_get(ctx, ef, PDF_NAME(F));
+	}
+	if (!name && !file)
+	{
+		name = pdf_dict_get(ctx, fs, PDF_NAME(Unix));
+		if (any_name == NULL)
+			any_name = name;
+		file = pdf_dict_get(ctx, ef, PDF_NAME(Unix));
+	}
+	if (!name && !file)
+	{
+		name = pdf_dict_get(ctx, fs, PDF_NAME(DOS));
+		if (any_name == NULL)
+			any_name = name;
+		file = pdf_dict_get(ctx, ef, PDF_NAME(DOS));
+	}
+	if (!name && !file)
+	{
+		name = pdf_dict_get(ctx, fs, PDF_NAME(Mac));
+		if (any_name == NULL)
+			any_name = name;
+		file = pdf_dict_get(ctx, ef, PDF_NAME(Mac));
+	}
+
+	/* bug708587: Some bad files have the name under one
+	 * entry (e.g. UF), and the entry in EF under another
+	 * (e.g. F). Strictly speaking this is against the
+	 * spec, but we'd rather find the embedded file than
+	 * not. */
+	if (any_name && !file)
+	{
+		name = any_name;
+		file = pdf_dict_get(ctx, ef, PDF_NAME(UF));
+		if (file == NULL)
+			file = pdf_dict_get(ctx, ef, PDF_NAME(F));
+		if (file == NULL)
+			file = pdf_dict_get(ctx, ef, PDF_NAME(Unix));
+		if (file == NULL)
+			file = pdf_dict_get(ctx, ef, PDF_NAME(DOS));
+		if (file == NULL)
+			file = pdf_dict_get(ctx, ef, PDF_NAME(Mac));
+	}
+
+	if (namep)
+		*namep = name;
+
+	return name ? file : NULL;
+}
+
+static char *
+convert_file_spec_to_URI(fz_context *ctx, pdf_document *doc, pdf_obj *file_spec, pdf_obj *dest, int is_remote)
+{
+	pdf_obj *str = NULL;
+	int is_url;
+
+	if (pdf_is_string(ctx, file_spec))
+		str = file_spec;
+	else if (pdf_is_dict(ctx, file_spec))
+		(void)get_file_stream_and_name(ctx, file_spec, &str);
+
+	if (!pdf_is_string(ctx, str))
+	{
+		fz_warn(ctx, "cannot parse file specification");
+		return NULL;
+	}
+
+	is_url = pdf_dict_get(ctx, file_spec, PDF_NAME(FS)) == PDF_NAME(URL);
+
+	if (is_url)
+		return pdf_parse_link_dest_to_file_with_uri(ctx, doc, pdf_to_text_string(ctx, str), dest);
+	else
+		return pdf_parse_link_dest_to_file_with_path(ctx, doc, pdf_to_text_string(ctx, str), dest, is_remote);
+}
+
+int
+pdf_is_filespec(fz_context *ctx, pdf_obj *fs)
+{
+	pdf_obj *name;
+	pdf_obj *type = pdf_dict_get(ctx, fs, PDF_NAME(Type));
+
+	if (type == NULL || !pdf_name_eq(ctx, type, PDF_NAME(Filespec)))
+		return 0;
+
+	(void)get_file_stream_and_name(ctx, fs, &name);
+
+	return name != NULL;
+}
+
+int
+pdf_is_embedded_file(fz_context *ctx, pdf_obj *fs)
+{
+	pdf_obj *type = pdf_dict_get(ctx, fs, PDF_NAME(Type));
+
+	if (type == NULL || !pdf_name_eq(ctx, type, PDF_NAME(Filespec)))
+		return 0;
+
+	return pdf_is_stream(ctx, get_file_stream_and_name(ctx, fs, NULL));
+}
+
+void
+pdf_get_filespec_params(fz_context *ctx, pdf_obj *fs, pdf_filespec_params *out)
+{
+	pdf_obj *file, *params, *filename, *subtype;
+	if (!out)
+		return;
+
+	memset(out, 0, sizeof(*out));
+	out->created = -1;
+	out->modified = -1;
+	out->size = -1;
+
+	file = get_file_stream_and_name(ctx, fs, &filename);
+	if (!pdf_is_stream(ctx, file))
+		return;
+
+	params = pdf_dict_get(ctx, file, PDF_NAME(Params));
+	out->filename = pdf_to_text_string(ctx, filename);
+
+	subtype = pdf_dict_get(ctx, file, PDF_NAME(Subtype));
+	if (!subtype)
+		out->mimetype = "application/octet-stream";
+	else
+		out->mimetype = pdf_to_name(ctx, subtype);
+	out->size = pdf_dict_get_int(ctx, params, PDF_NAME(Size));
+	out->created = pdf_dict_get_date(ctx, params, PDF_NAME(CreationDate));
+	out->modified = pdf_dict_get_date(ctx, params, PDF_NAME(ModDate));
+}
+
+fz_buffer *
+pdf_load_embedded_file_contents(fz_context *ctx, pdf_obj *fs)
+{
+	pdf_obj *file = get_file_stream_and_name(ctx, fs, NULL);
+
+	if (!pdf_is_stream(ctx, file))
+		return NULL;
+
+	return pdf_load_stream(ctx, file);
+}
+
+int
+pdf_verify_embedded_file_checksum(fz_context *ctx, pdf_obj *fs)
+{
+	unsigned char digest[16];
+	pdf_obj *params;
+	const char *checksum;
+	fz_buffer *contents;
+	int valid = 0;
+	size_t len;
+	pdf_obj *file = get_file_stream_and_name(ctx, fs, NULL);
+
+	if (!pdf_is_stream(ctx, file))
+		return 1;
+
+	params = pdf_dict_get(ctx, file, PDF_NAME(Params));
+	checksum = pdf_dict_get_string(ctx, params, PDF_NAME(CheckSum), &len);
+	if (!checksum || strlen(checksum) == 0)
+		return 1;
+
+	valid = 0;
+
+	fz_try(ctx)
+	{
+		contents = pdf_load_stream(ctx, file);
+		fz_md5_buffer(ctx, contents, digest);
+		if (len == nelem(digest) && !memcmp(digest, checksum, nelem(digest)))
+			valid = 1;
+	}
+	fz_always(ctx)
+		fz_drop_buffer(ctx, contents);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return valid;
+}
+
+static const char *
+pdf_guess_mime_type_from_file_name(fz_context *ctx, const char *filename)
+{
+	const char *ext = filename ? strrchr(filename, '.') : NULL;
+	if (ext)
+	{
+		if (!fz_strcasecmp(ext, ".pdf")) return "application/pdf";
+		if (!fz_strcasecmp(ext, ".xml")) return "application/xml";
+		if (!fz_strcasecmp(ext, ".zip")) return "application/zip";
+		if (!fz_strcasecmp(ext, ".tar")) return "application/x-tar";
+
+		/* Text */
+		if (!fz_strcasecmp(ext, ".txt")) return "text/plain";
+		if (!fz_strcasecmp(ext, ".rtf")) return "application/rtf";
+		if (!fz_strcasecmp(ext, ".csv")) return "text/csv";
+		if (!fz_strcasecmp(ext, ".html")) return "text/html";
+		if (!fz_strcasecmp(ext, ".htm")) return "text/html";
+		if (!fz_strcasecmp(ext, ".css")) return "text/css";
+
+		/* Office */
+		if (!fz_strcasecmp(ext, ".doc")) return "application/msword";
+		if (!fz_strcasecmp(ext, ".ppt")) return "application/vnd.ms-powerpoint";
+		if (!fz_strcasecmp(ext, ".xls")) return "application/vnd.ms-excel";
+		if (!fz_strcasecmp(ext, ".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+		if (!fz_strcasecmp(ext, ".pptx")) return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
+		if (!fz_strcasecmp(ext, ".xlsx")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+		if (!fz_strcasecmp(ext, ".odt")) return "application/vnd.oasis.opendocument.text";
+		if (!fz_strcasecmp(ext, ".odp")) return "application/vnd.oasis.opendocument.presentation";
+		if (!fz_strcasecmp(ext, ".ods")) return "application/vnd.oasis.opendocument.spreadsheet";
+
+		/* Image */
+		if (!fz_strcasecmp(ext, ".bmp")) return "image/bmp";
+		if (!fz_strcasecmp(ext, ".gif")) return "image/gif";
+		if (!fz_strcasecmp(ext, ".jpeg")) return "image/jpeg";
+		if (!fz_strcasecmp(ext, ".jpg")) return "image/jpeg";
+		if (!fz_strcasecmp(ext, ".png")) return "image/png";
+		if (!fz_strcasecmp(ext, ".svg")) return "image/svg+xml";
+		if (!fz_strcasecmp(ext, ".tif")) return "image/tiff";
+		if (!fz_strcasecmp(ext, ".tiff")) return "image/tiff";
+
+		/* Sound */
+		if (!fz_strcasecmp(ext, ".flac")) return "audio/flac";
+		if (!fz_strcasecmp(ext, ".mp3")) return "audio/mpeg";
+		if (!fz_strcasecmp(ext, ".ogg")) return "audio/ogg";
+		if (!fz_strcasecmp(ext, ".wav")) return "audio/wav";
+
+		/* Movie */
+		if (!fz_strcasecmp(ext, ".avi")) return "video/x-msvideo";
+		if (!fz_strcasecmp(ext, ".mov")) return "video/quicktime";
+		if (!fz_strcasecmp(ext, ".mp4")) return "video/mp4";
+		if (!fz_strcasecmp(ext, ".webm")) return "video/webm";
+	}
+	return "application/octet-stream";
+}
+
+pdf_obj *
+pdf_add_embedded_file(fz_context *ctx, pdf_document *doc,
+	const char *filename, const char *mimetype, fz_buffer *contents,
+	int64_t created, int64_t modified, int add_checksum)
+{
+	pdf_obj *file = NULL;
+	pdf_obj *filespec = NULL;
+	pdf_obj *params = NULL;
+
+	fz_var(file);
+	fz_var(filespec);
+
+	if (!mimetype)
+		mimetype = pdf_guess_mime_type_from_file_name(ctx, filename);
+
+	pdf_begin_operation(ctx, doc, "Embed file");
+	fz_try(ctx)
+	{
+		file = pdf_add_new_dict(ctx, doc, 3);
+		pdf_dict_put(ctx, file, PDF_NAME(Type), PDF_NAME(EmbeddedFile));
+		pdf_dict_put_name(ctx, file, PDF_NAME(Subtype), mimetype);
+		pdf_update_stream(ctx, doc, file, contents, 0);
+
+		params = pdf_dict_put_dict(ctx, file, PDF_NAME(Params), 4);
+		pdf_dict_put_int(ctx, params, PDF_NAME(Size), fz_buffer_storage(ctx, contents, NULL));
+		if (created >= 0)
+				pdf_dict_put_date(ctx, params, PDF_NAME(CreationDate), created);
+		if (modified >= 0)
+				pdf_dict_put_date(ctx, params, PDF_NAME(ModDate), modified);
+		if (add_checksum)
+		{
+			unsigned char digest[16];
+			fz_md5_buffer(ctx, contents, digest);
+				pdf_dict_put_string(ctx, params, PDF_NAME(CheckSum), (const char *) digest, nelem(digest));
+		}
+
+		filespec = pdf_add_filespec(ctx, doc, filename, file);
+	}
+	fz_always(ctx)
+		pdf_drop_obj(ctx, file);
+	fz_catch(ctx)
+	{
+		pdf_drop_obj(ctx, filespec);
+		pdf_abandon_operation(ctx, doc);
+		fz_rethrow(ctx);
+	}
+
+	return filespec;
+}
+
+char *
+pdf_parse_link_action(fz_context *ctx, pdf_document *doc, pdf_obj *action, int pagenum)
+{
+	pdf_obj *obj, *dest, *file_spec;
+
+	if (!action)
+		return NULL;
+
+	obj = pdf_dict_get(ctx, action, PDF_NAME(S));
+	if (pdf_name_eq(ctx, PDF_NAME(GoTo), obj))
+	{
+		dest = pdf_dict_get(ctx, action, PDF_NAME(D));
+		return pdf_parse_link_dest(ctx, doc, dest);
+	}
+	else if (pdf_name_eq(ctx, PDF_NAME(URI), obj))
+	{
+		/* URI entries are ASCII strings */
+		const char *uri = pdf_dict_get_text_string(ctx, action, PDF_NAME(URI));
+		if (!fz_is_external_link(ctx, uri))
+		{
+			pdf_obj *uri_base_obj = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/URI/Base");
+			const char *uri_base = uri_base_obj ? pdf_to_text_string(ctx, uri_base_obj) : "file://";
+			char *new_uri = Memento_label(fz_malloc(ctx, strlen(uri_base) + strlen(uri) + 1), "link_action");
+			strcpy(new_uri, uri_base);
+			strcat(new_uri, uri);
+			return new_uri;
+		}
+		return fz_strdup(ctx, uri);
+	}
+	else if (pdf_name_eq(ctx, PDF_NAME(Launch), obj))
+	{
+		file_spec = pdf_dict_get(ctx, action, PDF_NAME(F));
+		return convert_file_spec_to_URI(ctx, doc, file_spec, NULL, 0);
+	}
+	else if (pdf_name_eq(ctx, PDF_NAME(GoToR), obj))
+	{
+		dest = pdf_dict_get(ctx, action, PDF_NAME(D));
+		file_spec = pdf_dict_get(ctx, action, PDF_NAME(F));
+		return convert_file_spec_to_URI(ctx, doc, file_spec, dest, 1);
+	}
+	else if (pdf_name_eq(ctx, PDF_NAME(Named), obj))
+	{
+		dest = pdf_dict_get(ctx, action, PDF_NAME(N));
+
+		if (pdf_name_eq(ctx, PDF_NAME(FirstPage), dest))
+			pagenum = 0;
+		else if (pdf_name_eq(ctx, PDF_NAME(LastPage), dest))
+			pagenum = pdf_count_pages(ctx, doc) - 1;
+		else if (pdf_name_eq(ctx, PDF_NAME(PrevPage), dest) && pagenum >= 0)
+		{
+			if (pagenum > 0)
+				pagenum--;
+		}
+		else if (pdf_name_eq(ctx, PDF_NAME(NextPage), dest) && pagenum >= 0)
+		{
+			if (pagenum < pdf_count_pages(ctx, doc) - 1)
+				pagenum++;
+		}
+		else
+			return NULL;
+
+		return fz_asprintf(ctx, "#page=%d", pagenum + 1);
+	}
+
+	return NULL;
+}
+
+static void pdf_drop_link_imp(fz_context *ctx, fz_link *link)
+{
+	pdf_drop_obj(ctx, ((pdf_link *) link)->obj);
+}
+
+static void pdf_set_link_rect(fz_context *ctx, fz_link *link_, fz_rect rect)
+{
+	pdf_link *link = (pdf_link *) link_;
+	if (link == NULL)
+		return;
+
+	if (!link->page)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "link not bound to a page");
+
+	pdf_begin_operation(ctx, link->page->doc, "Set link rectangle");
+
+	fz_try(ctx)
+	{
+		pdf_dict_put_rect(ctx, link->obj, PDF_NAME(Rect), rect);
+		link->super.rect = rect;
+		pdf_end_operation(ctx, link->page->doc);
+	}
+	fz_catch(ctx)
+	{
+		pdf_abandon_operation(ctx, link->page->doc);
+		fz_rethrow(ctx);
+	}
+}
+
+static void pdf_set_link_uri(fz_context *ctx, fz_link *link_, const char *uri)
+{
+	pdf_link *link = (pdf_link *) link_;
+	if (link == NULL)
+		return;
+
+	if (!link->page)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "link not bound to a page");
+
+	pdf_begin_operation(ctx, link->page->doc, "Set link uri");
+
+	fz_try(ctx)
+	{
+		pdf_dict_put_drop(ctx, link->obj, PDF_NAME(A),
+				pdf_new_action_from_link(ctx, link->page->doc, uri));
+		fz_free(ctx, link->super.uri);
+		link->super.uri = fz_strdup(ctx, uri);
+		pdf_end_operation(ctx, link->page->doc);
+	}
+	fz_catch(ctx)
+	{
+		pdf_abandon_operation(ctx, link->page->doc);
+		fz_rethrow(ctx);
+	}
+}
+
+fz_link *pdf_new_link(fz_context *ctx, pdf_page *page, fz_rect rect, const char *uri, pdf_obj *obj)
+{
+	pdf_link *link = fz_new_derived_link(ctx, pdf_link, rect, uri);
+	link->super.drop = pdf_drop_link_imp;
+	link->super.set_rect_fn = pdf_set_link_rect;
+	link->super.set_uri_fn = pdf_set_link_uri;
+	link->page = page; /* only borrowed, as the page owns the link */
+	link->obj = pdf_keep_obj(ctx, obj);
+	return &link->super;
+}
+
+static fz_link *
+pdf_load_link(fz_context *ctx, pdf_document *doc, pdf_page *page, pdf_obj *dict, int pagenum, fz_matrix page_ctm)
+{
+	pdf_obj *action;
+	pdf_obj *obj;
+	fz_rect bbox;
+	char *uri;
+	fz_link *link = NULL;
+
+	obj = pdf_dict_get(ctx, dict, PDF_NAME(Subtype));
+	if (!pdf_name_eq(ctx, obj, PDF_NAME(Link)))
+		return NULL;
+
+	obj = pdf_dict_get(ctx, dict, PDF_NAME(Rect));
+	if (!obj)
+		return NULL;
+
+	bbox = pdf_to_rect(ctx, obj);
+	bbox = fz_transform_rect(bbox, page_ctm);
+
+	obj = pdf_dict_get(ctx, dict, PDF_NAME(Dest));
+	if (obj)
+		uri = pdf_parse_link_dest(ctx, doc, obj);
+	else
+	{
+		action = pdf_dict_get(ctx, dict, PDF_NAME(A));
+		/* fall back to additional action button's down/up action */
+		if (!action)
+			action = pdf_dict_geta(ctx, pdf_dict_get(ctx, dict, PDF_NAME(AA)), PDF_NAME(U), PDF_NAME(D));
+		uri = pdf_parse_link_action(ctx, doc, action, pagenum);
+	}
+
+	if (!uri)
+		return NULL;
+
+	fz_try(ctx)
+		link = (fz_link *) pdf_new_link(ctx, page, bbox, uri, dict);
+	fz_always(ctx)
+		fz_free(ctx, uri);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return link;
+}
+
+fz_link *
+pdf_load_link_annots(fz_context *ctx, pdf_document *doc, pdf_page *page, pdf_obj *annots, int pagenum, fz_matrix page_ctm)
+{
+	fz_link *link, *head, *tail;
+	pdf_obj *obj;
+	int i, n;
+
+	head = tail = NULL;
+	link = NULL;
+
+	n = pdf_array_len(ctx, annots);
+	for (i = 0; i < n; i++)
+	{
+		/* FIXME: Move the try/catch out of the loop for performance? */
+		fz_try(ctx)
+		{
+			obj = pdf_array_get(ctx, annots, i);
+			link = pdf_load_link(ctx, doc, page, obj, pagenum, page_ctm);
+		}
+		fz_catch(ctx)
+		{
+			fz_rethrow_if(ctx, FZ_ERROR_TRYLATER);
+			fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
+			fz_report_error(ctx);
+			link = NULL;
+		}
+
+		if (link)
+		{
+			if (!head)
+				head = tail = link;
+			else
+			{
+				tail->next = link;
+				tail = link;
+			}
+		}
+	}
+
+	return head;
+}
+
+void pdf_nuke_links(fz_context *ctx, pdf_page *page)
+{
+	pdf_link *link;
+	link = (pdf_link *) page->links;
+	while (link)
+	{
+		pdf_drop_obj(ctx, link->obj);
+		link->obj = NULL;
+		link = (pdf_link *) link->super.next;
+	}
+	fz_drop_link(ctx, page->links);
+	page->links = NULL;
+}
+
+void pdf_sync_links(fz_context *ctx, pdf_page *page)
+{
+	pdf_obj *annots;
+
+	pdf_nuke_links(ctx, page);
+
+	annots = pdf_dict_get(ctx, page->obj, PDF_NAME(Annots));
+	if (annots)
+	{
+		fz_rect page_cropbox;
+		fz_matrix page_ctm;
+		pdf_page_transform(ctx, page, &page_cropbox, &page_ctm);
+		page->links = pdf_load_link_annots(ctx, page->doc, page, annots, page->super.number, page_ctm);
+	}
+}
+
+#define isnanorzero(x) (isnan(x) || (x) == 0)
+
+static char*
+format_explicit_dest_link_uri(fz_context *ctx, const char *schema, const char *uri, fz_link_dest dest)
+{
+	int pageno = dest.loc.page < 0 ? 1 : dest.loc.page + 1;
+	int has_frag;
+
+	if (!schema)
+		schema = "";
+	if (!uri)
+		uri = "";
+
+	has_frag = !!strchr(uri, '#');
+
+	switch (dest.type)
+	{
+	default:
+		return fz_asprintf(ctx, "%s%s%cpage=%d", schema, uri, "#&"[has_frag], pageno);
+	case FZ_LINK_DEST_FIT:
+		return fz_asprintf(ctx, "%s%s%cpage=%d&view=Fit", schema, uri, "#&"[has_frag], pageno);
+	case FZ_LINK_DEST_FIT_B:
+		return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitB", schema, uri, "#&"[has_frag], pageno);
+	case FZ_LINK_DEST_FIT_H:
+		if (isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitH", schema, uri, "#&"[has_frag], pageno);
+		else
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitH,%g", schema, uri, "#&"[has_frag], pageno, dest.y);
+	case FZ_LINK_DEST_FIT_BH:
+		if (isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitBH", schema, uri, "#&"[has_frag], pageno);
+		else
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitBH,%g", schema, uri, "#&"[has_frag], pageno, dest.y);
+	case FZ_LINK_DEST_FIT_V:
+		if (isnan(dest.x))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitV", schema, uri, "#&"[has_frag], pageno);
+		else
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitV,%g", schema, uri, "#&"[has_frag], pageno, dest.x);
+	case FZ_LINK_DEST_FIT_BV:
+		if (isnan(dest.x))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitBV", schema, uri, "#&"[has_frag], pageno);
+		else
+			return fz_asprintf(ctx, "%s%s%cpage=%d&view=FitBV,%g", schema, uri, "#&"[has_frag], pageno, dest.x);
+	case FZ_LINK_DEST_XYZ:
+		if (!isnanorzero(dest.zoom) && !isnan(dest.x) && !isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&zoom=%g,%g,%g", schema, uri, "#&"[has_frag], pageno, dest.zoom, dest.x, dest.y);
+		else if (!isnanorzero(dest.zoom) && !isnan(dest.x) && isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&zoom=%g,%g,nan", schema, uri, "#&"[has_frag], pageno, dest.zoom, dest.x);
+		else if (!isnanorzero(dest.zoom) && isnan(dest.x) && !isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&zoom=%g,nan,%g", schema, uri, "#&"[has_frag], pageno, dest.zoom, dest.y);
+		else if (!isnanorzero(dest.zoom) && isnan(dest.x) && isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&zoom=%g,nan,nan", schema, uri, "#&"[has_frag], pageno, dest.zoom);
+		else if (isnanorzero(dest.zoom)&& !isnan(dest.x) && !isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&zoom=nan,%g,%g", schema, uri, "#&"[has_frag], pageno, dest.x, dest.y);
+		else if (isnanorzero(dest.zoom) && !isnan(dest.x) && isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&zoom=nan,%g,nan", schema, uri, "#&"[has_frag], pageno, dest.x);
+		else if (isnanorzero(dest.zoom) && isnan(dest.x) && !isnan(dest.y))
+			return fz_asprintf(ctx, "%s%s%cpage=%d&zoom=nan,nan,%g", schema, uri, "#&"[has_frag], pageno, dest.y);
+		else
+			return fz_asprintf(ctx, "%s%s%cpage=%d", schema, uri, "#&"[has_frag], pageno);
+	case FZ_LINK_DEST_FIT_R:
+		return fz_asprintf(ctx, "%s%s%cpage=%d&viewrect=%g,%g,%g,%g", schema, uri, "#&"[has_frag], pageno,
+			dest.x, dest.y, dest.w, dest.h);
+	}
+}
+
+static char*
+format_named_dest_link_uri(fz_context *ctx, const char *schema, const char *path, const char *name)
+{
+	if (!schema)
+		schema = "";
+	if (!path)
+		path = "";
+	return fz_asprintf(ctx, "%s%s#nameddest=%s", schema, path, name);
+}
+
+static char *
+pdf_format_remote_link_uri_from_name(fz_context *ctx, const char *name)
+{
+	char *encoded_name = NULL;
+	char *uri = NULL;
+
+	encoded_name = fz_encode_uri_component(ctx, name);
+	fz_try(ctx)
+		uri = format_named_dest_link_uri(ctx, NULL, NULL, encoded_name);
+	fz_always(ctx)
+		fz_free(ctx, encoded_name);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return uri;
+}
+
+static int
+is_file_uri(fz_context *ctx, const char *uri)
+{
+	return uri && !strncmp(uri, "file:", 5);
+}
+
+static int
+has_explicit_dest(fz_context *ctx, const char *uri)
+{
+	const char *fragment;
+	if (uri == NULL || (fragment = strchr(uri, '#')) == NULL)
+		return 0;
+	return strstr(fragment, "page=") != NULL;
+}
+
+static int
+has_named_dest(fz_context *ctx, const char *uri)
+{
+	const char *fragment;
+	if (uri == NULL || (fragment = strchr(uri, '#')) == NULL)
+		return 0;
+	return (strstr(fragment, "nameddest=") || !has_explicit_dest(ctx, uri));
+}
+
+static char *
+parse_file_uri_path(fz_context *ctx, const char *uri)
+{
+	char *frag, *path, *temp;
+
+	temp = fz_strdup(ctx, uri + 5);
+	fz_try(ctx)
+	{
+		frag = strchr(temp, '#');
+		if (frag)
+			*frag = 0;
+		path = fz_decode_uri_component(ctx, temp);
+		fz_cleanname(path);
+	}
+	fz_always(ctx)
+		fz_free(ctx, temp);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return path;
+}
+
+static char *
+parse_uri_named_dest(fz_context *ctx, const char *uri)
+{
+	const char *nameddest_s = strstr(uri, "nameddest=");
+	if (nameddest_s)
+	{
+		char *temp = fz_strdup(ctx, nameddest_s + 10);
+		char *dest;
+		fz_try(ctx)
+		{
+			char *ampersand = strchr(temp, '&');
+			if (ampersand)
+				*ampersand = 0;
+			dest = fz_decode_uri_component(ctx, temp);
+		}
+		fz_always(ctx)
+			fz_free(ctx, temp);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+		return dest;
+	}
+
+	// We know there must be a # because of the check in has_named_dest
+	return fz_decode_uri_component(ctx, strchr(uri, '#') + 1);
+}
+
+static float next_float(const char *str, int eatcomma, char **end)
+{
+	if (eatcomma && *str == ',')
+		++str;
+	return fz_strtof(str, end);
+}
+
+static fz_link_dest
+pdf_new_explicit_dest_from_uri(fz_context *ctx, pdf_document *doc, const char *uri)
+{
+	char *page, *rect, *zoom, *view;
+	fz_link_dest val = fz_make_link_dest_none();
+
+	uri = uri ? strchr(uri, '#') : NULL;
+
+	page = uri ? strstr(uri, "page=") : NULL;
+	rect = uri ? strstr(uri, "viewrect=") : NULL;
+	zoom = uri ? strstr(uri, "zoom=") : NULL;
+	view = uri ? strstr(uri, "view=") : NULL;
+
+	val.loc.chapter = 0;
+
+	if (page)
+	{
+		val.loc.page = fz_atoi(page+5) - 1;
+		val.loc.page = fz_maxi(val.loc.page, 0);
+	}
+	else
+		val.loc.page = 0;
+
+	if (rect)
+	{
+		rect += 9;
+		val.type = FZ_LINK_DEST_FIT_R;
+		val.x = next_float(rect, 0, &rect);
+		val.y = next_float(rect, 1, &rect);
+		val.w = next_float(rect, 1, &rect);
+		val.h = next_float(rect, 1, &rect);
+	}
+	else if (zoom)
+	{
+		zoom += 5;
+		val.type = FZ_LINK_DEST_XYZ;
+		val.zoom = next_float(zoom, 0, &zoom);
+		val.x = next_float(zoom, 1, &zoom);
+		val.y = next_float(zoom, 1, &zoom);
+		if (val.zoom <= 0 || isinf(val.zoom))
+			val.zoom = 100;
+	}
+	else if (view)
+	{
+		view += 5;
+		if (!fz_strncasecmp(view, "FitH", 4))
+		{
+			view += 4;
+			val.type = FZ_LINK_DEST_FIT_H;
+			val.y = strchr(view, ',') ? next_float(view, 1, &view) : NAN;
+		}
+		else if (!fz_strncasecmp(view, "FitBH", 5))
+		{
+			view += 5;
+			val.type = FZ_LINK_DEST_FIT_BH;
+			val.y = strchr(view, ',') ? next_float(view, 1, &view) : NAN;
+		}
+		else if (!fz_strncasecmp(view, "FitV", 4))
+		{
+			view += 4;
+			val.type = FZ_LINK_DEST_FIT_V;
+			val.x = strchr(view, ',') ? next_float(view, 1, &view) : NAN;
+		}
+		else if (!fz_strncasecmp(view, "FitBV", 5))
+		{
+			view += 5;
+			val.type = FZ_LINK_DEST_FIT_BV;
+			val.x = strchr(view, ',') ? next_float(view, 1, &view) : NAN;
+		}
+		else if (!fz_strncasecmp(view, "FitB", 4))
+		{
+			val.type = FZ_LINK_DEST_FIT_B;
+		}
+		else if (!fz_strncasecmp(view, "Fit", 3))
+		{
+			val.type = FZ_LINK_DEST_FIT;
+		}
+	}
+
+	return val;
+}
+
+char *
+pdf_append_named_dest_to_uri(fz_context *ctx, const char *uri, const char *name)
+{
+	char *encoded_name = NULL;
+	char *new_uri = NULL;
+	int has_frag;
+
+	if (!uri)
+		uri = "";
+
+	has_frag = !!strchr(uri, '#');
+
+	encoded_name = fz_encode_uri_component(ctx, name);
+	fz_try(ctx)
+		new_uri = fz_asprintf(ctx, "%s%cnameddest=%s", uri, "#&"[has_frag], encoded_name);
+	fz_always(ctx)
+		fz_free(ctx, encoded_name);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return new_uri;
+}
+
+char *
+pdf_append_explicit_dest_to_uri(fz_context *ctx, const char *uri, fz_link_dest dest)
+{
+	return format_explicit_dest_link_uri(ctx, NULL, uri, dest);
+}
+
+char *
+pdf_new_uri_from_path_and_named_dest(fz_context *ctx, const char *path, const char *name)
+{
+	const char *schema = NULL;
+	char *encoded_name = NULL;
+	char *encoded_path = NULL;
+	char *uri = NULL;
+
+	fz_var(encoded_name);
+	fz_var(encoded_path);
+
+	fz_try(ctx)
+	{
+		if (path && strlen(path) > 0)
+		{
+			if (path[0] == '/')
+				schema = "file://";
+			else
+				schema = "file:";
+			encoded_path = fz_encode_uri_pathname(ctx, path);
+			fz_cleanname(encoded_path);
+		}
+
+		encoded_name = fz_encode_uri_component(ctx, name);
+		uri = format_named_dest_link_uri(ctx, schema, encoded_path, encoded_name);
+	}
+	fz_always(ctx)
+	{
+		fz_free(ctx, encoded_name);
+		fz_free(ctx, encoded_path);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return uri;
+}
+
+char *
+pdf_new_uri_from_path_and_explicit_dest(fz_context *ctx, const char *path, fz_link_dest dest)
+{
+	const char *schema = NULL;
+	char *encoded_path = NULL;
+	char *uri = NULL;
+
+	fz_var(encoded_path);
+
+	fz_try(ctx)
+	{
+		if (path && strlen(path) > 0)
+		{
+			if (path[0] == '/')
+				schema = "file://";
+			else
+				schema = "file:";
+			encoded_path = fz_encode_uri_pathname(ctx, path);
+			fz_cleanname(encoded_path);
+		}
+
+		uri = format_explicit_dest_link_uri(ctx, schema, encoded_path, dest);
+	}
+	fz_always(ctx)
+		fz_free(ctx, encoded_path);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return uri;
+}
+
+char *
+pdf_new_uri_from_explicit_dest(fz_context *ctx, fz_link_dest dest)
+{
+	return format_explicit_dest_link_uri(ctx, NULL, NULL, dest);
+}
+
+char *
+pdf_parse_link_dest(fz_context *ctx, pdf_document *doc, pdf_obj *dest)
+{
+	if (pdf_is_array(ctx, dest) && pdf_array_len(ctx, dest) >= 1)
+	{
+		fz_link_dest destination = fz_make_link_dest_none();
+		populate_destination(ctx, doc, dest, 0, &destination);
+		return format_explicit_dest_link_uri(ctx, NULL, NULL, destination);
+	}
+	else if (pdf_is_name(ctx, dest))
+	{
+		const char *name = pdf_to_name(ctx, dest);
+		return pdf_format_remote_link_uri_from_name(ctx, name);
+	}
+	else if (pdf_is_string(ctx, dest))
+	{
+		const char *name = pdf_to_text_string(ctx, dest);
+		return pdf_format_remote_link_uri_from_name(ctx, name);
+	}
+	else
+	{
+		fz_warn(ctx, "invalid link destination");
+		return NULL;
+	}
+}
+
+static pdf_obj *
+pdf_add_filespec_from_link(fz_context *ctx, pdf_document *doc, const char *uri)
+{
+	char *file = NULL;
+	pdf_obj *filespec = NULL;
+	fz_try(ctx)
+	{
+		if (is_file_uri(ctx, uri))
+		{
+			file = parse_file_uri_path(ctx, uri);
+			filespec = pdf_add_filespec(ctx, doc, file, NULL);
+		}
+		else if (fz_is_external_link(ctx, uri))
+			filespec = pdf_add_url_filespec(ctx, doc, uri);
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "can not add non-uri as file specification");
+	}
+	fz_always(ctx)
+		fz_free(ctx, file);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+	return filespec;
+}
+
+
+pdf_obj *
+pdf_new_action_from_link(fz_context *ctx, pdf_document *doc, const char *uri)
+{
+	pdf_obj *action = pdf_new_dict(ctx, doc, 2);
+	char *file = NULL;
+
+	fz_var(file);
+
+	if (uri == NULL)
+		return NULL;
+
+	fz_try(ctx)
+	{
+		if (uri[0] == '#')
+		{
+			pdf_dict_put(ctx, action, PDF_NAME(S), PDF_NAME(GoTo));
+			pdf_dict_put_drop(ctx, action, PDF_NAME(D),
+				pdf_new_dest_from_link(ctx, doc, uri, 0));
+		}
+		else if (!strncmp(uri, "file:", 5))
+		{
+			pdf_dict_put(ctx, action, PDF_NAME(S), PDF_NAME(GoToR));
+			pdf_dict_put_drop(ctx, action, PDF_NAME(D),
+				pdf_new_dest_from_link(ctx, doc, uri, 1));
+			pdf_dict_put_drop(ctx, action, PDF_NAME(F),
+				pdf_add_filespec_from_link(ctx, doc, uri));
+		}
+		else if (fz_is_external_link(ctx, uri))
+		{
+			pdf_dict_put(ctx, action, PDF_NAME(S), PDF_NAME(URI));
+			pdf_dict_put_text_string(ctx, action, PDF_NAME(URI), uri);
+		}
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "unsupported link URI type");
+	}
+	fz_always(ctx)
+		fz_free(ctx, file);
+	fz_catch(ctx)
+	{
+		pdf_drop_obj(ctx, action);
+		fz_rethrow(ctx);
+	}
+
+	return action;
+}
+
+
+pdf_obj *pdf_add_filespec(fz_context *ctx, pdf_document *doc, const char *filename, pdf_obj *embedded_file)
+{
+	pdf_obj *filespec = NULL;
+	char *asciiname = NULL;
+	const char *s;
+	size_t len, i;
+
+	if (!filename)
+		filename = "";
+
+	fz_var(asciiname);
+	fz_var(filespec);
+
+	fz_try(ctx)
+	{
+		len = strlen(filename) + 1;
+		asciiname = fz_malloc(ctx, len);
+
+		for (i = 0, s = filename; *s && i + 1 < len; ++i)
+		{
+			int c;
+			s += fz_chartorune(&c, s);
+			asciiname[i] = (c >= 32 && c <= 126) ? c : '_';
+		}
+		asciiname[i] = 0;
+
+		filespec = pdf_add_new_dict(ctx, doc, 4);
+		pdf_dict_put(ctx, filespec, PDF_NAME(Type), PDF_NAME(Filespec));
+		pdf_dict_put_text_string(ctx, filespec, PDF_NAME(F), asciiname);
+		pdf_dict_put_text_string(ctx, filespec, PDF_NAME(UF), filename);
+		if (embedded_file)
+		{
+			pdf_obj *ef = pdf_dict_put_dict(ctx, filespec, PDF_NAME(EF), 1);
+			pdf_dict_put(ctx, ef, PDF_NAME(F), embedded_file);
+			pdf_dict_put(ctx, ef, PDF_NAME(UF), embedded_file);
+		}
+	}
+	fz_always(ctx)
+		fz_free(ctx, asciiname);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return filespec;
+}
+
+pdf_obj *pdf_add_url_filespec(fz_context *ctx, pdf_document *doc, const char *url)
+{
+	pdf_obj *filespec = pdf_add_new_dict(ctx, doc, 3);
+	fz_try(ctx)
+	{
+		pdf_dict_put(ctx, filespec, PDF_NAME(Type), PDF_NAME(Filespec));
+		pdf_dict_put(ctx, filespec, PDF_NAME(FS), PDF_NAME(URL));
+		pdf_dict_put_text_string(ctx, filespec, PDF_NAME(F), url);
+	}
+	fz_catch(ctx)
+	{
+		pdf_drop_obj(ctx, filespec);
+		fz_rethrow(ctx);
+	}
+	return filespec;
+}
+
+pdf_obj *
+pdf_new_dest_from_link(fz_context *ctx, pdf_document *doc, const char *uri, int is_remote)
+{
+	pdf_obj *dest = NULL;
+
+	fz_var(dest);
+
+	if (has_named_dest(ctx, uri))
+	{
+		char *name = parse_uri_named_dest(ctx, uri);
+
+		fz_try(ctx)
+			dest = pdf_new_text_string(ctx, name);
+		fz_always(ctx)
+			fz_free(ctx, name);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+	else
+	{
+		fz_matrix ctm, invctm;
+		fz_link_dest val;
+		pdf_obj *pageobj;
+		fz_point p;
+		fz_rect r;
+
+		fz_try(ctx)
+		{
+			val = pdf_new_explicit_dest_from_uri(ctx, doc, uri);
+
+			dest = pdf_new_array(ctx, doc, 6);
+
+			if (is_remote)
+			{
+				pdf_array_push_int(ctx, dest, val.loc.page);
+				invctm = fz_identity;
+			}
+			else
+			{
+				pageobj = pdf_lookup_page_obj(ctx, doc, val.loc.page);
+				pdf_array_push(ctx, dest, pageobj);
+
+				pdf_page_obj_transform(ctx, pageobj, NULL, &ctm);
+				invctm = fz_invert_matrix(ctm);
+			}
+
+			switch (val.type)
+			{
+			default:
+			case FZ_LINK_DEST_FIT:
+				pdf_array_push(ctx, dest, PDF_NAME(Fit));
+				break;
+			case FZ_LINK_DEST_FIT_H:
+				p = fz_transform_point_xy(0, val.y, invctm);
+				pdf_array_push(ctx, dest, PDF_NAME(FitH));
+				if (isnan(p.y))
+					pdf_array_push(ctx, dest, PDF_NULL);
+				else
+					pdf_array_push_real(ctx, dest, p.y);
+				break;
+			case FZ_LINK_DEST_FIT_BH:
+				p = fz_transform_point_xy(0, val.y, invctm);
+				pdf_array_push(ctx, dest, PDF_NAME(FitBH));
+				if (isnan(p.y))
+					pdf_array_push(ctx, dest, PDF_NULL);
+				else
+					pdf_array_push_real(ctx, dest, p.y);
+				break;
+			case FZ_LINK_DEST_FIT_V:
+				p = fz_transform_point_xy(val.x, 0, invctm);
+				pdf_array_push(ctx, dest, PDF_NAME(FitV));
+				if (isnan(p.x))
+					pdf_array_push(ctx, dest, PDF_NULL);
+				else
+					pdf_array_push_real(ctx, dest, p.x);
+				break;
+			case FZ_LINK_DEST_FIT_BV:
+				p = fz_transform_point_xy(val.x, 0, invctm);
+				pdf_array_push(ctx, dest, PDF_NAME(FitBV));
+				if (isnan(p.x))
+					pdf_array_push(ctx, dest, PDF_NULL);
+				else
+					pdf_array_push_real(ctx, dest, p.x);
+				break;
+			case FZ_LINK_DEST_XYZ:
+				if (invctm.a == 0 && invctm.d == 0)
+				{
+					/* Rotating by 90 or 270 degrees. */
+					p = fz_transform_point_xy(isnan(val.x) ? 0 : val.x, isnan(val.y) ? 0 : val.y, invctm);
+					if (isnan(val.x))
+						p.y = val.x;
+					if (isnan(val.y))
+						p.x = val.y;
+				}
+				else if (invctm.b == 0 && invctm.c == 0)
+				{
+					/* No rotation, or 180 degrees. */
+					p = fz_transform_point_xy(isnan(val.x) ? 0 : val.x, isnan(val.y) ? 0 : val.y, invctm);
+					if (isnan(val.x))
+						p.x = val.x;
+					if (isnan(val.y))
+						p.y = val.y;
+				}
+				else
+					p = fz_transform_point_xy(val.x, val.y, invctm);
+				pdf_array_push(ctx, dest, PDF_NAME(XYZ));
+				if (isnan(p.x))
+					pdf_array_push(ctx, dest, PDF_NULL);
+				else
+					pdf_array_push_real(ctx, dest, p.x);
+				if (isnan(p.y))
+					pdf_array_push(ctx, dest, PDF_NULL);
+				else
+					pdf_array_push_real(ctx, dest, p.y);
+				if (isnan(val.zoom))
+					pdf_array_push(ctx, dest, PDF_NULL);
+				else
+					pdf_array_push_real(ctx, dest, val.zoom / 100);
+				break;
+			case FZ_LINK_DEST_FIT_R:
+				r.x0 = val.x;
+				r.y0 = val.y;
+				r.x1 = val.x + val.w;
+				r.y1 = val.y + val.h;
+				fz_transform_rect(r, invctm);
+				pdf_array_push(ctx, dest, PDF_NAME(FitR));
+				pdf_array_push_real(ctx, dest, r.x0);
+				pdf_array_push_real(ctx, dest, r.y0);
+				pdf_array_push_real(ctx, dest, r.x1);
+				pdf_array_push_real(ctx, dest, r.y1);
+				break;
+			}
+		}
+		fz_catch(ctx)
+		{
+			pdf_drop_obj(ctx, dest);
+			fz_rethrow(ctx);
+		}
+	}
+
+	return dest;
+}
+
+fz_link_dest
+pdf_resolve_link_dest(fz_context *ctx, pdf_document *doc, const char *uri)
+{
+	fz_link_dest dest = fz_make_link_dest_none();
+	pdf_obj *page_obj;
+	fz_matrix page_ctm;
+	fz_rect mediabox;
+	pdf_obj *needle = NULL;
+	char *name = NULL;
+	char *desturi = NULL;
+	pdf_obj *destobj = NULL;
+
+	fz_var(needle);
+	fz_var(name);
+
+	fz_try(ctx)
+	{
+		if (has_explicit_dest(ctx, uri))
+		{
+			dest = pdf_new_explicit_dest_from_uri(ctx, doc, uri);
+			if (!isnan(dest.x) || !isnan(dest.y) || !isnan(dest.w) || !isnan(dest.h))
+			{
+				page_obj = pdf_lookup_page_obj(ctx, doc, dest.loc.page);
+				pdf_page_obj_transform(ctx, page_obj, &mediabox, &page_ctm);
+				mediabox = fz_transform_rect(mediabox, page_ctm);
+
+				/* clamp coordinates to remain on page */
+				dest.x = fz_clamp(dest.x, 0, mediabox.x1 - mediabox.x0);
+				dest.y = fz_clamp(dest.y, 0, mediabox.y1 - mediabox.y0);
+				dest.w = fz_clamp(dest.w, 0, mediabox.x1 - dest.x);
+				dest.h = fz_clamp(dest.h, 0, mediabox.y1 - dest.y);
+			}
+		}
+		else if (has_named_dest(ctx, uri))
+		{
+			name = parse_uri_named_dest(ctx, uri);
+
+			needle = pdf_new_text_string(ctx, name);
+			destobj = resolve_dest(ctx, doc, needle);
+			if (destobj)
+			{
+				fz_link_dest destdest;
+				desturi = pdf_parse_link_dest(ctx, doc, destobj);
+				destdest = pdf_resolve_link_dest(ctx, doc, desturi);
+				if (dest.type == FZ_LINK_DEST_XYZ && isnan(dest.x) && isnan(dest.y) && isnan(dest.zoom))
+					dest = destdest;
+				else
+					dest.loc = destdest.loc;
+			}
+		}
+		else
+			dest.loc.page = fz_atoi(uri) - 1;
+	}
+	fz_always(ctx)
+	{
+		fz_free(ctx, desturi);
+		fz_free(ctx, name);
+		pdf_drop_obj(ctx, needle);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return dest.loc.page >= 0 ? dest : fz_make_link_dest_none();
+}
+
+int
+pdf_resolve_link(fz_context *ctx, pdf_document *doc, const char *uri, float *xp, float *yp)
+{
+	fz_link_dest dest = pdf_resolve_link_dest(ctx, doc, uri);
+	if (xp) *xp = dest.x;
+	if (yp) *yp = dest.y;
+	return dest.loc.page;
+}