diff src_classic/helper-other.i @ 1:1d09e1dec1d9 upstream

ADD: PyMuPDF v1.26.4: the original sdist. It does not yet contain MuPDF. This normally will be downloaded when building PyMuPDF.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:37:51 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src_classic/helper-other.i	Mon Sep 15 11:37:51 2025 +0200
@@ -0,0 +1,1344 @@
+%{
+/*
+# ------------------------------------------------------------------------
+# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com
+# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html
+#
+# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a
+# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
+# maintained and developed by Artifex Software, Inc. https://artifex.com.
+# ------------------------------------------------------------------------
+*/
+fz_buffer *JM_object_to_buffer(fz_context *ctx, pdf_obj *val, int a, int b);
+PyObject *JM_EscapeStrFromBuffer(fz_context *ctx, fz_buffer *buff);
+pdf_obj *JM_pdf_obj_from_str(fz_context *ctx, pdf_document *doc, char *src);
+
+// exception handling
+void *JM_ReturnException(fz_context *ctx)
+{
+    PyErr_SetString(JM_Exc_CurrentException, fz_caught_message(ctx));
+    JM_Exc_CurrentException = PyExc_RuntimeError;
+    return NULL;
+}
+
+
+static int LIST_APPEND_DROP(PyObject *list, PyObject *item)
+{
+    if (!list || !PyList_Check(list) || !item) return -2;
+    int rc = PyList_Append(list, item);
+    Py_DECREF(item);
+    return rc;
+}
+
+static int DICT_SETITEM_DROP(PyObject *dict, PyObject *key, PyObject *value)
+{
+    if (!dict || !PyDict_Check(dict) || !key || !value) return -2;
+    int rc = PyDict_SetItem(dict, key, value);
+    Py_DECREF(value);
+    return rc;
+}
+
+static int DICT_SETITEMSTR_DROP(PyObject *dict, const char *key, PyObject *value)
+{
+    if (!dict || !PyDict_Check(dict) || !key || !value) return -2;
+    int rc = PyDict_SetItemString(dict, key, value);
+    Py_DECREF(value);
+    return rc;
+}
+
+
+//--------------------------------------
+// Ensure valid journalling state
+//--------------------------------------
+int JM_have_operation(fz_context *ctx, pdf_document *pdf)
+{
+    if (pdf->journal && !pdf_undoredo_step(ctx, pdf, 0)) {
+        return 0;
+    }
+    return 1;
+}
+
+//----------------------------------
+// Set a PDF dict key to some value
+//----------------------------------
+static pdf_obj
+*JM_set_object_value(fz_context *ctx, pdf_obj *obj, const char *key, char *value)
+{
+    fz_buffer *res = NULL;
+    pdf_obj *new_obj = NULL, *testkey = NULL;
+    PyObject *skey = PyUnicode_FromString(key);  // Python version of dict key
+    PyObject *slash = PyUnicode_FromString("/");  // PDF path separator
+    PyObject *list = NULL, *newval=NULL, *newstr=NULL, *nullval=NULL;
+    const char eyecatcher[] = "fitz: replace me!";
+    pdf_document *pdf = NULL;
+    fz_try(ctx)
+    {
+        pdf = pdf_get_bound_document(ctx, obj);
+        // split PDF key at path seps and take last key part
+        list = PyUnicode_Split(skey, slash, -1);
+        Py_ssize_t len = PySequence_Size(list);
+        Py_ssize_t i = len - 1;
+        Py_DECREF(skey);
+        skey = PySequence_GetItem(list, i);
+
+        PySequence_DelItem(list, i);  // del the last sub-key
+        len =  PySequence_Size(list);  // remaining length
+        testkey = pdf_dict_getp(ctx, obj, key);  // check if key already exists
+        if (!testkey) {
+            /*-----------------------------------------------------------------
+            No, it will be created here. But we cannot allow this happening if
+            indirect objects are referenced. So we check all higher level
+            sub-paths for indirect references.
+            -----------------------------------------------------------------*/
+            while (len > 0) {
+                PyObject *t = PyUnicode_Join(slash, list);  // next high level
+                if (pdf_is_indirect(ctx, pdf_dict_getp(ctx, obj, JM_StrAsChar(t)))) {
+                    Py_DECREF(t);
+                    fz_throw(ctx, FZ_ERROR_GENERIC, "path to '%s' has indirects", JM_StrAsChar(skey));
+                }
+                PySequence_DelItem(list, len - 1);  // del last sub-key
+                len = PySequence_Size(list);  // remaining length
+                Py_DECREF(t);
+            }
+        }
+        // Insert our eyecatcher. Will create all sub-paths in the chain, or
+        // respectively remove old value of key-path.
+        pdf_dict_putp_drop(ctx, obj, key, pdf_new_text_string(ctx, eyecatcher));
+        testkey = pdf_dict_getp(ctx, obj, key);
+        if (!pdf_is_string(ctx, testkey)) {
+            fz_throw(ctx, FZ_ERROR_GENERIC, "cannot insert value for '%s'", key);
+        }
+        const char *temp = pdf_to_text_string(ctx, testkey);
+        if (strcmp(temp, eyecatcher) != 0) {
+            fz_throw(ctx, FZ_ERROR_GENERIC, "cannot insert value for '%s'", key);
+        }
+        // read the result as a string
+        res = JM_object_to_buffer(ctx, obj, 1, 0);
+        PyObject *objstr = JM_EscapeStrFromBuffer(ctx, res);
+
+        // replace 'eyecatcher' by desired 'value'
+        nullval = PyUnicode_FromFormat("/%s(%s)", JM_StrAsChar(skey), eyecatcher);
+        newval = PyUnicode_FromFormat("/%s %s", JM_StrAsChar(skey), value);
+        newstr = PyUnicode_Replace(objstr, nullval, newval, 1);
+
+        // make PDF object from resulting string
+        new_obj = JM_pdf_obj_from_str(ctx, pdf, JM_StrAsChar(newstr));
+    }
+    fz_always(ctx) {
+        fz_drop_buffer(ctx, res);
+        Py_CLEAR(skey);
+        Py_CLEAR(slash);
+        Py_CLEAR(list);
+        Py_CLEAR(newval);
+        Py_CLEAR(newstr);
+        Py_CLEAR(nullval);
+    }
+    fz_catch(ctx) {
+        fz_rethrow(ctx);
+    }
+    return new_obj;
+}
+
+
+static void
+JM_get_page_labels(fz_context *ctx, PyObject *liste, pdf_obj *nums)
+{
+    int pno, i, n = pdf_array_len(ctx, nums);
+    char *c = NULL;
+    pdf_obj *val;
+    fz_buffer *res = NULL;
+    for (i = 0; i < n; i += 2) {
+        pdf_obj *key = pdf_resolve_indirect(ctx, pdf_array_get(ctx, nums, i));
+        pno = pdf_to_int(ctx, key);
+        val = pdf_resolve_indirect(ctx, pdf_array_get(ctx, nums, i + 1));
+        res = JM_object_to_buffer(ctx, val, 1, 0);
+        fz_buffer_storage(ctx, res, &c);
+        LIST_APPEND_DROP(liste, Py_BuildValue("is", pno, c));
+        fz_drop_buffer(ctx, res);
+    }
+}
+
+
+PyObject *JM_EscapeStrFromBuffer(fz_context *ctx, fz_buffer *buff)
+{
+    if (!buff) return EMPTY_STRING;
+    unsigned char *s = NULL;
+    size_t len = fz_buffer_storage(ctx, buff, &s);
+    PyObject *val = PyUnicode_DecodeRawUnicodeEscape((const char *) s, (Py_ssize_t) len, "replace");
+    if (!val) {
+        val = EMPTY_STRING;
+        PyErr_Clear();
+    }
+    return val;
+}
+
+PyObject *JM_UnicodeFromBuffer(fz_context *ctx, fz_buffer *buff)
+{
+    unsigned char *s = NULL;
+    Py_ssize_t len = (Py_ssize_t) fz_buffer_storage(ctx, buff, &s);
+    PyObject *val = PyUnicode_DecodeUTF8((const char *) s, len, "replace");
+    if (!val) {
+        val = EMPTY_STRING;
+        PyErr_Clear();
+    }
+    return val;
+}
+
+PyObject *JM_UnicodeFromStr(const char *c)
+{
+    if (!c) return EMPTY_STRING;
+    PyObject *val = Py_BuildValue("s", c);
+    if (!val) {
+        val = EMPTY_STRING;
+        PyErr_Clear();
+    }
+    return val;
+}
+
+PyObject *JM_EscapeStrFromStr(const char *c)
+{
+    if (!c) return EMPTY_STRING;
+    PyObject *val = PyUnicode_DecodeRawUnicodeEscape(c, (Py_ssize_t) strlen(c), "replace");
+    if (!val) {
+        val = EMPTY_STRING;
+        PyErr_Clear();
+    }
+    return val;
+}
+
+
+// list of valid unicodes of a fz_font
+void JM_valid_chars(fz_context *ctx, fz_font *font, void *arr)
+{
+	FT_Face face = font->ft_face;
+	FT_ULong ucs;
+	FT_UInt gid;
+	long *table = (long *)arr;
+	fz_lock(ctx, FZ_LOCK_FREETYPE);
+	ucs = FT_Get_First_Char(face, &gid);
+	while (gid > 0)
+	{
+		if (gid < (FT_ULong)face->num_glyphs && face->num_glyphs > 0)
+			table[gid] = (long)ucs;
+		ucs = FT_Get_Next_Char(face, ucs, &gid);
+	}
+	fz_unlock(ctx, FZ_LOCK_FREETYPE);
+	return;
+}
+
+
+// redirect MuPDF warnings
+void JM_mupdf_warning(void *user, const char *message)
+{
+    LIST_APPEND_DROP(JM_mupdf_warnings_store, JM_EscapeStrFromStr(message));
+    if (JM_mupdf_show_warnings) {
+        PySys_WriteStderr("mupdf: %s\n", message);
+    }
+}
+
+// redirect MuPDF errors
+void JM_mupdf_error(void *user, const char *message)
+{
+    LIST_APPEND_DROP(JM_mupdf_warnings_store, JM_EscapeStrFromStr(message));
+    if (JM_mupdf_show_errors) {
+        PySys_WriteStderr("mupdf: %s\n", message);
+    }
+}
+
+// a simple tracer
+void JM_TRACE(const char *id)
+{
+    PySys_WriteStdout("%s\n", id);
+}
+
+
+// put a warning on Python-stdout
+void JM_Warning(const char *id)
+{
+    PySys_WriteStdout("warning: %s\n", id);
+}
+
+#if JM_MEMORY == 1
+//-----------------------------------------------------------------------------
+// The following 3 functions replace MuPDF standard memory allocation.
+// This will ensure, that MuPDF memory handling becomes part of Python's
+// memory management.
+//-----------------------------------------------------------------------------
+static void *JM_Py_Malloc(void *opaque, size_t size)
+{
+    void *mem = PyMem_Malloc((Py_ssize_t) size);
+    if (mem) return mem;
+    fz_throw(gctx, FZ_ERROR_MEMORY, "malloc of %zu bytes failed", size);
+}
+
+static void *JM_Py_Realloc(void *opaque, void *old, size_t size)
+{
+    void *mem = PyMem_Realloc(old, (Py_ssize_t) size);
+    if (mem) return mem;
+    fz_throw(gctx, FZ_ERROR_MEMORY, "realloc of %zu bytes failed", size);
+}
+
+static void JM_PY_Free(void *opaque, void *ptr)
+{
+    PyMem_Free(ptr);
+}
+
+const fz_alloc_context JM_Alloc_Context =
+{
+	NULL,
+	JM_Py_Malloc,
+	JM_Py_Realloc,
+	JM_PY_Free
+};
+#endif
+
+PyObject *JM_fitz_config()
+{
+#if defined(TOFU)
+#define have_TOFU JM_BOOL(0)
+#else
+#define have_TOFU JM_BOOL(1)
+#endif
+#if defined(TOFU_CJK)
+#define have_TOFU_CJK JM_BOOL(0)
+#else
+#define have_TOFU_CJK JM_BOOL(1)
+#endif
+#if defined(TOFU_CJK_EXT)
+#define have_TOFU_CJK_EXT JM_BOOL(0)
+#else
+#define have_TOFU_CJK_EXT JM_BOOL(1)
+#endif
+#if defined(TOFU_CJK_LANG)
+#define have_TOFU_CJK_LANG JM_BOOL(0)
+#else
+#define have_TOFU_CJK_LANG JM_BOOL(1)
+#endif
+#if defined(TOFU_EMOJI)
+#define have_TOFU_EMOJI JM_BOOL(0)
+#else
+#define have_TOFU_EMOJI JM_BOOL(1)
+#endif
+#if defined(TOFU_HISTORIC)
+#define have_TOFU_HISTORIC JM_BOOL(0)
+#else
+#define have_TOFU_HISTORIC JM_BOOL(1)
+#endif
+#if defined(TOFU_SYMBOL)
+#define have_TOFU_SYMBOL JM_BOOL(0)
+#else
+#define have_TOFU_SYMBOL JM_BOOL(1)
+#endif
+#if defined(TOFU_SIL)
+#define have_TOFU_SIL JM_BOOL(0)
+#else
+#define have_TOFU_SIL JM_BOOL(1)
+#endif
+#if defined(TOFU_BASE14)
+#define have_TOFU_BASE14 JM_BOOL(0)
+#else
+#define have_TOFU_BASE14 JM_BOOL(1)
+#endif
+    PyObject *dict = PyDict_New();
+    DICT_SETITEMSTR_DROP(dict, "plotter-g", JM_BOOL(FZ_PLOTTERS_G));
+    DICT_SETITEMSTR_DROP(dict, "plotter-rgb", JM_BOOL(FZ_PLOTTERS_RGB));
+    DICT_SETITEMSTR_DROP(dict, "plotter-cmyk", JM_BOOL(FZ_PLOTTERS_CMYK));
+    DICT_SETITEMSTR_DROP(dict, "plotter-n", JM_BOOL(FZ_PLOTTERS_N));
+    DICT_SETITEMSTR_DROP(dict, "pdf", JM_BOOL(FZ_ENABLE_PDF));
+    DICT_SETITEMSTR_DROP(dict, "xps", JM_BOOL(FZ_ENABLE_XPS));
+    DICT_SETITEMSTR_DROP(dict, "svg", JM_BOOL(FZ_ENABLE_SVG));
+    DICT_SETITEMSTR_DROP(dict, "cbz", JM_BOOL(FZ_ENABLE_CBZ));
+    DICT_SETITEMSTR_DROP(dict, "img", JM_BOOL(FZ_ENABLE_IMG));
+    DICT_SETITEMSTR_DROP(dict, "html", JM_BOOL(FZ_ENABLE_HTML));
+    DICT_SETITEMSTR_DROP(dict, "epub", JM_BOOL(FZ_ENABLE_EPUB));
+    DICT_SETITEMSTR_DROP(dict, "jpx", JM_BOOL(FZ_ENABLE_JPX));
+    DICT_SETITEMSTR_DROP(dict, "js", JM_BOOL(FZ_ENABLE_JS));
+    DICT_SETITEMSTR_DROP(dict, "tofu", have_TOFU);
+    DICT_SETITEMSTR_DROP(dict, "tofu-cjk", have_TOFU_CJK);
+    DICT_SETITEMSTR_DROP(dict, "tofu-cjk-ext", have_TOFU_CJK_EXT);
+    DICT_SETITEMSTR_DROP(dict, "tofu-cjk-lang", have_TOFU_CJK_LANG);
+    DICT_SETITEMSTR_DROP(dict, "tofu-emoji", have_TOFU_EMOJI);
+    DICT_SETITEMSTR_DROP(dict, "tofu-historic", have_TOFU_HISTORIC);
+    DICT_SETITEMSTR_DROP(dict, "tofu-symbol", have_TOFU_SYMBOL);
+    DICT_SETITEMSTR_DROP(dict, "tofu-sil", have_TOFU_SIL);
+    DICT_SETITEMSTR_DROP(dict, "icc", JM_BOOL(FZ_ENABLE_ICC));
+    DICT_SETITEMSTR_DROP(dict, "base14", have_TOFU_BASE14);
+    DICT_SETITEMSTR_DROP(dict, "py-memory", JM_BOOL(JM_MEMORY));
+    return dict;
+}
+
+//----------------------------------------------------------------------------
+// Update a color float array with values from a Python sequence.
+// Any error condition is treated as a no-op.
+//----------------------------------------------------------------------------
+void JM_color_FromSequence(PyObject *color, int *n, float col[4])
+{
+    if (!color || color == Py_None) {
+        *n = -1;
+        return;
+    }
+    if (PyFloat_Check(color)) { // maybe just a single float
+        *n = 1;
+        float c = (float) PyFloat_AsDouble(color);
+        if (!INRANGE(c, 0, 1)) {
+            c = 1;
+        }
+        col[0] = c;
+        return;
+    }
+
+    if (!PySequence_Check(color)) {
+        *n = -1;
+        return;
+    }
+    int len = (int) PySequence_Size(color), rc;
+    if (len == 0) {
+        *n = 0;
+        return;
+    }
+    if (!INRANGE(len, 1, 4) || len == 2) {
+        *n = -1;
+        return;
+    }
+
+    double mcol[4] = {0,0,0,0}; // local color storage
+    Py_ssize_t i;
+    for (i = 0; i < len; i++) {
+        rc = JM_FLOAT_ITEM(color, i, &mcol[i]);
+        if (!INRANGE(mcol[i], 0, 1) || rc == 1) mcol[i] = 1;
+    }
+
+    *n = len;
+    for (i = 0; i < len; i++)
+        col[i] = (float) mcol[i];
+    return;
+}
+
+// return extension for fitz image type
+const char *JM_image_extension(int type)
+{
+    switch (type) {
+        case(FZ_IMAGE_FAX): return "fax";
+        case(FZ_IMAGE_RAW): return "raw";
+        case(FZ_IMAGE_FLATE): return "flate";
+        case(FZ_IMAGE_LZW): return "lzw";
+        case(FZ_IMAGE_RLD): return "rld";
+        case(FZ_IMAGE_BMP): return "bmp";
+        case(FZ_IMAGE_GIF): return "gif";
+        case(FZ_IMAGE_JBIG2): return "jb2";
+        case(FZ_IMAGE_JPEG): return "jpeg";
+        case(FZ_IMAGE_JPX): return "jpx";
+        case(FZ_IMAGE_JXR): return "jxr";
+        case(FZ_IMAGE_PNG): return "png";
+        case(FZ_IMAGE_PNM): return "pnm";
+        case(FZ_IMAGE_TIFF): return "tiff";
+        // case(FZ_IMAGE_PSD): return "psd";
+        case(FZ_IMAGE_UNKNOWN): return "n/a";
+        default: return "n/a";
+    }
+}
+
+//----------------------------------------------------------------------------
+// Turn fz_buffer into a Python bytes object
+//----------------------------------------------------------------------------
+PyObject *JM_BinFromBuffer(fz_context *ctx, fz_buffer *buffer)
+{
+    if (!buffer) {
+        return PyBytes_FromString("");
+    }
+    unsigned char *c = NULL;
+    size_t len = fz_buffer_storage(ctx, buffer, &c);
+    return PyBytes_FromStringAndSize((const char *) c, (Py_ssize_t) len);
+}
+
+//----------------------------------------------------------------------------
+// Turn fz_buffer into a Python bytearray object
+//----------------------------------------------------------------------------
+PyObject *JM_BArrayFromBuffer(fz_context *ctx, fz_buffer *buffer)
+{
+    if (!buffer) {
+        return PyByteArray_FromStringAndSize("", 0);
+    }
+    unsigned char *c = NULL;
+    size_t len = fz_buffer_storage(ctx, buffer, &c);
+    return PyByteArray_FromStringAndSize((const char *) c, (Py_ssize_t) len);
+}
+
+
+//----------------------------------------------------------------------------
+// compress char* into a new buffer
+//----------------------------------------------------------------------------
+fz_buffer *JM_compress_buffer(fz_context *ctx, fz_buffer *inbuffer)
+{
+    fz_buffer *buf = NULL;
+    fz_try(ctx) {
+        size_t compressed_length = 0;
+        unsigned char *data = fz_new_deflated_data_from_buffer(ctx,
+                              &compressed_length, inbuffer, FZ_DEFLATE_BEST);
+        if (data == NULL || compressed_length == 0)
+            return NULL;
+        buf = fz_new_buffer_from_data(ctx, data, compressed_length);
+        fz_resize_buffer(ctx, buf, compressed_length);
+    }
+    fz_catch(ctx) {
+        fz_drop_buffer(ctx, buf);
+        fz_rethrow(ctx);
+    }
+    return buf;
+}
+
+//----------------------------------------------------------------------------
+// update a stream object
+// compress stream when beneficial
+//----------------------------------------------------------------------------
+void JM_update_stream(fz_context *ctx, pdf_document *doc, pdf_obj *obj, fz_buffer *buffer, int compress)
+{
+
+    fz_buffer *nres = NULL;
+    size_t len = fz_buffer_storage(ctx, buffer, NULL);
+    size_t nlen = len;
+
+    if (compress == 1 && len > 30) {  // ignore small stuff
+        nres = JM_compress_buffer(ctx, buffer);
+        nlen = fz_buffer_storage(ctx, nres, NULL);
+    }
+
+    if (nlen < len && nres && compress==1) {  // was it worth the effort?
+        pdf_dict_put(ctx, obj, PDF_NAME(Filter), PDF_NAME(FlateDecode));
+        pdf_update_stream(ctx, doc, obj, nres, 1);
+    } else {
+        pdf_update_stream(ctx, doc, obj, buffer, 0);
+    }
+    fz_drop_buffer(ctx, nres);
+}
+
+//-----------------------------------------------------------------------------
+// return hex characters for n characters in input 'in'
+//-----------------------------------------------------------------------------
+void hexlify(int n, unsigned char *in, unsigned char *out)
+{
+    const unsigned char hdigit[17] = "0123456789abcedf";
+    int i, i1, i2;
+    for (i = 0; i < n; i++) {
+        i1 = in[i]>>4;
+        i2 = in[i] - i1*16;
+        out[2*i] = hdigit[i1];
+        out[2*i + 1] = hdigit[i2];
+    }
+    out[2*n] = 0;
+}
+
+//----------------------------------------------------------------------------
+// Make fz_buffer from a PyBytes, PyByteArray, or io.BytesIO object
+//----------------------------------------------------------------------------
+fz_buffer *JM_BufferFromBytes(fz_context *ctx, PyObject *stream)
+{
+    char *c = NULL;
+    PyObject *mybytes = NULL;
+    size_t len = 0;
+    fz_buffer *res = NULL;
+    fz_var(res);
+    fz_try(ctx) {
+        if (PyBytes_Check(stream)) {
+            c = PyBytes_AS_STRING(stream);
+            len = (size_t) PyBytes_GET_SIZE(stream);
+        } else if (PyByteArray_Check(stream)) {
+            c = PyByteArray_AS_STRING(stream);
+            len = (size_t) PyByteArray_GET_SIZE(stream);
+        } else if (PyObject_HasAttrString(stream, "getvalue")) {
+            // we assume here that this delivers what we expect
+            mybytes = PyObject_CallMethod(stream, "getvalue", NULL);
+            c = PyBytes_AS_STRING(mybytes);
+            len = (size_t) PyBytes_GET_SIZE(mybytes);
+        }
+        // if none of the above, c is NULL and we return an empty buffer
+        if (c) {
+            res = fz_new_buffer_from_copied_data(ctx, (const unsigned char *) c, len);
+        } else {
+            res = fz_new_buffer(ctx, 1);
+            fz_append_byte(ctx, res, 10);
+        }
+        fz_terminate_buffer(ctx, res);
+    }
+    fz_always(ctx) {
+        Py_CLEAR(mybytes);
+        PyErr_Clear();
+    }
+    fz_catch(ctx) {
+        fz_drop_buffer(ctx, res);
+        fz_rethrow(ctx);
+    }
+    return res;
+}
+
+
+//----------------------------------------------------------------------------
+// Deep-copies a source page to the target.
+// Modified version of function of pdfmerge.c: we also copy annotations, but
+// we skip some subtypes. In addition we rotate output.
+//----------------------------------------------------------------------------
+static void
+page_merge(fz_context *ctx, pdf_document *doc_des, pdf_document *doc_src, int page_from, int page_to, int rotate, int links, int copy_annots, pdf_graft_map *graft_map)
+{
+    pdf_obj *page_ref = NULL;
+    pdf_obj *page_dict = NULL;
+    pdf_obj *obj = NULL, *ref = NULL;
+
+    // list of object types (per page) we want to copy
+    static pdf_obj * const known_page_objs[] = {
+        PDF_NAME(Contents),
+        PDF_NAME(Resources),
+        PDF_NAME(MediaBox),
+        PDF_NAME(CropBox),
+        PDF_NAME(BleedBox),
+        PDF_NAME(TrimBox),
+        PDF_NAME(ArtBox),
+        PDF_NAME(Rotate),
+        PDF_NAME(UserUnit)
+    };
+
+    int i, n;
+
+    fz_var(ref);
+    fz_var(page_dict);
+
+    fz_try(ctx) {
+        page_ref = pdf_lookup_page_obj(ctx, doc_src, page_from);
+
+        // make new page dict in dest doc
+        page_dict = pdf_new_dict(ctx, doc_des, 4);
+        pdf_dict_put(ctx, page_dict, PDF_NAME(Type), PDF_NAME(Page));
+
+        for (i = 0; i < (int) nelem(known_page_objs); i++) {
+            obj = pdf_dict_get_inheritable(ctx, page_ref, known_page_objs[i]);
+            if (obj != NULL) {
+                pdf_dict_put_drop(ctx, page_dict, known_page_objs[i], pdf_graft_mapped_object(ctx, graft_map, obj));
+            }
+        }
+
+        // Copy annotations, but skip Link, Popup, IRT, Widget types
+        // If selected, remove dict keys P (parent) and Popup
+        if (copy_annots) {
+            pdf_obj *old_annots = pdf_dict_get(ctx, page_ref, PDF_NAME(Annots));
+            n = pdf_array_len(ctx, old_annots);
+            if (n > 0) {
+                pdf_obj *new_annots = pdf_dict_put_array(ctx, page_dict, PDF_NAME(Annots), n);
+                for (i = 0; i < n; i++) {
+                    pdf_obj *o = pdf_array_get(ctx, old_annots, i);
+                    if (!pdf_is_dict(ctx, o)) continue;  // skip non-dict items
+                    if (pdf_dict_get(ctx, o, PDF_NAME(IRT))) continue;
+                    pdf_obj *subtype = pdf_dict_get(ctx, o, PDF_NAME(Subtype));
+                    if (pdf_name_eq(ctx, subtype, PDF_NAME(Link))) continue;
+                    if (pdf_name_eq(ctx, subtype, PDF_NAME(Popup))) continue;
+                    if (pdf_name_eq(ctx, subtype, PDF_NAME(Widget))) continue;
+                    pdf_dict_del(ctx, o, PDF_NAME(Popup));
+                    pdf_dict_del(ctx, o, PDF_NAME(P));
+                    pdf_obj *copy_o = pdf_graft_mapped_object(ctx, graft_map, o);
+                    pdf_obj *annot = pdf_new_indirect(ctx, doc_des,
+                                     pdf_to_num(ctx, copy_o), 0);
+                    pdf_array_push_drop(ctx, new_annots, annot);
+                    pdf_drop_obj(ctx, copy_o);
+                }
+            }
+        }
+        // rotate the page
+        if (rotate != -1) {
+            pdf_dict_put_int(ctx, page_dict, PDF_NAME(Rotate), (int64_t) rotate);
+        }
+        // Now add the page dictionary to dest PDF
+        ref = pdf_add_object(ctx, doc_des, page_dict);
+
+        // Insert new page at specified location
+        pdf_insert_page(ctx, doc_des, page_to, ref);
+
+    }
+    fz_always(ctx) {
+        pdf_drop_obj(ctx, page_dict);
+        pdf_drop_obj(ctx, ref);
+    }
+    fz_catch(ctx) {
+        fz_rethrow(ctx);
+    }
+}
+
+//-----------------------------------------------------------------------------
+// Copy a range of pages (spage, epage) from a source PDF to a specified
+// location (apage) of the target PDF.
+// If spage > epage, the sequence of source pages is reversed.
+//-----------------------------------------------------------------------------
+void JM_merge_range(fz_context *ctx, pdf_document *doc_des, pdf_document *doc_src, int spage, int epage, int apage, int rotate, int links, int annots, int show_progress, pdf_graft_map *graft_map)
+{
+    int page, afterpage;
+    afterpage = apage;
+    int counter = 0;  // copied pages counter
+    int total = fz_absi(epage - spage) + 1;  // total pages to copy
+
+    fz_try(ctx) {
+        if (spage < epage) {
+            for (page = spage; page <= epage; page++, afterpage++) {
+                page_merge(ctx, doc_des, doc_src, page, afterpage, rotate, links, annots, graft_map);
+                counter++;
+                if (show_progress > 0 && counter % show_progress == 0) {
+                    PySys_WriteStdout("Inserted %i of %i pages.\n", counter, total);
+                }
+            }
+        } else {
+            for (page = spage; page >= epage; page--, afterpage++) {
+                page_merge(ctx, doc_des, doc_src, page, afterpage, rotate, links, annots, graft_map);
+                counter++;
+                if (show_progress > 0 && counter % show_progress == 0) {
+                    PySys_WriteStdout("Inserted %i of %i pages.\n", counter, total);
+                }
+            }
+        }
+    }
+
+    fz_catch(ctx) {
+        fz_rethrow(ctx);
+    }
+}
+
+//----------------------------------------------------------------------------
+// Return list of outline xref numbers. Recursive function. Arguments:
+// 'obj' first OL item
+// 'xrefs' empty Python list
+//----------------------------------------------------------------------------
+PyObject *JM_outline_xrefs(fz_context *ctx, pdf_obj *obj, PyObject *xrefs)
+{
+    pdf_obj *first, *parent, *thisobj;
+    if (!obj) return xrefs;
+    PyObject *newxref = NULL;
+    thisobj = obj;
+    while (thisobj) {
+        newxref = PyLong_FromLong((long) pdf_to_num(ctx, thisobj));
+        if (PySequence_Contains(xrefs, newxref) ||
+            pdf_dict_get(ctx, thisobj, PDF_NAME(Type))) {
+            // circular ref or top of chain: terminate
+            Py_DECREF(newxref);
+            break;
+        }
+        LIST_APPEND_DROP(xrefs, newxref);
+        first = pdf_dict_get(ctx, thisobj, PDF_NAME(First));  // try go down
+        if (pdf_is_dict(ctx, first)) xrefs = JM_outline_xrefs(ctx, first, xrefs);
+        thisobj = pdf_dict_get(ctx, thisobj, PDF_NAME(Next));  // try go next
+        parent = pdf_dict_get(ctx, thisobj, PDF_NAME(Parent));  // get parent
+        if (!pdf_is_dict(ctx, thisobj)) {
+            thisobj = parent;
+        }
+    }
+    return xrefs;
+}
+
+
+//-------------------------------------------------------------------
+// Return the contents of a font file, identified by xref
+//-------------------------------------------------------------------
+fz_buffer *JM_get_fontbuffer(fz_context *ctx, pdf_document *doc, int xref)
+{
+    if (xref < 1) return NULL;
+    pdf_obj *o, *obj = NULL, *desft, *stream = NULL;
+    o = pdf_load_object(ctx, doc, xref);
+    desft = pdf_dict_get(ctx, o, PDF_NAME(DescendantFonts));
+    if (desft) {
+        obj = pdf_resolve_indirect(ctx, pdf_array_get(ctx, desft, 0));
+        obj = pdf_dict_get(ctx, obj, PDF_NAME(FontDescriptor));
+    } else {
+        obj = pdf_dict_get(ctx, o, PDF_NAME(FontDescriptor));
+    }
+
+    if (!obj) {
+        pdf_drop_obj(ctx, o);
+        PySys_WriteStdout("invalid font - FontDescriptor missing");
+        return NULL;
+    }
+    pdf_drop_obj(ctx, o);
+    o = obj;
+
+    obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile));
+    if (obj) stream = obj;             // ext = "pfa"
+
+    obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile2));
+    if (obj) stream = obj;             // ext = "ttf"
+
+    obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile3));
+    if (obj) {
+        stream = obj;
+
+        obj = pdf_dict_get(ctx, obj, PDF_NAME(Subtype));
+        if (obj && !pdf_is_name(ctx, obj)) {
+            PySys_WriteStdout("invalid font descriptor subtype");
+            return NULL;
+        }
+
+        if (pdf_name_eq(ctx, obj, PDF_NAME(Type1C)))
+            ; /*Prev code did: ext = "cff", but this has no effect. */
+        else if (pdf_name_eq(ctx, obj, PDF_NAME(CIDFontType0C)))
+            ; /*Prev code did: ext = "cid", but this has no effect. */
+        else if (pdf_name_eq(ctx, obj, PDF_NAME(OpenType)))
+            ; /*Prev code did: ext = "otf", but this has no effect. */
+        else
+            PySys_WriteStdout("warning: unhandled font type '%s'", pdf_to_name(ctx, obj));
+    }
+
+    if (!stream) {
+        PySys_WriteStdout("warning: unhandled font type");
+        return NULL;
+    }
+
+    return pdf_load_stream(ctx, stream);
+}
+
+//-----------------------------------------------------------------------------
+// Return the file extension of a font file, identified by xref
+//-----------------------------------------------------------------------------
+char *JM_get_fontextension(fz_context *ctx, pdf_document *doc, int xref)
+{
+    if (xref < 1) return "n/a";
+    pdf_obj *o, *obj = NULL, *desft;
+    o = pdf_load_object(ctx, doc, xref);
+    desft = pdf_dict_get(ctx, o, PDF_NAME(DescendantFonts));
+    if (desft) {
+        obj = pdf_resolve_indirect(ctx, pdf_array_get(ctx, desft, 0));
+        obj = pdf_dict_get(ctx, obj, PDF_NAME(FontDescriptor));
+    } else {
+        obj = pdf_dict_get(ctx, o, PDF_NAME(FontDescriptor));
+    }
+
+    pdf_drop_obj(ctx, o);
+    if (!obj) return "n/a";  // this is a base-14 font
+
+    o = obj;  // we have the FontDescriptor
+
+    obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile));
+    if (obj) return "pfa";
+
+    obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile2));
+    if (obj) return "ttf";
+
+    obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile3));
+    if (obj) {
+        obj = pdf_dict_get(ctx, obj, PDF_NAME(Subtype));
+        if (obj && !pdf_is_name(ctx, obj)) {
+            PySys_WriteStdout("invalid font descriptor subtype");
+            return "n/a";
+        }
+        if (pdf_name_eq(ctx, obj, PDF_NAME(Type1C)))
+            return "cff";
+        else if (pdf_name_eq(ctx, obj, PDF_NAME(CIDFontType0C)))
+            return "cid";
+        else if (pdf_name_eq(ctx, obj, PDF_NAME(OpenType)))
+            return "otf";
+        else
+            PySys_WriteStdout("unhandled font type '%s'", pdf_to_name(ctx, obj));
+    }
+
+    return "n/a";
+}
+
+
+//-----------------------------------------------------------------------------
+// create PDF object from given string (new in v1.14.0: MuPDF dropped it)
+//-----------------------------------------------------------------------------
+pdf_obj *JM_pdf_obj_from_str(fz_context *ctx, pdf_document *doc, char *src)
+{
+    pdf_obj *result = NULL;
+    pdf_lexbuf lexbuf;
+    fz_stream *stream = fz_open_memory(ctx, (unsigned char *)src, strlen(src));
+
+    pdf_lexbuf_init(ctx, &lexbuf, PDF_LEXBUF_SMALL);
+
+    fz_try(ctx) {
+        result = pdf_parse_stm_obj(ctx, doc, stream, &lexbuf);
+    }
+
+    fz_always(ctx) {
+        pdf_lexbuf_fin(ctx, &lexbuf);
+        fz_drop_stream(ctx, stream);
+    }
+
+    fz_catch(ctx) {
+        fz_rethrow(ctx);
+    }
+
+    return result;
+
+}
+
+//----------------------------------------------------------------------------
+// return normalized /Rotate value:one of 0, 90, 180, 270
+//----------------------------------------------------------------------------
+int JM_norm_rotation(int rotate)
+{
+    while (rotate < 0) rotate += 360;
+    while (rotate >= 360) rotate -= 360;
+    if (rotate % 90 != 0) return 0;
+    return rotate;
+}
+
+
+//----------------------------------------------------------------------------
+// return a PDF page's /Rotate value: one of (0, 90, 180, 270)
+//----------------------------------------------------------------------------
+int JM_page_rotation(fz_context *ctx, pdf_page *page)
+{
+    int rotate = 0;
+    fz_try(ctx)
+    {
+        rotate = pdf_to_int(ctx,
+                pdf_dict_get_inheritable(ctx, page->obj, PDF_NAME(Rotate)));
+        rotate = JM_norm_rotation(rotate);
+    }
+    fz_catch(ctx) return 0;
+    return rotate;
+}
+
+
+//----------------------------------------------------------------------------
+// return a PDF page's MediaBox
+//----------------------------------------------------------------------------
+fz_rect JM_mediabox(fz_context *ctx, pdf_obj *page_obj)
+{
+    fz_rect mediabox, page_mediabox;
+
+    mediabox = pdf_to_rect(ctx, pdf_dict_get_inheritable(ctx, page_obj,
+        PDF_NAME(MediaBox)));
+    if (fz_is_empty_rect(mediabox) || fz_is_infinite_rect(mediabox))
+    {
+        mediabox.x0 = 0;
+        mediabox.y0 = 0;
+        mediabox.x1 = 612;
+        mediabox.y1 = 792;
+    }
+
+    page_mediabox.x0 = fz_min(mediabox.x0, mediabox.x1);
+    page_mediabox.y0 = fz_min(mediabox.y0, mediabox.y1);
+    page_mediabox.x1 = fz_max(mediabox.x0, mediabox.x1);
+    page_mediabox.y1 = fz_max(mediabox.y0, mediabox.y1);
+
+    if (page_mediabox.x1 - page_mediabox.x0 < 1 ||
+        page_mediabox.y1 - page_mediabox.y0 < 1)
+        page_mediabox = fz_unit_rect;
+
+    return page_mediabox;
+}
+
+
+//----------------------------------------------------------------------------
+// return a PDF page's CropBox
+//----------------------------------------------------------------------------
+fz_rect JM_cropbox(fz_context *ctx, pdf_obj *page_obj)
+{
+    fz_rect mediabox = JM_mediabox(ctx, page_obj);
+    fz_rect cropbox = pdf_to_rect(ctx,
+                pdf_dict_get_inheritable(ctx, page_obj, PDF_NAME(CropBox)));
+    if (fz_is_infinite_rect(cropbox) || fz_is_empty_rect(cropbox))
+        cropbox = mediabox;
+    float y0 = mediabox.y1 - cropbox.y1;
+    float y1 = mediabox.y1 - cropbox.y0;
+    cropbox.y0 = y0;
+    cropbox.y1 = y1;
+    return cropbox;
+}
+
+
+//----------------------------------------------------------------------------
+// calculate width and height of the UNROTATED page
+//----------------------------------------------------------------------------
+fz_point JM_cropbox_size(fz_context *ctx, pdf_obj *page_obj)
+{
+    fz_point size;
+    fz_try(ctx)
+    {
+        fz_rect rect = JM_cropbox(ctx, page_obj);
+        float w = (rect.x0 < rect.x1 ? rect.x1 - rect.x0 : rect.x0 - rect.x1);
+        float h = (rect.y0 < rect.y1 ? rect.y1 - rect.y0 : rect.y0 - rect.y1);
+        size = fz_make_point(w, h);
+    }
+    fz_catch(ctx) fz_rethrow(ctx);
+    return size;
+}
+
+
+//----------------------------------------------------------------------------
+// calculate page rotation matrices
+//----------------------------------------------------------------------------
+fz_matrix JM_rotate_page_matrix(fz_context *ctx, pdf_page *page)
+{
+    if (!page) return fz_identity;  // no valid pdf page given
+    int rotation = JM_page_rotation(ctx, page);
+    if (rotation == 0) return fz_identity;  // no rotation
+    fz_matrix m;
+    fz_point cb_size = JM_cropbox_size(ctx, page->obj);
+    float w = cb_size.x;
+    float h = cb_size.y;
+    if (rotation == 90)
+        m = fz_make_matrix(0, 1, -1, 0, h, 0);
+    else if (rotation == 180)
+        m = fz_make_matrix(-1, 0, 0, -1, w, h);
+    else
+        m = fz_make_matrix(0, -1, 1, 0, 0, w);
+    return m;
+}
+
+
+fz_matrix JM_derotate_page_matrix(fz_context *ctx, pdf_page *page)
+{  // just the inverse of rotation
+    return fz_invert_matrix(JM_rotate_page_matrix(ctx, page));
+}
+
+
+//-----------------------------------------------------------------------------
+// Insert a font in a PDF
+//-----------------------------------------------------------------------------
+PyObject *
+JM_insert_font(fz_context *ctx, pdf_document *pdf, char *bfname, char *fontfile,
+    PyObject *fontbuffer, int set_simple, int idx, int wmode, int serif,
+    int encoding, int ordering)
+{
+    pdf_obj *font_obj = NULL;
+    fz_font *font = NULL;
+    fz_buffer *res = NULL;
+    const unsigned char *data = NULL;
+    int size, ixref = 0, index = 0, simple = 0;
+    PyObject *value=NULL, *name=NULL, *subt=NULL, *exto = NULL;
+
+    fz_var(exto);
+    fz_var(name);
+    fz_var(subt);
+    fz_var(res);
+    fz_var(font);
+    fz_var(font_obj);
+    fz_try(ctx) {
+        ENSURE_OPERATION(ctx, pdf);
+        //-------------------------------------------------------------
+        // check for CJK font
+        //-------------------------------------------------------------
+        if (ordering > -1) {
+            data = fz_lookup_cjk_font(ctx, ordering, &size, &index);
+        }
+        if (data) {
+            font = fz_new_font_from_memory(ctx, NULL, data, size, index, 0);
+            font_obj = pdf_add_cjk_font(ctx, pdf, font, ordering, wmode, serif);
+            exto = JM_UnicodeFromStr("n/a");
+            simple = 0;
+            goto weiter;
+        }
+
+        //-------------------------------------------------------------
+        // check for PDF Base-14 font
+        //-------------------------------------------------------------
+        if (bfname) {
+            data = fz_lookup_base14_font(ctx, bfname, &size);
+        }
+        if (data) {
+            font = fz_new_font_from_memory(ctx, bfname, data, size, 0, 0);
+            font_obj = pdf_add_simple_font(ctx, pdf, font, encoding);
+            exto = JM_UnicodeFromStr("n/a");
+            simple = 1;
+            goto weiter;
+        }
+
+        if (fontfile) {
+            font = fz_new_font_from_file(ctx, NULL, fontfile, idx, 0);
+        } else {
+            res = JM_BufferFromBytes(ctx, fontbuffer);
+            if (!res) {
+                RAISEPY(ctx, MSG_FILE_OR_BUFFER, PyExc_ValueError);
+            }
+            font = fz_new_font_from_buffer(ctx, NULL, res, idx, 0);
+        }
+
+        if (!set_simple) {
+            font_obj = pdf_add_cid_font(ctx, pdf, font);
+            simple = 0;
+        } else {
+            font_obj = pdf_add_simple_font(ctx, pdf, font, encoding);
+            simple = 2;
+        }
+
+        weiter: ;
+        ixref = pdf_to_num(ctx, font_obj);
+        name = JM_EscapeStrFromStr(pdf_to_name(ctx,
+                    pdf_dict_get(ctx, font_obj, PDF_NAME(BaseFont))));
+
+        subt = JM_UnicodeFromStr(pdf_to_name(ctx,
+                    pdf_dict_get(ctx, font_obj, PDF_NAME(Subtype))));
+
+        if (!exto)
+            exto = JM_UnicodeFromStr(JM_get_fontextension(ctx, pdf, ixref));
+
+        float asc = fz_font_ascender(ctx, font);
+        float dsc = fz_font_descender(ctx, font);
+        value = Py_BuildValue("[i,{s:O,s:O,s:O,s:O,s:i,s:f,s:f}]",
+                                ixref,
+                                "name", name,        // base font name
+                                "type", subt,        // subtype
+                                "ext", exto,         // file extension
+                                "simple", JM_BOOL(simple), // simple font?
+                                "ordering", ordering, // CJK font?
+                                "ascender", asc,
+                                "descender", dsc
+                                );
+    }
+    fz_always(ctx) {
+        Py_CLEAR(exto);
+        Py_CLEAR(name);
+        Py_CLEAR(subt);
+        fz_drop_buffer(ctx, res);
+        fz_drop_font(ctx, font);
+        pdf_drop_obj(ctx, font_obj);
+    }
+    fz_catch(ctx) {
+        fz_rethrow(ctx);
+    }
+    return value;
+}
+
+
+//-----------------------------------------------------------------------------
+// compute image insertion matrix
+//-----------------------------------------------------------------------------
+fz_matrix
+calc_image_matrix(int width, int height, PyObject *tr, int rotate, int keep)
+{
+    float large, small, fw, fh, trw, trh, f, w, h;
+    fz_rect trect = JM_rect_from_py(tr);
+    fz_matrix rot = fz_rotate((float) rotate);
+    trw = trect.x1 - trect.x0;
+    trh = trect.y1 - trect.y0;
+    w = trw;
+    h = trh;
+    if (keep) {
+        large = (float) Py_MAX(width, height);
+        fw = (float) width / large;
+        fh = (float) height / large;
+    } else {
+        fw = fh = 1;
+    }
+    small = Py_MIN(fw, fh);
+    if (rotate != 0 && rotate != 180) {
+        f = fw;
+        fw = fh;
+        fh = f;
+    }
+    if (fw < 1) {
+        if ((trw / fw) > (trh / fh)) {
+            w = trh * small;
+            h = trh;
+        } else {
+            w = trw;
+            h = trw / small;
+        }
+    } else if (fw != fh) {
+        if ((trw / fw) > (trh / fh)) {
+            w = trh / small;
+            h = trh;
+        } else {
+            w = trw;
+            h = trw * small;
+        }
+    } else {
+        w = trw;
+        h = trh;
+    }
+    fz_point tmp = fz_make_point((trect.x0 + trect.x1) / 2,
+                                 (trect.y0 + trect.y1) / 2);
+    fz_matrix mat = fz_make_matrix(1, 0, 0, 1, -0.5, -0.5);
+    mat = fz_concat(mat, rot);
+    mat = fz_concat(mat, fz_scale(w, h));
+    mat = fz_concat(mat, fz_translate(tmp.x, tmp.y));
+    return mat;
+}
+
+// --------------------------------------------------------
+// Callback function for the Story class
+// --------------------------------------------------------
+static PyObject *make_story_elpos = NULL; // Py function returning object
+void Story_Callback(fz_context *ctx, void *opaque, fz_story_element_position *pos)
+{
+#define SETATTR(a, v) PyObject_SetAttrString(arg, a, v);Py_DECREF(v)
+    // ------------------------------------------------------------------------
+    // 'opaque' is a tuple (userfunc, userdict), where 'userfunc' is a function
+    // in the user's script and 'userdict' is a dictionary containing any
+    // additional parameters of the user
+    // userfunc will be called with the joined info of userdict and pos.
+    // ------------------------------------------------------------------------
+    PyObject *callarg = (PyObject *) opaque;
+    PyObject *userfunc = PyTuple_GET_ITEM(callarg, 0);
+    PyObject *userdict = PyTuple_GET_ITEM(callarg, 1);
+
+    PyObject *this_module = PyImport_AddModule("fitz");  // get our module
+    if (!make_story_elpos) {  // locate ElementPosition maker once
+        make_story_elpos = Py_BuildValue("s", "make_story_elpos");
+    }
+    // get access to ElementPosition() object
+    PyObject *arg = PyObject_CallMethodObjArgs(this_module, make_story_elpos, NULL);
+    Py_INCREF(arg);
+    SETATTR("depth", Py_BuildValue("i", pos->depth));
+    SETATTR("heading", Py_BuildValue("i", pos->heading));
+    SETATTR("id", Py_BuildValue("s", pos->id));
+    SETATTR("rect", JM_py_from_rect(pos->rect));
+    SETATTR("text", Py_BuildValue("s", pos->text));
+    SETATTR("open_close", Py_BuildValue("i", pos->open_close));
+    SETATTR("rect_num", Py_BuildValue("i", pos->rectangle_num));
+    SETATTR("href", Py_BuildValue("s", pos->href));
+
+    // iterate over userdict items and set their attributes
+    PyObject *pkey = NULL;
+    PyObject *pval = NULL;
+    Py_ssize_t ppos = 0;
+    while (PyDict_Next(userdict, &ppos, &pkey, &pval)) {
+            PyObject_SetAttr(arg, pkey, pval);
+    }
+    PyObject_CallFunctionObjArgs(userfunc, arg, NULL);
+#undef SETATTR
+}
+
+// -----------------------------------------------------------
+// Return last archive if it is a tree and mount points match
+// -----------------------------------------------------------
+fz_archive *JM_last_tree(fz_context *ctx, fz_archive *arch, const char *mount)
+{
+    typedef struct
+    {
+        fz_archive *arch;
+        char *dir;
+    } multi_archive_entry;
+
+    typedef struct
+    {
+        fz_archive super;
+        int len;
+        int max;
+        multi_archive_entry *sub;
+    } fz_multi_archive;
+
+    if (!arch) {
+        return NULL;
+    }
+
+    fz_multi_archive *multi = (fz_multi_archive *) arch;
+    if (multi->len == 0) {  // archive is empty
+        return NULL;
+    }
+    int i = multi->len - 1;  // read last sub archive
+    multi_archive_entry *e = &multi->sub[i];
+    fz_archive *arch_ = e->arch;
+    const char *mount_ = e->dir;
+    const char *fmt = fz_archive_format(ctx, arch_);
+    if (strcmp(fmt, "tree") != 0) {  // not a tree archive
+        return NULL;
+    }
+    if ((mount_ && mount && strcmp(mount, mount_) == 0) || (!mount && !mount_)) {  // last sub archive is eligible!
+        return arch_;
+    }
+    return NULL;
+}
+
+fz_archive *JM_archive_from_py(fz_context *ctx, fz_archive *arch, PyObject *path, const char *mount, int *drop_sub)
+{
+    fz_stream *stream = NULL;
+    fz_buffer *buff = NULL;
+    *drop_sub = 1;
+    fz_archive *sub = NULL;
+    const char *my_mount = mount;
+    fz_try(ctx) {
+        // tree archive: tuple of memory items
+        // check if we can add to last sub-archive
+        sub = JM_last_tree(ctx, arch, my_mount);
+        if (!sub) {
+            sub = fz_new_tree_archive(ctx, NULL);
+        } else {
+            *drop_sub = 0;  // never drop last sub-archive
+        }
+
+        // a single tree item
+        if (PyBytes_Check(path) || PyByteArray_Check(path) || PyObject_HasAttrString(path, "getvalue")) {
+            buff = JM_BufferFromBytes(ctx, path);
+            fz_tree_archive_add_buffer(ctx, sub, mount, buff);
+            goto finished;
+        }
+
+        // a tuple of tree items
+        Py_ssize_t i, n = PyTuple_Size(path);
+        for (i = 0; i < n; i++) {
+            PyObject *item = PyTuple_GET_ITEM(path, i);
+            PyObject *i0 = PySequence_GetItem(item, 0);  // data
+            PyObject *i1 = PySequence_GetItem(item, 1);  // name
+            buff = JM_BufferFromBytes(ctx, i0);
+            fz_tree_archive_add_buffer(ctx, sub, PyUnicode_AsUTF8(i1), buff);
+            fz_drop_buffer(ctx, buff);
+            Py_DECREF(i0);
+            Py_DECREF(i1);
+        }
+        buff = NULL;
+        goto finished;
+
+        finished:;
+    }
+
+    fz_always(ctx) {
+        fz_drop_buffer(ctx, buff);
+        fz_drop_stream(ctx, stream);
+    }
+
+    fz_catch(ctx) {
+        fz_rethrow(ctx);
+    }
+
+    return sub;
+}
+
+
+int JM_rects_overlap(const fz_rect a, const fz_rect b)
+{
+    if (0
+            || a.x0 >= b.x1
+            || a.y0 >= b.y1
+            || a.x1 <= b.x0
+            || a.y1 <= b.y0
+            )
+        return 0;
+    return 1;
+}
+
+//-----------------------------------------------------------------------------
+// dummy structure for various tools and utilities
+//-----------------------------------------------------------------------------
+struct Tools {int index;};
+
+typedef struct fz_item fz_item;
+
+struct fz_item
+{
+	void *key;
+	fz_storable *val;
+	size_t size;
+	fz_item *next;
+	fz_item *prev;
+	fz_store *store;
+	const fz_store_type *type;
+};
+
+struct fz_store
+{
+	int refs;
+
+	/* Every item in the store is kept in a doubly linked list, ordered
+	 * by usage (so LRU entries are at the end). */
+	fz_item *head;
+	fz_item *tail;
+
+	/* We have a hash table that allows to quickly find a subset of the
+	 * entries (those whose keys are indirect objects). */
+	fz_hash_table *hash;
+
+	/* We keep track of the size of the store, and keep it below max. */
+	size_t max;
+	size_t size;
+
+	int defer_reap_count;
+	int needs_reaping;
+};
+
+%}