From 8ebce9c149112d59552ed530361f80372455fdb2 Mon Sep 17 00:00:00 2001
From: Tor Andersson <tor.andersson@artifex.com>
Date: Mon, 10 Sep 2018 18:18:49 +0200
Subject: Add Emscripten / WebAssembly build.

Requires Linux (or possibly MacOS X) and an installed emsdk to build.
---
 platform/wasm/Makefile          |   2 +
 platform/wasm/build.sh          |  28 +++++
 platform/wasm/readme.html       | 135 ++++++++++++++++++++++
 platform/wasm/view-as-html.html |  45 ++++++++
 platform/wasm/view-as-svg.html  |  44 +++++++
 platform/wasm/view-page.html    |  74 ++++++++++++
 platform/wasm/view.html         |  84 ++++++++++++++
 platform/wasm/wrap.c            | 247 ++++++++++++++++++++++++++++++++++++++++
 platform/wasm/wrap.js           |  51 +++++++++
 9 files changed, 710 insertions(+)
 create mode 100644 platform/wasm/Makefile
 create mode 100644 platform/wasm/build.sh
 create mode 100644 platform/wasm/readme.html
 create mode 100644 platform/wasm/view-as-html.html
 create mode 100644 platform/wasm/view-as-svg.html
 create mode 100644 platform/wasm/view-page.html
 create mode 100644 platform/wasm/view.html
 create mode 100644 platform/wasm/wrap.c
 create mode 100644 platform/wasm/wrap.js

(limited to 'platform')

diff --git a/platform/wasm/Makefile b/platform/wasm/Makefile
new file mode 100644
index 00000000..fcf5b24f
--- /dev/null
+++ b/platform/wasm/Makefile
@@ -0,0 +1,2 @@
+default:
+	bash build.sh
diff --git a/platform/wasm/build.sh b/platform/wasm/build.sh
new file mode 100644
index 00000000..ee092dc8
--- /dev/null
+++ b/platform/wasm/build.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+make -j4 -C ../.. generate
+
+source /opt/emsdk/emsdk_env.sh
+
+echo Building library:
+make -j4 -C ../.. \
+	OS=wasm build=release \
+	XCFLAGS="-DTOFU -DTOFU_CJK -DFZ_ENABLE_SVG=0 -DFZ_ENABLE_HTML=0 -DFZ_ENABLE_EPUB=0 -DFZ_ENABLE_JS=0" \
+	libs
+
+echo
+echo Linking WebAssembly:
+emcc -Wall -Os -o libmupdf.js \
+	-s WASM=1 \
+	-s VERBOSE=0 \
+	-s ABORTING_MALLOC=0 \
+	-s TOTAL_MEMORY=134217728 \
+	-s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
+	-s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE='[$Browser,"memcpy","memset","malloc","free"]' \
+	-I ../../include \
+	--pre-js wrap.js \
+	wrap.c \
+	../../build/wasm/release/libmupdf.a \
+	../../build/wasm/release/libmupdf-third.a
+
+echo Done.
diff --git a/platform/wasm/readme.html b/platform/wasm/readme.html
new file mode 100644
index 00000000..5d6b97ed
--- /dev/null
+++ b/platform/wasm/readme.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>MuPDF / WebAssembly</title>
+<meta charset="utf-8">
+<style>
+pre,dl{margin-left:2em;}
+dt{margin-top:0.5em;font-family:monospace}
+</style>
+</head>
+<body>
+
+<h1>MuPDF WebAssembly</h1>
+
+<h2>Building</h2>
+
+<p>
+The WebAssembly build has only been tested on Linux at the moment.
+If you use any other platform, you are on your own.
+
+<p>
+In order to build this you will need to install the
+<a href="https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html">Emscripten SDK</a>
+in <tt>/opt/emsdk</tt>.
+If you install it elsewhere, you will need to edit the platform/wasm/build.sh
+script to point to the appropriate location.
+
+<p>
+From the MuPDF project, you can run <tt>make wasm</tt> to build the WebAssembly
+library. The results of the build are a libmupdf.wasm binary and
+libmupdf.js script, placed in platform/wasm/.
+
+<p>
+In order to build a web application based on MuPDF, you will need to copy
+these two files and make them available to your page.
+
+<p>
+The libmupdf.wasm binary is quite large, because it contains not only the MuPDF
+library code, but also the 14 core PDF fonts, various CJK mapping resources,
+and ICC profiles. In order to keep it as small as possible, it is built with a
+minimal features set that does not include CJK fonts, EPUB support, etc.
+
+<h2>Using</h2>
+
+<p>
+The example script in platform/wasm/view.html shows how to use the
+MuPDF WebAssembly library.
+
+<p>
+The first part is including the libmupdf.js script, which pulls in
+and instantiates the WebAssembly module:
+
+<pre>
+&lt;script src="libmupdf.js"&gt;&lt;/script&gt;
+</pre>
+
+<p>
+MuPDF uses the Emscripten virtual file system to load a document, so you will
+need to seed it with the file you want to view in a Module.preRun
+callback function:
+
+<pre>
+&lt;script&gt;
+Module.preRun = function () {
+	FS.createPreloadedFile(".", "input.pdf", "input.pdf", true, false);
+}
+&lt;/script&gt;
+</pre>
+
+<p>
+When the filesystem has finished preloading the file data and initialized the
+code, it will call the Module.postRun callback. From here, you can
+use the 'mupdf' object to call various functions to open the document and
+render pages into various formats.
+
+<dl>
+<dt>mupdf.openDocument(filename)
+<dd>Open a document and return a handle.
+<dt>mupdf.freeDocument(doc)
+<dd>Free a document and its associated resources.
+<dt>mupdf.documentTitle(doc)
+<dd>Return the document title as a string.
+<dt>mupdf.documentOutline(doc)
+<dd>Return a DOM element containing the table of contents formatted as
+an unordered HTML list with links to pages using anchor fragments "#page%d".
+<dt>mupdf.countPages(doc)
+<dd>Return the number of pages in the document.
+<dt>mupdf.pageWidth(doc, page, dpi) and mupdf.pageHeight(doc, page, dpi)
+<dd>Return the dimensions of a page. Page numbering starts at 1.
+<dt>mupdf.drawPageAsPNG(doc, page, dpi)
+<dd>Render the page and return a PNG image formatted as a data URI,
+suitable for using as an image source attribute.
+<dt>mupdf.pageLinks(doc, page, dpi)
+<dd>Retrieve an HTML string describing the links on a page,
+suitable for including as the innerHTML of an image map.
+<dt>mupdf.drawPageAsSVG(doc, page)
+<dd>Return a string with the contents of the page in SVG format.
+<dt>mupdf.drawPageAsHTML(doc, page)
+<dd>Return a string with the contents of the page in HTML format,
+using absolute positioned elements.
+</dl>
+
+<h2>Example</h2>
+
+<p>
+Here is a very simple example of loading a document and drawing its first page:
+
+<pre>
+&lt;!DOCTYPE html&gt;
+&lt;html&gt;
+&lt;head&gt;
+&lt;script src="libmupdf.js"&gt;&lt;/script&gt;
+&lt;script&gt;
+Module.preRun = function () {
+	FS.createPreloadedFile(".", "input.pdf", "input.pdf", true, false);
+}
+Module.postRun = function () {
+	var DPI = 96;
+	var doc = mupdf.openDocument("input.pdf");
+	var img = document.getElementById("page1");
+	img.src = mupdf.drawPageAsPNG(doc, 1, DPI);
+	var map = document.getElementById("page1map");
+	map.innerHTML = mupdf.pageLinks(doc, 1, DPI);
+}
+&lt;/script&gt;
+&lt;/head&gt;
+&lt;body&gt;
+&lt;img id="page1" usemap="#page1map"&gt;
+&lt;map id="page1map" name="page1map"&gt;&lt;/map&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre>
+
+</body>
+</html>
diff --git a/platform/wasm/view-as-html.html b/platform/wasm/view-as-html.html
new file mode 100644
index 00000000..d31d23a2
--- /dev/null
+++ b/platform/wasm/view-as-html.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Loading...</title>
+<meta charset="utf-8">
+<style>
+html,body,table,tr,td,div{background-color:gray;margin:0;padding:0;}
+ul{margin:0;padding-left:1em;}
+#outline{background-color:silver;padding:1em;padding-left:2em;}
+#pages{margin:0em;width:100%;}
+.page>div{margin:1em auto;}
+</style>
+<script src="libmupdf.js"></script>
+<script>
+var filename = new URL(window.location.href).searchParams.get("file");
+if (!filename)
+	filename = "pdfref13.pdf";
+Module.preRun = function () {
+	FS.createPreloadedFile(".", filename, filename, true, false);
+};
+Module.postRun = function () {
+	var currentDocument = null;
+	var pageCount = 0;
+	var currentPage = 1;
+	function loadNextPage() {
+		var element = document.createElement('div');
+		element.id = 'page' + currentPage;
+		element.className = 'page';
+		element.innerHTML = mupdf.drawPageAsHTML(currentDocument, currentPage);
+		element.getElementsByTagName("div")[0].style.backgroundImage = 'url(' + mupdf.drawPage(currentDocument, currentPage, 96) + ')'
+		document.getElementById("pages").appendChild(element);
+		if (++currentPage <= pageCount)
+			setTimeout(loadNextPage, 0);
+	}
+	currentDocument = mupdf.openDocument(filename);
+	document.title = mupdf.documentTitle(currentDocument);
+	pageCount = mupdf.countPages(currentDocument);
+	loadNextPage();
+}
+</script>
+</head>
+<body>
+<div id="pages"></div>
+</body>
+</html>
diff --git a/platform/wasm/view-as-svg.html b/platform/wasm/view-as-svg.html
new file mode 100644
index 00000000..4781751d
--- /dev/null
+++ b/platform/wasm/view-as-svg.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Loading...</title>
+<meta charset="utf-8">
+<style>
+html,body,table,tr,td,div{background-color:gray;margin:0;padding:0;}
+ul{margin:0;padding-left:1em;}
+#outline{background-color:silver;padding:1em;padding-left:2em;}
+#pages{margin:0em;width:100%;}
+.page>svg{margin:1em 1em;background-color:white;}
+</style>
+<script src="libmupdf.js"></script>
+<script>
+var filename = new URL(window.location.href).searchParams.get("file");
+if (!filename)
+	filename = "pdfref13.pdf";
+Module.preRun = function () {
+	FS.createPreloadedFile(".", filename, filename, true, false);
+};
+Module.postRun = function () {
+	var currentDocument = null;
+	var pageCount = 0;
+	var currentPage = 1;
+	function loadNextPage() {
+		var element = document.createElement('div');
+		element.id = 'page' + currentPage;
+		element.className = 'page';
+		element.innerHTML = mupdf.drawPageAsSVG(currentDocument, currentPage);
+		document.getElementById("pages").appendChild(element);
+		if (++currentPage <= pageCount)
+			setTimeout(loadNextPage, 0);
+	}
+	currentDocument = mupdf.openDocument(filename);
+	document.title = mupdf.documentTitle(currentDocument);
+	pageCount = mupdf.countPages(currentDocument);
+	loadNextPage();
+}
+</script>
+</head>
+<body>
+<div id="pages"></div>
+</body>
+</html>
diff --git a/platform/wasm/view-page.html b/platform/wasm/view-page.html
new file mode 100644
index 00000000..ce56962f
--- /dev/null
+++ b/platform/wasm/view-page.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Loading...</title>
+<meta charset="utf-8">
+<script src="libmupdf.js"></script>
+<script>
+var filename = new URL(window.location.href).searchParams.get("file");
+if (!filename)
+	filename = "pdfref13.pdf";
+Module.preRun = function () {
+	FS.createPreloadedFile(".", filename, filename, true, false);
+};
+Module.postRun = function () {
+	loadDocument(filename);
+};
+
+var currentDocument = null;
+var currentPage = 1;
+var currentZoom = 72;
+var pageCount = 0;
+function loadDocument(filename) {
+	currentDocument = mupdf.openDocument(filename);
+	document.title = mupdf.documentTitle(currentDocument);
+	pageCount = mupdf.countPages(currentDocument);
+	updatePage();
+}
+function updatePage() {
+	document.getElementById("page").src = mupdf.drawPageAsPNG(currentDocument, currentPage, currentZoom);
+	document.getElementById("pageNumber").value = currentPage;
+}
+function nextPage() {
+	if (currentPage <= pageCount) {
+		++currentPage;
+		updatePage();
+	}
+}
+function prevPage() {
+	if (currentPage > 1) {
+		--currentPage;
+		updatePage();
+	}
+}
+function gotoPage() {
+	var page = parseInt(document.getElementById("pageNumber").value)
+	if (page >= 1 && page <= pageCount) {
+		currentPage = page;
+		updatePage();
+	}
+}
+function zoomIn() {
+	currentZoom *= 1.2;
+	updatePage();
+}
+function zoomOut() {
+	currentZoom /= 1.2;
+	updatePage();
+}
+</script>
+<style>
+body{margin:2em;background-color:#aaa}
+</style>
+</head>
+<body>
+<p>
+<button onClick="prevPage()">&lt;</button>
+<input id="pageNumber" onChange="gotoPage()" type="text" size="5">
+<button onClick="nextPage()">&gt;</button>
+<button onClick="zoomIn()">+</button>
+<button onClick="zoomOut()">-</button>
+<p>
+<img id="page">
+</body>
+</html>
diff --git a/platform/wasm/view.html b/platform/wasm/view.html
new file mode 100644
index 00000000..0ff34916
--- /dev/null
+++ b/platform/wasm/view.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Loading...</title>
+<meta charset="utf-8">
+<style>
+body { background-color:gray; margin:0; padding:0; }
+#outline { position:fixed; top:0; left:0; width:20em; max-width:20em; height:100%; overflow-y:scroll; background-color:white; }
+#outline ul { margin:0; padding-left:1em; font-size:small; }
+#outline a { text-decoration:none; }
+#outline a:hover { text-decoration:underline; }
+#outline + #pages { margin-left:20em; }
+#pages img { display:block; margin:1em auto; background-color:white; }
+</style>
+<script src="libmupdf.js"></script>
+<script>
+
+var currentDocument = null
+var blankPages = [];
+var DPI = 96;
+
+var filename = new URL(window.location.href).searchParams.get("file");
+if (!filename)
+	filename = "pdfref13.pdf";
+
+Module.preRun = function () {
+	FS.createPreloadedFile(".", filename, filename, true, false);
+};
+
+Module.postRun = function () {
+	var pagesDiv = document.getElementById("pages");
+
+	console.log("mupdf: opening", filename);
+	currentDocument = mupdf.openDocument(filename);
+	document.title = mupdf.documentTitle(currentDocument);
+
+	var outline = mupdf.documentOutline(currentDocument);
+	if (outline) {
+		var outlineDiv = document.createElement("div");
+		outlineDiv.id = "outline";
+		outlineDiv.appendChild(outline);
+		document.body.insertBefore(outlineDiv, pagesDiv);
+	}
+
+	var i, n = mupdf.countPages(currentDocument);
+	for (i = 1; i <= n; ++i) {
+		var img = new Image();
+		img.id = "page" + i;
+		img.pageNumber = i;
+		img.width = mupdf.pageWidth(currentDocument, i, DPI);
+		img.height = mupdf.pageHeight(currentDocument, i, DPI);
+		img.useMap = "#map" + i;
+		var map = document.createElement("map");
+		map.name = "map" + i;
+		map.innerHTML = mupdf.pageLinks(currentDocument, i, DPI);
+		pagesDiv.appendChild(img);
+		pagesDiv.appendChild(map);
+		blankPages[i] = img;
+	}
+
+	console.log("mupdf: loaded", n, "pages");
+
+	document.onscroll();
+}
+
+document.onscroll = function () {
+	function isVisible(element) {
+		return ((window.pageYOffset + window.innerHeight) > element.offsetTop) &&
+			(window.pageYOffset < (element.offsetTop + element.scrollHeight));
+	}
+	var i, n = blankPages.length;
+	for (i = 1; i <= n; ++i) {
+		if (blankPages[i] && isVisible(blankPages[i])) {
+			console.log("mupdf: drawing page", i);
+			blankPages[i].src = mupdf.drawPageAsPNG(currentDocument, i, DPI);
+			blankPages[i] = undefined;
+		}
+	};
+}
+
+</script>
+</head>
+<body><div id="pages"></div></body>
+</html>
diff --git a/platform/wasm/wrap.c b/platform/wasm/wrap.c
new file mode 100644
index 00000000..d2a3379e
--- /dev/null
+++ b/platform/wasm/wrap.c
@@ -0,0 +1,247 @@
+#include "emscripten.h"
+#include "mupdf/fitz.h"
+
+static fz_context *ctx;
+
+EMSCRIPTEN_KEEPALIVE
+void initContext(void)
+{
+	ctx = fz_new_context(NULL, NULL, 100<<20);
+	fz_register_document_handlers(ctx);
+}
+
+EMSCRIPTEN_KEEPALIVE
+fz_document *openDocument(const char *filename)
+{
+	return fz_open_document(ctx, filename);
+}
+
+EMSCRIPTEN_KEEPALIVE
+void freeDocument(fz_document *doc)
+{
+	fz_drop_document(ctx, doc);
+}
+
+EMSCRIPTEN_KEEPALIVE
+int countPages(fz_document *doc)
+{
+	return fz_count_pages(ctx, doc);
+}
+
+static fz_page *lastPage = NULL;
+
+static void loadPage(fz_document *doc, int number)
+{
+	static fz_document *lastPageDoc = NULL;
+	static int lastPageNumber = -1;
+	if (lastPageNumber != number || lastPageDoc != doc)
+	{
+		if (lastPage)
+			fz_drop_page(ctx, lastPage);
+		lastPage = fz_load_page(ctx, doc, number-1);
+		lastPageDoc = doc;
+		lastPageNumber = number;
+	}
+}
+
+EMSCRIPTEN_KEEPALIVE
+char *drawPageAsHTML(fz_document *doc, int number)
+{
+	static unsigned char *data = NULL;
+	fz_stext_page *text;
+	fz_buffer *buf;
+	fz_output *out;
+
+	fz_free(ctx, data);
+	data = NULL;
+
+	loadPage(doc, number);
+
+	buf = fz_new_buffer(ctx, 0);
+	{
+		out = fz_new_output_with_buffer(ctx, buf);
+		{
+			text = fz_new_stext_page_from_page(ctx, lastPage, NULL);
+			fz_print_stext_page_as_html(ctx, out, text);
+			fz_drop_stext_page(ctx, text);
+		}
+		fz_write_byte(ctx, out, 0);
+		fz_close_output(ctx, out);
+		fz_drop_output(ctx, out);
+	}
+	fz_buffer_extract(ctx, buf, &data);
+	fz_drop_buffer(ctx, buf);
+
+	return (char*)data;
+}
+
+EMSCRIPTEN_KEEPALIVE
+char *drawPageAsSVG(fz_document *doc, int number)
+{
+	static int id = 0;
+	static unsigned char *data = NULL;
+	fz_buffer *buf;
+	fz_output *out;
+	fz_device *dev;
+	fz_rect bbox;
+
+	fz_free(ctx, data);
+	data = NULL;
+
+	loadPage(doc, number);
+
+	buf = fz_new_buffer(ctx, 0);
+	{
+		out = fz_new_output_with_buffer(ctx, buf);
+		{
+			bbox = fz_bound_page(ctx, lastPage);
+			dev = fz_new_svg_device(ctx, out, bbox.x1-bbox.x0, bbox.y1-bbox.y0, FZ_SVG_TEXT_AS_PATH, 0, &id);
+			fz_run_page(ctx, lastPage, dev, fz_identity, NULL);
+			fz_close_device(ctx, dev);
+			fz_drop_device(ctx, dev);
+		}
+		fz_write_byte(ctx, out, 0);
+		fz_close_output(ctx, out);
+		fz_drop_output(ctx, out);
+	}
+	fz_buffer_extract(ctx, buf, &data);
+	fz_drop_buffer(ctx, buf);
+
+	return (char*)data;
+}
+
+EMSCRIPTEN_KEEPALIVE
+char *drawPageAsPNG(fz_document *doc, int number, float dpi)
+{
+	static unsigned char *data = NULL;
+	float zoom = dpi / 72;
+	fz_pixmap *pix;
+	fz_buffer *buf;
+	fz_output *out;
+
+	fz_free(ctx, data);
+	data = NULL;
+
+	loadPage(doc, number);
+
+	buf = fz_new_buffer(ctx, 0);
+	{
+		out = fz_new_output_with_buffer(ctx, buf);
+		{
+			pix = fz_new_pixmap_from_page(ctx, lastPage, fz_scale(zoom, zoom), fz_device_rgb(ctx), 0);
+			fz_write_pixmap_as_data_uri(ctx, out, pix);
+			fz_drop_pixmap(ctx, pix);
+		}
+		fz_write_byte(ctx, out, 0);
+		fz_close_output(ctx, out);
+		fz_drop_output(ctx, out);
+	}
+	fz_buffer_extract(ctx, buf, &data);
+	fz_drop_buffer(ctx, buf);
+
+	return (char*)data;
+}
+
+static fz_irect pageBounds(fz_document *doc, int number, float dpi)
+{
+	loadPage(doc, number);
+	return fz_round_rect(fz_transform_rect(fz_bound_page(ctx, lastPage), fz_scale(dpi/72, dpi/72)));
+}
+
+EMSCRIPTEN_KEEPALIVE
+int pageWidth(fz_document *doc, int number, float dpi)
+{
+	fz_irect bbox = pageBounds(doc, number, dpi);
+	return bbox.x1 - bbox.x0;
+}
+
+EMSCRIPTEN_KEEPALIVE
+int pageHeight(fz_document *doc, int number, float dpi)
+{
+	fz_irect bbox = pageBounds(doc, number, dpi);
+	return bbox.y1 - bbox.y0;
+}
+
+EMSCRIPTEN_KEEPALIVE
+char *pageLinks(fz_document *doc, int number, float dpi)
+{
+	static unsigned char *data = NULL;
+	fz_buffer *buf;
+	fz_link *links, *link;
+
+	fz_free(ctx, data);
+	data = NULL;
+
+	loadPage(doc, number);
+
+	buf = fz_new_buffer(ctx, 0);
+	{
+		links = fz_load_links(ctx, lastPage);
+		{
+			for (link = links; link; link = link->next)
+			{
+				fz_irect bbox = fz_round_rect(fz_transform_rect(link->rect, fz_scale(dpi/72, dpi/72)));
+				fz_append_printf(ctx, buf, "<area shape=\"rect\" coords=\"%d,%d,%d,%d\"",
+					bbox.x0, bbox.y0, bbox.x1, bbox.y1);
+				if (fz_is_external_link(ctx, link->uri))
+					fz_append_printf(ctx, buf, " href=\"%s\">\n", link->uri);
+				else
+				{
+					int linkNumber = fz_resolve_link(ctx, doc, link->uri, NULL, NULL);
+					fz_append_printf(ctx, buf, " href=\"#page%d\">\n", linkNumber+1);
+				}
+			}
+		}
+		fz_append_byte(ctx, buf, 0);
+		fz_drop_link(ctx, links);
+	}
+	fz_buffer_extract(ctx, buf, &data);
+	fz_drop_buffer(ctx, buf);
+
+	return (char*)data;
+}
+
+EMSCRIPTEN_KEEPALIVE
+char *documentTitle(fz_document *doc)
+{
+	static char buf[100];
+	if (fz_lookup_metadata(ctx, doc, FZ_META_INFO_TITLE, buf, sizeof buf) > 0)
+		return buf;
+	return "Untitled";
+}
+
+EMSCRIPTEN_KEEPALIVE
+fz_outline *loadOutline(fz_document *doc)
+{
+	return fz_load_outline(ctx, doc);
+}
+
+EMSCRIPTEN_KEEPALIVE
+void freeOutline(fz_outline *outline)
+{
+	fz_drop_outline(ctx, outline);
+}
+
+EMSCRIPTEN_KEEPALIVE
+char *outlineTitle(fz_outline *node)
+{
+	return node->title;
+}
+
+EMSCRIPTEN_KEEPALIVE
+int outlinePage(fz_outline *node)
+{
+	return node->page + 1;
+}
+
+EMSCRIPTEN_KEEPALIVE
+fz_outline *outlineDown(fz_outline *node)
+{
+	return node->down;
+}
+
+EMSCRIPTEN_KEEPALIVE
+fz_outline *outlineNext(fz_outline *node)
+{
+	return node->next;
+}
diff --git a/platform/wasm/wrap.js b/platform/wasm/wrap.js
new file mode 100644
index 00000000..ef5df57b
--- /dev/null
+++ b/platform/wasm/wrap.js
@@ -0,0 +1,51 @@
+var mupdf = {};
+
+Module.noExitRuntime = true;
+Module.noInitialRun = true;
+
+Module.onRuntimeInitialized = function () {
+	Module.ccall('initContext');
+	mupdf.openDocument = Module.cwrap('openDocument', 'number', ['string']);
+	mupdf.freeDocument = Module.cwrap('freeDocument', 'null', ['number']);
+	mupdf.documentTitle = Module.cwrap('documentTitle', 'string', ['number']);
+	mupdf.countPages = Module.cwrap('countPages', 'number', ['number']);
+	mupdf.pageWidth = Module.cwrap('pageWidth', 'number', ['number', 'number', 'number']);
+	mupdf.pageHeight = Module.cwrap('pageHeight', 'number', ['number', 'number', 'number']);
+	mupdf.pageLinks = Module.cwrap('pageLinks', 'string', ['number', 'number', 'number']);
+	mupdf.drawPageAsPNG = Module.cwrap('drawPageAsPNG', 'string', ['number', 'number', 'number']);
+	mupdf.drawPageAsHTML = Module.cwrap('drawPageAsHTML', 'string', ['number', 'number']);
+	mupdf.drawPageAsSVG = Module.cwrap('drawPageAsSVG', 'string', ['number', 'number']);
+	mupdf.loadOutline = Module.cwrap('loadOutline', 'number', ['number']);
+	mupdf.freeOutline = Module.cwrap('freeOutline', null, ['number']);
+	mupdf.outlineTitle = Module.cwrap('outlineTitle', 'string', ['number']);
+	mupdf.outlinePage = Module.cwrap('outlinePage', 'number', ['number']);
+	mupdf.outlineDown = Module.cwrap('outlineDown', 'number', ['number']);
+	mupdf.outlineNext = Module.cwrap('outlineNext', 'number', ['number']);
+};
+
+mupdf.documentOutline = function (doc) {
+	function makeOutline(node) {
+		var ul = document.createElement('ul');
+		while (node) {
+			var li = document.createElement('li');
+			var a = document.createElement('a');
+			a.href = '#page' + mupdf.outlinePage(node);
+			a.textContent = mupdf.outlineTitle(node);
+			li.appendChild(a);
+			var down = mupdf.outlineDown(node);
+			if (down) {
+				li.appendChild(makeOutline(down));
+			}
+			ul.appendChild(li);
+			node = mupdf.outlineNext(node);
+		}
+		return ul;
+	}
+	var root = mupdf.loadOutline(doc);
+	if (root) {
+		var ul = makeOutline(root);
+		mupdf.freeOutline(root);
+		return ul;
+	}
+	return null;
+}
-- 
cgit v1.2.3