view src_classic/fitz_old.i @ 40:aa33339d6b8a upstream

ADD: MuPDF v1.26.10: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.5.
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 11 Oct 2025 11:31:38 +0200
parents 1d09e1dec1d9
children
line wrap: on
line source

%module fitz
%pythonbegin %{
%}
//------------------------------------------------------------------------
// SWIG macros: handle fitz exceptions
//------------------------------------------------------------------------
%define FITZEXCEPTION(meth, cond)
%exception meth
{
    $action
    if (cond) {
        return JM_ReturnException(gctx);
    }
}
%enddef


%define FITZEXCEPTION2(meth, cond)
%exception meth
{
    $action
    if (cond) {
        const char *msg = fz_caught_message(gctx);
        if (strcmp(msg, MSG_BAD_FILETYPE) == 0) {
            PyErr_SetString(PyExc_ValueError, msg);
        } else {
            PyErr_SetString(JM_Exc_FileDataError, MSG_BAD_DOCUMENT);
        }
        return NULL;
    }
}
%enddef

//------------------------------------------------------------------------
// SWIG macro: check that a document is not closed / encrypted
//------------------------------------------------------------------------
%define CLOSECHECK(meth, doc)
%pythonprepend meth %{doc
if self.is_closed or self.is_encrypted:
    raise ValueError("document closed or encrypted")%}
%enddef

%define CLOSECHECK0(meth, doc)
%pythonprepend meth%{doc
if self.is_closed:
    raise ValueError("document closed")%}
%enddef

//------------------------------------------------------------------------
// SWIG macro: check if object has a valid parent
//------------------------------------------------------------------------
%define PARENTCHECK(meth, doc)
%pythonprepend meth %{doc
CheckParent(self)%}
%enddef


//------------------------------------------------------------------------
// SWIG macro: ensure object still exists
//------------------------------------------------------------------------
%define ENSURE_OWNERSHIP(meth, doc)
%pythonprepend meth %{doc
EnsureOwnership(self)%}
%enddef

%include "mupdf/fitz/version.h"

%{
#define MEMDEBUG 0
#if MEMDEBUG == 1
    #define DEBUGMSG1(x) PySys_WriteStderr("[DEBUG] free %s ", x)
    #define DEBUGMSG2 PySys_WriteStderr("... done!\n")
#else
    #define DEBUGMSG1(x)
    #define DEBUGMSG2
#endif

#ifndef FLT_EPSILON
  #define FLT_EPSILON 1e-5
#endif

#define SWIG_FILE_WITH_INIT

// JM_MEMORY controls what allocators we tell MuPDF to use when we call
// fz_new_context():
//
//  JM_MEMORY=0: MuPDF uses malloc()/free().
//  JM_MEMORY=1: MuPDF uses PyMem_Malloc()/PyMem_Free().
//
// There are also a small number of places where we call malloc() or
// PyMem_Malloc() ourselves, depending on JM_MEMORY.
//
#define JM_MEMORY 0

#if JM_MEMORY == 1
    #define JM_Alloc(type, len) PyMem_New(type, len)
    #define JM_Free(x) PyMem_Del(x)
#else
    #define JM_Alloc(type, len) (type *) malloc(sizeof(type)*len)
    #define JM_Free(x) free(x)
#endif

#define EMPTY_STRING PyUnicode_FromString("")
#define EXISTS(x) (x != NULL && PyObject_IsTrue(x)==1)
#define RAISEPY(context, msg, exc) {JM_Exc_CurrentException=exc; fz_throw(context, FZ_ERROR_GENERIC, msg);}
#define ASSERT_PDF(cond) if (cond == NULL) RAISEPY(gctx, MSG_IS_NO_PDF, PyExc_RuntimeError)
#define ENSURE_OPERATION(ctx, pdf) if (!JM_have_operation(ctx, pdf)) RAISEPY(ctx, "No journalling operation started", PyExc_RuntimeError)
#define INRANGE(v, low, high) ((low) <= v && v <= (high))
#define JM_BOOL(x) PyBool_FromLong((long) (x))
#define JM_PyErr_Clear if (PyErr_Occurred()) PyErr_Clear()

#define JM_StrAsChar(x) (char *)PyUnicode_AsUTF8(x)
#define JM_BinFromChar(x) PyBytes_FromString(x)
#define JM_BinFromCharSize(x, y) PyBytes_FromStringAndSize(x, (Py_ssize_t) y)

#include <mupdf/fitz.h>
#include <mupdf/pdf.h>
#include <time.h>
// freetype includes >> --------------------------------------------------
#include <ft2build.h>
#include FT_FREETYPE_H
#ifdef FT_FONT_FORMATS_H
#include FT_FONT_FORMATS_H
#else
#include FT_XFREE86_H
#endif
#include FT_TRUETYPE_TABLES_H

#ifndef FT_SFNT_HEAD
#define FT_SFNT_HEAD ft_sfnt_head
#endif
// << freetype includes --------------------------------------------------

void JM_delete_widget(fz_context *ctx, pdf_page *page, pdf_annot *annot);
static void JM_get_page_labels(fz_context *ctx, PyObject *liste, pdf_obj *nums);
static int DICT_SETITEMSTR_DROP(PyObject *dict, const char *key, PyObject *value);
static int LIST_APPEND_DROP(PyObject *list, PyObject *item);
static int LIST_APPEND_DROP(PyObject *list, PyObject *item);
static fz_irect JM_irect_from_py(PyObject *r);
static fz_matrix JM_matrix_from_py(PyObject *m);
static fz_point JM_normalize_vector(float x, float y);
static fz_point JM_point_from_py(PyObject *p);
static fz_quad JM_quad_from_py(PyObject *r);
static fz_rect JM_rect_from_py(PyObject *r);
static int JM_FLOAT_ITEM(PyObject *obj, Py_ssize_t idx, double *result);
static int JM_INT_ITEM(PyObject *obj, Py_ssize_t idx, int *result);
static PyObject *JM_py_from_irect(fz_irect r);
static PyObject *JM_py_from_matrix(fz_matrix m);
static PyObject *JM_py_from_point(fz_point p);
static PyObject *JM_py_from_quad(fz_quad q);
static PyObject *JM_py_from_rect(fz_rect r);
static void show(const char* prefix, PyObject* obj);


// additional headers ----------------------------------------------
#if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR == 23 && FZ_VERSION_PATCH < 8
pdf_obj *pdf_lookup_page_loc(fz_context *ctx, pdf_document *doc, int needle, pdf_obj **parentp, int *indexp);
fz_pixmap *fz_scale_pixmap(fz_context *ctx, fz_pixmap *src, float x, float y, float w, float h, const fz_irect *clip);
int fz_pixmap_size(fz_context *ctx, fz_pixmap *src);
void fz_subsample_pixmap(fz_context *ctx, fz_pixmap *tile, int factor);
void fz_copy_pixmap_rect(fz_context *ctx, fz_pixmap *dest, fz_pixmap *src, fz_irect b, const fz_default_colorspaces *default_cs);
void fz_write_pixmap_as_jpeg(fz_context *ctx, fz_output *out, fz_pixmap *pix, int jpg_quality);
#endif
static const float JM_font_ascender(fz_context *ctx, fz_font *font);
static const float JM_font_descender(fz_context *ctx, fz_font *font);
// end of additional headers --------------------------------------------

static PyObject *JM_mupdf_warnings_store;
static int JM_mupdf_show_errors;
static int JM_mupdf_show_warnings;
static PyObject *JM_Exc_FileDataError;
static PyObject *JM_Exc_CurrentException;
%}

//------------------------------------------------------------------------
// global context
//------------------------------------------------------------------------
%init %{
    #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22
    /* Stop Memento backtraces if we reach the Python interpreter.
    `cfunction_call()` isn't the only way that Python calls C though, so we
    might need extra calls to Memento_addBacktraceLimitFnname().
    
    We put this inside `#ifdef MEMENTO` because memento.h's disabling macro
    causes "warning: statement with no effect" from cc. */
    #ifdef MEMENTO
        Memento_addBacktraceLimitFnname("cfunction_call");
    #endif
    #endif

    /*
    We end up with Memento leaks from fz_new_context()'s allocs even when our
    atexit handler calls fz_drop_context(), so remove these from Memento's
    accounting.
    */
    Memento_startLeaking();
#if JM_MEMORY == 1
    gctx = fz_new_context(&JM_Alloc_Context, NULL, FZ_STORE_DEFAULT);
#else
    gctx = fz_new_context(NULL, NULL, FZ_STORE_DEFAULT);
#endif
    Memento_stopLeaking();
    if(!gctx)
    {
        PyErr_SetString(PyExc_RuntimeError, "Fatal error: cannot create global context.");
        return NULL;
    }
    fz_register_document_handlers(gctx);

//------------------------------------------------------------------------
// START redirect stdout/stderr
//------------------------------------------------------------------------
JM_mupdf_warnings_store = PyList_New(0);
JM_mupdf_show_errors = 1;
JM_mupdf_show_warnings = 0;
char user[] = "PyMuPDF";
fz_set_warning_callback(gctx, JM_mupdf_warning, &user);
fz_set_error_callback(gctx, JM_mupdf_error, &user);
JM_Exc_FileDataError = NULL;
JM_Exc_CurrentException = PyExc_RuntimeError;
//------------------------------------------------------------------------
// STOP redirect stdout/stderr
//------------------------------------------------------------------------
// init global constants
//------------------------------------------------------------------------
dictkey_align = PyUnicode_InternFromString("align");
dictkey_ascender = PyUnicode_InternFromString("ascender");
dictkey_bbox = PyUnicode_InternFromString("bbox");
dictkey_blocks = PyUnicode_InternFromString("blocks");
dictkey_bpc = PyUnicode_InternFromString("bpc");
dictkey_c = PyUnicode_InternFromString("c");
dictkey_chars = PyUnicode_InternFromString("chars");
dictkey_color = PyUnicode_InternFromString("color");
dictkey_colorspace = PyUnicode_InternFromString("colorspace");
dictkey_content = PyUnicode_InternFromString("content");
dictkey_creationDate = PyUnicode_InternFromString("creationDate");
dictkey_cs_name = PyUnicode_InternFromString("cs-name");
dictkey_da = PyUnicode_InternFromString("da");
dictkey_dashes = PyUnicode_InternFromString("dashes");
dictkey_desc = PyUnicode_InternFromString("desc");
dictkey_desc = PyUnicode_InternFromString("descender");
dictkey_descender = PyUnicode_InternFromString("descender");
dictkey_dir = PyUnicode_InternFromString("dir");
dictkey_effect = PyUnicode_InternFromString("effect");
dictkey_ext = PyUnicode_InternFromString("ext");
dictkey_filename = PyUnicode_InternFromString("filename");
dictkey_fill = PyUnicode_InternFromString("fill");
dictkey_flags = PyUnicode_InternFromString("flags");
dictkey_font = PyUnicode_InternFromString("font");
dictkey_glyph = PyUnicode_InternFromString("glyph");
dictkey_height = PyUnicode_InternFromString("height");
dictkey_id = PyUnicode_InternFromString("id");
dictkey_image = PyUnicode_InternFromString("image");
dictkey_items = PyUnicode_InternFromString("items");
dictkey_length = PyUnicode_InternFromString("length");
dictkey_lines = PyUnicode_InternFromString("lines");
dictkey_matrix = PyUnicode_InternFromString("transform");
dictkey_modDate = PyUnicode_InternFromString("modDate");
dictkey_name = PyUnicode_InternFromString("name");
dictkey_number = PyUnicode_InternFromString("number");
dictkey_origin = PyUnicode_InternFromString("origin");
dictkey_rect = PyUnicode_InternFromString("rect");
dictkey_size = PyUnicode_InternFromString("size");
dictkey_smask = PyUnicode_InternFromString("smask");
dictkey_spans = PyUnicode_InternFromString("spans");
dictkey_stroke = PyUnicode_InternFromString("stroke");
dictkey_style = PyUnicode_InternFromString("style");
dictkey_subject = PyUnicode_InternFromString("subject");
dictkey_text = PyUnicode_InternFromString("text");
dictkey_title = PyUnicode_InternFromString("title");
dictkey_type = PyUnicode_InternFromString("type");
dictkey_ufilename = PyUnicode_InternFromString("ufilename");
dictkey_width = PyUnicode_InternFromString("width");
dictkey_wmode = PyUnicode_InternFromString("wmode");
dictkey_xref = PyUnicode_InternFromString("xref");
dictkey_xres = PyUnicode_InternFromString("xres");
dictkey_yres = PyUnicode_InternFromString("yres");

atexit( cleanup);
%}

%header %{
fz_context *gctx;

static void cleanup()
{
    fz_drop_context( gctx);
}

static int JM_UNIQUE_ID = 0;

struct DeviceWrapper {
    fz_device *device;
    fz_display_list *list;
};
%}

//------------------------------------------------------------------------
// include version information and several other helpers
//------------------------------------------------------------------------
%pythoncode %{
import sys
import io
import math
import os
import weakref
import hashlib
import typing
import binascii
import re
import tarfile
import zipfile
import pathlib
import string

# PDF names must not contain these characters:
INVALID_NAME_CHARS = set(string.whitespace + "()<>[]{}/%" + chr(0))

TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX")
point_like = "point_like"
rect_like = "rect_like"
matrix_like = "matrix_like"
quad_like = "quad_like"

# ByteString is gone from typing in 3.14.
# collections.abc.Buffer available from 3.12 only
try:
    ByteString = typing.ByteString
except AttributeError:
    ByteString = bytes | bytearray | memoryview

AnyType = typing.Any
OptInt = typing.Union[int, None]
OptFloat = typing.Optional[float]
OptStr = typing.Optional[str]
OptDict = typing.Optional[dict]
OptBytes = typing.Optional[ByteString]
OptSeq = typing.Optional[typing.Sequence]

try:
    from pymupdf_fonts import fontdescriptors, fontbuffers

    fitz_fontdescriptors = fontdescriptors.copy()
    for k in fitz_fontdescriptors.keys():
        fitz_fontdescriptors[k]["loader"] = fontbuffers[k]
    del fontdescriptors, fontbuffers
except ImportError:
    fitz_fontdescriptors = {}
%}
%include version.i
%include helper-git-versions.i
%include helper-defines.i
%include helper-globals.i
%include helper-geo-c.i
%include helper-other.i
%include helper-pixmap.i
%include helper-geo-py.i
%include helper-annot.i
%include helper-fields.i
%include helper-python.i
%include helper-portfolio.i
%include helper-select.i
%include helper-stext.i
%include helper-xobject.i
%include helper-pdfinfo.i
%include helper-convert.i
%include helper-fileobj.i
%include helper-devices.i

%{
// Declaring these structs here prevents gcc from generating warnings like:
//
//      warning: 'struct Document' declared inside parameter list will not be visible outside of this definition or declaration
//
struct Colorspace;
struct Document;
struct Font;
struct Graftmap;
struct TextPage;
struct TextWriter;
struct DocumentWriter;
struct Xml;
struct Archive;
struct Story;
%}

//------------------------------------------------------------------------
// fz_document
//------------------------------------------------------------------------
struct Document
{
    %extend
    {
        ~Document()
        {
            DEBUGMSG1("Document");
            fz_document *this_doc = (fz_document *) $self;
            fz_drop_document(gctx, this_doc);
            DEBUGMSG2;
        }
        FITZEXCEPTION2(Document, !result)

        %pythonprepend Document %{
        """Creates a document. Use 'open' as a synonym.

        Notes:
            Basic usages:
            open() - new PDF document
            open(filename) - string, pathlib.Path, or file object.
            open(filename, fileype=type) - overwrite filename extension.
            open(type, buffer) - type: extension, buffer: bytes object.
            open(stream=buffer, filetype=type) - keyword version of previous.
            Parameters rect, width, height, fontsize: layout reflowable
                 document on open (e.g. EPUB). Ignored if n/a.
        """
        self.is_closed = False
        self.is_encrypted = False
        self.isEncrypted = False
        self.metadata    = None
        self.FontInfos   = []
        self.Graftmaps   = {}
        self.ShownPages  = {}
        self.InsertedImages  = {}
        self._page_refs  = weakref.WeakValueDictionary()

        if not filename or type(filename) is str:
            pass
        elif hasattr(filename, "absolute"):
            filename = str(filename)
        elif hasattr(filename, "name"):
            filename = filename.name
        else:
            msg = "bad filename"
            raise TypeError(msg)

        if stream != None:
            if type(stream) is bytes:
                self.stream = stream
            elif type(stream) is bytearray:
                self.stream = bytes(stream)
            elif type(stream) is io.BytesIO:
                self.stream = stream.getvalue()
            else:
                msg = "bad type: 'stream'"
                raise TypeError(msg)
            stream = self.stream
            if not (filename or filetype):
                filename = "pdf"
        else:
            self.stream = None

        if filename and self.stream == None:
            self.name = filename
            from_file = True
        else:
            from_file = False
            self.name = ""

        if from_file:
            if not os.path.exists(filename):
                msg = f"no such file: '{filename}'"
                raise FileNotFoundError(msg)
            elif not os.path.isfile(filename):
                msg = f"'{filename}' is no file"
                raise FileDataError(msg)
        if from_file and os.path.getsize(filename) == 0 or type(self.stream) is bytes and len(self.stream) == 0:
            msg = "cannot open empty document"
            raise EmptyFileError(msg)
        %}
        %pythonappend Document %{
            if self.thisown:
                self._graft_id = TOOLS.gen_id()
                if self.needs_pass is True:
                    self.is_encrypted = True
                    self.isEncrypted = True
                else: # we won't init until doc is decrypted
                    self.init_doc()
                # the following hack detects invalid/empty SVG files, which else may lead
                # to interpreter crashes
                if filename and filename.lower().endswith("svg") or filetype and "svg" in filetype.lower():
                    try:
                        _ = self.convert_to_pdf()  # this seems to always work
                    except:
                        raise FileDataError("cannot open broken document") from None
        %}

        Document(const char *filename=NULL, PyObject *stream=NULL,
                      const char *filetype=NULL, PyObject *rect=NULL,
                      float width=0, float height=0,
                      float fontsize=11)
        {
            int old_msg_option = JM_mupdf_show_errors;
            JM_mupdf_show_errors = 0;
            fz_document *doc = NULL;
            const fz_document_handler *handler;
            char *c = NULL;
            char *magic = NULL;
            size_t len = 0;
            fz_stream *data = NULL;
            float w = width, h = height;
            fz_rect r = JM_rect_from_py(rect);
            if (!fz_is_infinite_rect(r)) {
                w = r.x1 - r.x0;
                h = r.y1 - r.y0;
            }

            fz_try(gctx) {
                if (stream != Py_None) { // stream given, **MUST** be bytes!
                    c = PyBytes_AS_STRING(stream); // just a pointer, no new obj
                    len = (size_t) PyBytes_Size(stream);
                    data = fz_open_memory(gctx, (const unsigned char *) c, len);
                    magic = (char *)filename;
                    if (!magic) magic = (char *)filetype;
                    handler = fz_recognize_document(gctx, magic);
                    if (!handler) {
                        RAISEPY(gctx, MSG_BAD_FILETYPE, PyExc_ValueError);
                    }
                    doc = fz_open_document_with_stream(gctx, magic, data);
                } else {
                    if (filename && strlen(filename)) {
                        if (!filetype || strlen(filetype) == 0) {
                            doc = fz_open_document(gctx, filename);
                        } else {
                            handler = fz_recognize_document(gctx, filetype);
                            if (!handler) {
                                RAISEPY(gctx, MSG_BAD_FILETYPE, PyExc_ValueError);
                            }
                            #if FZ_VERSION_MINOR >= 24
                            if (handler->open)
                            {
                                fz_stream* filename_stream = fz_open_file(gctx, filename);
                                fz_try(gctx)
                                {
                                    doc = handler->open(gctx, filename_stream, NULL, NULL);
                                }
                                fz_always(gctx)
                                {
                                    fz_drop_stream(gctx, filename_stream);
                                }
                                fz_catch(gctx)
                                {
                                    fz_rethrow(gctx);
                                }
                            }
                            #else
                            if (handler->open) {
                                doc = handler->open(gctx, filename);
                            } else if (handler->open_with_stream) {
                                data = fz_open_file(gctx, filename);
                                doc = handler->open_with_stream(gctx, data);
                            }
                            #endif
                        }
                    } else {
                        pdf_document *pdf = pdf_create_document(gctx);
                        doc = (fz_document *) pdf;
                    }
                }
            }
            fz_always(gctx) {
                fz_drop_stream(gctx, data);
            }
            fz_catch(gctx) {
                JM_mupdf_show_errors = old_msg_option;
                return NULL;
            }
            if (w > 0 && h > 0) {
                fz_layout_document(gctx, doc, w, h, fontsize);
            } else if (fz_is_document_reflowable(gctx, doc)) {
                fz_layout_document(gctx, doc, 400, 600, 11);
            }
            return (struct Document *) doc;
        }


        FITZEXCEPTION(load_page, !result)
        %pythonprepend load_page %{
        """Load a page.

        'page_id' is either a 0-based page number or a tuple (chapter, pno),
        with chapter number and page number within that chapter.
        """

        if self.is_closed or self.is_encrypted:
            raise ValueError("document closed or encrypted")
        if page_id is None:
            page_id = 0
        if page_id not in self:
            raise ValueError("page not in document")
        if type(page_id) is int and page_id < 0:
            np = self.page_count
            while page_id < 0:
                page_id += np
        %}
        %pythonappend load_page %{
        val.thisown = True
        val.parent = weakref.proxy(self)
        self._page_refs[id(val)] = val
        val._annot_refs = weakref.WeakValueDictionary()
        val.number = page_id
        %}
        struct Page *
        load_page(PyObject *page_id)
        {
            fz_page *page = NULL;
            fz_document *doc = (fz_document *) $self;
            int pno = 0, chapter = 0;
            fz_try(gctx) {
                if (PySequence_Check(page_id)) {
                    if (JM_INT_ITEM(page_id, 0, &chapter) == 1) {
                        RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                    }
                    if (JM_INT_ITEM(page_id, 1, &pno) == 1) {
                        RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                    }
                    page = fz_load_chapter_page(gctx, doc, chapter, pno);
                } else {
                    pno = (int) PyLong_AsLong(page_id);
                    if (PyErr_Occurred()) {
                        RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                    }
                    page = fz_load_page(gctx, doc, pno);
                }
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            PyErr_Clear();
            return (struct Page *) page;
        }


        FITZEXCEPTION(_remove_links_to, !result)
        PyObject *_remove_links_to(PyObject *numbers)
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                remove_dest_range(gctx, pdf, numbers);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        CLOSECHECK0(_loadOutline, """Load first outline.""")
        struct Outline *_loadOutline()
        {
            fz_outline *ol = NULL;
            fz_document *doc = (fz_document *) $self;
            fz_try(gctx) {
                ol = fz_load_outline(gctx, doc);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Outline *) ol;
        }

        void _dropOutline(struct Outline *ol) {
            DEBUGMSG1("Outline");
            fz_outline *this_ol = (fz_outline *) ol;
            fz_drop_outline(gctx, this_ol);
            DEBUGMSG2;
        }

        FITZEXCEPTION(_insert_font, !result)
        CLOSECHECK0(_insert_font, """Utility: insert font from file or binary.""")
        PyObject *
        _insert_font(char *fontfile=NULL, PyObject *fontbuffer=NULL)
        {
            PyObject *value=NULL;
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self);

            fz_try(gctx) {
                ASSERT_PDF(pdf);
                if (!fontfile && !EXISTS(fontbuffer)) {
                    RAISEPY(gctx, MSG_FILE_OR_BUFFER, PyExc_ValueError);
                }
                value = JM_insert_font(gctx, pdf, NULL, fontfile, fontbuffer,
                            0, 0, 0, 0, 0, -1);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return value;
        }


        FITZEXCEPTION(get_outline_xrefs, !result)
        CLOSECHECK0(get_outline_xrefs, """Get list of outline xref numbers.""")
        PyObject *
        get_outline_xrefs()
        {
            PyObject *xrefs = PyList_New(0);
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self);
            if (!pdf) {
                return xrefs;
            }
            fz_try(gctx) {
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
                if (!root) goto finished;
                pdf_obj *olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines));
                if (!olroot) goto finished;
                pdf_obj *first = pdf_dict_get(gctx, olroot, PDF_NAME(First));
                if (!first) goto finished;
                xrefs = JM_outline_xrefs(gctx, first, xrefs);
                finished:;
            }
            fz_catch(gctx) {
                Py_DECREF(xrefs);
                return NULL;
            }
            return xrefs;
        }


        FITZEXCEPTION(xref_get_keys, !result)
        CLOSECHECK0(xref_get_keys, """Get the keys of PDF dict object at 'xref'. Use -1 for the PDF trailer.""")
        PyObject *
        xref_get_keys(int xref)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self);
            pdf_obj *obj=NULL;
            PyObject *rc = NULL;
            int i, n;
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1) && xref != -1) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                if (xref > 0) {
                    obj = pdf_load_object(gctx, pdf, xref);
                } else {
                    obj = pdf_trailer(gctx, pdf);
                }
                n = pdf_dict_len(gctx, obj);
                rc = PyTuple_New(n);
                if (!n) goto finished;
                for (i = 0; i < n; i++) {
                    const char *key = pdf_to_name(gctx, pdf_dict_get_key(gctx, obj, i));
                    PyTuple_SET_ITEM(rc, i, Py_BuildValue("s", key));
                }
                finished:;
            }
            fz_always(gctx) {
                if (xref > 0) {
                    pdf_drop_obj(gctx, obj);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(xref_get_key, !result)
        CLOSECHECK0(xref_get_key, """Get PDF dict key value of object at 'xref'.""")
        PyObject *
        xref_get_key(int xref, const char *key)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self);
            pdf_obj *obj=NULL, *subobj=NULL;
            PyObject *rc = NULL;
            fz_buffer *res = NULL;
            PyObject *text = NULL;
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1) && xref != -1) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                if (xref > 0) {
                    obj = pdf_load_object(gctx, pdf, xref);
                } else {
                    obj = pdf_trailer(gctx, pdf);
                }
                if (!obj) {
                    goto not_found;
                }
                subobj = pdf_dict_getp(gctx, obj, key);
                if (!subobj) {
                    goto not_found;
                }
                char *type;
                if (pdf_is_indirect(gctx, subobj)) {
                    type = "xref";
                    text = PyUnicode_FromFormat("%i 0 R", pdf_to_num(gctx, subobj));
                } else if (pdf_is_array(gctx, subobj)) {
                    type = "array";
                } else if (pdf_is_dict(gctx, subobj)) {
                    type = "dict";
                } else if (pdf_is_int(gctx, subobj)) {
                    type = "int";
                    text = PyUnicode_FromFormat("%i", pdf_to_int(gctx, subobj));
                } else if (pdf_is_real(gctx, subobj)) {
                    type = "float";
                } else if (pdf_is_null(gctx, subobj)) {
                    type = "null";
                    text = PyUnicode_FromString("null");
                } else if (pdf_is_bool(gctx, subobj)) {
                    type = "bool";
                    if (pdf_to_bool(gctx, subobj)) {
                        text = PyUnicode_FromString("true");
                    } else {
                        text = PyUnicode_FromString("false");
                    }
                } else if (pdf_is_name(gctx, subobj)) {
                    type = "name";
                    text = PyUnicode_FromFormat("/%s", pdf_to_name(gctx, subobj));
                } else if (pdf_is_string(gctx, subobj)) {
                    type = "string";
                    text = JM_UnicodeFromStr(pdf_to_text_string(gctx, subobj));
                } else {
                    type = "unknown";
                }
                if (!text) {
                    res = JM_object_to_buffer(gctx, subobj, 1, 0);
                    text = JM_UnicodeFromBuffer(gctx, res);
                }
                rc = Py_BuildValue("sO", type, text);
                Py_DECREF(text);
                goto finished;

                not_found:;
                rc = Py_BuildValue("ss", "null", "null");
                finished:;
            }
            fz_always(gctx) {
                if (xref > 0) {
                    pdf_drop_obj(gctx, obj);
                }
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(xref_set_key, !result)
        %pythonprepend xref_set_key %{
        """Set the value of a PDF dictionary key."""
        if self.is_closed or self.is_encrypted:
            raise ValueError("document closed or encrypted")
        if not key or not isinstance(key, str) or INVALID_NAME_CHARS.intersection(key) not in (set(), {"/"}):
            raise ValueError("bad 'key'")
        if not isinstance(value, str) or not value or value[0] == "/" and INVALID_NAME_CHARS.intersection(value[1:]) != set():
            raise ValueError("bad 'value'")
        %}
        PyObject *
        xref_set_key(int xref, const char *key, char *value)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self);
            pdf_obj *obj = NULL, *new_obj = NULL;
            int i, n;
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                if (!key || strlen(key) == 0) {
                    RAISEPY(gctx, "bad 'key'", PyExc_ValueError);
                }
                if (!value || strlen(value) == 0) {
                    RAISEPY(gctx, "bad 'value'", PyExc_ValueError);
                }
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1) && xref != -1) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                if (xref != -1) {
                    obj = pdf_load_object(gctx, pdf, xref);
                } else {
                    obj = pdf_trailer(gctx, pdf);
                }
                // if val=="null" and no path hierarchy, delete "key" from object
                // chr(47) = "/"
                if (strcmp(value, "null") == 0 && strchr(key, 47) == NULL) {
                    pdf_dict_dels(gctx, obj, key);
                    goto finished;
                }
                new_obj = JM_set_object_value(gctx, obj, key, value);
                if (!new_obj) {
                    goto finished;  // did not work: skip update
                }
                if (xref != -1) {
                    pdf_drop_obj(gctx, obj);
                    obj = NULL;
                    pdf_update_object(gctx, pdf, xref, new_obj);
                } else {
                    n = pdf_dict_len(gctx, new_obj);
                    for (i = 0; i < n; i++) {
                        pdf_dict_put(gctx, obj, pdf_dict_get_key(gctx, new_obj, i), pdf_dict_get_val(gctx, new_obj, i));
                    }
                }
                finished:;
            }
            fz_always(gctx) {
                if (xref != -1) {
                    pdf_drop_obj(gctx, obj);
                }
                pdf_drop_obj(gctx, new_obj);
                PyErr_Clear();
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(_extend_toc_items, !result)
        CLOSECHECK0(_extend_toc_items, """Add color info to all items of an extended TOC list.""")
        PyObject *
        _extend_toc_items(PyObject *items)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self);
            pdf_obj *bm, *col, *obj;
            int count, flags;
            PyObject *item=NULL, *itemdict=NULL, *xrefs, *bold, *italic, *collapse, *zoom;
            zoom = PyUnicode_FromString("zoom");
            bold = PyUnicode_FromString("bold");
            italic = PyUnicode_FromString("italic");
            collapse = PyUnicode_FromString("collapse");
            fz_try(gctx) {
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
                if (!root) goto finished;
                pdf_obj *olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines));
                if (!olroot) goto finished;
                pdf_obj *first = pdf_dict_get(gctx, olroot, PDF_NAME(First));
                if (!first) goto finished;
                xrefs = PyList_New(0);  // pre-allocate an empty list
                xrefs = JM_outline_xrefs(gctx, first, xrefs);
                Py_ssize_t i, n = PySequence_Size(xrefs), m = PySequence_Size(items);
                if (!n) goto finished;
                if (n != m) {
                    RAISEPY(gctx, "internal error finding outline xrefs", PyExc_IndexError);
                }
                int xref;

                // update all TOC item dictionaries
                for (i = 0; i < n; i++) {
                    JM_INT_ITEM(xrefs, i, &xref);
                    item = PySequence_ITEM(items, i);
                    itemdict = PySequence_ITEM(item, 3);
                    if (!itemdict || !PyDict_Check(itemdict)) {
                        RAISEPY(gctx, "need non-simple TOC format", PyExc_ValueError);
                    }
                    PyDict_SetItem(itemdict, dictkey_xref, PySequence_ITEM(xrefs, i));
                    bm = pdf_load_object(gctx, pdf, xref);
                    flags = pdf_to_int(gctx, (pdf_dict_get(gctx, bm, PDF_NAME(F))));
                    if (flags == 1) {
                        PyDict_SetItem(itemdict, italic, Py_True);
                    } else if (flags == 2) {
                        PyDict_SetItem(itemdict, bold, Py_True);
                    } else if (flags == 3) {
                        PyDict_SetItem(itemdict, italic, Py_True);
                        PyDict_SetItem(itemdict, bold, Py_True);
                    }
                    count = pdf_to_int(gctx, (pdf_dict_get(gctx, bm, PDF_NAME(Count))));
                    if (count < 0) {
                        PyDict_SetItem(itemdict, collapse, Py_True);
                    } else if (count > 0) {
                        PyDict_SetItem(itemdict, collapse, Py_False);
                    }
                    col = pdf_dict_get(gctx, bm, PDF_NAME(C));
                    if (pdf_is_array(gctx, col) && pdf_array_len(gctx, col) == 3) {
                        PyObject *color = PyTuple_New(3);
                        PyTuple_SET_ITEM(color, 0, Py_BuildValue("f", pdf_to_real(gctx, pdf_array_get(gctx, col, 0))));
                        PyTuple_SET_ITEM(color, 1, Py_BuildValue("f", pdf_to_real(gctx, pdf_array_get(gctx, col, 1))));
                        PyTuple_SET_ITEM(color, 2, Py_BuildValue("f", pdf_to_real(gctx, pdf_array_get(gctx, col, 2))));
                        DICT_SETITEM_DROP(itemdict, dictkey_color, color);
                    }
                    float z=0;
                    obj = pdf_dict_get(gctx, bm, PDF_NAME(Dest));
                    if (!obj || !pdf_is_array(gctx, obj)) {
                        obj = pdf_dict_getl(gctx, bm, PDF_NAME(A), PDF_NAME(D), NULL);
                    }
                    if (pdf_is_array(gctx, obj) && pdf_array_len(gctx, obj) == 5) {
                        z = pdf_to_real(gctx, pdf_array_get(gctx, obj, 4));
                    }
                    DICT_SETITEM_DROP(itemdict, zoom, Py_BuildValue("f", z));
                    PyList_SetItem(item, 3, itemdict);
                    PyList_SetItem(items, i, item);
                    pdf_drop_obj(gctx, bm);
                    bm = NULL;
                }
                finished:;
            }
            fz_always(gctx) {
                Py_CLEAR(xrefs);
                Py_CLEAR(bold);
                Py_CLEAR(italic);
                Py_CLEAR(collapse);
                Py_CLEAR(zoom);
                pdf_drop_obj(gctx, bm);
                PyErr_Clear();
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // EmbeddedFiles utility functions
        //----------------------------------------------------------------
        FITZEXCEPTION(_embfile_names, !result)
        CLOSECHECK0(_embfile_names, """Get list of embedded file names.""")
        PyObject *_embfile_names(PyObject *namelist)
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_specifics(gctx, doc);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                PyObject *val;
                pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                      PDF_NAME(Root),
                                      PDF_NAME(Names),
                                      PDF_NAME(EmbeddedFiles),
                                      PDF_NAME(Names),
                                      NULL);
                if (pdf_is_array(gctx, names)) {
                    int i, n = pdf_array_len(gctx, names);
                    for (i=0; i < n; i+=2) {
                        val = JM_EscapeStrFromStr(pdf_to_text_string(gctx,
                                         pdf_array_get(gctx, names, i)));
                        LIST_APPEND_DROP(namelist, val);
                    }
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION(_embfile_del, !result)
        PyObject *_embfile_del(int idx)
        {
            fz_try(gctx) {
                fz_document *doc = (fz_document *) $self;
                pdf_document *pdf = pdf_document_from_fz_document(gctx, doc);
                pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                      PDF_NAME(Root),
                                      PDF_NAME(Names),
                                      PDF_NAME(EmbeddedFiles),
                                      PDF_NAME(Names),
                                      NULL);
                pdf_array_delete(gctx, names, idx + 1);
                pdf_array_delete(gctx, names, idx);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION(_embfile_info, !result)
        PyObject *_embfile_info(int idx, PyObject *infodict)
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_document_from_fz_document(gctx, doc);
            char *name;
            int xref = 0, ci_xref=0;
            fz_try(gctx) {
                pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                      PDF_NAME(Root),
                                      PDF_NAME(Names),
                                      PDF_NAME(EmbeddedFiles),
                                      PDF_NAME(Names),
                                      NULL);

                pdf_obj *o = pdf_array_get(gctx, names, 2*idx+1);
                pdf_obj *ci = pdf_dict_get(gctx, o, PDF_NAME(CI));
                if (ci) {
                    ci_xref = pdf_to_num(gctx, ci);
                }
                DICT_SETITEMSTR_DROP(infodict, "collection", Py_BuildValue("i", ci_xref));
                name = (char *) pdf_to_text_string(gctx,
                                          pdf_dict_get(gctx, o, PDF_NAME(F)));
                DICT_SETITEM_DROP(infodict, dictkey_filename, JM_EscapeStrFromStr(name));

                name = (char *) pdf_to_text_string(gctx,
                                    pdf_dict_get(gctx, o, PDF_NAME(UF)));
                DICT_SETITEM_DROP(infodict, dictkey_ufilename, JM_EscapeStrFromStr(name));

                name = (char *) pdf_to_text_string(gctx,
                                    pdf_dict_get(gctx, o, PDF_NAME(Desc)));
                DICT_SETITEM_DROP(infodict, dictkey_desc, JM_UnicodeFromStr(name));

                int len = -1, DL = -1;
                pdf_obj *fileentry = pdf_dict_getl(gctx, o, PDF_NAME(EF), PDF_NAME(F), NULL);
                xref = pdf_to_num(gctx, fileentry);
                o = pdf_dict_get(gctx, fileentry, PDF_NAME(Length));
                if (o) len = pdf_to_int(gctx, o);

                o = pdf_dict_get(gctx, fileentry, PDF_NAME(DL));
                if (o) {
                    DL = pdf_to_int(gctx, o);
                } else {
                    o = pdf_dict_getl(gctx, fileentry, PDF_NAME(Params),
                                   PDF_NAME(Size), NULL);
                    if (o) DL = pdf_to_int(gctx, o);
                }
                DICT_SETITEM_DROP(infodict, dictkey_size, Py_BuildValue("i", DL));
                DICT_SETITEM_DROP(infodict, dictkey_length, Py_BuildValue("i", len));
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xref);
        }

        FITZEXCEPTION(_embfile_upd, !result)
        PyObject *_embfile_upd(int idx, PyObject *buffer = NULL, char *filename = NULL, char *ufilename = NULL, char *desc = NULL)
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_document_from_fz_document(gctx, doc);
            fz_buffer *res = NULL;
            fz_var(res);
            int xref = 0;
            fz_try(gctx) {
                pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                      PDF_NAME(Root),
                                      PDF_NAME(Names),
                                      PDF_NAME(EmbeddedFiles),
                                      PDF_NAME(Names),
                                      NULL);

                pdf_obj *entry = pdf_array_get(gctx, names, 2*idx+1);

                pdf_obj *filespec = pdf_dict_getl(gctx, entry, PDF_NAME(EF),
                                                  PDF_NAME(F), NULL);
                if (!filespec) {
                    RAISEPY(gctx, "bad PDF: no /EF object", JM_Exc_FileDataError);
                }
                res = JM_BufferFromBytes(gctx, buffer);
                if (EXISTS(buffer) && !res) {
                    RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError);
                }
                if (res && buffer != Py_None)
                {
                    JM_update_stream(gctx, pdf, filespec, res, 1);
                    // adjust /DL and /Size parameters
                    int64_t len = (int64_t) fz_buffer_storage(gctx, res, NULL);
                    pdf_obj *l = pdf_new_int(gctx, len);
                    pdf_dict_put(gctx, filespec, PDF_NAME(DL), l);
                    pdf_dict_putl(gctx, filespec, l, PDF_NAME(Params), PDF_NAME(Size), NULL);
                }
                xref = pdf_to_num(gctx, filespec);
                if (filename)
                    pdf_dict_put_text_string(gctx, entry, PDF_NAME(F), filename);

                if (ufilename)
                    pdf_dict_put_text_string(gctx, entry, PDF_NAME(UF), ufilename);

                if (desc)
                    pdf_dict_put_text_string(gctx, entry, PDF_NAME(Desc), desc);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx)
                return NULL;
            
            return Py_BuildValue("i", xref);
        }

        FITZEXCEPTION(_embeddedFileGet, !result)
        PyObject *_embeddedFileGet(int idx)
        {
            fz_document *doc = (fz_document *) $self;
            PyObject *cont = NULL;
            pdf_document *pdf = pdf_document_from_fz_document(gctx, doc);
            fz_buffer *buf = NULL;
            fz_var(buf);
            fz_try(gctx) {
                pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                      PDF_NAME(Root),
                                      PDF_NAME(Names),
                                      PDF_NAME(EmbeddedFiles),
                                      PDF_NAME(Names),
                                      NULL);

                pdf_obj *entry = pdf_array_get(gctx, names, 2*idx+1);
                pdf_obj *filespec = pdf_dict_getl(gctx, entry, PDF_NAME(EF),
                                                  PDF_NAME(F), NULL);
                buf = pdf_load_stream(gctx, filespec);
                cont = JM_BinFromBuffer(gctx, buf);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return cont;
        }

        FITZEXCEPTION(_embfile_add, !result)
        PyObject *_embfile_add(const char *name, PyObject *buffer, char *filename=NULL, char *ufilename=NULL, char *desc=NULL)
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_document_from_fz_document(gctx, doc);
            fz_buffer *data = NULL;
            fz_var(data);
            pdf_obj *names = NULL;
            int xref = 0; // xref of file entry
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                data = JM_BufferFromBytes(gctx, buffer);
                if (!data) {
                    RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError);
                }

                names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                      PDF_NAME(Root),
                                      PDF_NAME(Names),
                                      PDF_NAME(EmbeddedFiles),
                                      PDF_NAME(Names),
                                      NULL);
                if (!pdf_is_array(gctx, names)) {
                    pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf),
                                                 PDF_NAME(Root));
                    names = pdf_new_array(gctx, pdf, 6);  // an even number!
                    pdf_dict_putl_drop(gctx, root, names,
                                      PDF_NAME(Names),
                                      PDF_NAME(EmbeddedFiles),
                                      PDF_NAME(Names),
                                      NULL);
                }

                pdf_obj *fileentry = JM_embed_file(gctx, pdf, data,
                                                   filename,
                                                   ufilename,
                                                   desc, 1);
                xref = pdf_to_num(gctx, pdf_dict_getl(gctx, fileentry,
                                    PDF_NAME(EF), PDF_NAME(F), NULL));
                pdf_array_push_drop(gctx, names, pdf_new_text_string(gctx, name));
                pdf_array_push_drop(gctx, names, fileentry);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, data);
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            return Py_BuildValue("i", xref);
        }


        %pythoncode %{
        def embfile_names(self) -> list:
            """Get list of names of EmbeddedFiles."""
            filenames = []
            self._embfile_names(filenames)
            return filenames

        def _embeddedFileIndex(self, item: typing.Union[int, str]) -> int:
            filenames = self.embfile_names()
            msg = "'%s' not in EmbeddedFiles array." % str(item)
            if item in filenames:
                idx = filenames.index(item)
            elif item in range(len(filenames)):
                idx = item
            else:
                raise ValueError(msg)
            return idx

        def embfile_count(self) -> int:
            """Get number of EmbeddedFiles."""
            return len(self.embfile_names())

        def embfile_del(self, item: typing.Union[int, str]):
            """Delete an entry from EmbeddedFiles.

            Notes:
                The argument must be name or index of an EmbeddedFiles item.
                Physical deletion of data will happen on save to a new
                file with appropriate garbage option.
            Args:
                item: name or number of item.
            Returns:
                None
            """
            idx = self._embeddedFileIndex(item)
            return self._embfile_del(idx)

        def embfile_info(self, item: typing.Union[int, str]) -> dict:
            """Get information of an item in the EmbeddedFiles array.

            Args:
                item: number or name of item.
            Returns:
                Information dictionary.
            """
            idx = self._embeddedFileIndex(item)
            infodict = {"name": self.embfile_names()[idx]}
            xref = self._embfile_info(idx, infodict)
            t, date = self.xref_get_key(xref, "Params/CreationDate")
            if t != "null":
                infodict["creationDate"] = date
            t, date = self.xref_get_key(xref, "Params/ModDate")
            if t != "null":
                infodict["modDate"] = date
            t, md5 = self.xref_get_key(xref, "Params/CheckSum")
            if t != "null":
                infodict["checksum"] = binascii.hexlify(md5.encode()).decode()
            return infodict

        def embfile_get(self, item: typing.Union[int, str]) -> bytes:
            """Get the content of an item in the EmbeddedFiles array.

            Args:
                item: number or name of item.
            Returns:
                (bytes) The file content.
            """
            idx = self._embeddedFileIndex(item)
            return self._embeddedFileGet(idx)

        def embfile_upd(self, item: typing.Union[int, str],
                                 buffer: OptBytes =None,
                                 filename: OptStr =None,
                                 ufilename: OptStr =None,
                                 desc: OptStr =None,) -> None:
            """Change an item of the EmbeddedFiles array.

            Notes:
                Only provided parameters are changed. If all are omitted,
                the method is a no-op.
            Args:
                item: number or name of item.
                buffer: (binary data) the new file content.
                filename: (str) the new file name.
                ufilename: (unicode) the new filen ame.
                desc: (str) the new description.
            """
            idx = self._embeddedFileIndex(item)
            xref = self._embfile_upd(idx, buffer=buffer,
                                         filename=filename,
                                         ufilename=ufilename,
                                         desc=desc)
            date = get_pdf_now()
            self.xref_set_key(xref, "Params/ModDate", get_pdf_str(date))
            return xref

        def embfile_add(self, name: str, buffer: ByteString,
                                  filename: OptStr =None,
                                  ufilename: OptStr =None,
                                  desc: OptStr =None,) -> None:
            """Add an item to the EmbeddedFiles array.

            Args:
                name: name of the new item, must not already exist.
                buffer: (binary data) the file content.
                filename: (str) the file name, default: the name
                ufilename: (unicode) the file name, default: filename
                desc: (str) the description.
            """
            filenames = self.embfile_names()
            msg = "Name '%s' already exists." % str(name)
            if name in filenames:
                raise ValueError(msg)

            if filename is None:
                filename = name
            if ufilename is None:
                ufilename = unicode(filename, "utf8") if str is bytes else filename
            if desc is None:
                desc = name
            xref = self._embfile_add(name, buffer=buffer,
                                         filename=filename,
                                         ufilename=ufilename,
                                         desc=desc)
            date = get_pdf_now()
            self.xref_set_key(xref, "Type", "/EmbeddedFile")
            self.xref_set_key(xref, "Params/CreationDate", get_pdf_str(date))
            self.xref_set_key(xref, "Params/ModDate", get_pdf_str(date))
            return xref
        %}

        FITZEXCEPTION(convert_to_pdf, !result)
        %pythonprepend convert_to_pdf %{
        """Convert document to a PDF, selecting page range and optional rotation. Output bytes object."""
        if self.is_closed or self.is_encrypted:
            raise ValueError("document closed or encrypted")
        %}
        PyObject *convert_to_pdf(int from_page=0, int to_page=-1, int rotate=0)
        {
            PyObject *doc = NULL;
            fz_document *fz_doc = (fz_document *) $self;
            fz_try(gctx) {
                int fp = from_page, tp = to_page, srcCount = fz_count_pages(gctx, fz_doc);
                if (fp < 0) fp = 0;
                if (fp > srcCount - 1) fp = srcCount - 1;
                if (tp < 0) tp = srcCount - 1;
                if (tp > srcCount - 1) tp = srcCount - 1;
                Py_ssize_t len0 = PyList_Size(JM_mupdf_warnings_store);
                doc = JM_convert_to_pdf(gctx, fz_doc, fp, tp, rotate);
                Py_ssize_t len1 = PyList_Size(JM_mupdf_warnings_store);
                Py_ssize_t i = len0;
                while (i < len1) {
                    PySys_WriteStderr("%s\n", JM_StrAsChar(PyList_GetItem(JM_mupdf_warnings_store, i)));
                    i++;
                } 
            }
            fz_catch(gctx) {
                return NULL;
            }
            if (doc) {
                return doc;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(page_count, !result)
        CLOSECHECK0(page_count, """Number of pages.""")
        %pythoncode%{@property%}
        PyObject *page_count()
        {
            PyObject *ret;
            fz_try(gctx) {
                ret = PyLong_FromLong((long) fz_count_pages(gctx, (fz_document *) $self));
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            return ret;
        }

        FITZEXCEPTION(chapter_count, !result)
        CLOSECHECK0(chapter_count, """Number of chapters.""")
        %pythoncode%{@property%}
        PyObject *chapter_count()
        {
            PyObject *ret;
            fz_try(gctx) {
                ret = PyLong_FromLong((long) fz_count_chapters(gctx, (fz_document *) $self));
            }
            fz_catch(gctx) {
                return NULL;
            }
            return ret;
        }

        FITZEXCEPTION(last_location, !result)
        CLOSECHECK0(last_location, """Id (chapter, page) of last page.""")
        %pythoncode%{@property%}
        PyObject *last_location()
        {
            fz_document *this_doc = (fz_document *) $self;
            fz_location last_loc;
            fz_try(gctx) {
                last_loc = fz_last_page(gctx, this_doc);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("ii", last_loc.chapter, last_loc.page);
        }


        FITZEXCEPTION(chapter_page_count, !result)
        CLOSECHECK0(chapter_page_count, """Page count of chapter.""")
        PyObject *chapter_page_count(int chapter)
        {
            long pages = 0;
            fz_try(gctx) {
                int chapters = fz_count_chapters(gctx, (fz_document *) $self);
                if (chapter < 0 || chapter >= chapters) {
                    RAISEPY(gctx, "bad chapter number", PyExc_ValueError);
                }
                pages = (long) fz_count_chapter_pages(gctx, (fz_document *) $self, chapter);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return PyLong_FromLong(pages);
        }

        FITZEXCEPTION(prev_location, !result)
        %pythonprepend prev_location %{
        """Get (chapter, page) of previous page."""
        if self.is_closed or self.is_encrypted:
            raise ValueError("document closed or encrypted")
        if type(page_id) is int:
            page_id = (0, page_id)
        if page_id not in self:
            raise ValueError("page id not in document")
        if page_id  == (0, 0):
            return ()
        %}
        PyObject *prev_location(PyObject *page_id)
        {
            fz_document *this_doc = (fz_document *) $self;
            fz_location prev_loc, loc;
            PyObject *val;
            int pno;
            fz_try(gctx) {
                val = PySequence_GetItem(page_id, 0);
                if (!val) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                int chapter = (int) PyLong_AsLong(val);
                Py_DECREF(val);
                if (PyErr_Occurred()) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }

                val = PySequence_GetItem(page_id, 1);
                if (!val) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                pno = (int) PyLong_AsLong(val);
                Py_DECREF(val);
                if (PyErr_Occurred()) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                loc = fz_make_location(chapter, pno);
                prev_loc = fz_previous_page(gctx, this_doc, loc);
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            return Py_BuildValue("ii", prev_loc.chapter, prev_loc.page);
        }


        FITZEXCEPTION(next_location, !result)
        %pythonprepend next_location %{
        """Get (chapter, page) of next page."""
        if self.is_closed or self.is_encrypted:
            raise ValueError("document closed or encrypted")
        if type(page_id) is int:
            page_id = (0, page_id)
        if page_id not in self:
            raise ValueError("page id not in document")
        if tuple(page_id)  == self.last_location:
            return ()
        %}
        PyObject *next_location(PyObject *page_id)
        {
            fz_document *this_doc = (fz_document *) $self;
            fz_location next_loc, loc;
            PyObject *val;
            int pno;
            fz_try(gctx) {
                val = PySequence_GetItem(page_id, 0);
                if (!val) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                int chapter = (int) PyLong_AsLong(val);
                Py_DECREF(val);
                if (PyErr_Occurred()) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }

                val = PySequence_GetItem(page_id, 1);
                if (!val) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                pno = (int) PyLong_AsLong(val);
                Py_DECREF(val);
                if (PyErr_Occurred()) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                loc = fz_make_location(chapter, pno);
                next_loc = fz_next_page(gctx, this_doc, loc);
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            return Py_BuildValue("ii", next_loc.chapter, next_loc.page);
        }


        FITZEXCEPTION(location_from_page_number, !result)
        CLOSECHECK0(location_from_page_number, """Convert pno to (chapter, page).""")
        PyObject *location_from_page_number(int pno)
        {
            fz_document *this_doc = (fz_document *) $self;
            fz_location loc = fz_make_location(-1, -1);
            int page_count = fz_count_pages(gctx, this_doc);
            while (pno < 0) pno += page_count;
            fz_try(gctx) {
                if (pno >= page_count) {
                    RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError);
                }
                loc = fz_location_from_page_number(gctx, this_doc, pno);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("ii", loc.chapter, loc.page);
        }

        FITZEXCEPTION(page_number_from_location, !result)
        %pythonprepend page_number_from_location%{
        """Convert (chapter, pno) to page number."""
        if type(page_id) is int:
            np = self.page_count
            while page_id < 0:
                page_id += np
            page_id = (0, page_id)
        if page_id not in self:
            raise ValueError("page id not in document")
        %}
        PyObject *page_number_from_location(PyObject *page_id)
        {
            fz_document *this_doc = (fz_document *) $self;
            fz_location loc;
            long page_n = -1;
            PyObject *val;
            int pno;
            fz_try(gctx) {
                val = PySequence_GetItem(page_id, 0);
                if (!val) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                int chapter = (int) PyLong_AsLong(val);
                Py_DECREF(val);
                if (PyErr_Occurred()) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }

                val = PySequence_GetItem(page_id, 1);
                if (!val) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }
                pno = (int) PyLong_AsLong(val);
                Py_DECREF(val);
                if (PyErr_Occurred()) {
                    RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError);
                }

                loc = fz_make_location(chapter, pno);
                page_n = (long) fz_page_number_from_location(gctx, this_doc, loc);
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            return PyLong_FromLong(page_n);
        }

        FITZEXCEPTION(_getMetadata, !result)
        CLOSECHECK0(_getMetadata, """Get metadata.""")
        PyObject *
        _getMetadata(const char *key)
        {
            PyObject *res = NULL;
            fz_document *doc = (fz_document *) $self;
            int vsize;
            char *value;
            fz_try(gctx) {
                vsize = fz_lookup_metadata(gctx, doc, key, NULL, 0)+1;
                if(vsize > 1) {
                    value = JM_Alloc(char, vsize);
                    fz_lookup_metadata(gctx, doc, key, value, vsize);
                    res = JM_UnicodeFromStr(value);
                    JM_Free(value);
                } else {
                    res = EMPTY_STRING;
                }
            }
            fz_always(gctx) {
                PyErr_Clear();
            }
            fz_catch(gctx) {
                return EMPTY_STRING;
            }
            return res;
        }

        CLOSECHECK0(needs_pass, """Indicate password required.""")
        %pythoncode%{@property%}
        PyObject *needs_pass() {
            return JM_BOOL(fz_needs_password(gctx, (fz_document *) $self));
        }

        %pythoncode%{@property%}
        CLOSECHECK0(language, """Document language.""")
        PyObject *language()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_NONE;
            fz_text_language lang = pdf_document_language(gctx, pdf);
            char buf[8];
            if (lang == FZ_LANG_UNSET) Py_RETURN_NONE;
            return PyUnicode_FromString(fz_string_from_text_language(buf, lang));
        }

        FITZEXCEPTION(set_language, !result)
        PyObject *set_language(char *language=NULL)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                fz_text_language lang;
                if (!language)
                    lang = FZ_LANG_UNSET;
                else
                    lang = fz_text_language_from_string(language);
                pdf_set_document_language(gctx, pdf, lang);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_TRUE;
        }


        %pythonprepend resolve_link %{
        """Calculate internal link destination.

        Args:
            uri: (str) some Link.uri
            chapters: (bool) whether to use (chapter, page) format
        Returns:
            (page_id, x, y) where x, y are point coordinates on the page.
            page_id is either page number (if chapters=0), or (chapter, pno).
        """
        %}
        PyObject *resolve_link(char *uri=NULL, int chapters=0)
        {
            if (!uri) {
                if (chapters) return Py_BuildValue("(ii)ff", -1, -1, 0, 0);
                return Py_BuildValue("iff", -1, 0, 0);
            }
            fz_document *this_doc = (fz_document *) $self;
            float xp = 0, yp = 0;
            fz_location loc = {0, 0};
            fz_try(gctx) {
                loc = fz_resolve_link(gctx, (fz_document *) $self, uri, &xp, &yp);
            }
            fz_catch(gctx) {
                if (chapters) return Py_BuildValue("(ii)ff", -1, -1, 0, 0);
                return Py_BuildValue("iff", -1, 0, 0);
            }
            if (chapters)
                return Py_BuildValue("(ii)ff", loc.chapter, loc.page, xp, yp);
            int pno = fz_page_number_from_location(gctx, this_doc, loc);
            return Py_BuildValue("iff", pno, xp, yp);
        }

        FITZEXCEPTION(layout, !result)
        CLOSECHECK(layout, """Re-layout a reflowable document.""")
        %pythonappend layout %{
            self._reset_page_refs()
            self.init_doc()%}
        PyObject *layout(PyObject *rect = NULL, float width = 0, float height = 0, float fontsize = 11)
        {
            fz_document *doc = (fz_document *) $self;
            if (!fz_is_document_reflowable(gctx, doc)) Py_RETURN_NONE;
            fz_try(gctx) {
                float w = width, h = height;
                fz_rect r = JM_rect_from_py(rect);
                if (!fz_is_infinite_rect(r)) {
                    w = r.x1 - r.x0;
                    h = r.y1 - r.y0;
                }
                if (w <= 0.0f || h <= 0.0f) {
                    RAISEPY(gctx, "bad page size", PyExc_ValueError);
                }
                fz_layout_document(gctx, doc, w, h, fontsize);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION(make_bookmark, !result)
        CLOSECHECK(make_bookmark, """Make a page pointer before layouting document.""")
        PyObject *make_bookmark(PyObject *loc)
        {
            fz_document *doc = (fz_document *) $self;
            fz_location location;
            fz_bookmark mark;
            fz_try(gctx) {
                if (JM_INT_ITEM(loc, 0, &location.chapter) == 1) {
                    RAISEPY(gctx, MSG_BAD_LOCATION, PyExc_ValueError);
                }
                if (JM_INT_ITEM(loc, 1, &location.page) == 1) {
                    RAISEPY(gctx, MSG_BAD_LOCATION, PyExc_ValueError);
                }
                mark = fz_make_bookmark(gctx, doc, location);
                if (!mark) {
                    RAISEPY(gctx, MSG_BAD_LOCATION, PyExc_ValueError);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return PyLong_FromVoidPtr((void *) mark);
        }


        FITZEXCEPTION(find_bookmark, !result)
        CLOSECHECK(find_bookmark, """Find new location after layouting a document.""")
        PyObject *find_bookmark(PyObject *bm)
        {
            fz_document *doc = (fz_document *) $self;
            fz_location location;
            fz_try(gctx) {
                intptr_t mark = (intptr_t) PyLong_AsVoidPtr(bm);
                location = fz_lookup_bookmark(gctx, doc, mark);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("ii", location.chapter, location.page);
        }


        CLOSECHECK0(is_reflowable, """Check if document is layoutable.""")
        %pythoncode%{@property%}
        PyObject *is_reflowable()
        {
            return JM_BOOL(fz_is_document_reflowable(gctx, (fz_document *) $self));
        }

        FITZEXCEPTION(_deleteObject, !result)
        CLOSECHECK0(_deleteObject, """Delete object.""")
        PyObject *_deleteObject(int xref)
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_specifics(gctx, doc);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                if (!INRANGE(xref, 1, pdf_xref_len(gctx, pdf)-1)) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                pdf_delete_object(gctx, pdf, xref);
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            Py_RETURN_NONE;
        }

        FITZEXCEPTION(pdf_catalog, !result)
        CLOSECHECK0(pdf_catalog, """Get xref of PDF catalog.""")
        PyObject *pdf_catalog()
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_specifics(gctx, doc);
            int xref = 0;
            if (!pdf) return Py_BuildValue("i", xref);
            fz_try(gctx) {
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf),
                                             PDF_NAME(Root));
                xref = pdf_to_num(gctx, root);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xref);
        }

        FITZEXCEPTION(_getPDFfileid, !result)
        CLOSECHECK0(_getPDFfileid, """Get PDF file id.""")
        PyObject *_getPDFfileid()
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_specifics(gctx, doc);
            if (!pdf) Py_RETURN_NONE;
            PyObject *idlist = PyList_New(0);
            fz_buffer *buffer = NULL;
            unsigned char *hex;
            pdf_obj *o;
            int n, i, len;
            PyObject *bytes;

            fz_try(gctx) {
                pdf_obj *identity = pdf_dict_get(gctx, pdf_trailer(gctx, pdf),
                                             PDF_NAME(ID));
                if (identity) {
                    n = pdf_array_len(gctx, identity);
                    for (i = 0; i < n; i++) {
                        o = pdf_array_get(gctx, identity, i);
                        len = (int) pdf_to_str_len(gctx, o);
                        buffer = fz_new_buffer(gctx, 2 * len);
                        fz_buffer_storage(gctx, buffer, &hex);
                        hexlify(len, (unsigned char *) pdf_to_text_string(gctx, o), hex);
                        LIST_APPEND_DROP(idlist, JM_UnicodeFromStr(hex));
                        Py_CLEAR(bytes);
                        fz_drop_buffer(gctx, buffer);
                        buffer = NULL;
                    }
                }
            }
            fz_catch(gctx) {
                fz_drop_buffer(gctx, buffer);
            }
            return idlist;
        }

        CLOSECHECK0(version_count, """Count versions of PDF document.""")
        %pythoncode%{@property%}
        PyObject *version_count()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) return Py_BuildValue("i", 0);
            return Py_BuildValue("i", pdf_count_versions(gctx, pdf));
        }


        CLOSECHECK0(is_pdf, """Check for PDF.""")
        %pythoncode%{@property%}
        PyObject *is_pdf()
        {
            if (pdf_specifics(gctx, (fz_document *) $self)) Py_RETURN_TRUE;
            else Py_RETURN_FALSE;
        }

        #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 21
        /* The underlying struct members that these methods give access to, are
        not available. */
        CLOSECHECK0(has_xref_streams, """Check if xref table is a stream.""")
        %pythoncode%{@property%}
        PyObject *has_xref_streams()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE;
            if (pdf->has_xref_streams) Py_RETURN_TRUE;
            Py_RETURN_FALSE;
        }

        CLOSECHECK0(has_old_style_xrefs, """Check if xref table is old style.""")
        %pythoncode%{@property%}
        PyObject *has_old_style_xrefs()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE;
            if (pdf->has_old_style_xrefs) Py_RETURN_TRUE;
            Py_RETURN_FALSE;
        }
        #endif

        CLOSECHECK0(is_dirty, """True if PDF has unsaved changes.""")
        %pythoncode%{@property%}
        PyObject *is_dirty()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE;
            return JM_BOOL(pdf_has_unsaved_changes(gctx, pdf));
        }

        CLOSECHECK0(can_save_incrementally, """Check whether incremental saves are possible.""")
        PyObject *can_save_incrementally()
        {
            pdf_document *pdf = pdf_document_from_fz_document(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE; // gracefully handle non-PDF
            return JM_BOOL(pdf_can_be_saved_incrementally(gctx, pdf));
        }

        CLOSECHECK0(is_fast_webaccess, """Check whether we have a linearized PDF.""")
        %pythoncode%{@property%}
        PyObject *is_fast_webaccess()
        {
            pdf_document *pdf = pdf_document_from_fz_document(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE; // gracefully handle non-PDF
            return JM_BOOL(pdf_doc_was_linearized(gctx, pdf));
        }

        CLOSECHECK0(is_repaired, """Check whether PDF was repaired.""")
        %pythoncode%{@property%}
        PyObject *is_repaired()
        {
            pdf_document *pdf = pdf_document_from_fz_document(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE; // gracefully handle non-PDF
            return JM_BOOL(pdf_was_repaired(gctx, pdf));
        }

        FITZEXCEPTION(save_snapshot, !result)
        %pythonprepend save_snapshot %{
        """Save a file snapshot suitable for journalling."""
        if self.is_closed:
            raise ValueError("doc is closed")
        if type(filename) == str:
            pass
        elif hasattr(filename, "open"):  # assume: pathlib.Path
            filename = str(filename)
        elif hasattr(filename, "name"):  # assume: file object
            filename = filename.name
        else:
            raise ValueError("filename must be str, Path or file object")
        if filename == self.name:
            raise ValueError("cannot snapshot to original")
        %}
        PyObject *save_snapshot(const char *filename)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                pdf_save_snapshot(gctx, pdf, filename);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        CLOSECHECK0(authenticate, """Decrypt document.""")
        %pythonappend authenticate %{
        if val:  # the doc is decrypted successfully and we init the outline
            self.is_encrypted = False
            self.isEncrypted = False
            self.init_doc()
            self.thisown = True
        %}
        PyObject *authenticate(char *password)
        {
            return Py_BuildValue("i", fz_authenticate_password(gctx, (fz_document *) $self, (const char *) password));
        }

        //------------------------------------------------------------------
        // save a PDF
        //------------------------------------------------------------------
        FITZEXCEPTION(save, !result)
        %pythonprepend save %{
        """Save PDF to file, pathlib.Path or file pointer."""
        if self.is_closed or self.is_encrypted:
            raise ValueError("document closed or encrypted")
        if type(filename) == str:
            pass
        elif hasattr(filename, "open"):  # assume: pathlib.Path
            filename = str(filename)
        elif hasattr(filename, "name"):  # assume: file object
            filename = filename.name
        elif not hasattr(filename, "seek"):  # assume file object
            raise ValueError("filename must be str, Path or file object")
        if filename == self.name and not incremental:
            raise ValueError("save to original must be incremental")
        if self.page_count < 1:
            raise ValueError("cannot save with zero pages")
        if incremental:
            if self.name != filename or self.stream:
                raise ValueError("incremental needs original file")
        if user_pw and len(user_pw) > 40 or owner_pw and len(owner_pw) > 40:
            raise ValueError("password length must not exceed 40")
        %}

        PyObject *
        save(PyObject *filename, int garbage=0, int clean=0,
            int deflate=0, int deflate_images=0, int deflate_fonts=0,
            int incremental=0, int ascii=0, int expand=0, int linear=0,
            int no_new_id=0, int appearance=0,
            int pretty=0, int encryption=1, int permissions=4095,
            char *owner_pw=NULL, char *user_pw=NULL)
        {
            pdf_write_options opts = pdf_default_write_options;
            opts.do_incremental     = incremental;
            opts.do_ascii           = ascii;
            opts.do_compress        = deflate;
            opts.do_compress_images = deflate_images;
            opts.do_compress_fonts  = deflate_fonts;
            opts.do_decompress      = expand;
            opts.do_garbage         = garbage;
            opts.do_pretty          = pretty;
            opts.do_linear          = linear;
            opts.do_clean           = clean;
            opts.do_sanitize        = clean;
            opts.dont_regenerate_id = no_new_id;
            opts.do_appearance      = appearance;
            opts.do_encrypt         = encryption;
            opts.permissions        = permissions;
            if (owner_pw) {
                memcpy(&opts.opwd_utf8, owner_pw, strlen(owner_pw)+1);
            } else if (user_pw) {
                memcpy(&opts.opwd_utf8, user_pw, strlen(user_pw)+1);
            }
            if (user_pw) {
                memcpy(&opts.upwd_utf8, user_pw, strlen(user_pw)+1);
            }
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_specifics(gctx, doc);
            fz_output *out = NULL;
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                pdf->resynth_required = 0;
                JM_embedded_clean(gctx, pdf);
                if (no_new_id == 0) {
                    JM_ensure_identity(gctx, pdf);
                }
                if (PyUnicode_Check(filename)) {
                    pdf_save_document(gctx, pdf, JM_StrAsChar(filename), &opts);
                } else {
                    out = JM_new_output_fileptr(gctx, filename);
                    pdf_write_document(gctx, pdf, out, &opts);
                }
            }
            fz_always(gctx) {
                fz_drop_output(gctx, out);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode %{
        def write(self, garbage=False, clean=False,
            deflate=False, deflate_images=False, deflate_fonts=False,
            incremental=False, ascii=False, expand=False, linear=False,
            no_new_id=False, appearance=False, pretty=False, encryption=1, permissions=4095,
            owner_pw=None, user_pw=None):
            from io import BytesIO
            bio = BytesIO()
            self.save(bio, garbage=garbage, clean=clean,
            no_new_id=no_new_id, appearance=appearance,
            deflate=deflate, deflate_images=deflate_images, deflate_fonts=deflate_fonts,
            incremental=incremental, ascii=ascii, expand=expand, linear=linear,
            pretty=pretty, encryption=encryption, permissions=permissions,
            owner_pw=owner_pw, user_pw=user_pw)
            return bio.getvalue()
        %}

        //----------------------------------------------------------------
        // Insert pages from a source PDF into this PDF.
        // For reconstructing the links (_do_links method), we must save the
        // insertion point (start_at) if it was specified as -1.
        //----------------------------------------------------------------
        FITZEXCEPTION(insert_pdf, !result)
        %pythonprepend insert_pdf %{
        """Insert a page range from another PDF.

        Args:
            docsrc: PDF to copy from. Must be different object, but may be same file.
            from_page: (int) first source page to copy, 0-based, default 0.
            to_page: (int) last source page to copy, 0-based, default last page.
            start_at: (int) from_page will become this page number in target.
            rotate: (int) rotate copied pages, default -1 is no change.
            links: (int/bool) whether to also copy links.
            annots: (int/bool) whether to also copy annotations.
            show_progress: (int) progress message interval, 0 is no messages.
            final: (bool) indicates last insertion from this source PDF.
            _gmap: internal use only

        Copy sequence reversed if from_page > to_page."""

        if self.is_closed or self.is_encrypted:
            raise ValueError("document closed or encrypted")
        if self._graft_id == docsrc._graft_id:
            raise ValueError("source and target cannot be same object")
        sa = start_at
        if sa < 0:
            sa = self.page_count
        if len(docsrc) > show_progress > 0:
            inname = os.path.basename(docsrc.name)
            if not inname:
                inname = "memory PDF"
            outname = os.path.basename(self.name)
            if not outname:
                outname = "memory PDF"
            print("Inserting '%s' at '%s'" % (inname, outname))

        # retrieve / make a Graftmap to avoid duplicate objects
        isrt = docsrc._graft_id
        _gmap = self.Graftmaps.get(isrt, None)
        if _gmap is None:
            _gmap = Graftmap(self)
            self.Graftmaps[isrt] = _gmap
        %}

        %pythonappend insert_pdf %{
        self._reset_page_refs()
        if links:
            self._do_links(docsrc, from_page = from_page, to_page = to_page,
                        start_at = sa)
        if final == 1:
            self.Graftmaps[isrt] = None%}

        PyObject *
        insert_pdf(struct Document *docsrc,
            int from_page=-1,
            int to_page=-1,
            int start_at=-1,
            int rotate=-1,
            int links=1,
            int annots=1,
            int show_progress=0,
            int final = 1,
            struct Graftmap *_gmap=NULL)
        {
            fz_document *doc = (fz_document *) $self;
            fz_document *src = (fz_document *) docsrc;
            pdf_document *pdfout = pdf_specifics(gctx, doc);
            pdf_document *pdfsrc = pdf_specifics(gctx, src);
            int outCount = fz_count_pages(gctx, doc);
            int srcCount = fz_count_pages(gctx, src);

            // local copies of page numbers
            int fp = from_page, tp = to_page, sa = start_at;

            // normalize page numbers
            fp = Py_MAX(fp, 0);                // -1 = first page
            fp = Py_MIN(fp, srcCount - 1);     // but do not exceed last page

            if (tp < 0) tp = srcCount - 1;  // -1 = last page
            tp = Py_MIN(tp, srcCount - 1);     // but do not exceed last page

            if (sa < 0) sa = outCount;      // -1 = behind last page
            sa = Py_MIN(sa, outCount);         // but that is also the limit

            fz_try(gctx) {
                if (!pdfout || !pdfsrc) {
                    RAISEPY(gctx, "source or target not a PDF", PyExc_TypeError);
                }
                ENSURE_OPERATION(gctx, pdfout);
                JM_merge_range(gctx, pdfout, pdfsrc, fp, tp, sa, rotate, links, annots, show_progress, (pdf_graft_map *) _gmap);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode %{
        def insert_file(self, infile, from_page=-1, to_page=-1, start_at=-1, rotate=-1, links=True, annots=True,show_progress=0, final=1):
            """Insert an arbitrary supported document to an existing PDF.
            
            The infile may be given as a filename, a Document or a Pixmap.
            Other paramters - where applicable - equal those of insert_pdf().
            """
            src = None
            if isinstance(infile, Pixmap):
                if infile.colorspace.n > 3:
                    infile = Pixmap(csRGB, infile)
                src = Document("png", infile.tobytes())
            elif isinstance(infile, Document):
                src = infile
            else:
                src = Document(infile)
            if not src:
                raise ValueError("bad infile parameter")
            if not src.is_pdf:
                pdfbytes = src.convert_to_pdf()
                src = Document("pdf", pdfbytes)
            return self.insert_pdf(src, from_page=from_page, to_page=to_page, start_at=start_at, rotate=rotate,links=links, annots=annots, show_progress=show_progress, final=final)
        %}

        //------------------------------------------------------------------
        // Create and insert a new page (PDF)
        //------------------------------------------------------------------
        FITZEXCEPTION(_newPage, !result)
        CLOSECHECK(_newPage, """Make a new PDF page.""")
        %pythonappend _newPage %{self._reset_page_refs()%}
        PyObject *_newPage(int pno=-1, float width=595, float height=842)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_rect mediabox = fz_unit_rect;
            mediabox.x1 = width;
            mediabox.y1 = height;
            pdf_obj *resources = NULL, *page_obj = NULL;
            fz_buffer *contents = NULL;
            fz_var(contents);
            fz_var(page_obj);
            fz_var(resources);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                if (pno < -1) {
                    RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError);
                }
                ENSURE_OPERATION(gctx, pdf);
                // create /Resources and /Contents objects
                resources = pdf_add_new_dict(gctx, pdf, 1);
                page_obj = pdf_add_page(gctx, pdf, mediabox, 0, resources, contents);
                pdf_insert_page(gctx, pdf, pno, page_obj);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, contents);
                pdf_drop_obj(gctx, page_obj);
                pdf_drop_obj(gctx, resources);
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // Create sub-document to keep only selected pages.
        // Parameter is a Python sequence of the wanted page numbers.
        //------------------------------------------------------------------
        FITZEXCEPTION(select, !result)
        %pythonprepend select %{"""Build sub-pdf with page numbers in the list."""
if self.is_closed or self.is_encrypted:
    raise ValueError("document closed or encrypted")
if not self.is_pdf:
    raise ValueError("is no PDF")
if not hasattr(pyliste, "__getitem__"):
    raise ValueError("sequence required")
if len(pyliste) == 0 or min(pyliste) not in range(len(self)) or max(pyliste) not in range(len(self)):
    raise ValueError("bad page number(s)")
pyliste = tuple(pyliste)%}
        %pythonappend select %{self._reset_page_refs()%}
        PyObject *select(PyObject *pyliste)
        {
            // preparatory stuff:
            // (1) get underlying pdf document,
            // (2) transform Python list into integer array

            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            int *pages = NULL;
            fz_try(gctx) {
                // call retainpages (code copy of fz_clean_file.c)
                int i, len = (int) PyTuple_Size(pyliste);
                pages = fz_realloc_array(gctx, pages, len, int);
                for (i = 0; i < len; i++) {
                    pages[i] = (int) PyLong_AsLong(PyTuple_GET_ITEM(pyliste, (Py_ssize_t) i));
                }
                pdf_rearrange_pages(gctx, pdf, len, pages);
                if (pdf->rev_page_map)
                {
                    pdf_drop_page_tree(gctx, pdf);
                }
            }
            fz_always(gctx) {
                fz_free(gctx, pages);
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // remove one page
        //------------------------------------------------------------------
        FITZEXCEPTION(_delete_page, !result)
        PyObject *_delete_page(int pno)
        {
            fz_try(gctx) {
                fz_document *doc = (fz_document *) $self;
                pdf_document *pdf = pdf_specifics(gctx, doc);
                pdf_delete_page(gctx, pdf, pno);
                if (pdf->rev_page_map)
                {
                    pdf_drop_page_tree(gctx, pdf);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // get document permissions
        //------------------------------------------------------------------
        %pythoncode%{@property%}
        %pythonprepend permissions %{
        """Document permissions."""

        if self.is_encrypted:
            return 0
        %}
        PyObject *permissions()
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_document_from_fz_document(gctx, doc);

            // for PDF return result of standard function
            if (pdf)
                return Py_BuildValue("i", pdf_document_permissions(gctx, pdf));

            // otherwise simulate the PDF return value
            int perm = (int) 0xFFFFFFFC;  // all permissions granted
            // now switch off where needed
            if (!fz_has_permission(gctx, doc, FZ_PERMISSION_PRINT))
                perm = perm ^ PDF_PERM_PRINT;
            if (!fz_has_permission(gctx, doc, FZ_PERMISSION_EDIT))
                perm = perm ^ PDF_PERM_MODIFY;
            if (!fz_has_permission(gctx, doc, FZ_PERMISSION_COPY))
                perm = perm ^ PDF_PERM_COPY;
            if (!fz_has_permission(gctx, doc, FZ_PERMISSION_ANNOTATE))
                perm = perm ^ PDF_PERM_ANNOTATE;
            return Py_BuildValue("i", perm);
        }


        FITZEXCEPTION(journal_enable, !result)
        CLOSECHECK(journal_enable, """Activate document journalling.""")
        PyObject *journal_enable()
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                pdf_enable_journal(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(journal_start_op, !result)
        CLOSECHECK(journal_start_op, """Begin a journalling operation.""")
        PyObject *journal_start_op(const char *name=NULL)
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                if (!pdf->journal) {
                    RAISEPY(gctx, "Journalling not enabled", PyExc_RuntimeError);
                }
                if (name) {
                    pdf_begin_operation(gctx, pdf, name);
                } else {
                    pdf_begin_implicit_operation(gctx, pdf);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(journal_stop_op, !result)
        CLOSECHECK(journal_stop_op, """End a journalling operation.""")
        PyObject *journal_stop_op()
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                pdf_end_operation(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(journal_position, !result)
        CLOSECHECK(journal_position, """Show journalling state.""")
        PyObject *journal_position()
        {
            int rc, steps=0;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                rc = pdf_undoredo_state(gctx, pdf, &steps);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("ii", rc, steps);
        }


        FITZEXCEPTION(journal_op_name, !result)
        CLOSECHECK(journal_op_name, """Show operation name for given step.""")
        PyObject *journal_op_name(int step)
        {
            const char *name=NULL;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                name = pdf_undoredo_step(gctx, pdf, step);
            }
            fz_catch(gctx) {
                return NULL;
            }
            if (name) {
                return PyUnicode_FromString(name);
            } else {
                Py_RETURN_NONE;
            }
        }


        FITZEXCEPTION(journal_can_do, !result)
        CLOSECHECK(journal_can_do, """Show if undo and / or redo are possible.""")
        PyObject *journal_can_do()
        {
            int undo=0, redo=0;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                undo = pdf_can_undo(gctx, pdf);
                redo = pdf_can_redo(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("{s:N,s:N}", "undo", JM_BOOL(undo), "redo", JM_BOOL(redo));
        }


        FITZEXCEPTION(journal_undo, !result)
        CLOSECHECK(journal_undo, """Move backwards in the journal.""")
        PyObject *journal_undo()
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                pdf_undo(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_TRUE;
        }


        FITZEXCEPTION(journal_redo, !result)
        CLOSECHECK(journal_redo, """Move forward in the journal.""")
        PyObject *journal_redo()
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                pdf_redo(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_TRUE;
        }


        FITZEXCEPTION(journal_save, !result)
        CLOSECHECK(journal_save, """Save journal to a file.""")
        PyObject *journal_save(PyObject *filename)
        {
            fz_output *out = NULL;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                if (PyUnicode_Check(filename)) {
                    pdf_save_journal(gctx, pdf, (const char *) PyUnicode_AsUTF8(filename));
                } else {
                    out = JM_new_output_fileptr(gctx, filename);
                    pdf_write_journal(gctx, pdf, out);
                }
            }
            fz_always(gctx) {
                fz_drop_output(gctx, out);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(journal_load, !result)
        CLOSECHECK(journal_load, """Load a journal from a file.""")
        PyObject *journal_load(PyObject *filename)
        {
            fz_buffer *res = NULL;
            fz_stream *stm = NULL;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                if (PyUnicode_Check(filename)) {
                    pdf_load_journal(gctx, pdf, PyUnicode_AsUTF8(filename));
                } else {
                    res = JM_BufferFromBytes(gctx, filename);
                    stm = fz_open_buffer(gctx, res);
                    pdf_deserialise_journal(gctx, pdf, stm);
                }
                if (!pdf->journal) {
                    RAISEPY(gctx, "Journal and document do not match", JM_Exc_FileDataError);
                }
            }
            fz_always(gctx) {
                fz_drop_stream(gctx, stm);
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(journal_is_enabled, !result)
        CLOSECHECK(journal_is_enabled, """Check if journalling is enabled.""")
        PyObject *journal_is_enabled()
        {
            int enabled = 0;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                enabled = pdf && pdf->journal;
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_BOOL(enabled);
        }


        FITZEXCEPTION(_get_char_widths, !result)
        CLOSECHECK(_get_char_widths, """Return list of glyphs and glyph widths of a font.""")
        PyObject *_get_char_widths(int xref, char *bfname, char *ext,
                                 int ordering, int limit, int idx = 0)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            PyObject *wlist = NULL;
            int i, glyph, mylimit;
            mylimit = limit;
            if (mylimit < 256) mylimit = 256;
            const unsigned char *data;
            int size, index;
            fz_font *font = NULL;
            fz_buffer *buf = NULL;

            fz_try(gctx) {
                ASSERT_PDF(pdf);
                if (ordering >= 0) {
                    data = fz_lookup_cjk_font(gctx, ordering, &size, &index);
                    font = fz_new_font_from_memory(gctx, NULL, data, size, index, 0);
                    goto weiter;
                }
                data = fz_lookup_base14_font(gctx, bfname, &size);
                if (data) {
                    font = fz_new_font_from_memory(gctx, bfname, data, size, 0, 0);
                    goto weiter;
                }
                buf = JM_get_fontbuffer(gctx, pdf, xref);
                if (!buf) {
                    fz_throw(gctx, FZ_ERROR_GENERIC, "font at xref %d is not supported", xref);
                }
                font = fz_new_font_from_buffer(gctx, NULL, buf, idx, 0);

                weiter:;
                wlist = PyList_New(0);
                float adv;
                for (i = 0; i < mylimit; i++) {
                    glyph = fz_encode_character(gctx, font, i);
                    adv = fz_advance_glyph(gctx, font, glyph, 0);
                    if (ordering >= 0) {
                        glyph = i;
                    }
                    if (glyph > 0) {
                        LIST_APPEND_DROP(wlist, Py_BuildValue("if", glyph, adv));
                    } else {
                        LIST_APPEND_DROP(wlist, Py_BuildValue("if", glyph, 0.0));
                    }
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buf);
                fz_drop_font(gctx, font);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return wlist;
        }


        FITZEXCEPTION(page_xref, !result)
        CLOSECHECK0(page_xref, """Get xref of page number.""")
        PyObject *page_xref(int pno)
        {
            fz_document *this_doc = (fz_document *) $self;
            int page_count = fz_count_pages(gctx, this_doc);
            int n = pno;
            while (n < 0) n += page_count;
            pdf_document *pdf = pdf_specifics(gctx, this_doc);
            int xref = 0;
            fz_try(gctx) {
                if (n >= page_count) {
                    RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError);
                }
                ASSERT_PDF(pdf);
                xref = pdf_to_num(gctx, pdf_lookup_page_obj(gctx, pdf, n));
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xref);
        }


        FITZEXCEPTION(page_annot_xrefs, !result)
        CLOSECHECK0(page_annot_xrefs, """Get list annotations of page number.""")
        PyObject *page_annot_xrefs(int pno)
        {
            fz_document *this_doc = (fz_document *) $self;
            int page_count = fz_count_pages(gctx, this_doc);
            int n = pno;
            while (n < 0) n += page_count;
            pdf_document *pdf = pdf_specifics(gctx, this_doc);
            PyObject *annots = NULL;
            fz_try(gctx) {
                if (n >= page_count) {
                    RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError);
                }
                ASSERT_PDF(pdf);
                annots = JM_get_annot_xref_list(gctx, pdf_lookup_page_obj(gctx, pdf, n));
            }
            fz_catch(gctx) {
                return NULL;
            }
            return annots;
        }


        FITZEXCEPTION(page_cropbox, !result)
        CLOSECHECK0(page_cropbox, """Get CropBox of page number (without loading page).""")
        %pythonappend page_cropbox %{val = Rect(JM_TUPLE3(val))%}
        PyObject *page_cropbox(int pno)
        {
            fz_document *this_doc = (fz_document *) $self;
            int page_count = fz_count_pages(gctx, this_doc);
            int n = pno;
            while (n < 0) n += page_count;
            pdf_obj *pageref = NULL;
            fz_var(pageref);
            pdf_document *pdf = pdf_specifics(gctx, this_doc);
            fz_try(gctx) {
                if (n >= page_count) {
                    RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError);
                }
                ASSERT_PDF(pdf);
                pageref = pdf_lookup_page_obj(gctx, pdf, n);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_py_from_rect(JM_cropbox(gctx, pageref));
        }


        FITZEXCEPTION(_getPageInfo, !result)
        CLOSECHECK(_getPageInfo, """List fonts, images, XObjects used on a page.""")
        PyObject *_getPageInfo(int pno, int what)
        {
            fz_document *doc = (fz_document *) $self;
            pdf_document *pdf = pdf_specifics(gctx, doc);
            pdf_obj *pageref, *rsrc;
            PyObject *liste = NULL, *tracer = NULL;
            fz_var(liste);
            fz_var(tracer);
            fz_try(gctx) {
                int page_count = fz_count_pages(gctx, doc);
                int n = pno;  // pno < 0 is allowed
                while (n < 0) n += page_count;  // make it non-negative
                if (n >= page_count) {
                    RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError);
                }
                ASSERT_PDF(pdf);
                pageref = pdf_lookup_page_obj(gctx, pdf, n);
                rsrc = pdf_dict_get_inheritable(gctx,
                           pageref, PDF_NAME(Resources));
                liste = PyList_New(0);
                tracer = PyList_New(0);
                if (rsrc) {
                    JM_scan_resources(gctx, pdf, rsrc, liste, what, 0, tracer);
                }
            }
            fz_always(gctx) {
                Py_CLEAR(tracer);
            }
            fz_catch(gctx) {
                Py_CLEAR(liste);
                return NULL;
            }
            return liste;
        }

        FITZEXCEPTION(extract_font, !result)
        CLOSECHECK(extract_font, """Get a font by xref. Returns a tuple or dictionary.""")
        PyObject *extract_font(int xref=0, int info_only=0, PyObject *named=NULL)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);

            fz_try(gctx) {
                ASSERT_PDF(pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }

            fz_buffer *buffer = NULL;
            pdf_obj *obj, *basefont, *bname;
            PyObject *bytes = NULL;
            char *ext = NULL;
            PyObject *rc;
            fz_try(gctx) {
                obj = pdf_load_object(gctx, pdf, xref);
                pdf_obj *type = pdf_dict_get(gctx, obj, PDF_NAME(Type));
                pdf_obj *subtype = pdf_dict_get(gctx, obj, PDF_NAME(Subtype));
                if(pdf_name_eq(gctx, type, PDF_NAME(Font)) &&
                   strncmp(pdf_to_name(gctx, subtype), "CIDFontType", 11) != 0) {
                    basefont = pdf_dict_get(gctx, obj, PDF_NAME(BaseFont));
                    if (!basefont || pdf_is_null(gctx, basefont)) {
                        bname = pdf_dict_get(gctx, obj, PDF_NAME(Name));
                    } else {
                        bname = basefont;
                    }
                    ext = JM_get_fontextension(gctx, pdf, xref);
                    if (strcmp(ext, "n/a") != 0 && !info_only) {
                        buffer = JM_get_fontbuffer(gctx, pdf, xref);
                        bytes = JM_BinFromBuffer(gctx, buffer);
                        fz_drop_buffer(gctx, buffer);
                    } else {
                        bytes = Py_BuildValue("y", "");
                    }
                    if (PyObject_Not(named)) {
                        rc = PyTuple_New(4);
                        PyTuple_SET_ITEM(rc, 0, JM_EscapeStrFromStr(pdf_to_name(gctx, bname)));
                        PyTuple_SET_ITEM(rc, 1, JM_UnicodeFromStr(ext));
                        PyTuple_SET_ITEM(rc, 2, JM_UnicodeFromStr(pdf_to_name(gctx, subtype)));
                        PyTuple_SET_ITEM(rc, 3, bytes);
                    } else {
                        rc = PyDict_New();
                        DICT_SETITEM_DROP(rc, dictkey_name, JM_EscapeStrFromStr(pdf_to_name(gctx, bname)));
                        DICT_SETITEM_DROP(rc, dictkey_ext, JM_UnicodeFromStr(ext));
                        DICT_SETITEM_DROP(rc, dictkey_type, JM_UnicodeFromStr(pdf_to_name(gctx, subtype)));
                        DICT_SETITEM_DROP(rc, dictkey_content, bytes);
                    }
                } else {
                    if (PyObject_Not(named)) {
                        rc = Py_BuildValue("sssy", "", "", "", "");
                    } else {
                        rc = PyDict_New();
                        DICT_SETITEM_DROP(rc, dictkey_name, Py_BuildValue("s", ""));
                        DICT_SETITEM_DROP(rc, dictkey_ext, Py_BuildValue("s", ""));
                        DICT_SETITEM_DROP(rc, dictkey_type, Py_BuildValue("s", ""));
                        DICT_SETITEM_DROP(rc, dictkey_content, Py_BuildValue("y", ""));
                    }
                }
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, obj);
                JM_PyErr_Clear;
            }
            fz_catch(gctx) {
                if (PyObject_Not(named)) {
                    rc = Py_BuildValue("sssy", "invalid-name", "", "", "");
                } else {
                    rc = PyDict_New();
                    DICT_SETITEM_DROP(rc, dictkey_name, Py_BuildValue("s", "invalid-name"));
                    DICT_SETITEM_DROP(rc, dictkey_ext, Py_BuildValue("s", ""));
                    DICT_SETITEM_DROP(rc, dictkey_type, Py_BuildValue("s", ""));
                    DICT_SETITEM_DROP(rc, dictkey_content, Py_BuildValue("y", ""));
                }
            }
            return rc;
        }


        FITZEXCEPTION(extract_image, !result)
        CLOSECHECK(extract_image, """Get image by xref. Returns a dictionary.""")
        PyObject *extract_image(int xref)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            pdf_obj *obj = NULL;
            fz_buffer *res = NULL;
            fz_image *img = NULL;
            PyObject *rc = NULL;
            const char *ext = NULL;
            const char *cs_name = NULL;
            int img_type = 0, xres, yres, colorspace;
            int smask = 0, width, height, bpc;
            fz_compressed_buffer *cbuf = NULL;
            fz_var(img);
            fz_var(res);
            fz_var(obj);

            fz_try(gctx) {
                ASSERT_PDF(pdf);
                if (!INRANGE(xref, 1, pdf_xref_len(gctx, pdf)-1)) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                obj = pdf_new_indirect(gctx, pdf, xref, 0);
                pdf_obj *subtype = pdf_dict_get(gctx, obj, PDF_NAME(Subtype));

                if (!pdf_name_eq(gctx, subtype, PDF_NAME(Image))) {
                    RAISEPY(gctx, "not an image", PyExc_ValueError);
                }

                pdf_obj *o = pdf_dict_geta(gctx, obj, PDF_NAME(SMask), PDF_NAME(Mask));
                if (o) smask = pdf_to_num(gctx, o);

                if (pdf_is_jpx_image(gctx, obj)) {
                    img_type = FZ_IMAGE_JPX;
                    res = pdf_load_stream(gctx, obj);
                    ext = "jpx";
                }
                if (JM_is_jbig2_image(gctx, obj)) {
                    img_type = FZ_IMAGE_JBIG2;
                    res = pdf_load_stream(gctx, obj);
                    ext = "jb2";
                }
                if (img_type == FZ_IMAGE_UNKNOWN) {
                    res = pdf_load_raw_stream(gctx, obj);
                    unsigned char *c = NULL;
                    fz_buffer_storage(gctx, res, &c);
                    img_type = fz_recognize_image_format(gctx, c);
                    ext = JM_image_extension(img_type);
                }
                if (img_type == FZ_IMAGE_UNKNOWN) {
                    fz_drop_buffer(gctx, res);
                    res = NULL;
                    img = pdf_load_image(gctx, pdf, obj);
                    cbuf = fz_compressed_image_buffer(gctx, img);
                    if (cbuf &&
                        cbuf->params.type != FZ_IMAGE_RAW &&
                        cbuf->params.type != FZ_IMAGE_FAX &&
                        cbuf->params.type != FZ_IMAGE_FLATE && 
                        cbuf->params.type != FZ_IMAGE_LZW && 
                        cbuf->params.type != FZ_IMAGE_RLD) {
                        img_type = cbuf->params.type;
                        ext = JM_image_extension(img_type);
                        res = cbuf->buffer;
                    } else {
                        res = fz_new_buffer_from_image_as_png(gctx, img,
                                fz_default_color_params);
                        ext = "png";
                    }
                } else {
                    img = fz_new_image_from_buffer(gctx, res);
                }

                fz_image_resolution(img, &xres, &yres);
                width = img->w;
                height = img->h;
                colorspace = img->n;
                bpc = img->bpc;
                cs_name = fz_colorspace_name(gctx, img->colorspace);

                rc = PyDict_New();
                DICT_SETITEM_DROP(rc, dictkey_ext,
                                    JM_UnicodeFromStr(ext));
                DICT_SETITEM_DROP(rc, dictkey_smask,
                                    Py_BuildValue("i", smask));
                DICT_SETITEM_DROP(rc, dictkey_width,
                                    Py_BuildValue("i", width));
                DICT_SETITEM_DROP(rc, dictkey_height,
                                    Py_BuildValue("i", height));
                DICT_SETITEM_DROP(rc, dictkey_colorspace,
                                    Py_BuildValue("i", colorspace));
                DICT_SETITEM_DROP(rc, dictkey_bpc,
                                    Py_BuildValue("i", bpc));
                DICT_SETITEM_DROP(rc, dictkey_xres,
                                    Py_BuildValue("i", xres));
                DICT_SETITEM_DROP(rc, dictkey_yres,
                                    Py_BuildValue("i", yres));
                DICT_SETITEM_DROP(rc, dictkey_cs_name,
                                    JM_UnicodeFromStr(cs_name));
                DICT_SETITEM_DROP(rc, dictkey_image,
                                    JM_BinFromBuffer(gctx, res));
            }
            fz_always(gctx) {
                fz_drop_image(gctx, img);
                if (!cbuf) fz_drop_buffer(gctx, res);
                pdf_drop_obj(gctx, obj);
            }

            fz_catch(gctx) {
                Py_CLEAR(rc);
                fz_warn(gctx, "%s", fz_caught_message(gctx));
                Py_RETURN_FALSE;
            }
            if (!rc)
                Py_RETURN_NONE;
            return rc;
        }


        //------------------------------------------------------------------
        // Delete all bookmarks (table of contents)
        // returns list of deleted (now available) xref numbers
        //------------------------------------------------------------------
        CLOSECHECK(_delToC, """Delete the TOC.""")
        %pythonappend _delToC %{self.init_doc()%}
        PyObject *_delToC()
        {
            PyObject *xrefs = PyList_New(0);          // create Python list
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) return xrefs;                   // not a pdf

            pdf_obj *root, *olroot, *first;
            int xref_count, olroot_xref, i, xref;

            // get the main root
            root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
            // get the outline root
            olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines));
            if (!olroot) return xrefs;                // no outlines or some problem

            first = pdf_dict_get(gctx, olroot, PDF_NAME(First)); // first outline

            xrefs = JM_outline_xrefs(gctx, first, xrefs);
            xref_count = (int) PyList_Size(xrefs);

            olroot_xref = pdf_to_num(gctx, olroot);        // delete OL root
            pdf_delete_object(gctx, pdf, olroot_xref);     // delete OL root
            pdf_dict_del(gctx, root, PDF_NAME(Outlines));  // delete OL root

            for (i = 0; i < xref_count; i++)
            {
                JM_INT_ITEM(xrefs, i, &xref);
                pdf_delete_object(gctx, pdf, xref);      // delete outline item
            }
            LIST_APPEND_DROP(xrefs, Py_BuildValue("i", olroot_xref));
            
            return xrefs;
        }


        //------------------------------------------------------------------
        // Check: is xref a stream object?
        //------------------------------------------------------------------
        CLOSECHECK0(xref_is_stream, """Check if xref is a stream object.""")
        PyObject *xref_is_stream(int xref=0)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE;  // not a PDF
            return JM_BOOL(pdf_obj_num_is_stream(gctx, pdf, xref));
        }

        //------------------------------------------------------------------
        // Return or set NeedAppearances
        //------------------------------------------------------------------
        %pythonprepend need_appearances
%{"""Get/set the NeedAppearances value."""
if self.is_closed:
    raise ValueError("document closed")
if not self.is_form_pdf:
    return None
%}
        PyObject *need_appearances(PyObject *value=NULL)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            int oldval = -1;
            pdf_obj *app = NULL;
            char appkey[] = "NeedAppearances";
            fz_try(gctx) {
                pdf_obj *form = pdf_dict_getp(gctx, pdf_trailer(gctx, pdf),
                                "Root/AcroForm");
                app = pdf_dict_gets(gctx, form, appkey);
                if (pdf_is_bool(gctx, app)) {
                    oldval = pdf_to_bool(gctx, app);
                }

                if (EXISTS(value)) {
                    pdf_dict_puts_drop(gctx, form, appkey, PDF_TRUE);
                } else if (value == Py_False) {
                    pdf_dict_puts_drop(gctx, form, appkey, PDF_FALSE);
                }
            }
            fz_catch(gctx) {
                Py_RETURN_NONE;
            }
            if (value != Py_None) {
                return value;
            }
            if (oldval >= 0) {
                return JM_BOOL(oldval);
            }
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // Return the /SigFlags value
        //------------------------------------------------------------------
        CLOSECHECK0(get_sigflags, """Get the /SigFlags value.""")
        int get_sigflags()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) return -1;  // not a PDF
            int sigflag = -1;
            fz_try(gctx) {
                pdf_obj *sigflags = pdf_dict_getl(gctx,
                                        pdf_trailer(gctx, pdf),
                                        PDF_NAME(Root),
                                        PDF_NAME(AcroForm),
                                        PDF_NAME(SigFlags),
                                        NULL);
                if (sigflags) {
                    sigflag = (int) pdf_to_int(gctx, sigflags);
                }
            }
            fz_catch(gctx) {
                return -1;  // any problem
            }
            return sigflag;
        }

        //------------------------------------------------------------------
        // Check: is this an AcroForm with at least one field?
        //------------------------------------------------------------------
        CLOSECHECK0(is_form_pdf, """Either False or PDF field count.""")
        %pythoncode%{@property%}
        PyObject *is_form_pdf()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_FALSE;  // not a PDF
            int count = -1;  // init count
            fz_try(gctx) {
                pdf_obj *fields = pdf_dict_getl(gctx,
                                                pdf_trailer(gctx, pdf),
                                                PDF_NAME(Root),
                                                PDF_NAME(AcroForm),
                                                PDF_NAME(Fields),
                                                NULL);
                if (pdf_is_array(gctx, fields)) {
                    count = pdf_array_len(gctx, fields);
                }
            }
            fz_catch(gctx) {
                Py_RETURN_FALSE;
            }
            if (count >= 0) {
                return Py_BuildValue("i", count);
            } else {
                Py_RETURN_FALSE;
            }
        }

        //------------------------------------------------------------------
        // Return the list of field font resource names
        //------------------------------------------------------------------
        CLOSECHECK0(FormFonts, """Get list of field font resource names.""")
        %pythoncode%{@property%}
        PyObject *FormFonts()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_NONE;           // not a PDF
            pdf_obj *fonts = NULL;
            PyObject *liste = PyList_New(0);
            fz_var(liste);
            fz_try(gctx) {
                fonts = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(DR), PDF_NAME(Font), NULL);
                if (fonts && pdf_is_dict(gctx, fonts))       // fonts exist
                {
                    int i, n = pdf_dict_len(gctx, fonts);
                    for (i = 0; i < n; i++)
                    {
                        pdf_obj *f = pdf_dict_get_key(gctx, fonts, i);
                        LIST_APPEND_DROP(liste, JM_UnicodeFromStr(pdf_to_name(gctx, f)));
                    }
                }
            }
            fz_catch(gctx) {
                Py_DECREF(liste);
                Py_RETURN_NONE;  // any problem yields None
            }
            return liste;
        }

        //------------------------------------------------------------------
        // Add a field font
        //------------------------------------------------------------------
        FITZEXCEPTION(_addFormFont, !result)
        CLOSECHECK(_addFormFont, """Add new form font.""")
        PyObject *_addFormFont(char *name, char *font)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_NONE;  // not a PDF
            pdf_obj *fonts = NULL;
            fz_try(gctx) {
                fonts = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root),
                             PDF_NAME(AcroForm), PDF_NAME(DR), PDF_NAME(Font), NULL);
                if (!fonts || !pdf_is_dict(gctx, fonts)) {
                    RAISEPY(gctx, "PDF has no form fonts yet", PyExc_RuntimeError);
                }
                pdf_obj *k = pdf_new_name(gctx, (const char *) name);
                pdf_obj *v = JM_pdf_obj_from_str(gctx, pdf, font);
                pdf_dict_put(gctx, fonts, k, v);
            }
            fz_catch(gctx) NULL;
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // Get Xref Number of Outline Root, create it if missing
        //------------------------------------------------------------------
        FITZEXCEPTION(_getOLRootNumber, !result)
        CLOSECHECK(_getOLRootNumber, """Get xref of Outline Root, create it if missing.""")
        PyObject *_getOLRootNumber()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            pdf_obj *ind_obj = NULL;
            pdf_obj *olroot2 = NULL;
            int ret;
            fz_var(ind_obj);
            fz_var(olroot2);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                // get main root
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
                // get outline root
                pdf_obj *olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines));
                if (!olroot)
                {
                    olroot2 = pdf_new_dict(gctx, pdf, 4);
                    pdf_dict_put(gctx, olroot2, PDF_NAME(Type), PDF_NAME(Outlines));
                    ind_obj = pdf_add_object(gctx, pdf, olroot2);
                    pdf_dict_put(gctx, root, PDF_NAME(Outlines), ind_obj);
                    olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines));
                    
                }
                ret = pdf_to_num(gctx, olroot);
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, ind_obj);
                pdf_drop_obj(gctx, olroot2);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", ret);
        }

        //------------------------------------------------------------------
        // Get a new Xref number
        //------------------------------------------------------------------
        FITZEXCEPTION(get_new_xref, !result)
        CLOSECHECK(get_new_xref, """Make a new xref.""")
        PyObject *get_new_xref()
        {
            int xref = 0;
            fz_try(gctx) {
                fz_document *doc = (fz_document *) $self;
                pdf_document *pdf = pdf_specifics(gctx, doc);
                ASSERT_PDF(pdf);
                ENSURE_OPERATION(gctx, pdf);
                xref = pdf_create_object(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xref);
        }

        //------------------------------------------------------------------
        // Get Length of XREF table
        //------------------------------------------------------------------
        FITZEXCEPTION(xref_length, !result)
        CLOSECHECK0(xref_length, """Get length of xref table.""")
        PyObject *xref_length()
        {
            int xreflen = 0;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                if (pdf) xreflen = pdf_xref_len(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xreflen);
        }

        //------------------------------------------------------------------
        // Get XML Metadata
        //------------------------------------------------------------------
        CLOSECHECK0(get_xml_metadata, """Get document XML metadata.""")
        PyObject *get_xml_metadata()
        {
            PyObject *rc = NULL;
            fz_buffer *buff = NULL;
            pdf_obj *xml = NULL;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                if (pdf) {
                    xml = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root), PDF_NAME(Metadata), NULL);
                }
                if (xml) {
                    buff = pdf_load_stream(gctx, xml);
                    rc = JM_UnicodeFromBuffer(gctx, buff);
                } else {
                    rc = EMPTY_STRING;
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buff);
                PyErr_Clear();
            }
            fz_catch(gctx) {
                return EMPTY_STRING;
            }
            return rc;
        }

        //------------------------------------------------------------------
        // Get XML Metadata xref
        //------------------------------------------------------------------
        FITZEXCEPTION(xref_xml_metadata, !result)
        CLOSECHECK0(xref_xml_metadata, """Get xref of document XML metadata.""")
        PyObject *xref_xml_metadata()
        {
            int xref = 0;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
                ASSERT_PDF(pdf);
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
                if (!root) {
                    RAISEPY(gctx, MSG_BAD_PDFROOT, JM_Exc_FileDataError);
                }
                pdf_obj *xml = pdf_dict_get(gctx, root, PDF_NAME(Metadata));
                if (xml) xref = pdf_to_num(gctx, xml);
            }
            fz_catch(gctx) {;}
            return Py_BuildValue("i", xref);
        }

        //------------------------------------------------------------------
        // Delete XML Metadata
        //------------------------------------------------------------------
        FITZEXCEPTION(del_xml_metadata, !result)
        CLOSECHECK(del_xml_metadata, """Delete XML metadata.""")
        PyObject *del_xml_metadata()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
                if (root) pdf_dict_del(gctx, root, PDF_NAME(Metadata));
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // Set XML-based Metadata
        //------------------------------------------------------------------
        FITZEXCEPTION(set_xml_metadata, !result)
        CLOSECHECK(set_xml_metadata, """Store XML document level metadata.""")
        PyObject *set_xml_metadata(char *metadata)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_buffer *res = NULL;
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
                if (!root) {
                    RAISEPY(gctx, MSG_BAD_PDFROOT, JM_Exc_FileDataError);
                }
                res = fz_new_buffer_from_copied_data(gctx, (const unsigned char *) metadata, strlen(metadata));
                pdf_obj *xml = pdf_dict_get(gctx, root, PDF_NAME(Metadata));
                if (xml) {
                    JM_update_stream(gctx, pdf, xml, res, 0);
                } else {
                    xml = pdf_add_stream(gctx, pdf, res, NULL, 0);
                    pdf_dict_put(gctx, xml, PDF_NAME(Type), PDF_NAME(Metadata));
                    pdf_dict_put(gctx, xml, PDF_NAME(Subtype), PDF_NAME(XML));
                    pdf_dict_put_drop(gctx, root, PDF_NAME(Metadata), xml);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // Get Object String of xref
        //------------------------------------------------------------------
        FITZEXCEPTION(xref_object, !result)
        CLOSECHECK0(xref_object, """Get xref object source as a string.""")
        PyObject *xref_object(int xref, int compressed=0, int ascii=0)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            pdf_obj *obj = NULL;
            PyObject *text = NULL;
            fz_buffer *res=NULL;
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1) && xref != -1) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                if (xref > 0) {
                    obj = pdf_load_object(gctx, pdf, xref);
                } else {
                    obj = pdf_trailer(gctx, pdf);
                }
                res = JM_object_to_buffer(gctx, pdf_resolve_indirect(gctx, obj), compressed, ascii);
                text = JM_EscapeStrFromBuffer(gctx, res);
            }
            fz_always(gctx) {
                if (xref > 0) {
                    pdf_drop_obj(gctx, obj);
                }
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) return EMPTY_STRING;
            return text;
        }
        %pythoncode %{
        def pdf_trailer(self, compressed: bool=False, ascii:bool=False)->str:
            """Get PDF trailer as a string."""
            return self.xref_object(-1, compressed=compressed, ascii=ascii)%}


        //------------------------------------------------------------------
        // Get compressed stream of an object by xref
        // Py_RETURN_NONE if not stream
        //------------------------------------------------------------------
        FITZEXCEPTION(xref_stream_raw, !result)
        CLOSECHECK(xref_stream_raw, """Get xref stream without decompression.""")
        PyObject *xref_stream_raw(int xref)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            PyObject *r = NULL;
            pdf_obj *obj = NULL;
            fz_var(obj);
            fz_buffer *res = NULL;
            fz_var(res);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1) && xref != -1) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                if (xref >= 0) {
                    obj = pdf_new_indirect(gctx, pdf, xref, 0);
                } else {
                    obj = pdf_trailer(gctx, pdf);
                }
                if (pdf_is_stream(gctx, obj))
                {
                    res = pdf_load_raw_stream_number(gctx, pdf, xref);
                    r = JM_BinFromBuffer(gctx, res);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
                if (xref >= 0) {
                    pdf_drop_obj(gctx, obj);
                }
            }
            fz_catch(gctx)
            {
                Py_CLEAR(r);
                return NULL;
            }
            if (!r) Py_RETURN_NONE;
            return r;
        }

        //------------------------------------------------------------------
        // Get decompressed stream of an object by xref
        // Py_RETURN_NONE if not stream
        //------------------------------------------------------------------
        FITZEXCEPTION(xref_stream, !result)
        CLOSECHECK(xref_stream, """Get decompressed xref stream.""")
        PyObject *xref_stream(int xref)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            PyObject *r = Py_None;
            pdf_obj *obj = NULL;
            fz_var(obj);
            fz_buffer *res = NULL;
            fz_var(res);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1) && xref != -1) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                if (xref >= 0) {
                    obj = pdf_new_indirect(gctx, pdf, xref, 0);
                } else {
                    obj = pdf_trailer(gctx, pdf);
                }
                if (pdf_is_stream(gctx, obj))
                {
                    res = pdf_load_stream_number(gctx, pdf, xref);
                    r = JM_BinFromBuffer(gctx, res);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
                if (xref >= 0) {
                    pdf_drop_obj(gctx, obj);
                }
            }
            fz_catch(gctx)
            {
                Py_CLEAR(r);
                return NULL;
            }
            return r;
        }

        //------------------------------------------------------------------
        // Update an Xref number with a new object given as a string
        //------------------------------------------------------------------
        FITZEXCEPTION(update_object, !result)
        CLOSECHECK(update_object, """Replace object definition source.""")
        PyObject *update_object(int xref, char *text, struct Page *page = NULL)
        {
            pdf_obj *new_obj;
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1)) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                ENSURE_OPERATION(gctx, pdf);
                // create new object with passed-in string
                new_obj = JM_pdf_obj_from_str(gctx, pdf, text);
                pdf_update_object(gctx, pdf, xref, new_obj);
                pdf_drop_obj(gctx, new_obj);
                if (page) {
                    pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page);
                    JM_refresh_links(gctx, pdfpage);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // Update a stream identified by its xref
        //------------------------------------------------------------------
        FITZEXCEPTION(update_stream, !result)
        CLOSECHECK(update_stream, """Replace xref stream part.""")
        PyObject *update_stream(int xref=0, PyObject *stream=NULL, int new=1, int compress=1)
        {
            pdf_obj *obj = NULL;
            fz_var(obj);
            fz_buffer *res = NULL;
            fz_var(res);
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1)) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                ENSURE_OPERATION(gctx, pdf);
                // get the object
                obj = pdf_new_indirect(gctx, pdf, xref, 0);
                if (!pdf_is_dict(gctx, obj)) {
                    RAISEPY(gctx, MSG_IS_NO_DICT, PyExc_ValueError);
                }
                res = JM_BufferFromBytes(gctx, stream);
                if (!res) {
                    RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError);
                }
                JM_update_stream(gctx, pdf, obj, res, compress);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
                pdf_drop_obj(gctx, obj);
            }
            fz_catch(gctx)
                return NULL;
            
            Py_RETURN_NONE;
        }


        //------------------------------------------------------------------
        // create / refresh the page map
        //------------------------------------------------------------------
        FITZEXCEPTION(_make_page_map, !result)
        CLOSECHECK0(_make_page_map, """Make an array page number -> page object.""")
        PyObject *_make_page_map()
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            if (!pdf) Py_RETURN_NONE;
            fz_try(gctx) {
                pdf_drop_page_tree(gctx, pdf);
                pdf_load_page_tree(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", pdf->map_page_count);
        }


        //------------------------------------------------------------------
        // full (deep) copy of one page
        //------------------------------------------------------------------
        FITZEXCEPTION(fullcopy_page, !result)
        CLOSECHECK0(fullcopy_page, """Make a full page duplicate.""")
        %pythonappend fullcopy_page %{self._reset_page_refs()%}
        PyObject *fullcopy_page(int pno, int to = -1)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            int page_count = pdf_count_pages(gctx, pdf);
            fz_buffer *res = NULL, *nres=NULL;
            fz_buffer *contents_buffer = NULL;
            fz_var(pdf);
            fz_var(res);
            fz_var(nres);
            fz_var(contents_buffer);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                if (!INRANGE(pno, 0, page_count - 1) ||
                    !INRANGE(to, -1, page_count - 1)) {
                    RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError);
                }

                pdf_obj *page1 = pdf_resolve_indirect(gctx,
                                 pdf_lookup_page_obj(gctx, pdf, pno));

                pdf_obj *page2 = pdf_deep_copy_obj(gctx, page1);
                pdf_obj *old_annots = pdf_dict_get(gctx, page2, PDF_NAME(Annots));

                // copy annotations, but remove Popup and IRT types
                if (old_annots) {
                    int i, n = pdf_array_len(gctx, old_annots);
                    pdf_obj *new_annots = pdf_new_array(gctx, pdf, n);
                    for (i = 0; i < n; i++) {
                        pdf_obj *o = pdf_array_get(gctx, old_annots, i);
                        pdf_obj *subtype = pdf_dict_get(gctx, o, PDF_NAME(Subtype));
                        if (pdf_name_eq(gctx, subtype, PDF_NAME(Popup))) continue;
                        if (pdf_dict_gets(gctx, o, "IRT")) continue;
                        pdf_obj *copy_o = pdf_deep_copy_obj(gctx,
                                            pdf_resolve_indirect(gctx, o));
                        int xref = pdf_create_object(gctx, pdf);
                        pdf_update_object(gctx, pdf, xref, copy_o);
                        pdf_drop_obj(gctx, copy_o);
                        copy_o = pdf_new_indirect(gctx, pdf, xref, 0);
                        pdf_dict_del(gctx, copy_o, PDF_NAME(Popup));
                        pdf_dict_del(gctx, copy_o, PDF_NAME(P));
                        pdf_array_push_drop(gctx, new_annots, copy_o);
                    }
                pdf_dict_put_drop(gctx, page2, PDF_NAME(Annots), new_annots);
                }

                // copy the old contents stream(s)
                res = JM_read_contents(gctx, page1);

                // create new /Contents object for page2
                if (res) {
                    contents_buffer = fz_new_buffer_from_copied_data(gctx, "  ", 1);
                    pdf_obj *contents = pdf_add_stream(gctx, pdf, contents_buffer, NULL, 0);
                    JM_update_stream(gctx, pdf, contents, res, 1);
                    pdf_dict_put_drop(gctx, page2, PDF_NAME(Contents), contents);
                }

                // now insert target page, making sure it is an indirect object
                int xref = pdf_create_object(gctx, pdf);  // get new xref
                pdf_update_object(gctx, pdf, xref, page2);  // store new page
                pdf_drop_obj(gctx, page2);  // give up this object for now

                page2 = pdf_new_indirect(gctx, pdf, xref, 0);  // reread object
                pdf_insert_page(gctx, pdf, to, page2);  // and store the page
                pdf_drop_obj(gctx, page2);
            }
            fz_always(gctx) {
                pdf_drop_page_tree(gctx, pdf);
                fz_drop_buffer(gctx, res);
                fz_drop_buffer(gctx, nres);
                fz_drop_buffer(gctx, contents_buffer);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //------------------------------------------------------------------
        // move or copy one page
        //------------------------------------------------------------------
        FITZEXCEPTION(_move_copy_page, !result)
        CLOSECHECK0(_move_copy_page, """Move or copy a PDF page reference.""")
        %pythonappend _move_copy_page %{self._reset_page_refs()%}
        PyObject *_move_copy_page(int pno, int nb, int before, int copy)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            int i1, i2, pos, count, same = 0;
            pdf_obj *parent1 = NULL, *parent2 = NULL, *parent = NULL;
            pdf_obj *kids1, *kids2;
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                // get the two page objects -----------------------------------
                // locate the /Kids arrays and indices in each
                pdf_obj *page1 = pdf_lookup_page_loc(gctx, pdf, pno, &parent1, &i1);
                kids1 = pdf_dict_get(gctx, parent1, PDF_NAME(Kids));

                pdf_obj *page2 = pdf_lookup_page_loc(gctx, pdf, nb, &parent2, &i2);
                (void) page2;
                kids2 = pdf_dict_get(gctx, parent2, PDF_NAME(Kids));

                if (before)  // calc index of source page in target /Kids
                    pos = i2;
                else
                    pos = i2 + 1;

                // same /Kids array? ------------------------------------------
                same = pdf_objcmp(gctx, kids1, kids2);

                // put source page in target /Kids array ----------------------
                if (!copy && same != 0)  // update parent in page object
                {
                    pdf_dict_put(gctx, page1, PDF_NAME(Parent), parent2);
                }
                pdf_array_insert(gctx, kids2, page1, pos);

                if (same != 0) // different /Kids arrays ----------------------
                {
                    parent = parent2;
                    while (parent)  // increase /Count objects in parents
                    {
                        count = pdf_dict_get_int(gctx, parent, PDF_NAME(Count));
                        pdf_dict_put_int(gctx, parent, PDF_NAME(Count), count + 1);
                        parent = pdf_dict_get(gctx, parent, PDF_NAME(Parent));
                    }
                    if (!copy)  // delete original item
                    {
                        pdf_array_delete(gctx, kids1, i1);
                        parent = parent1;
                        while (parent) // decrease /Count objects in parents
                        {
                            count = pdf_dict_get_int(gctx, parent, PDF_NAME(Count));
                            pdf_dict_put_int(gctx, parent, PDF_NAME(Count), count - 1);
                            parent = pdf_dict_get(gctx, parent, PDF_NAME(Parent));
                        }
                    }
                }
                else {  // same /Kids array
                    if (copy) {  // source page is copied
                        parent = parent2;
                        while (parent) // increase /Count object in parents
                        {
                            count = pdf_dict_get_int(gctx, parent, PDF_NAME(Count));
                            pdf_dict_put_int(gctx, parent, PDF_NAME(Count), count + 1);
                            parent = pdf_dict_get(gctx, parent, PDF_NAME(Parent));
                        }
                    } else {
                        if (i1 < pos)
                            pdf_array_delete(gctx, kids1, i1);
                        else
                            pdf_array_delete(gctx, kids1, i1 + 1);
                    }
                }
                if (pdf->rev_page_map) {  // page map no longer valid: drop it
                    pdf_drop_page_tree(gctx, pdf);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION(_remove_toc_item, !result)
        PyObject *_remove_toc_item(int xref)
        {
            // "remove" bookmark by letting it point to nowhere
            pdf_obj *item = NULL, *color;
            int i;
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_try(gctx) {
                item = pdf_new_indirect(gctx, pdf, xref, 0);
                pdf_dict_del(gctx, item, PDF_NAME(Dest));
                pdf_dict_del(gctx, item, PDF_NAME(A));
                color = pdf_new_array(gctx, pdf, 3);
                for (i=0; i < 3; i++) {
                    pdf_array_push_real(gctx, color, 0.8);
                }
                pdf_dict_put_drop(gctx, item, PDF_NAME(C), color);
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, item);
            }
            fz_catch(gctx){
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION(_update_toc_item, !result)
        PyObject *_update_toc_item(int xref, char *action=NULL, char *title=NULL, int flags=0, PyObject *collapse=NULL, PyObject *color=NULL)
        {
            // "update" bookmark by letting it point to nowhere
            pdf_obj *item = NULL;
            pdf_obj *obj = NULL;
            Py_ssize_t i;
            double f;
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            fz_try(gctx) {
                item = pdf_new_indirect(gctx, pdf, xref, 0);
                if (title) {
                    pdf_dict_put_text_string(gctx, item, PDF_NAME(Title), title);
                }
                if (action) {
                    pdf_dict_del(gctx, item, PDF_NAME(Dest));
                    obj = JM_pdf_obj_from_str(gctx, pdf, action);
                    pdf_dict_put_drop(gctx, item, PDF_NAME(A), obj);
                }
                pdf_dict_put_int(gctx, item, PDF_NAME(F), flags);
                if (EXISTS(color)) {
                    pdf_obj *c = pdf_new_array(gctx, pdf, 3);
                    for (i = 0; i < 3; i++) {
                        JM_FLOAT_ITEM(color, i, &f);
                        pdf_array_push_real(gctx, c, f);
                    }
                    pdf_dict_put_drop(gctx, item, PDF_NAME(C), c);
                } else if (color != Py_None) {
                    pdf_dict_del(gctx, item, PDF_NAME(C));
                }
                if (collapse != Py_None) {
                    if (pdf_dict_get(gctx, item, PDF_NAME(Count))) {
                        i = pdf_dict_get_int(gctx, item, PDF_NAME(Count));
                        if ((i < 0 && collapse == Py_False) || (i > 0 && collapse == Py_True)) {
                            i = i * (-1);
                            pdf_dict_put_int(gctx, item, PDF_NAME(Count), i);
                        }
                    }
                }
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, item);
            }
            fz_catch(gctx){
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //------------------------------------------------------------------
        // PDF page label getting / setting
        //------------------------------------------------------------------
        FITZEXCEPTION(_get_page_labels, !result)
        PyObject *
        _get_page_labels()
        {
            pdf_obj *obj, *nums, *kids;
            PyObject *rc = NULL;
            int i, n;
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);

            pdf_obj *pagelabels = NULL;
            fz_var(pagelabels);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                rc = PyList_New(0);
                pagelabels = pdf_new_name(gctx, "PageLabels");
                obj = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                   PDF_NAME(Root), pagelabels, NULL);
                if (!obj) {
                    goto finished;
                }
                // simple case: direct /Nums object
                nums = pdf_resolve_indirect(gctx,
                       pdf_dict_get(gctx, obj, PDF_NAME(Nums)));
                if (nums) {
                    JM_get_page_labels(gctx, rc, nums);
                    goto finished;
                }
                // case: /Kids/Nums
                nums = pdf_resolve_indirect(gctx,
                           pdf_dict_getl(gctx, obj, PDF_NAME(Kids), PDF_NAME(Nums), NULL)
                );
                if (nums) {
                    JM_get_page_labels(gctx, rc, nums);
                    goto finished;
                }
                // case: /Kids is an array of multiple /Nums
                kids = pdf_resolve_indirect(gctx,
                       pdf_dict_get(gctx, obj, PDF_NAME(Kids)));
                if (!kids || !pdf_is_array(gctx, kids)) {
                    goto finished;
                }

                n = pdf_array_len(gctx, kids);
                for (i = 0; i < n; i++) {
                    nums = pdf_resolve_indirect(gctx,
                           pdf_dict_get(gctx,
                           pdf_array_get(gctx, kids, i),
                           PDF_NAME(Nums)));
                    JM_get_page_labels(gctx, rc, nums);
                }
                finished:;
            }
            fz_always(gctx) {
                PyErr_Clear();
                pdf_drop_obj(gctx, pagelabels);
            }
            fz_catch(gctx){
                Py_CLEAR(rc);
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(_set_page_labels, !result)
        %pythonappend _set_page_labels %{
        xref = self.pdf_catalog()
        text = self.xref_object(xref, compressed=True)
        text = text.replace("/Nums[]", "/Nums[%s]" % labels)
        self.update_object(xref, text)%}
        PyObject *
        _set_page_labels(char *labels)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self);
            pdf_obj *pagelabels = NULL;
            fz_var(pagelabels);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                pagelabels = pdf_new_name(gctx, "PageLabels");
                pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root));
                pdf_dict_del(gctx, root, pagelabels);
                pdf_dict_putl_drop(gctx, root, pdf_new_array(gctx, pdf, 0), pagelabels, PDF_NAME(Nums), NULL);
            }
            fz_always(gctx) {
                PyErr_Clear();
                pdf_drop_obj(gctx, pagelabels);
            }
            fz_catch(gctx){
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //------------------------------------------------------------------
        // PDF Optional Content functions
        //------------------------------------------------------------------
        FITZEXCEPTION(get_layers, !result)
        CLOSECHECK0(get_layers, """Show optional OC layers.""")
        PyObject *
        get_layers()
        {
            PyObject *rc = NULL;
            pdf_layer_config info = {NULL, NULL};
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                int i, n = pdf_count_layer_configs(gctx, pdf);
                if (n == 1) {
                    pdf_obj *obj = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                   PDF_NAME(Root), PDF_NAME(OCProperties), PDF_NAME(Configs), NULL);
                    if (!pdf_is_array(gctx, obj)) n = 0;
                }
                rc = PyTuple_New(n);
                for (i = 0; i < n; i++) {
                    pdf_layer_config_info(gctx, pdf, i, &info);
                    PyObject *item = Py_BuildValue("{s:i,s:s,s:s}",
                        "number", i, "name", info.name, "creator", info.creator);
                    PyTuple_SET_ITEM(rc, i, item);
                    info.name = NULL;
                    info.creator = NULL;
                }
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(switch_layer, !result)
        CLOSECHECK0(switch_layer, """Activate an OC layer.""")
        PyObject *
        switch_layer(int config, int as_default=0)
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                pdf_obj *cfgs = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                   PDF_NAME(Root), PDF_NAME(OCProperties), PDF_NAME(Configs), NULL);
                if (!pdf_is_array(gctx, cfgs) || !pdf_array_len(gctx, cfgs)) {
                    if (config < 1) goto finished;
                    RAISEPY(gctx, MSG_BAD_OC_LAYER, PyExc_ValueError);
                }
                if (config < 0) goto finished;
                pdf_select_layer_config(gctx, pdf, config);
                if (as_default) {
                    pdf_set_layer_config_as_default(gctx, pdf);
                    pdf_read_ocg(gctx, pdf);
                }
                finished:;
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(get_layer, !result)
        CLOSECHECK0(get_layer, """Content of ON, OFF, RBGroups of an OC layer.""")
        PyObject *
        get_layer(int config=-1)
        {
            PyObject *rc;
            pdf_obj *obj = NULL;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                pdf_obj *ocp = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                   PDF_NAME(Root), PDF_NAME(OCProperties), NULL);
                if (!ocp) {
                    rc = Py_BuildValue("s", NULL);
                    goto finished;
                }
                if (config == -1) {
                    obj = pdf_dict_get(gctx, ocp, PDF_NAME(D));
                } else {
                    obj = pdf_array_get(gctx, pdf_dict_get(gctx, ocp, PDF_NAME(Configs)), config);
                }
                if (!obj) {
                    RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError);
                }
                rc = JM_get_ocg_arrays(gctx, obj);
                finished:;
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                PyErr_Clear();
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(set_layer, !result)
        %pythonprepend set_layer
%{"""Set the PDF keys /ON, /OFF, /RBGroups of an OC layer."""
if self.is_closed:
    raise ValueError("document closed")
ocgs = set(self.get_ocgs().keys())
if ocgs == set():
    raise ValueError("document has no optional content")

if on:
    if type(on) not in (list, tuple):
        raise ValueError("bad type: 'on'")
    s = set(on).difference(ocgs)
    if s != set():
        raise ValueError("bad OCGs in 'on': %s" % s)

if off:
    if type(off) not in (list, tuple):
        raise ValueError("bad type: 'off'")
    s = set(off).difference(ocgs)
    if s != set():
        raise ValueError("bad OCGs in 'off': %s" % s)

if locked:
    if type(locked) not in (list, tuple):
        raise ValueError("bad type: 'locked'")
    s = set(locked).difference(ocgs)
    if s != set():
        raise ValueError("bad OCGs in 'locked': %s" % s)

if rbgroups:
    if type(rbgroups) not in (list, tuple):
        raise ValueError("bad type: 'rbgroups'")
    for x in rbgroups:
        if not type(x) in (list, tuple):
            raise ValueError("bad RBGroup '%s'" % x)
        s = set(x).difference(ocgs)
        if s != set():
            raise ValueError("bad OCGs in RBGroup: %s" % s)

if basestate:
    basestate = str(basestate).upper()
    if basestate == "UNCHANGED":
        basestate = "Unchanged"
    if basestate not in ("ON", "OFF", "Unchanged"):
        raise ValueError("bad 'basestate'")
%}
        PyObject *
        set_layer(int config, const char *basestate=NULL, PyObject *on=NULL,
                    PyObject *off=NULL, PyObject *rbgroups=NULL, PyObject *locked=NULL)
        {
            pdf_obj *obj = NULL;
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                pdf_obj *ocp = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf),
                                   PDF_NAME(Root), PDF_NAME(OCProperties), NULL);
                if (!ocp) {
                    goto finished;
                }
                if (config == -1) {
                    obj = pdf_dict_get(gctx, ocp, PDF_NAME(D));
                } else {
                    obj = pdf_array_get(gctx, pdf_dict_get(gctx, ocp, PDF_NAME(Configs)), config);
                }
                if (!obj) {
                    RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError);
                }
                JM_set_ocg_arrays(gctx, obj, basestate, on, off, rbgroups, locked);
                pdf_read_ocg(gctx, pdf);
                finished:;
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(add_layer, !result)
        CLOSECHECK0(add_layer, """Add a new OC layer.""")
        PyObject *add_layer(char *name, char *creator=NULL, PyObject *on=NULL)
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                JM_add_layer_config(gctx, pdf, name, creator, on);
                pdf_read_ocg(gctx, pdf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(layer_ui_configs, !result)
        CLOSECHECK0(layer_ui_configs, """Show OC visibility status modifiable by user.""")
        PyObject *layer_ui_configs()
        {
            typedef struct
            {
                const char *text;
                int depth;
                pdf_layer_config_ui_type type;
                int selected;
                int locked;
            } pdf_layer_config_ui;
            PyObject *rc = NULL;

            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                pdf_layer_config_ui info;
                int i, n = pdf_count_layer_config_ui(gctx, pdf);
                rc = PyTuple_New(n);
                char *type = NULL;
                for (i = 0; i < n; i++) {
                    pdf_layer_config_ui_info(gctx, pdf, i, (void *) &info);
                    switch (info.type)
                    {
                        case (1): type = "checkbox"; break;
                        case (2): type = "radiobox"; break;
                        default: type = "label"; break;
                    }
                    PyObject *item = Py_BuildValue("{s:i,s:N,s:i,s:s,s:N,s:N}",
                        "number", i,
                        "text", JM_UnicodeFromStr(info.text),
                        "depth", info.depth,
                        "type", type,
                        "on", JM_BOOL(info.selected),
                        "locked", JM_BOOL(info.locked));
                    PyTuple_SET_ITEM(rc, i, item);
                }
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(set_layer_ui_config, !result)
        CLOSECHECK0(set_layer_ui_config, )
        %pythonprepend set_layer_ui_config %{
        """Set / unset OC intent configuration."""
        # The user might have given the name instead of sequence number, 
        # so select by that name and continue with corresp. number
        if isinstance(number, str):
            select = [ui["number"] for ui in self.layer_ui_configs() if ui["text"] == number]
            if select == []:
                raise ValueError(f"bad OCG '{number}'.")
            number = select[0]  # this is the number for the name
        %}
        PyObject *set_layer_ui_config(int number, int action=0)
        {
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                switch (action)
                {
                    case (1):
                        pdf_toggle_layer_config_ui(gctx, pdf, number);
                        break;
                    case (2):
                        pdf_deselect_layer_config_ui(gctx, pdf, number);
                        break;
                    default:
                        pdf_select_layer_config_ui(gctx, pdf, number);
                        break;
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(get_ocgs, !result)
        CLOSECHECK0(get_ocgs, """Show existing optional content groups.""")
        PyObject *
        get_ocgs()
        {
            PyObject *rc = NULL;
            pdf_obj *ci = pdf_new_name(gctx, "CreatorInfo");
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);
                pdf_obj *ocgs = pdf_dict_getl(gctx,
                                pdf_dict_get(gctx,
                                pdf_trailer(gctx, pdf), PDF_NAME(Root)),
                                PDF_NAME(OCProperties), PDF_NAME(OCGs), NULL);
                rc = PyDict_New();
                if (!pdf_is_array(gctx, ocgs)) goto fertig;
                int i, n = pdf_array_len(gctx, ocgs);
                for (i = 0; i < n; i++) {
                    pdf_obj *ocg = pdf_array_get(gctx, ocgs, i);
                    int xref = pdf_to_num(gctx, ocg);
                    const char *name = pdf_to_text_string(gctx, pdf_dict_get(gctx, ocg, PDF_NAME(Name)));
                    pdf_obj *obj = pdf_dict_getl(gctx, ocg, PDF_NAME(Usage), ci, PDF_NAME(Subtype), NULL);
                    const char *usage = NULL;
                    if (obj) usage = pdf_to_name(gctx, obj);
                    PyObject *intents = PyList_New(0);
                    pdf_obj *intent = pdf_dict_get(gctx, ocg, PDF_NAME(Intent));
                    if (intent) {
                        if (pdf_is_name(gctx, intent)) {
                            LIST_APPEND_DROP(intents, Py_BuildValue("s", pdf_to_name(gctx, intent)));
                        } else if (pdf_is_array(gctx, intent)) {
                            int j, m = pdf_array_len(gctx, intent);
                            for (j = 0; j < m; j++) {
                                pdf_obj *o = pdf_array_get(gctx, intent, j);
                                if (pdf_is_name(gctx, o))
                                    LIST_APPEND_DROP(intents, Py_BuildValue("s", pdf_to_name(gctx, o)));
                            }
                        }
                    }
                    int hidden = pdf_is_ocg_hidden(gctx, pdf, NULL, usage, ocg);
                    PyObject *item = Py_BuildValue("{s:s,s:O,s:O,s:s}",
                            "name", name,
                            "intent", intents,
                            "on", JM_BOOL(!hidden),
                            "usage", usage);
                    Py_DECREF(intents);
                    PyObject *temp = Py_BuildValue("i", xref);
                    DICT_SETITEM_DROP(rc, temp, item);
                    Py_DECREF(temp);
                }
                fertig:;
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, ci);
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(add_ocg, !result)
        CLOSECHECK0(add_ocg, """Add new optional content group.""")
        PyObject *
        add_ocg(char *name, int config=-1, int on=1, PyObject *intent=NULL, const char *usage=NULL)
        {
            int xref = 0;
            pdf_obj *obj = NULL, *cfg = NULL;
            pdf_obj *indocg = NULL;
            pdf_obj *ocg = NULL;
            pdf_obj *ci_name = NULL;
            fz_var(indocg);
            fz_var(ocg);
            fz_var(ci_name);
            fz_try(gctx) {
                pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self);
                ASSERT_PDF(pdf);

                // ------------------------------
                // make the OCG
                // ------------------------------
                ocg = pdf_add_new_dict(gctx, pdf, 3);
                pdf_dict_put(gctx, ocg, PDF_NAME(Type), PDF_NAME(OCG));
                pdf_dict_put_text_string(gctx, ocg, PDF_NAME(Name), name);
                pdf_obj *intents = pdf_dict_put_array(gctx, ocg, PDF_NAME(Intent), 2);
                if (!EXISTS(intent)) {
                    pdf_array_push(gctx, intents, PDF_NAME(View));
                } else if (!PyUnicode_Check(intent)) {
                    int i, n = PySequence_Size(intent);
                    for (i = 0; i < n; i++) {
                        PyObject *item = PySequence_ITEM(intent, i);
                        char *c = JM_StrAsChar(item);
                        if (c) {
                            pdf_array_push_drop(gctx, intents, pdf_new_name(gctx, c));
                        }
                        Py_DECREF(item);
                    }
                } else {
                    char *c = JM_StrAsChar(intent);
                    if (c) {
                        pdf_array_push_drop(gctx, intents, pdf_new_name(gctx, c));
                    }
                }
                pdf_obj *use_for = pdf_dict_put_dict(gctx, ocg, PDF_NAME(Usage), 3);
                ci_name = pdf_new_name(gctx, "CreatorInfo");
                pdf_obj *cre_info = pdf_dict_put_dict(gctx, use_for, ci_name, 2);
                pdf_dict_put_text_string(gctx, cre_info, PDF_NAME(Creator), "PyMuPDF");
                if (usage) {
                    pdf_dict_put_name(gctx, cre_info, PDF_NAME(Subtype), usage);
                } else {
                    pdf_dict_put_name(gctx, cre_info, PDF_NAME(Subtype), "Artwork");
                }
                indocg = pdf_add_object(gctx, pdf, ocg);

                // ------------------------------
                // Insert OCG in the right config
                // ------------------------------
                pdf_obj *ocp = JM_ensure_ocproperties(gctx, pdf);
                obj = pdf_dict_get(gctx, ocp, PDF_NAME(OCGs));
                pdf_array_push(gctx, obj, indocg);

                if (config > -1) {
                    obj = pdf_dict_get(gctx, ocp, PDF_NAME(Configs));
                    if (!pdf_is_array(gctx, obj)) {
                        RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError);
                    }
                    cfg = pdf_array_get(gctx, obj, config);
                    if (!cfg) {
                        RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError);
                    }
                } else {
                    cfg = pdf_dict_get(gctx, ocp, PDF_NAME(D));
                }

                obj = pdf_dict_get(gctx, cfg, PDF_NAME(Order));
                if (!obj) {
                    obj = pdf_dict_put_array(gctx, cfg, PDF_NAME(Order), 1);
                }
                pdf_array_push(gctx, obj, indocg);
                if (on) {
                    obj = pdf_dict_get(gctx, cfg, PDF_NAME(ON));
                    if (!obj) {
                        obj = pdf_dict_put_array(gctx, cfg, PDF_NAME(ON), 1);
                    }
                } else {
                    obj = pdf_dict_get(gctx, cfg, PDF_NAME(OFF));
                    if (!obj) {
                        obj = pdf_dict_put_array(gctx, cfg, PDF_NAME(OFF), 1);
                    }
                }
                pdf_array_push(gctx, obj, indocg);

                // let MuPDF take note: re-read OCProperties
                pdf_read_ocg(gctx, pdf);

                xref = pdf_to_num(gctx, indocg);
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, indocg);
                pdf_drop_obj(gctx, ocg);
                pdf_drop_obj(gctx, ci_name);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xref);
        }
        
        struct Annot;

        void internal_keep_annot(struct Annot* annot)
        {
            pdf_keep_annot(gctx, (pdf_annot*) annot);
        }

        //------------------------------------------------------------------
        // Initialize document: set outline and metadata properties
        //------------------------------------------------------------------
        %pythoncode %{
            def init_doc(self):
                if self.is_encrypted:
                    raise ValueError("cannot initialize - document still encrypted")
                self._outline = self._loadOutline()
                if self._outline:
                    self._outline.thisown = True
                self.metadata = dict([(k,self._getMetadata(v)) for k,v in {'format':'format', 'title':'info:Title', 'author':'info:Author','subject':'info:Subject', 'keywords':'info:Keywords','creator':'info:Creator', 'producer':'info:Producer', 'creationDate':'info:CreationDate', 'modDate':'info:ModDate', 'trapped':'info:Trapped'}.items()])
                self.metadata['encryption'] = None if self._getMetadata('encryption')=='None' else self._getMetadata('encryption')

            outline = property(lambda self: self._outline)


            def get_page_fonts(self, pno: int, full: bool =False) -> list:
                """Retrieve a list of fonts used on a page.
                """
                if self.is_closed or self.is_encrypted:
                    raise ValueError("document closed or encrypted")
                if not self.is_pdf:
                    return ()
                if type(pno) is not int:
                    try:
                        pno = pno.number
                    except:
                        raise ValueError("need a Page or page number")
                val = self._getPageInfo(pno, 1)
                if full is False:
                    return [v[:-1] for v in val]
                return val


            def get_page_images(self, pno: int, full: bool =False) -> list:
                """Retrieve a list of images used on a page.
                """
                if self.is_closed or self.is_encrypted:
                    raise ValueError("document closed or encrypted")
                if not self.is_pdf:
                    return ()
                if type(pno) is not int:
                    try:
                        pno = pno.number
                    except:
                        raise ValueError("need a Page or page number")
                val = self._getPageInfo(pno, 2)
                if full is False:
                    return [v[:-1] for v in val]
                return val


            def get_page_xobjects(self, pno: int) -> list:
                """Retrieve a list of XObjects used on a page.
                """
                if self.is_closed or self.is_encrypted:
                    raise ValueError("document closed or encrypted")
                if not self.is_pdf:
                    return ()
                if type(pno) is not int:
                    try:
                        pno = pno.number
                    except:
                        raise ValueError("need a Page or page number")
                val = self._getPageInfo(pno, 3)
                rc = [(v[0], v[1], v[2], Rect(v[3])) for v in val]
                return rc


            def xref_is_image(self, xref):
                """Check if xref is an image object."""
                if self.is_closed or self.is_encrypted:
                    raise ValueError("document closed or encrypted")
                if self.xref_get_key(xref, "Subtype")[1] == "/Image":
                    return True
                return False

            def xref_is_font(self, xref):
                """Check if xref is a font object."""
                if self.is_closed or self.is_encrypted:
                    raise ValueError("document closed or encrypted")
                if self.xref_get_key(xref, "Type")[1] == "/Font":
                    return True
                return False

            def xref_is_xobject(self, xref):
                """Check if xref is a form xobject."""
                if self.is_closed or self.is_encrypted:
                    raise ValueError("document closed or encrypted")
                if self.xref_get_key(xref, "Subtype")[1] == "/Form":
                    return True
                return False

            def copy_page(self, pno: int, to: int =-1):
                """Copy a page within a PDF document.

                This will only create another reference of the same page object.
                Args:
                    pno: source page number
                    to: put before this page, '-1' means after last page.
                """
                if self.is_closed:
                    raise ValueError("document closed")

                page_count = len(self)
                if (
                    pno not in range(page_count) or
                    to not in range(-1, page_count)
                   ):
                    raise ValueError("bad page number(s)")
                before = 1
                copy = 1
                if to == -1:
                    to = page_count - 1
                    before = 0

                return self._move_copy_page(pno, to, before, copy)

            def move_page(self, pno: int, to: int =-1):
                """Move a page within a PDF document.

                Args:
                    pno: source page number.
                    to: put before this page, '-1' means after last page.
                """
                if self.is_closed:
                    raise ValueError("document closed")

                page_count = len(self)
                if (
                    pno not in range(page_count) or
                    to not in range(-1, page_count)
                   ):
                    raise ValueError("bad page number(s)")
                before = 1
                copy = 0
                if to == -1:
                    to = page_count - 1
                    before = 0

                return self._move_copy_page(pno, to, before, copy)

            def delete_page(self, pno: int =-1):
                """ Delete one page from a PDF.
                """
                if not self.is_pdf:
                    raise ValueError("is no PDF")
                if self.is_closed:
                    raise ValueError("document closed")

                page_count = self.page_count
                while pno < 0:
                    pno += page_count

                if pno >= page_count:
                    raise ValueError("bad page number(s)")

                # remove TOC bookmarks pointing to deleted page
                toc = self.get_toc()
                ol_xrefs = self.get_outline_xrefs()
                for i, item in enumerate(toc):
                    if item[2] == pno + 1:
                        self._remove_toc_item(ol_xrefs[i])

                self._remove_links_to(frozenset((pno,)))
                self._delete_page(pno)
                self._reset_page_refs()


            def delete_pages(self, *args, **kw):
                """Delete pages from a PDF.

                Args:
                    Either keywords 'from_page'/'to_page', or two integers to
                    specify the first/last page to delete.
                    Or a list/tuple/range object, which can contain arbitrary
                    page numbers.
                """
                if not self.is_pdf:
                    raise ValueError("is no PDF")
                if self.is_closed:
                    raise ValueError("document closed")

                page_count = self.page_count  # page count of document
                f = t = -1
                if kw:  # check if keywords were used
                    if args:  # then no positional args are allowed
                        raise ValueError("cannot mix keyword and positional argument")
                    f = kw.get("from_page", -1)  # first page to delete
                    t = kw.get("to_page", -1)  # last page to delete
                    while f < 0:
                        f += page_count
                    while t < 0:
                        t += page_count
                    if not f <= t < page_count:
                        raise ValueError("bad page number(s)")
                    numbers = tuple(range(f, t + 1))
                else:
                    if len(args) > 2 or args == []:
                        raise ValueError("need 1 or 2 positional arguments")
                    if len(args) == 2:
                        f, t = args
                        if not (type(f) is int and type(t) is int):
                            raise ValueError("both arguments must be int")
                        if f > t:
                            f, t = t, f
                        if not f <= t < page_count:
                            raise ValueError("bad page number(s)")
                        numbers = tuple(range(f, t + 1))
                    else:
                        r = args[0]
                        if type(r) not in (int, range, list, tuple):
                            raise ValueError("need int or sequence if one argument")
                        numbers = tuple(r)

                numbers = list(map(int, set(numbers)))  # ensure unique integers
                if numbers == []:
                    print("nothing to delete")
                    return
                numbers.sort()
                if numbers[0] < 0 or numbers[-1] >= page_count:
                    raise ValueError("bad page number(s)")
                frozen_numbers = frozenset(numbers)
                toc = self.get_toc()
                for i, xref in enumerate(self.get_outline_xrefs()):
                    if toc[i][2] - 1 in frozen_numbers:
                        self._remove_toc_item(xref)  # remove target in PDF object

                self._remove_links_to(frozen_numbers)

                for i in reversed(numbers):  # delete pages, last to first
                    self._delete_page(i)

                self._reset_page_refs()


            def saveIncr(self):
                """ Save PDF incrementally"""
                return self.save(self.name, incremental=True, encryption=PDF_ENCRYPT_KEEP)


            def ez_save(self, filename, garbage=3, clean=False,
            deflate=True, deflate_images=True, deflate_fonts=True,
            incremental=False, ascii=False, expand=False, linear=False,
            pretty=False, encryption=1, permissions=4095,
            owner_pw=None, user_pw=None, no_new_id=True):
                """ Save PDF using some different defaults"""
                return self.save(filename, garbage=garbage,
                clean=clean,
                deflate=deflate,
                deflate_images=deflate_images,
                deflate_fonts=deflate_fonts,
                incremental=incremental,
                ascii=ascii,
                expand=expand,
                linear=linear,
                pretty=pretty,
                encryption=encryption,
                permissions=permissions,
                owner_pw=owner_pw,
                user_pw=user_pw,
                no_new_id=no_new_id,)


            def reload_page(self, page: "struct Page *") -> "struct Page *":
                """Make a fresh copy of a page."""
                old_annots = {}  # copy annot references to here
                pno = page.number  # save the page number
                for k, v in page._annot_refs.items():  # save the annot dictionary
                    # We need to call pdf_keep_annot() here, otherwise `v`'s
                    # refcount can reach zero even if there is an external
                    # reference.
                    self.internal_keep_annot(v)
                    old_annots[k] = v
                page._erase()  # remove the page
                page = None
                TOOLS.store_shrink(100)
                page = self.load_page(pno)  # reload the page

                # copy annot refs over to the new dictionary
                page_proxy = weakref.proxy(page)
                for k, v in old_annots.items():
                    annot = old_annots[k]
                    annot.parent = page_proxy  # refresh parent to new page
                    page._annot_refs[k] = annot
                return page


            @property
            def pagemode(self) -> str:
                """Return the PDF PageMode value.
                """
                xref = self.pdf_catalog()
                if xref == 0:
                    return None
                rc = self.xref_get_key(xref, "PageMode")
                if rc[0] == "null":
                    return "UseNone"
                if rc[0] == "name":
                    return rc[1][1:]
                return "UseNone"


            def set_pagemode(self, pagemode: str):
                """Set the PDF PageMode value."""
                valid = ("UseNone", "UseOutlines", "UseThumbs", "FullScreen", "UseOC", "UseAttachments")
                xref = self.pdf_catalog()
                if xref == 0:
                    raise ValueError("not a PDF")
                if not pagemode:
                    raise ValueError("bad PageMode value")
                if pagemode[0] == "/":
                    pagemode = pagemode[1:]
                for v in valid:
                    if pagemode.lower() == v.lower():
                        self.xref_set_key(xref, "PageMode", f"/{v}")
                        return True
                raise ValueError("bad PageMode value")


            @property
            def pagelayout(self) -> str:
                """Return the PDF PageLayout value.
                """
                xref = self.pdf_catalog()
                if xref == 0:
                    return None
                rc = self.xref_get_key(xref, "PageLayout")
                if rc[0] == "null":
                    return "SinglePage"
                if rc[0] == "name":
                    return rc[1][1:]
                return "SinglePage"


            def set_pagelayout(self, pagelayout: str):
                """Set the PDF PageLayout value."""
                valid = ("SinglePage", "OneColumn", "TwoColumnLeft", "TwoColumnRight", "TwoPageLeft", "TwoPageRight")
                xref = self.pdf_catalog()
                if xref == 0:
                    raise ValueError("not a PDF")
                if not pagelayout:
                    raise ValueError("bad PageLayout value")
                if pagelayout[0] == "/":
                    pagelayout = pagelayout[1:]
                for v in valid:
                    if pagelayout.lower() == v.lower():
                        self.xref_set_key(xref, "PageLayout", f"/{v}")
                        return True
                raise ValueError("bad PageLayout value")


            @property
            def markinfo(self) -> dict:
                """Return the PDF MarkInfo value."""
                xref = self.pdf_catalog()
                if xref == 0:
                    return None
                rc = self.xref_get_key(xref, "MarkInfo")
                if rc[0] == "null":
                    return {}
                if rc[0] == "xref":
                    xref = int(rc[1].split()[0])
                    val = self.xref_object(xref, compressed=True)
                elif rc[0] == "dict":
                    val = rc[1]
                else:
                    val = None
                if val == None or not (val[:2] == "<<" and val[-2:] == ">>"):
                    return {}
                valid = {"Marked": False, "UserProperties": False, "Suspects": False}
                val = val[2:-2].split("/")
                for v in val[1:]:
                    try:
                        key, value = v.split()
                    except:
                        return valid
                    if value == "true":
                        valid[key] = True
                return valid


            def set_markinfo(self, markinfo: dict) -> bool:
                """Set the PDF MarkInfo values."""
                xref = self.pdf_catalog()
                if xref == 0:
                    raise ValueError("not a PDF")
                if not markinfo or not isinstance(markinfo, dict):
                    return False
                valid = {"Marked": False, "UserProperties": False, "Suspects": False}
                
                if not set(valid.keys()).issuperset(markinfo.keys()):
                    badkeys = f"bad MarkInfo key(s): {set(markinfo.keys()).difference(valid.keys())}"
                    raise ValueError(badkeys)
                pdfdict = "<<"
                valid.update(markinfo)
                for key, value in valid.items():
                    value=str(value).lower()
                    if not value in ("true", "false"):
                        raise ValueError(f"bad key value '{key}': '{value}'")
                    pdfdict += f"/{key} {value}"
                pdfdict += ">>"
                self.xref_set_key(xref, "MarkInfo", pdfdict)
                return True


            def __repr__(self) -> str:
                m = "closed " if self.is_closed else ""
                if self.stream is None:
                    if self.name == "":
                        return m + "Document(<new PDF, doc# %i>)" % self._graft_id
                    return m + "Document('%s')" % (self.name,)
                return m + "Document('%s', <memory, doc# %i>)" % (self.name, self._graft_id)


            def __contains__(self, loc) -> bool:
                if type(loc) is int:
                    if loc < self.page_count:
                        return True
                    return False
                if type(loc) not in (tuple, list) or len(loc) != 2:
                    return False

                chapter, pno = loc
                if (type(chapter) != int or
                    chapter < 0 or
                    chapter >= self.chapter_count
                    ):
                    return False
                if (type(pno) != int or
                    pno < 0 or
                    pno >= self.chapter_page_count(chapter)
                    ):
                    return False

                return True


            def __getitem__(self, i: int =0)->"Page":
                assert isinstance(i, int) or (isinstance(i, tuple) and len(i) == 2 and all(isinstance(x, int) for x in i))
                if i not in self:
                    raise IndexError("page not in document")
                return self.load_page(i)


            def __delitem__(self, i: AnyType)->None:
                if not self.is_pdf:
                    raise ValueError("is no PDF")
                if type(i) is int:
                    return self.delete_page(i)
                if type(i) in (list, tuple, range):
                    return self.delete_pages(i)
                if type(i) is not slice:
                    raise ValueError("bad argument type")
                pc = self.page_count
                start = i.start if i.start else 0
                stop = i.stop if i.stop else pc
                step = i.step if i.step else 1
                while start < 0:
                    start += pc
                if start >= pc:
                    raise ValueError("bad page number(s)")
                while stop < 0:
                    stop += pc
                if stop > pc:
                    raise ValueError("bad page number(s)")
                return self.delete_pages(range(start, stop, step))


            def pages(self, start: OptInt =None, stop: OptInt =None, step: OptInt =None):
                """Return a generator iterator over a page range.

                Arguments have the same meaning as for the range() built-in.
                """
                # set the start value
                start = start or 0
                while start < 0:
                    start += self.page_count
                if start not in range(self.page_count):
                    raise ValueError("bad start page number")

                # set the stop value
                stop = stop if stop is not None and stop <= self.page_count else self.page_count

                # set the step value
                if step == 0:
                    raise ValueError("arg 3 must not be zero")
                if step is None:
                    if start > stop:
                        step = -1
                    else:
                        step = 1

                for pno in range(start, stop, step):
                    yield (self.load_page(pno))


            def __len__(self) -> int:
                return self.page_count

            def _forget_page(self, page: "struct Page *"):
                """Remove a page from document page dict."""
                pid = id(page)
                if pid in self._page_refs:
                    self._page_refs[pid] = None

            def _reset_page_refs(self):
                """Invalidate all pages in document dictionary."""
                if getattr(self, "is_closed", True):
                    return
                for page in self._page_refs.values():
                    if page:
                        page._erase()
                        page = None
                self._page_refs.clear()



            def _cleanup(self):
                self._reset_page_refs()
                for k in self.Graftmaps.keys():
                    self.Graftmaps[k] = None
                self.Graftmaps = {}
                self.ShownPages = {}
                self.InsertedImages  = {}
                self.FontInfos   = []
                self.metadata    = None
                self.stream      = None
                self.is_closed = True


            def close(self):
                """Close the document."""
                if getattr(self, "is_closed", False):
                    raise ValueError("document closed")
                self._cleanup()
                if getattr(self, "thisown", False):
                    self.__swig_destroy__(self)
                    return
                else:
                    raise RuntimeError("document object unavailable")

            def __del__(self):
                if not type(self) is Document:
                    return
                self._cleanup()
                if getattr(self, "thisown", False):
                    self.__swig_destroy__(self)

            def __enter__(self):
                return self

            def __exit__(self, *args):
                self.close()
            %}
    }
};

/*****************************************************************************/
// fz_page
/*****************************************************************************/
%nodefaultctor;
struct Page {
    %extend {
        ~Page()
        {
            DEBUGMSG1("Page");
            fz_page *this_page = (fz_page *) $self;
            fz_drop_page(gctx, this_page);
            DEBUGMSG2;
        }
        //----------------------------------------------------------------
        // bound()
        //----------------------------------------------------------------
        FITZEXCEPTION(bound, !result)
        PARENTCHECK(bound, """Get page rectangle.""")
        %pythonappend bound %{
        val = Rect(val)
        if val.is_infinite and self.parent.is_pdf:
            cb = self.cropbox
            w, h = cb.width, cb.height
            if self.rotation not in (0, 180):
                w, h = h, w
            val = Rect(0, 0, w, h)
            msg = TOOLS.mupdf_warnings(reset=False).splitlines()[-1]
            print(msg, file=sys.stderr)
        %}
        PyObject *bound() {
            fz_rect rect = fz_infinite_rect;
            fz_try(gctx) {
                rect = fz_bound_page(gctx, (fz_page *) $self);
            }
            fz_catch(gctx) {
                ;
            }
            return JM_py_from_rect(rect);
        }
        %pythoncode %{rect = property(bound, doc="page rectangle")%}

        //----------------------------------------------------------------
        // Page.get_image_bbox
        //----------------------------------------------------------------
        %pythonprepend get_image_bbox %{
        """Get rectangle occupied by image 'name'.

        'name' is either an item of the image list, or the referencing
        name string - elem[7] of the resp. item.
        Option 'transform' also returns the image transformation matrix.
        """
        CheckParent(self)
        doc = self.parent
        if doc.is_closed or doc.is_encrypted:
            raise ValueError("document closed or encrypted")

        inf_rect = Rect(1, 1, -1, -1)
        null_mat = Matrix()
        if transform:
            rc = (inf_rect, null_mat)
        else:
            rc = inf_rect

        if type(name) in (list, tuple):
            if not type(name[-1]) is int:
                raise ValueError("need item of full page image list")
            item = name
        else:
            imglist = [i for i in doc.get_page_images(self.number, True) if name == i[7]]
            if len(imglist) == 1:
                item = imglist[0]
            elif imglist == []:
                raise ValueError("bad image name")
            else:
                raise ValueError("found multiple images named '%s'." % name)
        xref = item[-1]
        if xref != 0 or transform == True:
            try:
                return self.get_image_rects(item, transform=transform)[0]
            except:
                return inf_rect
        %}
        %pythonappend get_image_bbox %{
        if not bool(val):
            return rc

        for v in val:
            if v[0] != item[-3]:
                continue
            q = Quad(v[1])
            bbox = q.rect
            if transform == 0:
                rc = bbox
                break

            hm = Matrix(util_hor_matrix(q.ll, q.lr))
            h = abs(q.ll - q.ul)
            w = abs(q.ur - q.ul)
            m0 = Matrix(1 / w, 0, 0, 1 / h, 0, 0)
            m = ~(hm * m0)
            rc = (bbox, m)
            break
        val = rc%}
        PyObject *
        get_image_bbox(PyObject *name, int transform=0)
        {
            pdf_page *pdf_page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            PyObject *rc =NULL;
            fz_try(gctx) {
                rc = JM_image_reporter(gctx, pdf_page);
            }
            fz_catch(gctx) {
                Py_RETURN_NONE;
            }
            return rc;
        }

        //----------------------------------------------------------------
        // run()
        //----------------------------------------------------------------
        FITZEXCEPTION(run, !result)
        PARENTCHECK(run, """Run page through a device.""")
        PyObject *run(struct DeviceWrapper *dw, PyObject *m)
        {
            fz_try(gctx) {
                fz_run_page(gctx, (fz_page *) $self, dw->device, JM_matrix_from_py(m), NULL);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // Page.extend_textpage
        //----------------------------------------------------------------
        FITZEXCEPTION(extend_textpage, !result)
        PyObject *
        extend_textpage(struct TextPage *tpage, int flags=0, PyObject *matrix=NULL)
        {
            fz_page *page = (fz_page *) $self;
            fz_stext_page *tp = (fz_stext_page *) tpage;
            fz_device *dev = NULL;
            fz_stext_options options;
            memset(&options, 0, sizeof options);
            options.flags = flags;
            fz_try(gctx) {
                fz_matrix ctm = JM_matrix_from_py(matrix);
                dev = fz_new_stext_device(gctx, tp, &options);
                fz_run_page(gctx, page, dev, ctm, NULL);
                fz_close_device(gctx, dev);
            }
            fz_always(gctx) {
                fz_drop_device(gctx, dev);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // Page.get_textpage
        //----------------------------------------------------------------
        FITZEXCEPTION(_get_textpage, !result)
        %pythonappend _get_textpage %{val.thisown = True%}
        struct TextPage *
        _get_textpage(PyObject *clip=NULL, int flags=0, PyObject *matrix=NULL)
        {
            fz_stext_page *tpage=NULL;
            fz_page *page = (fz_page *) $self;
            fz_device *dev = NULL;
            fz_stext_options options;
            memset(&options, 0, sizeof options);
            options.flags = flags;
            fz_try(gctx) {
                // Default to page's rect if `clip` not specified, for #2048.
                fz_rect rect = (clip==Py_None) ? fz_bound_page(gctx, page) : JM_rect_from_py(clip);
                fz_matrix ctm = JM_matrix_from_py(matrix);
                tpage = fz_new_stext_page(gctx, rect);
                dev = fz_new_stext_device(gctx, tpage, &options);
                fz_run_page(gctx, page, dev, ctm, NULL);
                fz_close_device(gctx, dev);
            }
            fz_always(gctx) {
                fz_drop_device(gctx, dev);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct TextPage *) tpage;
        }


        %pythoncode %{
        def get_textpage(self, clip: rect_like = None, flags: int = 0, matrix=None) -> "TextPage":
            CheckParent(self)
            if matrix is None:
                matrix = Matrix(1, 1)
            old_rotation = self.rotation
            if old_rotation != 0:
                self.set_rotation(0)
            try:
                textpage = self._get_textpage(clip, flags=flags, matrix=matrix)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            textpage.parent = weakref.proxy(self)
            return textpage
        %}

        /*  ****************** currently inactive
        //----------------------------------------------------------------
        // Page._get_textpage_ocr
        //----------------------------------------------------------------
        FITZEXCEPTION(_get_textpage_ocr, !result)
        %pythonappend _get_textpage_ocr %{val.thisown = True%}
        struct TextPage *
        _get_textpage_ocr(PyObject *clip=NULL, int flags=0, const char *language=NULL, const char *tessdata=NULL)
        {
            fz_stext_page *textpage=NULL;
            fz_try(gctx) {
                fz_rect rect = JM_rect_from_py(clip);
                textpage = JM_new_stext_page_ocr_from_page(gctx, (fz_page *) $self, rect, flags, language, tessdata);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct TextPage *) textpage;
        }
        ************************* */

        //----------------------------------------------------------------
        // Page.language
        //----------------------------------------------------------------
        %pythoncode%{@property%}
        %pythonprepend language %{"""Page language."""%}
        PyObject *language()
        {
            pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            if (!pdfpage) Py_RETURN_NONE;
            pdf_obj *lang = pdf_dict_get_inheritable(gctx, pdfpage->obj, PDF_NAME(Lang));
            if (!lang) Py_RETURN_NONE;
            return Py_BuildValue("s", pdf_to_str_buf(gctx, lang));
        }


        //----------------------------------------------------------------
        // Page.set_language
        //----------------------------------------------------------------
        FITZEXCEPTION(set_language, !result)
        PARENTCHECK(set_language, """Set PDF page default language.""")
        PyObject *set_language(char *language=NULL)
        {
            pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            fz_try(gctx) {
                ASSERT_PDF(pdfpage);
                fz_text_language lang;
                char buf[8];
                if (!language) {
                    pdf_dict_del(gctx, pdfpage->obj, PDF_NAME(Lang));
                } else {
                    lang = fz_text_language_from_string(language);
                    pdf_dict_put_text_string(gctx, pdfpage->obj,
                        PDF_NAME(Lang),
                        fz_string_from_text_language(buf, lang));
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_TRUE;
        }


        //----------------------------------------------------------------
        // Page.get_svg_image
        //----------------------------------------------------------------
        FITZEXCEPTION(get_svg_image, !result)
        PARENTCHECK(get_svg_image, """Make SVG image from page.""")
        PyObject *get_svg_image(PyObject *matrix = NULL, int text_as_path=1)
        {
            fz_rect mediabox = fz_bound_page(gctx, (fz_page *) $self);
            fz_device *dev = NULL;
            fz_buffer *res = NULL;
            PyObject *text = NULL;
            fz_matrix ctm = JM_matrix_from_py(matrix);
            fz_output *out = NULL;
            fz_var(out);
            fz_var(dev);
            fz_var(res);
            fz_rect tbounds = mediabox;
            int text_option = (text_as_path == 1) ? FZ_SVG_TEXT_AS_PATH : FZ_SVG_TEXT_AS_TEXT;
            tbounds = fz_transform_rect(tbounds, ctm);

            fz_try(gctx) {
                res = fz_new_buffer(gctx, 1024);
                out = fz_new_output_with_buffer(gctx, res);
                dev = fz_new_svg_device(gctx, out,
                            tbounds.x1-tbounds.x0,  // width
                            tbounds.y1-tbounds.y0,  // height
                            text_option, 1);
                fz_run_page(gctx, (fz_page *) $self, dev, ctm, NULL);
                fz_close_device(gctx, dev);
                text = JM_EscapeStrFromBuffer(gctx, res);
            }
            fz_always(gctx) {
                fz_drop_device(gctx, dev);
                fz_drop_output(gctx, out);
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return text;
        }


        //----------------------------------------------------------------
        // page set opacity
        //----------------------------------------------------------------
        FITZEXCEPTION(_set_opacity, !result)
        %pythonprepend _set_opacity %{
        if CA >= 1 and ca >= 1 and blendmode == None:
            return None
        tCA = int(round(max(CA , 0) * 100))
        if tCA >= 100:
            tCA = 99
        tca = int(round(max(ca, 0) * 100))
        if tca >= 100:
            tca = 99
        gstate = "fitzca%02i%02i" % (tCA, tca)
        %}
        PyObject *
        _set_opacity(char *gstate=NULL, float CA=1, float ca=1, char *blendmode=NULL)
        {
            if (!gstate) Py_RETURN_NONE;
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            fz_try(gctx) {
                ASSERT_PDF(page);
                pdf_obj *resources = pdf_dict_get(gctx, page->obj, PDF_NAME(Resources));
                if (!resources) {
                    resources = pdf_dict_put_dict(gctx, page->obj, PDF_NAME(Resources), 2);
                }
                pdf_obj *extg = pdf_dict_get(gctx, resources, PDF_NAME(ExtGState));
                if (!extg) {
                    extg = pdf_dict_put_dict(gctx, resources, PDF_NAME(ExtGState), 2);
                }
                int i, n = pdf_dict_len(gctx, extg);
                for (i = 0; i < n; i++) {
                    pdf_obj *o1 = pdf_dict_get_key(gctx, extg, i);
                    char *name = (char *) pdf_to_name(gctx, o1);
                    if (strcmp(name, gstate) == 0) goto finished;
                }
                pdf_obj *opa = pdf_new_dict(gctx, page->doc, 3);
                pdf_dict_put_real(gctx, opa, PDF_NAME(CA), (double) CA);
                pdf_dict_put_real(gctx, opa, PDF_NAME(ca), (double) ca);
                pdf_dict_puts_drop(gctx, extg, gstate, opa);
                finished:;
            }
            fz_always(gctx) {
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("s", gstate);
        }

        //----------------------------------------------------------------
        // page add_caret_annot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_caret_annot, !result)
        struct Annot *
        _add_caret_annot(PyObject *point)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            fz_try(gctx) {
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_CARET);
                if (point)
                {
                    fz_point p = JM_point_from_py(point);
                    fz_rect r = pdf_annot_rect(gctx, annot);
                    r = fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0);
                    pdf_set_annot_rect(gctx, annot, r);
                }
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // page addRedactAnnot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_redact_annot, !result)
        struct Annot *
        _add_redact_annot(PyObject *quad,
            PyObject *text=NULL,
            PyObject *da_str=NULL,
            int align=0,
            PyObject *fill=NULL,
            PyObject *text_color=NULL)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            float fcol[4] = { 1, 1, 1, 0};
            int nfcol = 0, i;
            fz_try(gctx) {
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_REDACT);
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                fz_quad q = JM_quad_from_py(quad);
                fz_rect r = fz_rect_from_quad(q);

                // TODO calculate de-rotated rect
                pdf_set_annot_rect(gctx, annot, r);
                if (EXISTS(fill)) {
                    JM_color_FromSequence(fill, &nfcol, fcol);
                    pdf_obj *arr = pdf_new_array(gctx, page->doc, nfcol);
                    for (i = 0; i < nfcol; i++) {
                        pdf_array_push_real(gctx, arr, fcol[i]);
                    }
                    pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(IC), arr);
                }
                if (EXISTS(text)) {
                    const char *otext = PyUnicode_AsUTF8(text);
                    pdf_dict_puts_drop(gctx, annot_obj, "OverlayText",
                                       pdf_new_text_string(gctx, otext));
                    pdf_dict_put_text_string(gctx,annot_obj, PDF_NAME(DA), PyUnicode_AsUTF8(da_str));
                    pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Q), (int64_t) align);
                }
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }

        //----------------------------------------------------------------
        // page addLineAnnot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_line_annot, !result)
        struct Annot *
        _add_line_annot(PyObject *p1, PyObject *p2)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            fz_try(gctx) {
                ASSERT_PDF(page);
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_LINE);
                fz_point a = JM_point_from_py(p1);
                fz_point b = JM_point_from_py(p2);
                pdf_set_annot_line(gctx, annot, a, b);
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }

        //----------------------------------------------------------------
        // page addTextAnnot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_text_annot, !result)
        struct Annot *
        _add_text_annot(PyObject *point,
            char *text,
            char *icon=NULL)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            fz_rect r;
            fz_point p = JM_point_from_py(point);
            fz_var(annot);
            fz_try(gctx) {
                ASSERT_PDF(page);
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_TEXT);
                r = pdf_annot_rect(gctx, annot);
                r = fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0);
                pdf_set_annot_rect(gctx, annot, r);
                pdf_set_annot_contents(gctx, annot, text);
                if (icon) {
                    pdf_set_annot_icon_name(gctx, annot, icon);
                }
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }

        //----------------------------------------------------------------
        // page addInkAnnot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_ink_annot, !result)
        struct Annot *
        _add_ink_annot(PyObject *list)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            PyObject *p = NULL, *sublist = NULL;
            pdf_obj *inklist = NULL, *stroke = NULL;
            fz_matrix ctm, inv_ctm;
            fz_point point;
            fz_var(annot);
            fz_try(gctx) {
                ASSERT_PDF(page);
                if (!PySequence_Check(list)) {
                    RAISEPY(gctx, MSG_BAD_ARG_INK_ANNOT, PyExc_ValueError);
                }
                pdf_page_transform(gctx, page, NULL, &ctm);
                inv_ctm = fz_invert_matrix(ctm);
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_INK);
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                Py_ssize_t i, j, n0 = PySequence_Size(list), n1;
                inklist = pdf_new_array(gctx, page->doc, n0);

                for (j = 0; j < n0; j++) {
                    sublist = PySequence_ITEM(list, j);
                    n1 = PySequence_Size(sublist);
                    stroke = pdf_new_array(gctx, page->doc, 2 * n1);

                    for (i = 0; i < n1; i++) {
                        p = PySequence_ITEM(sublist, i);
                        if (!PySequence_Check(p) || PySequence_Size(p) != 2) {
                            RAISEPY(gctx, MSG_BAD_ARG_INK_ANNOT, PyExc_ValueError);
                        }
                        point = fz_transform_point(JM_point_from_py(p), inv_ctm);
                        Py_CLEAR(p);
                        pdf_array_push_real(gctx, stroke, point.x);
                        pdf_array_push_real(gctx, stroke, point.y);
                    }

                    pdf_array_push_drop(gctx, inklist, stroke);
                    stroke = NULL;
                    Py_CLEAR(sublist);
                }

                pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(InkList), inklist);
                inklist = NULL;
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }

            fz_catch(gctx) {
                Py_CLEAR(p);
                Py_CLEAR(sublist);
                return NULL;
            }
            return (struct Annot *) annot;
        }

        //----------------------------------------------------------------
        // page addStampAnnot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_stamp_annot, !result)
        struct Annot *
        _add_stamp_annot(PyObject *rect, int stamp=0)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            pdf_obj *stamp_id[] = {PDF_NAME(Approved), PDF_NAME(AsIs),
                                   PDF_NAME(Confidential), PDF_NAME(Departmental),
                                   PDF_NAME(Experimental), PDF_NAME(Expired),
                                   PDF_NAME(Final), PDF_NAME(ForComment),
                                   PDF_NAME(ForPublicRelease), PDF_NAME(NotApproved),
                                   PDF_NAME(NotForPublicRelease), PDF_NAME(Sold),
                                   PDF_NAME(TopSecret), PDF_NAME(Draft)};
            int n = nelem(stamp_id);
            pdf_obj *name = stamp_id[0];
            fz_try(gctx) {
                ASSERT_PDF(page);
                fz_rect r = JM_rect_from_py(rect);
                if (fz_is_infinite_rect(r) || fz_is_empty_rect(r)) {
                    RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError);
                }
                if (INRANGE(stamp, 0, n-1)) {
                    name = stamp_id[stamp];
                }
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_STAMP);
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_set_annot_rect(gctx, annot, r);
                pdf_dict_put(gctx, annot_obj, PDF_NAME(Name), name);
                pdf_set_annot_contents(gctx, annot,
                        pdf_dict_get_name(gctx, annot_obj, PDF_NAME(Name)));
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }

        //----------------------------------------------------------------
        // page addFileAnnot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_file_annot, !result)
        struct Annot *
        _add_file_annot(PyObject *point,
            PyObject *buffer,
            char *filename,
            char *ufilename=NULL,
            char *desc=NULL,
            char *icon=NULL)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            char *uf = ufilename, *d = desc;
            if (!ufilename) uf = filename;
            if (!desc) d = filename;
            fz_buffer *filebuf = NULL;
            fz_rect r;
            fz_point p = JM_point_from_py(point);
            fz_var(filebuf);
            fz_try(gctx) {
                ASSERT_PDF(page);
                filebuf = JM_BufferFromBytes(gctx, buffer);
                if (!filebuf) {
                    RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError);
                }
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_FILE_ATTACHMENT);
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                r = pdf_annot_rect(gctx, annot);
                r = fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0);
                pdf_set_annot_rect(gctx, annot, r);
                int flags = PDF_ANNOT_IS_PRINT;
                pdf_set_annot_flags(gctx, annot, flags);

                if (icon)
                    pdf_set_annot_icon_name(gctx, annot, icon);

                pdf_obj *val = JM_embed_file(gctx, page->doc, filebuf,
                                    filename, uf, d, 1);
                pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(FS), val);
                pdf_dict_put_text_string(gctx, annot_obj, PDF_NAME(Contents), filename);
                pdf_update_annot(gctx, annot);
                pdf_set_annot_rect(gctx, annot, r);
                pdf_set_annot_flags(gctx, annot, flags);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, filebuf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // page: add a text marker annotation
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_text_marker, !result)
        %pythonprepend _add_text_marker %{
        CheckParent(self)
        if not self.parent.is_pdf:
            raise ValueError("is no PDF")%}

        %pythonappend _add_text_marker %{
        if not val:
            return None
        val.parent = weakref.proxy(self)
        self._annot_refs[id(val)] = val%}

        struct Annot *
        _add_text_marker(PyObject *quads, int annot_type)
        {
            pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            PyObject *item = NULL;
            int rotation = JM_page_rotation(gctx, pdfpage);
            fz_quad q;
            fz_var(annot);
            fz_var(item);
            fz_try(gctx) {
                if (rotation != 0) {
                    pdf_dict_put_int(gctx, pdfpage->obj, PDF_NAME(Rotate), 0);
                }
                annot = pdf_create_annot(gctx, pdfpage, annot_type);
                Py_ssize_t i, len = PySequence_Size(quads);
                for (i = 0; i < len; i++) {
                    item = PySequence_ITEM(quads, i);
                    q = JM_quad_from_py(item);
                    Py_DECREF(item);
                    pdf_add_annot_quad_point(gctx, annot, q);
                }
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_always(gctx) {
                if (rotation != 0) {
                    pdf_dict_put_int(gctx, pdfpage->obj, PDF_NAME(Rotate), rotation);
                }
            }
            fz_catch(gctx) {
                pdf_drop_annot(gctx, annot);
                return NULL;
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // page: add circle or rectangle annotation
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_square_or_circle, !result)
        struct Annot *
        _add_square_or_circle(PyObject *rect, int annot_type)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            fz_try(gctx) {
                fz_rect r = JM_rect_from_py(rect);
                if (fz_is_infinite_rect(r) || fz_is_empty_rect(r)) {
                    RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError);
                }
                annot = pdf_create_annot(gctx, page, annot_type);
                pdf_set_annot_rect(gctx, annot, r);
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // page: add multiline annotation
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_multiline, !result)
        struct Annot *
        _add_multiline(PyObject *points, int annot_type)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *annot = NULL;
            fz_try(gctx) {
                Py_ssize_t i, n = PySequence_Size(points);
                if (n < 2) {
                    RAISEPY(gctx, MSG_BAD_ARG_POINTS, PyExc_ValueError);
                }
                annot = pdf_create_annot(gctx, page, annot_type);
                for (i = 0; i < n; i++) {
                    PyObject *p = PySequence_ITEM(points, i);
                    if (PySequence_Size(p) != 2) {
                        Py_DECREF(p);
                        RAISEPY(gctx, MSG_BAD_ARG_POINTS, PyExc_ValueError);
                    }
                    fz_point point = JM_point_from_py(p);
                    Py_DECREF(p);
                    pdf_add_annot_vertex(gctx, annot, point);
                }

                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // page addFreetextAnnot
        //----------------------------------------------------------------
        FITZEXCEPTION(_add_freetext_annot, !result)
        %pythonappend _add_freetext_annot %{
        ap = val._getAP()
        BT = ap.find(b"BT")
        ET = ap.find(b"ET") + 2
        ap = ap[BT:ET]
        w = rect[2]-rect[0]
        h = rect[3]-rect[1]
        if rotate in (90, -90, 270):
            w, h = h, w
        re = b"0 0 %g %g re" % (w, h)
        ap = re + b"\nW\nn\n" + ap
        ope = None
        bwidth = b""
        fill_string = ColorCode(fill_color, "f").encode()
        if fill_string:
            fill_string += b"\n"
            ope = b"f"
        stroke_string = ColorCode(border_color, "c").encode()
        if stroke_string:
            stroke_string += b"\n"
            bwidth = b"1 w\n"
            ope = b"S"
        if fill_string and stroke_string:
            ope = b"B"
        if ope != None:
            ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap
        val._setAP(ap)
        %}
        struct Annot *
        _add_freetext_annot(PyObject *rect, char *text,
            float fontsize=11,
            char *fontname=NULL,
            PyObject *text_color=NULL,
            PyObject *fill_color=NULL,
            PyObject *border_color=NULL,
            int align=0,
            int rotate=0)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            float fcol[4] = {1, 1, 1, 1}; // fill color: white
            int nfcol = 0;
            JM_color_FromSequence(fill_color, &nfcol, fcol);
            float tcol[4] = {0, 0, 0, 0}; // std. text color: black
            int ntcol = 0;
            JM_color_FromSequence(text_color, &ntcol, tcol);
            fz_rect r = JM_rect_from_py(rect);
            pdf_annot *annot = NULL;
            fz_try(gctx) {
                if (fz_is_infinite_rect(r) || fz_is_empty_rect(r)) {
                    RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError);
                }
                annot = pdf_create_annot(gctx, page, PDF_ANNOT_FREE_TEXT);
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_set_annot_contents(gctx, annot, text);
                pdf_set_annot_rect(gctx, annot, r);
                pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Rotate), rotate);
                pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Q), align);

                if (nfcol > 0) {
                    pdf_set_annot_color(gctx, annot, nfcol, fcol);
                }

                // insert the default appearance string
                JM_make_annot_DA(gctx, annot, ntcol, tcol, fontname, fontsize);
                pdf_update_annot(gctx, annot);
                JM_add_annot_id(gctx, annot, "A");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }


    %pythoncode %{
        @property
        def rotation_matrix(self) -> Matrix:
            """Reflects page rotation."""
            return Matrix(TOOLS._rotate_matrix(self))

        @property
        def derotation_matrix(self) -> Matrix:
            """Reflects page de-rotation."""
            return Matrix(TOOLS._derotate_matrix(self))

        def add_caret_annot(self, point: point_like) -> "struct Annot *":
            """Add a 'Caret' annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_caret_annot(point)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_strikeout_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *":
            """Add a 'StrikeOut' annotation."""
            if quads is None:
                q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
            else:
                q = CheckMarkerArg(quads)
            return self._add_text_marker(q, PDF_ANNOT_STRIKE_OUT)


        def add_underline_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *":
            """Add a 'Underline' annotation."""
            if quads is None:
                q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
            else:
                q = CheckMarkerArg(quads)
            return self._add_text_marker(q, PDF_ANNOT_UNDERLINE)


        def add_squiggly_annot(self, quads=None, start=None,
                             stop=None, clip=None) -> "struct Annot *":
            """Add a 'Squiggly' annotation."""
            if quads is None:
                q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
            else:
                q = CheckMarkerArg(quads)
            return self._add_text_marker(q, PDF_ANNOT_SQUIGGLY)


        def add_highlight_annot(self, quads=None, start=None,
                              stop=None, clip=None) -> "struct Annot *":
            """Add a 'Highlight' annotation."""
            if quads is None:
                q = get_highlight_selection(self, start=start, stop=stop, clip=clip)
            else:
                q = CheckMarkerArg(quads)
            return self._add_text_marker(q, PDF_ANNOT_HIGHLIGHT)


        def add_rect_annot(self, rect: rect_like) -> "struct Annot *":
            """Add a 'Square' (rectangle) annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_square_or_circle(rect, PDF_ANNOT_SQUARE)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_circle_annot(self, rect: rect_like) -> "struct Annot *":
            """Add a 'Circle' (ellipse, oval) annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_square_or_circle(rect, PDF_ANNOT_CIRCLE)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_text_annot(self, point: point_like, text: str, icon: str ="Note") -> "struct Annot *":
            """Add a 'Text' (sticky note) annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_text_annot(point, text, icon=icon)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_line_annot(self, p1: point_like, p2: point_like) -> "struct Annot *":
            """Add a 'Line' annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_line_annot(p1, p2)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_polyline_annot(self, points: list) -> "struct Annot *":
            """Add a 'PolyLine' annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_multiline(points, PDF_ANNOT_POLY_LINE)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_polygon_annot(self, points: list) -> "struct Annot *":
            """Add a 'Polygon' annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_multiline(points, PDF_ANNOT_POLYGON)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_stamp_annot(self, rect: rect_like, stamp: int =0) -> "struct Annot *":
            """Add a ('rubber') 'Stamp' annotation."""
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_stamp_annot(rect, stamp)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_ink_annot(self, handwriting: list) -> "struct Annot *":
            """Add a 'Ink' ('handwriting') annotation.

            The argument must be a list of lists of point_likes.
            """
            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_ink_annot(handwriting)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_file_annot(self, point: point_like,
            buffer: ByteString,
            filename: str,
            ufilename: OptStr =None,
            desc: OptStr =None,
            icon: OptStr =None) -> "struct Annot *":
            """Add a 'FileAttachment' annotation."""

            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_file_annot(point,
                            buffer,
                            filename,
                            ufilename=ufilename,
                            desc=desc,
                            icon=icon)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_freetext_annot(self, rect: rect_like, text: str, fontsize: float =11,
                             fontname: OptStr =None, border_color: OptSeq =None,
                             text_color: OptSeq =None,
                             fill_color: OptSeq =None, align: int =0, rotate: int =0) -> "struct Annot *":
            """Add a 'FreeText' annotation."""

            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_freetext_annot(rect, text, fontsize=fontsize,
                        fontname=fontname, border_color=border_color,text_color=text_color,
                        fill_color=fill_color, align=align, rotate=rotate)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            return annot


        def add_redact_annot(self, quad, text: OptStr =None, fontname: OptStr =None,
                           fontsize: float =11, align: int =0, fill: OptSeq =None, text_color: OptSeq =None,
                           cross_out: bool =True) -> "struct Annot *":
            """Add a 'Redact' annotation."""
            da_str = None
            if text:
                CheckColor(fill)
                CheckColor(text_color)
                if not fontname:
                    fontname = "Helv"
                if not fontsize:
                    fontsize = 11
                if not text_color:
                    text_color = (0, 0, 0)
                if hasattr(text_color, "__float__"):
                    text_color = (text_color, text_color, text_color)
                if len(text_color) > 3:
                    text_color = text_color[:3]
                fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf"
                da_str = fmt.format(*text_color, f=fontname, s=fontsize)
                if fill is None:
                    fill = (1, 1, 1)
                if fill:
                    if hasattr(fill, "__float__"):
                        fill = (fill, fill, fill)
                    if len(fill) > 3:
                        fill = fill[:3]

            old_rotation = annot_preprocess(self)
            try:
                annot = self._add_redact_annot(quad, text=text, da_str=da_str,
                           align=align, fill=fill)
            finally:
                if old_rotation != 0:
                    self.set_rotation(old_rotation)
            annot_postprocess(self, annot)
            #-------------------------------------------------------------
            # change appearance to show a crossed-out rectangle
            #-------------------------------------------------------------
            if cross_out:
                ap_tab = annot._getAP().splitlines()[:-1]  # get the 4 commands only
                _, LL, LR, UR, UL = ap_tab
                ap_tab.append(LR)
                ap_tab.append(LL)
                ap_tab.append(UR)
                ap_tab.append(LL)
                ap_tab.append(UL)
                ap_tab.append(b"S")
                ap = b"\n".join(ap_tab)
                annot._setAP(ap, 0)
            return annot
        %}


        //----------------------------------------------------------------
        // page load annot by name or xref
        //----------------------------------------------------------------
        FITZEXCEPTION(_load_annot, !result)
        struct Annot *
        _load_annot(char *name, int xref)
        {
            pdf_annot *annot = NULL;
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            fz_try(gctx) {
                ASSERT_PDF(page);
                if (xref == 0)
                    annot = JM_get_annot_by_name(gctx, page, name);
                else
                    annot = JM_get_annot_by_xref(gctx, page, xref);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // page load widget by xref
        //----------------------------------------------------------------
        FITZEXCEPTION(load_widget, !result)
        %pythonprepend load_widget %{
        """Load a widget by its xref."""
        CheckParent(self)
        %}
        %pythonappend load_widget %{
        if not val:
            return val
        val.thisown = True
        val.parent = weakref.proxy(self)
        self._annot_refs[id(val)] = val
        widget = Widget()
        TOOLS._fill_widget(val, widget)
        val = widget
        %}
        struct Annot *
        load_widget(int xref)
        {
            pdf_annot *annot = NULL;
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            fz_try(gctx) {
                ASSERT_PDF(page);
                annot = JM_get_widget_by_xref(gctx, page, xref);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // page list Resource/Properties
        //----------------------------------------------------------------
        FITZEXCEPTION(_get_resource_properties, !result)
        PyObject *
        _get_resource_properties()
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            PyObject *rc;
            fz_try(gctx) {
                ASSERT_PDF(page);
                rc = JM_get_resource_properties(gctx, page->obj);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return rc;
        }


        //----------------------------------------------------------------
        // page list Resource/Properties
        //----------------------------------------------------------------
        FITZEXCEPTION(_set_resource_property, !result)
        PyObject *
        _set_resource_property(char *name, int xref)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            fz_try(gctx) {
                ASSERT_PDF(page);
                JM_set_resource_property(gctx, page->obj, name, xref);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode %{
def _get_optional_content(self, oc: OptInt) -> OptStr:
    if oc == None or oc == 0:
        return None
    doc = self.parent
    check = doc.xref_object(oc, compressed=True)
    if not ("/Type/OCG" in check or "/Type/OCMD" in check):
        raise ValueError("bad optional content: 'oc'")
    props = {}
    for p, x in self._get_resource_properties():
        props[x] = p
    if oc in props.keys():
        return props[oc]
    i = 0
    mc = "MC%i" % i
    while mc in props.values():
        i += 1
        mc = "MC%i" % i
    self._set_resource_property(mc, oc)
    return mc

def get_oc_items(self) -> list:
    """Get OCGs and OCMDs used in the page's contents.

    Returns:
        List of items (name, xref, type), where type is one of "ocg" / "ocmd",
        and name is the property name.
    """
    rc = []
    for pname, xref in self._get_resource_properties():
        text = self.parent.xref_object(xref, compressed=True)
        if "/Type/OCG" in text:
            octype = "ocg"
        elif "/Type/OCMD" in text:
            octype = "ocmd"
        else:
            continue
        rc.append((pname, xref, octype))
    return rc
%}

        //----------------------------------------------------------------
        // page get list of annot names
        //----------------------------------------------------------------
        PARENTCHECK(annot_names, """List of names of annotations, fields and links.""")
        PyObject *annot_names()
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);

            if (!page) {
                PyObject *rc = PyList_New(0);
                return rc;
            }
            return JM_get_annot_id_list(gctx, page);
        }


        //----------------------------------------------------------------
        // page retrieve list of annotation xrefs
        //----------------------------------------------------------------
        PARENTCHECK(annot_xrefs,"""List of xref numbers of annotations, fields and links.""")
        PyObject *annot_xrefs()
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            if (!page) {
                PyObject *rc = PyList_New(0);
                return rc;
            }
            return JM_get_annot_xref_list(gctx, page->obj);
        }


        %pythoncode %{
        def load_annot(self, ident: typing.Union[str, int]) -> "struct Annot *":
            """Load an annot by name (/NM key) or xref.

            Args:
                ident: identifier, either name (str) or xref (int).
            """

            CheckParent(self)
            if type(ident) is str:
                xref = 0
                name = ident
            elif type(ident) is int:
                xref = ident
                name = None
            else:
                raise ValueError("identifier must be string or integer")
            val = self._load_annot(name, xref)
            if not val:
                return val
            val.thisown = True
            val.parent = weakref.proxy(self)
            self._annot_refs[id(val)] = val
            return val


        #---------------------------------------------------------------------
        # page addWidget
        #---------------------------------------------------------------------
        def add_widget(self, widget: Widget) -> "struct Annot *":
            """Add a 'Widget' (form field)."""
            CheckParent(self)
            doc = self.parent
            if not doc.is_pdf:
                raise ValueError("is no PDF")
            widget._validate()
            annot = self._addWidget(widget.field_type, widget.field_name)
            if not annot:
                return None
            annot.thisown = True
            annot.parent = weakref.proxy(self) # owning page object
            self._annot_refs[id(annot)] = annot
            widget.parent = annot.parent
            widget._annot = annot
            widget.update()
            return annot
        %}

        FITZEXCEPTION(_addWidget, !result)
        struct Annot *_addWidget(int field_type, char *field_name)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_document *pdf = page->doc;
            pdf_annot *annot = NULL;
            fz_var(annot);
            fz_try(gctx) {
                annot = JM_create_widget(gctx, pdf, page, field_type, field_name);
                if (!annot) {
                    RAISEPY(gctx, "cannot create widget", PyExc_RuntimeError);
                }
                JM_add_annot_id(gctx, annot, "W");
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Annot *) annot;
        }

        //----------------------------------------------------------------
        // Page.get_displaylist
        //----------------------------------------------------------------
        FITZEXCEPTION(get_displaylist, !result)
        %pythonprepend get_displaylist %{
        """Make a DisplayList from the page for Pixmap generation.

        Include (default) or exclude annotations."""

        CheckParent(self)
        %}
        %pythonappend get_displaylist %{val.thisown = True%}
        struct DisplayList *get_displaylist(int annots=1)
        {
            fz_display_list *dl = NULL;
            fz_try(gctx) {
                if (annots) {
                    dl = fz_new_display_list_from_page(gctx, (fz_page *) $self);
                } else {
                    dl = fz_new_display_list_from_page_contents(gctx, (fz_page *) $self);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct DisplayList *) dl;
        }


        //----------------------------------------------------------------
        // Page.get_drawings
        //----------------------------------------------------------------
        %pythoncode %{
        def get_drawings(self, extended: bool = False) -> list:
            """Retrieve vector graphics. The extended version includes clips.

            Note:
            For greater comfort, this method converts point-like, rect-like, quad-like
            tuples of the C version to respective Point / Rect / Quad objects.
            It also adds default items that are missing in original path types.
            """
            allkeys = (
                    "closePath", "fill", "color", "width", "lineCap",
                    "lineJoin", "dashes", "stroke_opacity", "fill_opacity", "even_odd",
                )
            val = self.get_cdrawings(extended=extended)
            for i in range(len(val)):
                npath = val[i]
                if not npath["type"].startswith("clip"):
                    npath["rect"] = Rect(npath["rect"])
                else:
                    npath["scissor"] = Rect(npath["scissor"])
                if npath["type"]!="group":
                    items = npath["items"]
                    newitems = []
                    for item in items:
                        cmd = item[0]
                        rest = item[1:]
                        if  cmd == "re":
                            item = ("re", Rect(rest[0]).normalize(), rest[1])
                        elif cmd == "qu":
                            item = ("qu", Quad(rest[0]))
                        else:
                            item = tuple([cmd] + [Point(i) for i in rest])
                        newitems.append(item)
                    npath["items"] = newitems
                if npath["type"] in ("f", "s"):
                    for k in allkeys:
                        npath[k] = npath.get(k)
                val[i] = npath
            return val

        class Drawpath(object):
            """Reflects a path dictionary from get_cdrawings()."""
            def __init__(self, **args):
                self.__dict__.update(args)
        
        class Drawpathlist(object):
            """List of Path objects representing get_cdrawings() output."""
            def __init__(self):
                self.paths = []
                self.path_count = 0
                self.group_count = 0
                self.clip_count = 0
                self.fill_count = 0
                self.stroke_count = 0
                self.fillstroke_count = 0

            def append(self, path):
                self.paths.append(path)
                self.path_count += 1
                if path.type == "clip":
                    self.clip_count += 1
                elif path.type == "group":
                    self.group_count += 1
                elif path.type == "f":
                    self.fill_count += 1
                elif path.type == "s":
                    self.stroke_count += 1
                elif path.type == "fs":
                    self.fillstroke_count += 1

            def clip_parents(self, i):
                """Return list of parent clip paths.

                Args:
                    i: (int) return parents of this path.
                Returns:
                    List of the clip parents."""
                if i >= self.path_count:
                    raise IndexError("bad path index")
                while i < 0:
                    i += self.path_count
                lvl = self.paths[i].level
                clips = list(  # clip paths before identified one
                    reversed(
                        [
                            p
                            for p in self.paths[:i]
                            if p.type == "clip" and p.level < lvl
                        ]
                    )
                )
                if clips == []:  # none found: empty list
                    return []
                nclips = [clips[0]]  # init return list
                for p in clips[1:]:
                    if p.level >= nclips[-1].level:
                        continue  # only accept smaller clip levels
                    nclips.append(p)
                return nclips

            def group_parents(self, i):
                """Return list of parent group paths.

                Args:
                    i: (int) return parents of this path.
                Returns:
                    List of the group parents."""
                if i >= self.path_count:
                    raise IndexError("bad path index")
                while i < 0:
                    i += self.path_count
                lvl = self.paths[i].level
                groups = list(  # group paths before identified one
                    reversed(
                        [
                            p
                            for p in self.paths[:i]
                            if p.type == "group" and p.level < lvl
                        ]
                    )
                )
                if groups == []:  # none found: empty list
                    return []
                ngroups = [groups[0]]  # init return list
                for p in groups[1:]:
                    if p.level >= ngroups[-1].level:
                        continue  # only accept smaller group levels
                    ngroups.append(p)
                return ngroups

            def __getitem__(self, item):
                return self.paths.__getitem__(item)

            def __len__(self):
                return self.paths.__len__()


        def get_lineart(self) -> object:
            """Get page drawings paths.

            Note:
            For greater comfort, this method converts point-like, rect-like, quad-like
            tuples of the C version to respective Point / Rect / Quad objects.
            Also adds default items that are missing in original path types.
            In contrast to get_drawings(), this output is an object.
            """

            val = self.get_cdrawings(extended=True)
            paths = self.Drawpathlist()
            for path in val:
                npath = self.Drawpath(**path)
                if npath.type != "clip":
                    npath.rect = Rect(path["rect"])
                else:
                    npath.scissor = Rect(path["scissor"])
                if npath.type != "group":
                    items = path["items"]
                    newitems = []
                    for item in items:
                        cmd = item[0]
                        rest = item[1:]
                        if  cmd == "re":
                            item = ("re", Rect(rest[0]).normalize(), rest[1])
                        elif cmd == "qu":
                            item = ("qu", Quad(rest[0]))
                        else:
                            item = tuple([cmd] + [Point(i) for i in rest])
                        newitems.append(item)
                    npath.items = newitems
                
                if npath.type == "f":
                    npath.stroke_opacity = None
                    npath.dashes = None
                    npath.lineJoin = None
                    npath.lineCap = None
                    npath.color = None
                    npath.width = None

                paths.append(npath)

            val = None
            return paths
        %}


        FITZEXCEPTION(get_cdrawings, !result)
        %pythonprepend get_cdrawings %{
        """Extract vector graphics ("line art") from the page."""
        CheckParent(self)
        old_rotation = self.rotation
        if old_rotation != 0:
            self.set_rotation(0)
        %}
        %pythonappend get_cdrawings %{
        if old_rotation != 0:
            self.set_rotation(old_rotation)
        %}
        PyObject *
        get_cdrawings(PyObject *extended=NULL, PyObject *callback=NULL, PyObject *method=NULL)
        {
            fz_page *page = (fz_page *) $self;
            fz_device *dev = NULL;
            PyObject *rc = NULL;
            int clips = PyObject_IsTrue(extended);
            fz_var(rc);
            fz_try(gctx) {
                fz_rect prect = fz_bound_page(gctx, page);
                trace_device_ptm = fz_make_matrix(1, 0, 0, -1, 0, prect.y1);
                if (PyCallable_Check(callback) || method != Py_None) {
                    dev = JM_new_lineart_device(gctx, callback, clips, method);
                } else {
                    rc = PyList_New(0);
                    dev = JM_new_lineart_device(gctx, rc, clips, method);
                }
                fz_run_page(gctx, page, dev, fz_identity, NULL);
                fz_close_device(gctx, dev);
            }
            fz_always(gctx) {
                fz_drop_device(gctx, dev);
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                return NULL;
            }
            if (PyCallable_Check(callback) || method != Py_None) {
                Py_RETURN_NONE;
            }
            return rc;
        }


        FITZEXCEPTION(get_bboxlog, !result)
        %pythonprepend get_bboxlog %{
        CheckParent(self)
        old_rotation = self.rotation
        if old_rotation != 0:
            self.set_rotation(0)
        %}
        %pythonappend get_bboxlog %{
        if old_rotation != 0:
            self.set_rotation(old_rotation)
        %}
        PyObject *
        get_bboxlog(PyObject *layers=NULL)
        {
            fz_page *page = (fz_page *) $self;
            fz_device *dev = NULL;
            PyObject *rc = PyList_New(0);
            int inc_layers = PyObject_IsTrue(layers);
            fz_try(gctx) {
                dev = JM_new_bbox_device(gctx, rc, inc_layers);
                fz_run_page(gctx, page, dev, fz_identity, NULL);
                fz_close_device(gctx, dev);
            }
            fz_always(gctx) {
                fz_drop_device(gctx, dev);
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                return NULL;
            }
            return rc;
        }


        FITZEXCEPTION(get_texttrace, !result)
        %pythonprepend get_texttrace %{
        CheckParent(self)
        old_rotation = self.rotation
        if old_rotation != 0:
            self.set_rotation(0)
        %}
        %pythonappend get_texttrace %{
        if old_rotation != 0:
            self.set_rotation(old_rotation)
        %}
        PyObject *
        get_texttrace()
        {
            fz_page *page = (fz_page *) $self;
            fz_device *dev = NULL;
            PyObject *rc = PyList_New(0);
            fz_try(gctx) {
                dev = JM_new_texttrace_device(gctx, rc);
                fz_rect prect = fz_bound_page(gctx, page);
                trace_device_rot = fz_identity;
                trace_device_ptm = fz_make_matrix(1, 0, 0, -1, 0, prect.y1);
                fz_run_page(gctx, page, dev, fz_identity, NULL);
                fz_close_device(gctx, dev);
            }
            fz_always(gctx) {
                fz_drop_device(gctx, dev);
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                return NULL;
            }
            return rc;
        }


        //----------------------------------------------------------------
        // Page apply redactions
        //----------------------------------------------------------------
        FITZEXCEPTION(_apply_redactions, !result)
        PyObject *_apply_redactions(int images=PDF_REDACT_IMAGE_PIXELS)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            int success = 0;
            pdf_redact_options opts = {0};
            opts.black_boxes = 0;  // no black boxes
            opts.image_method = images;  // how to treat images
            fz_try(gctx) {
                ASSERT_PDF(page);
                success = pdf_redact_page(gctx, page->doc, page, &opts);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_BOOL(success);
        }


        //----------------------------------------------------------------
        // Page._makePixmap
        //----------------------------------------------------------------
        FITZEXCEPTION(_makePixmap, !result)
        struct Pixmap *
        _makePixmap(struct Document *doc,
            PyObject *ctm,
            struct Colorspace *cs,
            int alpha=0,
            int annots=1,
            PyObject *clip=NULL)
        {
            fz_pixmap *pix = NULL;
            fz_try(gctx) {
                pix = JM_pixmap_from_page(gctx, (fz_document *) doc, (fz_page *) $self, ctm, (fz_colorspace *) cs, alpha, annots, clip);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pix;
        }


        //----------------------------------------------------------------
        // Page.set_mediabox
        //----------------------------------------------------------------
        FITZEXCEPTION(set_mediabox, !result)
        PARENTCHECK(set_mediabox, """Set the MediaBox.""")
        PyObject *set_mediabox(PyObject *rect)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            fz_try(gctx) {
                ASSERT_PDF(page);
                fz_rect mediabox = JM_rect_from_py(rect);
                if (fz_is_empty_rect(mediabox) ||
                    fz_is_infinite_rect(mediabox)) {
                    RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError);
                }
                pdf_dict_put_rect(gctx, page->obj, PDF_NAME(MediaBox), mediabox);
                pdf_dict_del(gctx, page->obj, PDF_NAME(CropBox));
                pdf_dict_del(gctx, page->obj, PDF_NAME(ArtBox));
                pdf_dict_del(gctx, page->obj, PDF_NAME(BleedBox));
                pdf_dict_del(gctx, page->obj, PDF_NAME(TrimBox));
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // Page.load_links()
        //----------------------------------------------------------------
        PARENTCHECK(load_links, """Get first Link.""")
        %pythonappend load_links %{
            if val:
                val.thisown = True
                val.parent = weakref.proxy(self) # owning page object
                self._annot_refs[id(val)] = val
                if self.parent.is_pdf:
                    link_id = [x for x in self.annot_xrefs() if x[1] == PDF_ANNOT_LINK][0]
                    val.xref = link_id[0]
                    val.id = link_id[2]
                else:
                    val.xref = 0
                    val.id = ""
        %}
        struct Link *load_links()
        {
            fz_link *l = NULL;
            fz_try(gctx) {
                l = fz_load_links(gctx, (fz_page *) $self);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Link *) l;
        }
        %pythoncode %{first_link = property(load_links, doc="First link on page")%}

        //----------------------------------------------------------------
        // Page.first_annot
        //----------------------------------------------------------------
        PARENTCHECK(first_annot, """First annotation.""")
        %pythonappend first_annot %{
        if val:
            val.thisown = True
            val.parent = weakref.proxy(self) # owning page object
            self._annot_refs[id(val)] = val
        %}
        %pythoncode %{@property%}
        struct Annot *first_annot()
        {
            pdf_annot *annot = NULL;
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            if (page)
            {
                annot = pdf_first_annot(gctx, page);
                if (annot) pdf_keep_annot(gctx, annot);
            }
            return (struct Annot *) annot;
        }

        //----------------------------------------------------------------
        // first_widget
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(first_widget, """First widget/field.""")
        %pythonappend first_widget %{
        if val:
            val.thisown = True
            val.parent = weakref.proxy(self) # owning page object
            self._annot_refs[id(val)] = val
            widget = Widget()
            TOOLS._fill_widget(val, widget)
            val = widget
        %}
        struct Annot *first_widget()
        {
            pdf_annot *annot = NULL;
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            if (page) {
                annot = pdf_first_widget(gctx, page);
                if (annot) pdf_keep_annot(gctx, annot);
            }
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // Page.delete_link() - delete link
        //----------------------------------------------------------------
        PARENTCHECK(delete_link, """Delete a Link.""")
        %pythonappend delete_link %{
        if linkdict["xref"] == 0: return
        try:
            linkid = linkdict["id"]
            linkobj = self._annot_refs[linkid]
            linkobj._erase()
        except:
            pass
        %}
        void delete_link(PyObject *linkdict)
        {
            if (!PyDict_Check(linkdict)) return; // have no dictionary
            fz_try(gctx) {
                pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
                if (!page) goto finished;  // have no PDF
                int xref = (int) PyInt_AsLong(PyDict_GetItem(linkdict, dictkey_xref));
                if (xref < 1) goto finished;  // invalid xref
                pdf_obj *annots = pdf_dict_get(gctx, page->obj, PDF_NAME(Annots));
                if (!annots) goto finished;  // have no annotations
                int len = pdf_array_len(gctx, annots);
                if (len == 0) goto finished;
                int i, oxref = 0;

                for (i = 0; i < len; i++) {
                    oxref = pdf_to_num(gctx, pdf_array_get(gctx, annots, i));
                    if (xref == oxref) break;        // found xref in annotations
                }

                if (xref != oxref) goto finished;  // xref not in annotations
                pdf_array_delete(gctx, annots, i);   // delete entry in annotations
                pdf_delete_object(gctx, page->doc, xref);  // delete link obj
                pdf_dict_put(gctx, page->obj, PDF_NAME(Annots), annots);
                JM_refresh_links(gctx, page);
                finished:;

            }
            fz_catch(gctx) {;}
        }

        //----------------------------------------------------------------
        // Page.delete_annot() - delete annotation and return the next one
        //----------------------------------------------------------------
        %pythonprepend delete_annot %{
        """Delete annot and return next one."""
        CheckParent(self)
        CheckParent(annot)%}

        %pythonappend delete_annot %{
        if val:
            val.thisown = True
            val.parent = weakref.proxy(self) # owning page object
            val.parent._annot_refs[id(val)] = val
        %}

        struct Annot *delete_annot(struct Annot *annot)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_annot *irt_annot = NULL;
            while (1) {
                // first loop through all /IRT annots and remove them
                irt_annot = JM_find_annot_irt(gctx, (pdf_annot *) annot);
                if (!irt_annot)  // no more there
                    break;
                pdf_delete_annot(gctx, page, irt_annot);
            }
            pdf_annot *nextannot = pdf_next_annot(gctx, (pdf_annot *) annot);  // store next
            pdf_delete_annot(gctx, page, (pdf_annot *) annot);
            if (nextannot) {
                nextannot = pdf_keep_annot(gctx, nextannot);
            }
            return (struct Annot *) nextannot;
        }


        //----------------------------------------------------------------
        // mediabox: get the /MediaBox (PDF only)
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(mediabox, """The MediaBox.""")
        %pythonappend mediabox %{val = Rect(JM_TUPLE3(val))%}
        PyObject *mediabox()
        {
            fz_rect rect = fz_infinite_rect;
            fz_try(gctx) {
                pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
                if (!page) {
                    rect = fz_bound_page(gctx, (fz_page *) $self);
                } else {
                    rect = JM_mediabox(gctx, page->obj);
                }
            }
            fz_catch(gctx) {;}
            return JM_py_from_rect(rect);
        }


        //----------------------------------------------------------------
        // cropbox: get the /CropBox (PDF only)
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(cropbox, """The CropBox.""")
        %pythonappend cropbox %{val = Rect(JM_TUPLE3(val))%}
        PyObject *cropbox()
        {
            fz_rect rect = fz_infinite_rect;
            fz_try(gctx) {
                pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
                if (!page) {
                    rect = fz_bound_page(gctx, (fz_page *) $self);
                } else {
                    rect = JM_cropbox(gctx, page->obj);
                }
            }
            fz_catch(gctx) {;}
            return JM_py_from_rect(rect);
        }


        PyObject *_other_box(const char *boxtype)
        {
            fz_rect rect = fz_infinite_rect;
            fz_try(gctx) {
                pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
                if (page) {
                    pdf_obj *obj = pdf_dict_gets(gctx, page->obj, boxtype);
                    if (pdf_is_array(gctx, obj)) {
                        rect = pdf_to_rect(gctx, obj);
                    }
                }
            }
            fz_catch(gctx) {;}
            if (fz_is_infinite_rect(rect)) {
                Py_RETURN_NONE;
            }
            return JM_py_from_rect(rect);
        }


        //----------------------------------------------------------------
        // CropBox position: x0, y0 of /CropBox
        //----------------------------------------------------------------
        %pythoncode %{
        @property
        def cropbox_position(self):
            return self.cropbox.tl

        @property
        def artbox(self):
            """The ArtBox"""
            rect = self._other_box("ArtBox")
            if rect == None:
                return self.cropbox
            mb = self.mediabox
            return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])

        @property
        def trimbox(self):
            """The TrimBox"""
            rect = self._other_box("TrimBox")
            if rect == None:
                return self.cropbox
            mb = self.mediabox
            return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])

        @property
        def bleedbox(self):
            """The BleedBox"""
            rect = self._other_box("BleedBox")
            if rect == None:
                return self.cropbox
            mb = self.mediabox
            return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])

        def _set_pagebox(self, boxtype, rect):
            doc = self.parent
            if doc == None:
                raise ValueError("orphaned object: parent is None")

            if not doc.is_pdf:
                raise ValueError("is no PDF")

            valid_boxes = ("CropBox", "BleedBox", "TrimBox", "ArtBox")

            if boxtype not in valid_boxes:
                raise ValueError("bad boxtype")

            rect = Rect(rect)
            mb = self.mediabox
            rect = Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1])
            if not (mb.x0 <= rect.x0 < rect.x1 <= mb.x1 and mb.y0 <= rect.y0 < rect.y1 <= mb.y1):
                raise ValueError(f"{boxtype} not in MediaBox")

            doc.xref_set_key(self.xref, boxtype, "[%g %g %g %g]" % tuple(rect))


        def set_cropbox(self, rect):
            """Set the CropBox. Will also change Page.rect."""
            return self._set_pagebox("CropBox", rect)

        def set_artbox(self, rect):
            """Set the ArtBox."""
            return self._set_pagebox("ArtBox", rect)

        def set_bleedbox(self, rect):
            """Set the BleedBox."""
            return self._set_pagebox("BleedBox", rect)

        def set_trimbox(self, rect):
            """Set the TrimBox."""
            return self._set_pagebox("TrimBox", rect)
        %}


        //----------------------------------------------------------------
        // rotation - return page rotation
        //----------------------------------------------------------------
        PARENTCHECK(rotation, """Page rotation.""")
        %pythoncode %{@property%}
        int rotation()
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            if (!page) return 0;
            return JM_page_rotation(gctx, page);
        }

        /*********************************************************************/
        // set_rotation() - set page rotation
        /*********************************************************************/
        FITZEXCEPTION(set_rotation, !result)
        PARENTCHECK(set_rotation, """Set page rotation.""")
        PyObject *set_rotation(int rotation)
        {
            fz_try(gctx) {
                pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
                ASSERT_PDF(page);
                int rot = JM_norm_rotation(rotation);
                pdf_dict_put_int(gctx, page->obj, PDF_NAME(Rotate), (int64_t) rot);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        /*********************************************************************/
        // Page._addAnnot_FromString
        // Add new links provided as an array of string object definitions.
        /*********************************************************************/
        FITZEXCEPTION(_addAnnot_FromString, !result)
        PARENTCHECK(_addAnnot_FromString, """Add links from list of object sources.""")
        PyObject *_addAnnot_FromString(PyObject *linklist)
        {
            pdf_obj *annots, *annot, *ind_obj;
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            PyObject *txtpy = NULL;
            char *text = NULL;
            Py_ssize_t lcount = PyTuple_Size(linklist); // link count
            if (lcount < 1) Py_RETURN_NONE;
            Py_ssize_t i = -1;
            fz_var(text);

            // insert links from the provided sources
            fz_try(gctx) {
                ASSERT_PDF(page);
                if (!PyTuple_Check(linklist)) {
                    RAISEPY(gctx, "bad 'linklist' argument", PyExc_ValueError);
                }
                if (!pdf_dict_get(gctx, page->obj, PDF_NAME(Annots))) {
                    pdf_dict_put_array(gctx, page->obj, PDF_NAME(Annots), lcount);
                }
                annots = pdf_dict_get(gctx, page->obj, PDF_NAME(Annots));
                for (i = 0; i < lcount; i++) {
                    fz_try(gctx) {
                        for (; i < lcount; i++) {
                            text = JM_StrAsChar(PyTuple_GET_ITEM(linklist, i));
                    if (!text) {
                        PySys_WriteStderr("skipping bad link / annot item %zi.\n", i);
                        continue;
                    }
                        annot = pdf_add_object_drop(gctx, page->doc,
                                JM_pdf_obj_from_str(gctx, page->doc, text));
                        ind_obj = pdf_new_indirect(gctx, page->doc, pdf_to_num(gctx, annot), 0);
                        pdf_array_push_drop(gctx, annots, ind_obj);
                        pdf_drop_obj(gctx, annot);
                    }
                    }
                    fz_catch(gctx) {
                        PySys_WriteStderr("skipping bad link / annot item %zi.\n", i);
                    }
                }
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // Page clean contents stream
        //----------------------------------------------------------------
        FITZEXCEPTION(clean_contents, !result)
        %pythonprepend clean_contents
%{"""Clean page /Contents into one object."""
CheckParent(self)
if not sanitize and not self.is_wrapped:
    self.wrap_contents()%}
        PyObject *clean_contents(int sanitize=1)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            if (!page) {
                Py_RETURN_NONE;
            }
            #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22
            pdf_filter_factory list[2] = { 0 };
            pdf_sanitize_filter_options sopts = { 0 };
            pdf_filter_options filter = {
                1,     // recurse: true
                0,     // instance forms
                0,     // do not ascii-escape binary data
                0,     // no_update
                NULL,  // end_page_opaque
                NULL,  // end page
                list,  // filters
                };
            if (sanitize) {
              list[0].filter = pdf_new_sanitize_filter;
              list[0].options = &sopts;
            }
            #else
            pdf_filter_options filter = {
                NULL,  // opaque
                NULL,  // image filter
                NULL,  // text filter
                NULL,  // after text
                NULL,  // end page
                1,     // recurse: true
                1,     // instance forms
                1,     // sanitize plus filtering
                0      // do not ascii-escape binary data
                };
            filter.sanitize = sanitize;
            #endif
            fz_try(gctx) {
                pdf_filter_page_contents(gctx, page->doc, page, &filter);
            }
            fz_catch(gctx) {
                Py_RETURN_NONE;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // Show a PDF page
        //----------------------------------------------------------------
        FITZEXCEPTION(_show_pdf_page, !result)
        PyObject *_show_pdf_page(struct Page *fz_srcpage, int overlay=1, PyObject *matrix=NULL, int xref=0, int oc=0, PyObject *clip = NULL, struct Graftmap *graftmap = NULL, char *_imgname = NULL)
        {
            pdf_obj *xobj1=NULL, *xobj2=NULL, *resources;
            fz_buffer *res=NULL, *nres=NULL;
            fz_rect cropbox = JM_rect_from_py(clip);
            fz_matrix mat = JM_matrix_from_py(matrix);
            int rc_xref = xref;
            fz_var(xobj1);
            fz_var(xobj2);
            fz_try(gctx) {
                pdf_page *tpage = pdf_page_from_fz_page(gctx, (fz_page *) $self);
                pdf_obj *tpageref = tpage->obj;
                pdf_document *pdfout = tpage->doc;    // target PDF
                ENSURE_OPERATION(gctx, pdfout);
                //-------------------------------------------------------------
                // convert the source page to a Form XObject
                //-------------------------------------------------------------
                xobj1 = JM_xobject_from_page(gctx, pdfout, (fz_page *) fz_srcpage,
                                             xref, (pdf_graft_map *) graftmap);
                if (!rc_xref) rc_xref = pdf_to_num(gctx, xobj1);

                //-------------------------------------------------------------
                // create referencing XObject (controls display on target page)
                //-------------------------------------------------------------
                // fill reference to xobj1 into the /Resources
                //-------------------------------------------------------------
                pdf_obj *subres1 = pdf_new_dict(gctx, pdfout, 5);
                pdf_dict_puts(gctx, subres1, "fullpage", xobj1);
                pdf_obj *subres  = pdf_new_dict(gctx, pdfout, 5);
                pdf_dict_put_drop(gctx, subres, PDF_NAME(XObject), subres1);

                res = fz_new_buffer(gctx, 20);
                fz_append_string(gctx, res, "/fullpage Do");

                xobj2 = pdf_new_xobject(gctx, pdfout, cropbox, mat, subres, res);
                if (oc > 0) {
                    JM_add_oc_object(gctx, pdfout, pdf_resolve_indirect(gctx, xobj2), oc);
                }
                pdf_drop_obj(gctx, subres);
                fz_drop_buffer(gctx, res);

                //-------------------------------------------------------------
                // update target page with xobj2:
                //-------------------------------------------------------------
                // 1. insert Xobject in Resources
                //-------------------------------------------------------------
                resources = pdf_dict_get_inheritable(gctx, tpageref, PDF_NAME(Resources));
                subres = pdf_dict_get(gctx, resources, PDF_NAME(XObject));
                if (!subres) {
                    subres = pdf_dict_put_dict(gctx, resources, PDF_NAME(XObject), 5);
                }

                pdf_dict_puts(gctx, subres, _imgname, xobj2);

                //-------------------------------------------------------------
                // 2. make and insert new Contents object
                //-------------------------------------------------------------
                nres = fz_new_buffer(gctx, 50);       // buffer for Do-command
                fz_append_string(gctx, nres, " q /");    // Do-command
                fz_append_string(gctx, nres, _imgname);
                fz_append_string(gctx, nres, " Do Q ");

                JM_insert_contents(gctx, pdfout, tpageref, nres, overlay);
                fz_drop_buffer(gctx, nres);
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, xobj1);
                pdf_drop_obj(gctx, xobj2);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", rc_xref);
        }

        //----------------------------------------------------------------
        // insert an image
        //----------------------------------------------------------------
        FITZEXCEPTION(_insert_image, !result)
        PyObject *
        _insert_image(char *filename=NULL,
                struct Pixmap *pixmap=NULL,
                PyObject *stream=NULL,
                PyObject *imask=NULL,
                PyObject *clip=NULL,
                int overlay=1,
                int rotate=0,
                int keep_proportion=1,
                int oc=0,
                int width=0,
                int height=0,
                int xref=0,
                int alpha=-1,
                const char *_imgname=NULL,
                PyObject *digests=NULL)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_document *pdf = page->doc;
            float w = width, h = height;
            fz_pixmap *pm = NULL;
            fz_pixmap *pix = NULL;
            fz_image *mask = NULL, *zimg = NULL, *image = NULL, *freethis = NULL;
            pdf_obj *resources, *xobject, *ref;
            fz_buffer *nres = NULL,  *imgbuf = NULL, *maskbuf = NULL;
            fz_compressed_buffer *cbuf1 = NULL;
            int xres, yres, bpc, img_xref = xref, rc_digest = 0;
            unsigned char digest[16];
            PyObject *md5_py = NULL, *temp;
            const char *template = "\nq\n%g %g %g %g %g %g cm\n/%s Do\nQ\n";

            fz_try(gctx) {
                if (xref > 0) {
                    ref = pdf_new_indirect(gctx, pdf, xref, 0);
                    w = pdf_to_int(gctx,
                        pdf_dict_geta(gctx, ref,
                        PDF_NAME(Width), PDF_NAME(W)));
                    h = pdf_to_int(gctx,
                        pdf_dict_geta(gctx, ref,
                        PDF_NAME(Height), PDF_NAME(H)));
                    if ((w + h) == 0) {
                        RAISEPY(gctx, MSG_IS_NO_IMAGE, PyExc_ValueError);
                    }
                    goto have_xref;
                }
                if (EXISTS(stream)) {
                    imgbuf = JM_BufferFromBytes(gctx, stream);
                    goto have_stream;
                }
                if (filename) {
                    imgbuf = fz_read_file(gctx, filename);
                    goto have_stream;
                }
            // process pixmap ---------------------------------
                fz_pixmap *arg_pix = (fz_pixmap *) pixmap;
                w = arg_pix->w;
                h = arg_pix->h;
                fz_md5_pixmap(gctx, arg_pix, digest);
                md5_py = PyBytes_FromStringAndSize(digest, 16);
                temp = PyDict_GetItem(digests, md5_py);
                if (temp) {
                    img_xref = (int) PyLong_AsLong(temp);
                    ref = pdf_new_indirect(gctx, page->doc, img_xref, 0);
                    goto have_xref;
                }
                if (arg_pix->alpha == 0) {
                    image = fz_new_image_from_pixmap(gctx, arg_pix, NULL);
                } else {
                    pm = fz_convert_pixmap(gctx, arg_pix, NULL, NULL, NULL,
                            fz_default_color_params, 1);
                    pm->alpha = 0;
                    pm->colorspace = NULL;
                    mask = fz_new_image_from_pixmap(gctx, pm, NULL);
                    image = fz_new_image_from_pixmap(gctx, arg_pix, mask);
                }
                goto have_image;

            // process stream ---------------------------------
            have_stream:;
                fz_md5 state;
                fz_md5_init(&state);
                fz_md5_update(&state, imgbuf->data, imgbuf->len);
                if (imask != Py_None) {
                    maskbuf = JM_BufferFromBytes(gctx, imask);
                    fz_md5_update(&state, maskbuf->data, maskbuf->len);
                }
                fz_md5_final(&state, digest);
                md5_py = PyBytes_FromStringAndSize(digest, 16);
                temp = PyDict_GetItem(digests, md5_py);
                if (temp) {
                    img_xref = (int) PyLong_AsLong(temp);
                    ref = pdf_new_indirect(gctx, page->doc, img_xref, 0);
                    w = pdf_to_int(gctx,
                        pdf_dict_geta(gctx, ref,
                        PDF_NAME(Width), PDF_NAME(W)));
                    h = pdf_to_int(gctx,
                        pdf_dict_geta(gctx, ref,
                        PDF_NAME(Height), PDF_NAME(H)));
                    goto have_xref;
                }
                image = fz_new_image_from_buffer(gctx, imgbuf);
                w = image->w;
                h = image->h;
                if (imask == Py_None) {
                    goto have_image;
                }

                cbuf1 = fz_compressed_image_buffer(gctx, image);
                if (!cbuf1) {
                    RAISEPY(gctx, "uncompressed image cannot have mask", PyExc_ValueError);
                }
                bpc = image->bpc;
                fz_colorspace *colorspace = image->colorspace;
                fz_image_resolution(image, &xres, &yres);
                mask = fz_new_image_from_buffer(gctx, maskbuf);
                zimg = fz_new_image_from_compressed_buffer(gctx, w, h,
                            bpc, colorspace, xres, yres, 1, 0, NULL,
                            NULL, cbuf1, mask);
                freethis = image;
                image = zimg;
                zimg = NULL;
                goto have_image;

            have_image:;
                ref =  pdf_add_image(gctx, pdf, image);
                if (oc) {
                    JM_add_oc_object(gctx, pdf, ref, oc);
                }
                img_xref = pdf_to_num(gctx, ref);
                DICT_SETITEM_DROP(digests, md5_py, Py_BuildValue("i", img_xref));
                rc_digest = 1;
            have_xref:;
                resources = pdf_dict_get_inheritable(gctx, page->obj,
                                PDF_NAME(Resources));
                if (!resources) {
                    resources = pdf_dict_put_dict(gctx, page->obj,
                                    PDF_NAME(Resources), 2);
                }
                xobject = pdf_dict_get(gctx, resources, PDF_NAME(XObject));
                if (!xobject) {
                    xobject = pdf_dict_put_dict(gctx, resources,
                                  PDF_NAME(XObject), 2);
                }
                fz_matrix mat = calc_image_matrix(w, h, clip, rotate, keep_proportion);
                pdf_dict_puts_drop(gctx, xobject, _imgname, ref);
                nres = fz_new_buffer(gctx, 50);
                fz_append_printf(gctx, nres, template,
                                 mat.a, mat.b, mat.c, mat.d, mat.e, mat.f, _imgname);
                JM_insert_contents(gctx, pdf, page->obj, nres, overlay);
            }
            fz_always(gctx) {
                if (freethis) {
                    fz_drop_image(gctx, freethis);
                } else {
                    fz_drop_image(gctx, image);
                }
                fz_drop_image(gctx, mask);
                fz_drop_image(gctx, zimg);
                fz_drop_pixmap(gctx, pix);
                fz_drop_pixmap(gctx, pm);
                fz_drop_buffer(gctx, imgbuf);
                fz_drop_buffer(gctx, maskbuf);
                fz_drop_buffer(gctx, nres);
            }
            fz_catch(gctx) {
                return NULL;
            }

            if (rc_digest) {
                return Py_BuildValue("iO", img_xref, digests);
            } else {
                return Py_BuildValue("iO", img_xref, Py_None);
            }
        }


        //----------------------------------------------------------------
        // Page.refresh()
        //----------------------------------------------------------------
        %pythoncode %{
        def refresh(self):
            doc = self.parent
            page = doc.reload_page(self)
            self = page
        %}


        //----------------------------------------------------------------
        // insert font
        //----------------------------------------------------------------
        %pythoncode
%{
def insert_font(self, fontname="helv", fontfile=None, fontbuffer=None,
               set_simple=False, wmode=0, encoding=0):
    doc = self.parent
    if doc is None:
        raise ValueError("orphaned object: parent is None")
    idx = 0

    if fontname.startswith("/"):
        fontname = fontname[1:]
    inv_chars = INVALID_NAME_CHARS.intersection(fontname)
    if inv_chars != set():
        raise ValueError(f"bad fontname chars {inv_chars}")

    font = CheckFont(self, fontname)
    if font is not None:                    # font already in font list of page
        xref = font[0]                      # this is the xref
        if CheckFontInfo(doc, xref):        # also in our document font list?
            return xref                     # yes: we are done
        # need to build the doc FontInfo entry - done via get_char_widths
        doc.get_char_widths(xref)
        return xref

    #--------------------------------------------------------------------------
    # the font is not present for this page
    #--------------------------------------------------------------------------

    bfname = Base14_fontdict.get(fontname.lower(), None) # BaseFont if Base-14 font

    serif = 0
    CJK_number = -1
    CJK_list_n = ["china-t", "china-s", "japan", "korea"]
    CJK_list_s = ["china-ts", "china-ss", "japan-s", "korea-s"]

    try:
        CJK_number = CJK_list_n.index(fontname)
        serif = 0
    except:
        pass

    if CJK_number < 0:
        try:
            CJK_number = CJK_list_s.index(fontname)
            serif = 1
        except:
            pass

    if fontname.lower() in fitz_fontdescriptors.keys():
        import pymupdf_fonts
        fontbuffer = pymupdf_fonts.myfont(fontname)  # make a copy
        del pymupdf_fonts

    # install the font for the page
    if fontfile != None:
        if type(fontfile) is str:
            fontfile_str = fontfile
        elif hasattr(fontfile, "absolute"):
            fontfile_str = str(fontfile)
        elif hasattr(fontfile, "name"):
            fontfile_str = fontfile.name
        else:
            raise ValueError("bad fontfile")
    else:
        fontfile_str = None
    val = self._insertFont(fontname, bfname, fontfile_str, fontbuffer, set_simple, idx,
                           wmode, serif, encoding, CJK_number)

    if not val:                   # did not work, error return
        return val

    xref = val[0]                 # xref of installed font
    fontdict = val[1]

    if CheckFontInfo(doc, xref):  # check again: document already has this font
        return xref               # we are done

    # need to create document font info
    doc.get_char_widths(xref, fontdict=fontdict)
    return xref

%}

        FITZEXCEPTION(_insertFont, !result)
        PyObject *_insertFont(char *fontname, char *bfname,
                             char *fontfile,
                             PyObject *fontbuffer,
                             int set_simple, int idx,
                             int wmode, int serif,
                             int encoding, int ordering)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            pdf_document *pdf;
            pdf_obj *resources, *fonts, *font_obj;
            PyObject *value;
            fz_try(gctx) {
                ASSERT_PDF(page);
                pdf = page->doc;

                value = JM_insert_font(gctx, pdf, bfname, fontfile,fontbuffer,
                            set_simple, idx, wmode, serif, encoding, ordering);

                // get the objects /Resources, /Resources/Font
                resources = pdf_dict_get_inheritable(gctx, page->obj, PDF_NAME(Resources));
                fonts = pdf_dict_get(gctx, resources, PDF_NAME(Font));
                if (!fonts) {  // page has no fonts yet
                    fonts = pdf_new_dict(gctx, pdf, 5);
                    pdf_dict_putl_drop(gctx, page->obj, fonts, PDF_NAME(Resources), PDF_NAME(Font), NULL);
                }
                // store font in resources and fonts objects will contain named reference to font
                int xref = 0;
                JM_INT_ITEM(value, 0, &xref);
                if (!xref) {
                    RAISEPY(gctx, "cannot insert font", PyExc_RuntimeError);
                }
                font_obj = pdf_new_indirect(gctx, pdf, xref, 0);
                pdf_dict_puts_drop(gctx, fonts, fontname, font_obj);
            }
            fz_always(gctx) {
                ;
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            return value;
        }

        //----------------------------------------------------------------
        // Get page transformation matrix
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(transformation_matrix, """Page transformation matrix.""")
        %pythonappend transformation_matrix %{
        if self.rotation % 360 == 0:
            val = Matrix(val)
        else:
            val = Matrix(1, 0, 0, -1, 0, self.cropbox.height)
        %}
        PyObject *transformation_matrix()
        {
            fz_matrix ctm = fz_identity;
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            if (!page) return JM_py_from_matrix(ctm);
            fz_try(gctx) {
                pdf_page_transform(gctx, page, NULL, &ctm);
            }
            fz_catch(gctx) {;}
            return JM_py_from_matrix(ctm);
        }

        //----------------------------------------------------------------
        // Page Get list of contents objects
        //----------------------------------------------------------------
        FITZEXCEPTION(get_contents, !result)
        PARENTCHECK(get_contents, """Get xrefs of /Contents objects.""")
        PyObject *get_contents()
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self);
            PyObject *list = NULL;
            pdf_obj *contents = NULL, *icont = NULL;
            int i, xref;
            size_t n = 0;
            fz_try(gctx) {
                ASSERT_PDF(page);
                contents = pdf_dict_get(gctx, page->obj, PDF_NAME(Contents));
                if (pdf_is_array(gctx, contents)) {
                    n = pdf_array_len(gctx, contents);
                    list = PyList_New(n);
                    for (i = 0; i < n; i++) {
                        icont = pdf_array_get(gctx, contents, i);
                        xref = pdf_to_num(gctx, icont);
                        PyList_SET_ITEM(list, i, Py_BuildValue("i", xref));
                    }
                }
                else if (contents) {
                    list = PyList_New(1);
                    xref = pdf_to_num(gctx, contents);
                    PyList_SET_ITEM(list, 0, Py_BuildValue("i", xref));
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            if (list) {
                return list;
            }
            return PyList_New(0);
        }

        //----------------------------------------------------------------
        //
        //----------------------------------------------------------------
        %pythoncode %{
        def set_contents(self, xref: int)->None:
            """Set object at 'xref' as the page's /Contents."""
            CheckParent(self)
            doc = self.parent
            if doc.is_closed:
                raise ValueError("document closed")
            if not doc.is_pdf:
                raise ValueError("is no PDF")
            if not xref in range(1, doc.xref_length()):
                raise ValueError("bad xref")
            if not doc.xref_is_stream(xref):
                raise ValueError("xref is no stream")
            doc.xref_set_key(self.xref, "Contents", "%i 0 R" % xref)


        @property
        def is_wrapped(self):
            """Check if /Contents is wrapped with string pair "q" / "Q"."""
            if getattr(self, "was_wrapped", False):  # costly checks only once
                return True
            cont = self.read_contents().split()
            if cont == []:  # no contents treated as okay
                self.was_wrapped = True
                return True
            if cont[0] != b"q" or cont[-1] != b"Q":
                return False  # potential "geometry" issue
            self.was_wrapped = True  # cheap check next time
            return True


        def wrap_contents(self):
            if self.is_wrapped:  # avoid unnecessary wrapping
                return
            TOOLS._insert_contents(self, b"q\n", False)
            TOOLS._insert_contents(self, b"\nQ", True)
            self.was_wrapped = True  # indicate not needed again


        def links(self, kinds=None):
            """ Generator over the links of a page.

            Args:
                kinds: (list) link kinds to subselect from. If none,
                       all links are returned. E.g. kinds=[LINK_URI]
                       will only yield URI links.
            """
            all_links = self.get_links()
            for link in all_links:
                if kinds is None or link["kind"] in kinds:
                    yield (link)


        def annots(self, types=None):
            """ Generator over the annotations of a page.

            Args:
                types: (list) annotation types to subselect from. If none,
                       all annotations are returned. E.g. types=[PDF_ANNOT_LINE]
                       will only yield line annotations.
            """
            skip_types = (PDF_ANNOT_LINK, PDF_ANNOT_POPUP, PDF_ANNOT_WIDGET)
            if not hasattr(types, "__getitem__"):
                annot_xrefs = [a[0] for a in self.annot_xrefs() if a[1] not in skip_types]
            else:
                annot_xrefs = [a[0] for a in self.annot_xrefs() if a[1] in types and a[1] not in skip_types]
            for xref in annot_xrefs:
                annot = self.load_annot(xref)
                annot._yielded=True
                yield annot


        def widgets(self, types=None):
            """ Generator over the widgets of a page.

            Args:
                types: (list) field types to subselect from. If none,
                        all fields are returned. E.g. types=[PDF_WIDGET_TYPE_TEXT]
                        will only yield text fields.
            """
            widget_xrefs = [a[0] for a in self.annot_xrefs() if a[1] == PDF_ANNOT_WIDGET]
            for xref in widget_xrefs:
                widget = self.load_widget(xref)
                if types == None or widget.field_type in types:
                    yield (widget)


        def __str__(self):
            CheckParent(self)
            x = self.parent.name
            if self.parent.stream is not None:
                x = "<memory, doc# %i>" % (self.parent._graft_id,)
            if x == "":
                x = "<new PDF, doc# %i>" % self.parent._graft_id
            return "page %s of %s" % (self.number, x)

        def __repr__(self):
            CheckParent(self)
            x = self.parent.name
            if self.parent.stream is not None:
                x = "<memory, doc# %i>" % (self.parent._graft_id,)
            if x == "":
                x = "<new PDF, doc# %i>" % self.parent._graft_id
            return "page %s of %s" % (self.number, x)

        def _reset_annot_refs(self):
            """Invalidate / delete all annots of this page."""
            for annot in self._annot_refs.values():
                if annot:
                    annot._erase()
            self._annot_refs.clear()

        @property
        def xref(self):
            """PDF xref number of page."""
            CheckParent(self)
            return self.parent.page_xref(self.number)

        def _erase(self):
            self._reset_annot_refs()
            self._image_infos = None
            try:
                self.parent._forget_page(self)
            except:
                pass
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)
            self.parent = None
            self.number = None


        def __del__(self):
            self._erase()


        def get_fonts(self, full=False):
            """List of fonts defined in the page object."""
            CheckParent(self)
            return self.parent.get_page_fonts(self.number, full=full)


        def get_images(self, full=False):
            """List of images defined in the page object."""
            CheckParent(self)
            ret = self.parent.get_page_images(self.number, full=full)
            return ret


        def get_xobjects(self):
            """List of xobjects defined in the page object."""
            CheckParent(self)
            return self.parent.get_page_xobjects(self.number)


        def read_contents(self):
            """All /Contents streams concatenated to one bytes object."""
            return TOOLS._get_all_contents(self)


        @property
        def mediabox_size(self):
            return Point(self.mediabox.x1, self.mediabox.y1)
        %}
    }
};
%clearnodefaultctor;

//------------------------------------------------------------------------
// Pixmap
//------------------------------------------------------------------------
struct Pixmap
{
    %extend {
        ~Pixmap() {
            DEBUGMSG1("Pixmap");
            fz_pixmap *this_pix = (fz_pixmap *) $self;
            fz_drop_pixmap(gctx, this_pix);
            DEBUGMSG2;
        }
        FITZEXCEPTION(Pixmap, !result)
        %pythonprepend Pixmap
%{"""Pixmap(colorspace, irect, alpha) - empty pixmap.
Pixmap(colorspace, src) - copy changing colorspace.
Pixmap(src, width, height,[clip]) - scaled copy, float dimensions.
Pixmap(src, alpha=True) - copy adding / dropping alpha.
Pixmap(source, mask) - from a non-alpha and a mask pixmap.
Pixmap(file) - from an image file.
Pixmap(memory) - from an image in memory (bytes).
Pixmap(colorspace, width, height, samples, alpha) - from samples data.
Pixmap(PDFdoc, xref) - from an image xref in a PDF document.
"""%}
        //----------------------------------------------------------------
        // create empty pixmap with colorspace and IRect
        //----------------------------------------------------------------
        Pixmap(struct Colorspace *cs, PyObject *bbox, int alpha = 0)
        {
            fz_pixmap *pm = NULL;
            fz_try(gctx) {
                pm = fz_new_pixmap_with_bbox(gctx, (fz_colorspace *) cs, JM_irect_from_py(bbox), NULL, alpha);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pm;
        }

        //----------------------------------------------------------------
        // copy pixmap, converting colorspace
        //----------------------------------------------------------------
        Pixmap(struct Colorspace *cs, struct Pixmap *spix)
        {
            fz_pixmap *pm = NULL;
            fz_try(gctx) {
                if (!fz_pixmap_colorspace(gctx, (fz_pixmap *) spix)) {
                    RAISEPY(gctx, "source colorspace must not be None", PyExc_ValueError);
                }
                fz_colorspace *cspace = NULL;
                if (cs) {
                    cspace = (fz_colorspace *) cs;
                }
                if (cspace) {
                    pm = fz_convert_pixmap(gctx, (fz_pixmap *) spix, cspace, NULL, NULL, fz_default_color_params, 1);
                } else {
                    pm = fz_new_pixmap_from_alpha_channel(gctx, (fz_pixmap *) spix);
                    if (!pm) {
                        RAISEPY(gctx, MSG_PIX_NOALPHA, PyExc_RuntimeError);
                    }
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pm;
        }


        //----------------------------------------------------------------
        // add mask to a pixmap w/o alpha channel
        //----------------------------------------------------------------
        Pixmap(struct Pixmap *spix, struct Pixmap *mpix)
        {
            fz_pixmap *dst = NULL;
            fz_pixmap *spm = (fz_pixmap *) spix;
            fz_pixmap *mpm = (fz_pixmap *) mpix;
            fz_try(gctx) {
                if (!spix) {  // intercept NULL for spix: make alpha only pix
                    dst = fz_new_pixmap_from_alpha_channel(gctx, mpm);
                    if (!dst) {
                        RAISEPY(gctx, MSG_PIX_NOALPHA, PyExc_RuntimeError);
                    }
                } else {
                    dst = fz_new_pixmap_from_color_and_mask(gctx, spm, mpm);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) dst;
        }


        //----------------------------------------------------------------
        // create pixmap as scaled copy of another one
        //----------------------------------------------------------------
        Pixmap(struct Pixmap *spix, float w, float h, PyObject *clip=NULL)
        {
            fz_pixmap *pm = NULL;
            fz_pixmap *src_pix = (fz_pixmap *) spix;
            fz_try(gctx) {
                fz_irect bbox = JM_irect_from_py(clip);
                if (clip != Py_None && (fz_is_infinite_irect(bbox) || fz_is_empty_irect(bbox))) {
                    RAISEPY(gctx, "bad clip parameter", PyExc_ValueError);
                }
                if (!fz_is_infinite_irect(bbox)) {
                    pm = fz_scale_pixmap(gctx, src_pix, src_pix->x, src_pix->y, w, h, &bbox);
                } else {
                    pm = fz_scale_pixmap(gctx, src_pix, src_pix->x, src_pix->y, w, h, NULL);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pm;
        }


        //----------------------------------------------------------------
        // copy pixmap & add / drop the alpha channel
        //----------------------------------------------------------------
        Pixmap(struct Pixmap *spix, int alpha=1)
        {
            fz_pixmap *pm = NULL, *src_pix = (fz_pixmap *) spix;
            int n, w, h, i;
            fz_separations *seps = NULL;
            fz_try(gctx) {
                if (!INRANGE(alpha, 0, 1)) {
                    RAISEPY(gctx, "bad alpha value", PyExc_ValueError);
                }
                fz_colorspace *cs = fz_pixmap_colorspace(gctx, src_pix);
                if (!cs && !alpha) {
                    RAISEPY(gctx, "cannot drop alpha for 'NULL' colorspace", PyExc_ValueError);
                }
                n = fz_pixmap_colorants(gctx, src_pix);
                w = fz_pixmap_width(gctx, src_pix);
                h = fz_pixmap_height(gctx, src_pix);
                pm = fz_new_pixmap(gctx, cs, w, h, seps, alpha);
                pm->x = src_pix->x;
                pm->y = src_pix->y;
                pm->xres = src_pix->xres;
                pm->yres = src_pix->yres;

                // copy samples data ------------------------------------------
                unsigned char *sptr = src_pix->samples;
                unsigned char *tptr = pm->samples;
                if (src_pix->alpha == pm->alpha) {  // identical samples
                    memcpy(tptr, sptr, w * h * (n + alpha));
                } else {
                    for (i = 0; i < w * h; i++) {
                        memcpy(tptr, sptr, n);
                        tptr += n;
                        if (pm->alpha) {
                            tptr[0] = 255;
                            tptr++;
                        }
                        sptr += n + src_pix->alpha;
                    }
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pm;
        }

        //----------------------------------------------------------------
        // create pixmap from samples data
        //----------------------------------------------------------------
        Pixmap(struct Colorspace *cs, int w, int h, PyObject *samples, int alpha=0)
        {
            int n = fz_colorspace_n(gctx, (fz_colorspace *) cs);
            int stride = (n + alpha) * w;
            fz_separations *seps = NULL;
            fz_buffer *res = NULL;
            fz_pixmap *pm = NULL;
            fz_try(gctx) {
                size_t size = 0;
                unsigned char *c = NULL;
                res = JM_BufferFromBytes(gctx, samples);
                if (!res) {
                    RAISEPY(gctx, "bad samples data", PyExc_ValueError);
                }
                size = fz_buffer_storage(gctx, res, &c);
                if (stride * h != size) {
                    RAISEPY(gctx, "bad samples length", PyExc_ValueError);
                }
                pm = fz_new_pixmap(gctx, (fz_colorspace *) cs, w, h, seps, alpha);
                memcpy(pm->samples, c, size);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pm;
        }


        //----------------------------------------------------------------
        // create pixmap from filename, file object, pathlib.Path or memory
        //----------------------------------------------------------------
        Pixmap(PyObject *imagedata)
        {
            fz_buffer *res = NULL;
            fz_image *img = NULL;
            fz_pixmap *pm = NULL;
            PyObject *fname = NULL;
            PyObject *name = PyUnicode_FromString("name");
            fz_try(gctx) {
                if (PyObject_HasAttrString(imagedata, "resolve")) {
                    fname = PyObject_CallMethod(imagedata, "__str__", NULL);
                    if (fname) {
                        img = fz_new_image_from_file(gctx, JM_StrAsChar(fname));
                    }
                } else if (PyObject_HasAttr(imagedata, name)) {
                    fname = PyObject_GetAttr(imagedata, name);
                    if (fname) {
                        img = fz_new_image_from_file(gctx, JM_StrAsChar(fname));
                    }
                } else if (PyUnicode_Check(imagedata)) {
                    img = fz_new_image_from_file(gctx, JM_StrAsChar(imagedata));
                } else {
                    res = JM_BufferFromBytes(gctx, imagedata);
                    if (!res || !fz_buffer_storage(gctx, res, NULL)) {
                        RAISEPY(gctx, "bad image data", PyExc_ValueError);
                    }
                    img = fz_new_image_from_buffer(gctx, res);
                }
                pm = fz_get_pixmap_from_image(gctx, img, NULL, NULL, NULL, NULL);
                int xres, yres;
                fz_image_resolution(img, &xres, &yres);
                pm->xres = xres;
                pm->yres = yres;
            }
            fz_always(gctx) {
                Py_CLEAR(fname);
                Py_CLEAR(name);
                fz_drop_image(gctx, img);
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pm;
        }


        //----------------------------------------------------------------
        // Create pixmap from PDF image identified by XREF number
        //----------------------------------------------------------------
        Pixmap(struct Document *doc, int xref)
        {
            fz_image *img = NULL;
            fz_pixmap *pix = NULL;
            pdf_obj *ref = NULL;
            pdf_obj *type;
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc);
            fz_try(gctx) {
                ASSERT_PDF(pdf);
                int xreflen = pdf_xref_len(gctx, pdf);
                if (!INRANGE(xref, 1, xreflen-1)) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                ref = pdf_new_indirect(gctx, pdf, xref, 0);
                type = pdf_dict_get(gctx, ref, PDF_NAME(Subtype));
                if (!pdf_name_eq(gctx, type, PDF_NAME(Image)) &&
                    !pdf_name_eq(gctx, type, PDF_NAME(Alpha)) &&
                    !pdf_name_eq(gctx, type, PDF_NAME(Luminosity))) {
                    RAISEPY(gctx, MSG_IS_NO_IMAGE, PyExc_ValueError);
                }
                img = pdf_load_image(gctx, pdf, ref);
                pix = fz_get_pixmap_from_image(gctx, img, NULL, NULL, NULL, NULL);
            }
            fz_always(gctx) {
                fz_drop_image(gctx, img);
                pdf_drop_obj(gctx, ref);
            }
            fz_catch(gctx) {
                fz_drop_pixmap(gctx, pix);
                return NULL;
            }
            return (struct Pixmap *) pix;
        }


        //----------------------------------------------------------------
        // warp
        //----------------------------------------------------------------
        FITZEXCEPTION(warp, !result)
        %pythonprepend warp %{
        """Return pixmap from a warped quad."""
        EnsureOwnership(self)
        if not quad.is_convex: raise ValueError("quad must be convex")%}
        struct Pixmap *warp(PyObject *quad, int width, int height)
        {
            fz_point points[4];
            fz_quad q = JM_quad_from_py(quad);
            fz_pixmap *dst = NULL;
            points[0] = q.ul;
            points[1] = q.ur;
            points[2] = q.lr;
            points[3] = q.ll;

            fz_try(gctx) {
                dst = fz_warp_pixmap(gctx, (fz_pixmap *) $self, points, width, height);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) dst;
        }


        //----------------------------------------------------------------
        // shrink
        //----------------------------------------------------------------
        ENSURE_OWNERSHIP(shrink, """Divide width and height by 2**factor.
        E.g. factor=1 shrinks to 25% of original size (in place).""")
        void shrink(int factor)
        {
            if (factor < 1)
            {
                JM_Warning("ignoring shrink factor < 1");
                return;
            }
            fz_subsample_pixmap(gctx, (fz_pixmap *) $self, factor);
        }

        //----------------------------------------------------------------
        // apply gamma correction
        //----------------------------------------------------------------
        ENSURE_OWNERSHIP(gamma_with, """Apply correction with some float.
gamma=1 is a no-op.""")
        void gamma_with(float gamma)
        {
            if (!fz_pixmap_colorspace(gctx, (fz_pixmap *) $self))
            {
                JM_Warning("colorspace invalid for function");
                return;
            }
            fz_gamma_pixmap(gctx, (fz_pixmap *) $self, gamma);
        }

        //----------------------------------------------------------------
        // tint pixmap with color
        //----------------------------------------------------------------
        %pythonprepend tint_with
%{"""Tint colors with modifiers for black and white."""
EnsureOwnership(self)
if not self.colorspace or self.colorspace.n > 3:
    print("warning: colorspace invalid for function")
    return%}
        void tint_with(int black, int white)
        {
            fz_tint_pixmap(gctx, (fz_pixmap *) $self, black, white);
        }

        //-----------------------------------------------------------------
        // clear all of pixmap samples to 0x00 */
        //-----------------------------------------------------------------
        ENSURE_OWNERSHIP(clear_with, """Fill all color components with same value.""")
        void clear_with()
        {
            fz_clear_pixmap(gctx, (fz_pixmap *) $self);
        }

        //-----------------------------------------------------------------
        // clear total pixmap with value */
        //-----------------------------------------------------------------
        void clear_with(int value)
        {
            fz_clear_pixmap_with_value(gctx, (fz_pixmap *) $self, value);
        }

        //-----------------------------------------------------------------
        // clear pixmap rectangle with value
        //-----------------------------------------------------------------
        void clear_with(int value, PyObject *bbox)
        {
            JM_clear_pixmap_rect_with_value(gctx, (fz_pixmap *) $self, value, JM_irect_from_py(bbox));
        }

        //-----------------------------------------------------------------
        // copy pixmaps
        //-----------------------------------------------------------------
        FITZEXCEPTION(copy, !result)
        ENSURE_OWNERSHIP(copy, """Copy bbox from another Pixmap.""")
        PyObject *copy(struct Pixmap *src, PyObject *bbox)
        {
            fz_try(gctx) {
                fz_pixmap *pm = (fz_pixmap *) $self, *src_pix = (fz_pixmap *) src;
                if (!fz_pixmap_colorspace(gctx, src_pix)) {
                    RAISEPY(gctx, "cannot copy pixmap with NULL colorspace", PyExc_ValueError);
                }
                if (pm->alpha != src_pix->alpha) {
                    RAISEPY(gctx, "source and target alpha must be equal", PyExc_ValueError);
                }
                fz_copy_pixmap_rect(gctx, pm, src_pix, JM_irect_from_py(bbox), NULL);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //-----------------------------------------------------------------
        // set alpha values
        //-----------------------------------------------------------------
        FITZEXCEPTION(set_alpha, !result)
        ENSURE_OWNERSHIP(set_alpha, """Set alpha channel to values contained in a byte array.
If None, all alphas are 255.

Args:
    alphavalues: (bytes) with length (width * height) or 'None'.
    premultiply: (bool, True) premultiply colors with alpha values.
    opaque: (tuple, length colorspace.n) this color receives opacity 0.
    matte: (tuple, length colorspace.n) preblending background color.
""")
        PyObject *set_alpha(PyObject *alphavalues=NULL, int premultiply=1, PyObject *opaque=NULL, PyObject *matte=NULL)
        {
            fz_buffer *res = NULL;
            fz_pixmap *pix = (fz_pixmap *) $self;
            unsigned char alpha = 0, m = 0;
            fz_try(gctx) {
                if (pix->alpha == 0) {
                    RAISEPY(gctx, MSG_PIX_NOALPHA, PyExc_ValueError);
                }
                size_t i, k, j;
                size_t n = fz_pixmap_colorants(gctx, pix);
                size_t w = (size_t) fz_pixmap_width(gctx, pix);
                size_t h = (size_t) fz_pixmap_height(gctx, pix);
                size_t balen = w * h * (n+1);
                int colors[4];  // make this color opaque
                int bgcolor[4];  // preblending background color
                int zero_out = 0, bground = 0;
                if (opaque && PySequence_Check(opaque) && PySequence_Size(opaque) == n) {
                    for (i = 0; i < n; i++) {
                        if (JM_INT_ITEM(opaque, i, &colors[i]) == 1) {
                            RAISEPY(gctx, "bad opaque components", PyExc_ValueError);
                        }
                    }
                    zero_out = 1;
                }
                if (matte && PySequence_Check(matte) && PySequence_Size(matte) == n) {
                    for (i = 0; i < n; i++) {
                        if (JM_INT_ITEM(matte, i, &bgcolor[i]) == 1) {
                            RAISEPY(gctx, "bad matte components", PyExc_ValueError);
                        }
                    }
                    bground = 1;
                }
                unsigned char *data = NULL;
                size_t data_len = 0;
                if (alphavalues && PyObject_IsTrue(alphavalues)) {
                    res = JM_BufferFromBytes(gctx, alphavalues);
                    data_len = fz_buffer_storage(gctx, res, &data);
                    if (data_len < w * h) {
                        RAISEPY(gctx, "bad alpha values", PyExc_ValueError);
                    }
                }
                i = k = j = 0;
                int data_fix = 255;
                while (i < balen) {
                    alpha = data[k];
                    if (zero_out) {
                        for (j = i; j < i+n; j++) {
                            if (pix->samples[j] != (unsigned char) colors[j - i]) {
                                data_fix = 255;
                                break;
                            } else {
                                data_fix = 0;
                            }
                        }
                    }
                    if (data_len) {
                        if (data_fix == 0) {
                            pix->samples[i+n] = 0;
                        } else {
                            pix->samples[i+n] = alpha;
                        }
                        if (premultiply && !bground) {
                            for (j = i; j < i+n; j++) {
                                pix->samples[j] = fz_mul255(pix->samples[j], alpha);
                            }
                        } else if (bground) {
                            for (j = i; j < i+n; j++) {
                                m = (unsigned char) bgcolor[j - i];
                                pix->samples[j] = m + fz_mul255((pix->samples[j] - m), alpha);
                            }
                        }
                    } else {
                        pix->samples[i+n] = data_fix;
                    }
                    i += n+1;
                    k += 1;
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //-----------------------------------------------------------------
        // Pixmap._tobytes
        //-----------------------------------------------------------------
        FITZEXCEPTION(_tobytes, !result)
        PyObject *_tobytes(int format, int jpg_quality)
        {
            fz_output *out = NULL;
            fz_buffer *res = NULL;
            PyObject *barray = NULL;
            fz_pixmap *pm = (fz_pixmap *) $self;
            fz_try(gctx) {
                size_t size = fz_pixmap_stride(gctx, pm) * pm->h;
                res = fz_new_buffer(gctx, size);
                out = fz_new_output_with_buffer(gctx, res);

                switch(format) {
                    case(1):
                        fz_write_pixmap_as_png(gctx, out, pm);
                        break;
                    case(2):
                        fz_write_pixmap_as_pnm(gctx, out, pm);
                        break;
                    case(3):
                        fz_write_pixmap_as_pam(gctx, out, pm);
                        break;
                    case(5):           // Adobe Photoshop Document
                        fz_write_pixmap_as_psd(gctx, out, pm);
                        break;
                    case(6):           // Postscript format
                        fz_write_pixmap_as_ps(gctx, out, pm);
                        break;
                    #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22
                    case(7):           // JPEG format
                        #if FZ_VERSION_MINOR < 24
                        fz_write_pixmap_as_jpeg(gctx, out, pm, jpg_quality);
                        #else
                        fz_write_pixmap_as_jpeg(gctx, out, pm, jpg_quality, 0 /*invert_cmyk*/);
                        #endif
                        break;
                    #endif
                    default:
                        fz_write_pixmap_as_png(gctx, out, pm);
                        break;
                }
                barray = JM_BinFromBuffer(gctx, res);
            }
            fz_always(gctx) {
                fz_drop_output(gctx, out);
                fz_drop_buffer(gctx, res);
            }

            fz_catch(gctx) {
                return NULL;
            }
            return barray;
        }

        %pythoncode %{
def tobytes(self, output="png", jpg_quality=95):
    """Convert to binary image stream of desired type.

    Can be used as input to GUI packages like tkinter.

    Args:
        output: (str) image type, default is PNG. Others are JPG, JPEG, PNM, PGM, PPM,
                PBM, PAM, PSD, PS.
    Returns:
        Bytes object.
    """
    EnsureOwnership(self)
    valid_formats = {"png": 1, "pnm": 2, "pgm": 2, "ppm": 2, "pbm": 2,
                     "pam": 3, "psd": 5, "ps": 6, "jpg": 7, "jpeg": 7}
                     
    idx = valid_formats.get(output.lower(), None)
    if idx==None:
        raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}")
    if self.alpha and idx in (2, 6, 7):
        raise ValueError("'%s' cannot have alpha" % output)
    if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4):
        raise ValueError("unsupported colorspace for '%s'" % output)
    if idx == 7:
        self.set_dpi(self.xres, self.yres)
    barray = self._tobytes(idx, jpg_quality)
    return barray
    %}


        //-----------------------------------------------------------------
        // output as PDF-OCR
        //-----------------------------------------------------------------
        FITZEXCEPTION(pdfocr_save, !result)
        %pythonprepend pdfocr_save %{
        """Save pixmap as an OCR-ed PDF page."""
        EnsureOwnership(self)
        if not os.getenv("TESSDATA_PREFIX") and not tessdata:
            raise RuntimeError("No OCR support: TESSDATA_PREFIX not set")
        %}
        ENSURE_OWNERSHIP(pdfocr_save, )
        PyObject *pdfocr_save(PyObject *filename, int compress=1, char *language=NULL, char *tessdata=NULL)
        {
            fz_pdfocr_options opts;
            memset(&opts, 0, sizeof opts);
            opts.compress = compress;
            if (language) {
                fz_strlcpy(opts.language, language, sizeof(opts.language));
            }
            if (tessdata) {
                fz_strlcpy(opts.datadir, tessdata, sizeof(opts.language));
            }
            fz_output *out = NULL;
            fz_pixmap *pix = (fz_pixmap *) $self;
            fz_try(gctx) {
                if (PyUnicode_Check(filename)) {
                    fz_save_pixmap_as_pdfocr(gctx, pix, (char *) PyUnicode_AsUTF8(filename), 0, &opts);
                } else {
                    out = JM_new_output_fileptr(gctx, filename);
                    fz_write_pixmap_as_pdfocr(gctx, out, pix, &opts);
                }
            }
            fz_always(gctx) {
                fz_drop_output(gctx, out);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode %{
        def pdfocr_tobytes(self, compress=True, language="eng", tessdata=None):
            """Save pixmap as an OCR-ed PDF page.

            Args:
                compress: (bool) compress, default 1 (True).
                language: (str) language(s) occurring on page, default "eng" (English),
                        multiples like "eng+ger" for English and German.
                tessdata: (str) folder name of Tesseract's language support. Must be
                        given if environment variable TESSDATA_PREFIX is not set.
            Notes:
                On failure, make sure Tesseract is installed and you have set the
                environment variable "TESSDATA_PREFIX" to the folder containing your
                Tesseract's language support data.
            """
            if not os.getenv("TESSDATA_PREFIX") and not tessdata:
                raise RuntimeError("No OCR support: TESSDATA_PREFIX not set")
            EnsureOwnership(self)
            from io import BytesIO
            bio = BytesIO()
            self.pdfocr_save(bio, compress=compress, language=language, tessdata=tessdata)
            return bio.getvalue()
        %}


        //-----------------------------------------------------------------
        // _writeIMG
        //-----------------------------------------------------------------
        FITZEXCEPTION(_writeIMG, !result)
        PyObject *_writeIMG(char *filename, int format, int jpg_quality)
        {
            fz_try(gctx) {
                fz_pixmap *pm = (fz_pixmap *) $self;
                switch(format) {
                    case(1):
                        fz_save_pixmap_as_png(gctx, pm, filename);
                        break;
                    case(2):
                        fz_save_pixmap_as_pnm(gctx, pm, filename);
                        break;
                    case(3):
                        fz_save_pixmap_as_pam(gctx, pm, filename);
                        break;
                    case(5): // Adobe Photoshop Document
                        fz_save_pixmap_as_psd(gctx, pm, filename);
                        break;
                    case(6): // Postscript
                        fz_save_pixmap_as_ps(gctx, pm, filename, 0);
                        break;
                    #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22
                    case(7): // JPEG
                        fz_save_pixmap_as_jpeg(gctx, pm, filename, jpg_quality);
                        break;
                    #endif
                    default:
                        fz_save_pixmap_as_png(gctx, pm, filename);
                        break;
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }
        %pythoncode %{
def save(self, filename, output=None, jpg_quality=95):
    """Output as image in format determined by filename extension.

    Args:
        output: (str) only use to overrule filename extension. Default is PNG.
                Others are JPEG, JPG, PNM, PGM, PPM, PBM, PAM, PSD, PS.
    """
    EnsureOwnership(self)
    valid_formats = {"png": 1, "pnm": 2, "pgm": 2, "ppm": 2, "pbm": 2,
                     "pam": 3, "psd": 5, "ps": 6, "jpg": 7, "jpeg": 7}
                     
    if type(filename) is str:
        pass
    elif hasattr(filename, "absolute"):
        filename = str(filename)
    elif hasattr(filename, "name"):
        filename = filename.name
    if output is None:
        _, ext = os.path.splitext(filename)
        output = ext[1:]

    idx = valid_formats.get(output.lower(), None)
    if idx == None:
        raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}")
    if self.alpha and idx in (2, 6, 7):
        raise ValueError("'%s' cannot have alpha" % output)
    if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4):
        raise ValueError("unsupported colorspace for '%s'" % output)
    if idx == 7:
        self.set_dpi(self.xres, self.yres)
    return self._writeIMG(filename, idx, jpg_quality)

def pil_save(self, *args, unmultiply=False, **kwargs):
    """Write to image file using Pillow.

    Args are passed to Pillow's Image.save method, see their documentation.
    Use instead of save when other output formats are desired.

    :arg bool unmultiply: generates Pillow mode "RGBa" instead of "RGBA".
        Relevant for colorspace RGB with alpha only.
    """
    EnsureOwnership(self)
    try:
        from PIL import Image
    except ImportError:
        print("Pillow not installed")
        raise

    cspace = self.colorspace
    if cspace is None:
        mode = "L"
    elif cspace.n == 1:
        mode = "L" if self.alpha == 0 else "LA"
    elif cspace.n == 3:
        mode = "RGB" if self.alpha == 0 else "RGBA"
        if mode == "RGBA" and unmultiply:
            mode = "RGBa"
    else:
        mode = "CMYK"

    img = Image.frombytes(mode, (self.width, self.height), self.samples)

    if "dpi" not in kwargs.keys():
        kwargs["dpi"] = (self.xres, self.yres)

    img.save(*args, **kwargs)

def pil_tobytes(self, *args, unmultiply=False, **kwargs):
    """Convert to binary image stream using pillow.

    Args are passed to Pillow's Image.save method, see their documentation.
    Use instead of 'tobytes' when other output formats are needed.
    """
    EnsureOwnership(self)
    from io import BytesIO
    bytes_out = BytesIO()
    self.pil_save(bytes_out, *args, unmultiply=unmultiply, **kwargs)
    return bytes_out.getvalue()

        %}
        //-----------------------------------------------------------------
        // invert_irect
        //-----------------------------------------------------------------
        %pythonprepend invert_irect
        %{"""Invert the colors inside a bbox."""%}
        PyObject *invert_irect(PyObject *bbox = NULL)
        {
            fz_pixmap *pm = (fz_pixmap *) $self;
            if (!fz_pixmap_colorspace(gctx, pm))
                {
                    JM_Warning("ignored for stencil pixmap");
                    return JM_BOOL(0);
                }

            fz_irect r = JM_irect_from_py(bbox);
            if (fz_is_infinite_irect(r))
                r = fz_pixmap_bbox(gctx, pm);

            return JM_BOOL(JM_invert_pixmap_rect(gctx, pm, r));
        }

        //-----------------------------------------------------------------
        // get one pixel as a list
        //-----------------------------------------------------------------
        FITZEXCEPTION(pixel, !result)
        ENSURE_OWNERSHIP(pixel, """Get color tuple of pixel (x, y).
Includes alpha byte if applicable.""")
        PyObject *pixel(int x, int y)
        {
            PyObject *p = NULL;
            fz_try(gctx) {
                fz_pixmap *pm = (fz_pixmap *) $self;
                if (!INRANGE(x, 0, pm->w - 1) || !INRANGE(y, 0, pm->h - 1)) {
                    RAISEPY(gctx, MSG_PIXEL_OUTSIDE, PyExc_ValueError);
                }
                int n = pm->n;
                int stride = fz_pixmap_stride(gctx, pm);
                int j, i = stride * y + n * x;
                p = PyTuple_New(n);
                for (j = 0; j < n; j++) {
                    PyTuple_SET_ITEM(p, j, Py_BuildValue("i", pm->samples[i + j]));
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return p;
        }

        //-----------------------------------------------------------------
        // Set one pixel to a given color tuple
        //-----------------------------------------------------------------
        FITZEXCEPTION(set_pixel, !result)
        ENSURE_OWNERSHIP(set_pixel, """Set color of pixel (x, y).""")
        PyObject *set_pixel(int x, int y, PyObject *color)
        {
            fz_try(gctx) {
                fz_pixmap *pm = (fz_pixmap *) $self;
                if (!INRANGE(x, 0, pm->w - 1) || !INRANGE(y, 0, pm->h - 1)) {
                    RAISEPY(gctx, MSG_PIXEL_OUTSIDE, PyExc_ValueError);
                }
                int n = pm->n;
                if (!PySequence_Check(color) || PySequence_Size(color) != n) {
                    RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError);
                }
                int i, j;
                unsigned char c[5];
                for (j = 0; j < n; j++) {
                    if (JM_INT_ITEM(color, j, &i) == 1) {
                        RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError);
                    }
                    if (!INRANGE(i, 0, 255)) {
                        RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError);
                    }
                    c[j] = (unsigned char) i;
                }
                int stride = fz_pixmap_stride(gctx, pm);
                i = stride * y + n * x;
                for (j = 0; j < n; j++) {
                    pm->samples[i + j] = c[j];
                }
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //-----------------------------------------------------------------
        // Set Pixmap origin
        //-----------------------------------------------------------------
        ENSURE_OWNERSHIP(set_origin, """Set top-left coordinates.""")
        PyObject *set_origin(int x, int y)
        {
            fz_pixmap *pm = (fz_pixmap *) $self;
            pm->x = x;
            pm->y = y;
            Py_RETURN_NONE;
        }

        ENSURE_OWNERSHIP(set_dpi, """Set resolution in both dimensions.""")
        PyObject *set_dpi(int xres, int yres)
        {
            fz_pixmap *pm = (fz_pixmap *) $self;
            pm->xres = xres;
            pm->yres = yres;
            Py_RETURN_NONE;
        }

        //-----------------------------------------------------------------
        // Set a rect to a given color tuple
        //-----------------------------------------------------------------
        FITZEXCEPTION(set_rect, !result)
        ENSURE_OWNERSHIP(set_rect, """Set color of all pixels in bbox.""")
        PyObject *set_rect(PyObject *bbox, PyObject *color)
        {
            PyObject *rc = NULL;
            fz_try(gctx) {
                fz_pixmap *pm = (fz_pixmap *) $self;
                Py_ssize_t j, n = (Py_ssize_t) pm->n;
                if (!PySequence_Check(color) || PySequence_Size(color) != n) {
                    RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError);
                }
                unsigned char c[5];
                int i;
                for (j = 0; j < n; j++) {
                    if (JM_INT_ITEM(color, j, &i) == 1) {
                        RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError);
                    }
                    if (!INRANGE(i, 0, 255)) {
                        RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError);
                    }
                    c[j] = (unsigned char) i;
                }
                i = JM_fill_pixmap_rect_with_color(gctx, pm, c, JM_irect_from_py(bbox));
                rc = JM_BOOL(i);
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            return rc;
        }

        //-----------------------------------------------------------------
        // check if monochrome
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(is_monochrome, """Check if pixmap is monochrome.""")
        PyObject *is_monochrome()
        {
            return JM_BOOL(fz_is_pixmap_monochrome(gctx, (fz_pixmap *) $self));
        }

        //-----------------------------------------------------------------
        // check if unicolor (only one color there)
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(is_unicolor, """Check if pixmap has only one color.""")
        PyObject *is_unicolor()
        {
            fz_pixmap *pm = (fz_pixmap *) $self;
            size_t i, n = pm->n, count = pm->w * pm->h * n;
            unsigned char *s = pm->samples;
            for (i = n; i < count; i += n) {
                if (memcmp(s, s + i, n) != 0) {
                    Py_RETURN_FALSE;
                }
            }
            Py_RETURN_TRUE;
        }


        //-----------------------------------------------------------------
        // count each pixmap color
        //-----------------------------------------------------------------
        FITZEXCEPTION(color_count, !result)
        ENSURE_OWNERSHIP(color_count, """Return count of each color.""")
        PyObject *color_count(int colors=0, PyObject *clip=NULL)
        {
            fz_pixmap *pm = (fz_pixmap *) $self;
            PyObject *rc = NULL;
            fz_try(gctx) {
                rc = JM_color_count(gctx, pm, clip);
                if (!rc) {
                    RAISEPY(gctx, MSG_COLOR_COUNT_FAILED, PyExc_RuntimeError);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            if (!colors) {
                Py_ssize_t len = PyDict_Size(rc);
                Py_DECREF(rc);
                return PyLong_FromSsize_t(len);
            }
            return rc;
        }

        %pythoncode %{
        def color_topusage(self, clip=None):
            """Return most frequent color and its usage ratio."""
            EnsureOwnership(self)
            allpixels = 0
            cnt = 0
            if clip != None and self.irect in Rect(clip):
                clip = self.irect
            for pixel, count in self.color_count(colors=True,clip=clip).items():
                allpixels += count
                if count > cnt:
                    cnt = count
                    maxpixel = pixel
            if not allpixels:
                return (1, bytes([255] * self.n))
            return (cnt / allpixels, maxpixel)

        %}

        //-----------------------------------------------------------------
        // MD5 digest of pixmap
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(digest, """MD5 digest of pixmap (bytes).""")
        PyObject *digest()
        {
            unsigned char digest[16];
            fz_md5_pixmap(gctx, (fz_pixmap *) $self, digest);
            return PyBytes_FromStringAndSize(digest, 16);
        }

        //-----------------------------------------------------------------
        // get length of one image row
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(stride, """Length of one image line (width * n).""")
        PyObject *stride()
        {
            return PyLong_FromSize_t((size_t) fz_pixmap_stride(gctx, (fz_pixmap *) $self));
        }

        //-----------------------------------------------------------------
        // x, y, width, height, xres, yres, n
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(xres, """Resolution in x direction.""")
        int xres()
        {
            fz_pixmap *this_pix = (fz_pixmap *) $self;
            return this_pix->xres;
        }

        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(yres, """Resolution in y direction.""")
        int yres()
        {
            fz_pixmap *this_pix = (fz_pixmap *) $self;
            return this_pix->yres;
        }

        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(w, """The width.""")
        PyObject *w()
        {
            return PyLong_FromSize_t((size_t) fz_pixmap_width(gctx, (fz_pixmap *) $self));
        }

        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(h, """The height.""")
        PyObject *h()
        {
            return PyLong_FromSize_t((size_t) fz_pixmap_height(gctx, (fz_pixmap *) $self));
        }

        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(x, """x component of Pixmap origin.""")
        int x()
        {
            return fz_pixmap_x(gctx, (fz_pixmap *) $self);
        }

        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(y, """y component of Pixmap origin.""")
        int y()
        {
            return fz_pixmap_y(gctx, (fz_pixmap *) $self);
        }

        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(n, """The size of one pixel.""")
        int n()
        {
            return fz_pixmap_components(gctx, (fz_pixmap *) $self);
        }

        //-----------------------------------------------------------------
        // check alpha channel
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(alpha, """Indicates presence of alpha channel.""")
        int alpha()
        {
            return fz_pixmap_alpha(gctx, (fz_pixmap *) $self);
        }

        //-----------------------------------------------------------------
        // get colorspace of pixmap
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(colorspace, """Pixmap Colorspace.""")
        struct Colorspace *colorspace()
        {
            return (struct Colorspace *) fz_pixmap_colorspace(gctx, (fz_pixmap *) $self);
        }

        //-----------------------------------------------------------------
        // return irect of pixmap
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(irect, """Pixmap bbox - an IRect object.""")
        %pythonappend irect %{val = IRect(val)%}
        PyObject *irect()
        {
            return JM_py_from_irect(fz_pixmap_bbox(gctx, (fz_pixmap *) $self));
        }

        //-----------------------------------------------------------------
        // return size of pixmap
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(size, """Pixmap size.""")
        PyObject *size()
        {
            return PyLong_FromSize_t(fz_pixmap_size(gctx, (fz_pixmap *) $self));
        }

        //-----------------------------------------------------------------
        // samples
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(samples_mv, """Pixmap samples memoryview.""")
        PyObject *samples_mv()
        {
            fz_pixmap *pm = (fz_pixmap *) $self;
            Py_ssize_t s = (Py_ssize_t) pm->w;
            s *= pm->h;
            s *= pm->n;
            return PyMemoryView_FromMemory((char *) pm->samples, s, PyBUF_READ);
        }


        %pythoncode %{@property%}
        ENSURE_OWNERSHIP(samples_ptr, """Pixmap samples pointer.""")
        PyObject *samples_ptr()
        {
            fz_pixmap *pm = (fz_pixmap *) $self;
            return PyLong_FromVoidPtr((void *) pm->samples);
        }

        %pythoncode %{
        @property
        def samples(self)->bytes:
            return bytes(self.samples_mv)

        width  = w
        height = h

        def __len__(self):
            return self.size

        def __repr__(self):
            EnsureOwnership(self)
            if not type(self) is Pixmap: return
            if self.colorspace:
                return "Pixmap(%s, %s, %s)" % (self.colorspace.name, self.irect, self.alpha)
            else:
                return "Pixmap(%s, %s, %s)" % ('None', self.irect, self.alpha)

        def __enter__(self):
            return self

        def __exit__(self, *args):
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)

        def __del__(self):
            if not type(self) is Pixmap:
                return
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)

        %}
    }
};

/* fz_colorspace */
struct Colorspace
{
    %extend {
        ~Colorspace()
        {
            DEBUGMSG1("Colorspace");
            fz_colorspace *this_cs = (fz_colorspace *) $self;
            fz_drop_colorspace(gctx, this_cs);
            DEBUGMSG2;
        }

        %pythonprepend Colorspace
        %{"""Supported are GRAY, RGB and CMYK."""%}
        Colorspace(int type)
        {
            fz_colorspace *cs = NULL;
            switch(type) {
                case CS_GRAY:
                    cs = fz_device_gray(gctx);
                    break;
                case CS_CMYK:
                    cs = fz_device_cmyk(gctx);
                    break;
                case CS_RGB:
                default:
                    cs = fz_device_rgb(gctx);
                    break;
            }
            fz_keep_colorspace(gctx, cs);
            return (struct Colorspace *) cs;
        }
        //-----------------------------------------------------------------
        // number of bytes to define color of one pixel
        //-----------------------------------------------------------------
        %pythoncode %{@property%}
        %pythonprepend n %{"""Size of one pixel."""%}
        PyObject *n()
        {
            return Py_BuildValue("i", fz_colorspace_n(gctx, (fz_colorspace *) $self));
        }

        //-----------------------------------------------------------------
        // name of colorspace
        //-----------------------------------------------------------------
        PyObject *_name()
        {
            return JM_UnicodeFromStr(fz_colorspace_name(gctx, (fz_colorspace *) $self));
        }

        %pythoncode %{
        @property
        def name(self):
            """Name of the Colorspace."""

            if self.n == 1:
                return csGRAY._name()
            elif self.n == 3:
                return csRGB._name()
            elif self.n == 4:
                return csCMYK._name()
            return self._name()

        def __repr__(self):
            x = ("", "GRAY", "", "RGB", "CMYK")[self.n]
            return "Colorspace(CS_%s) - %s" % (x, self.name)
        %}
    }
};


/* fz_device wrapper */
%rename(Device) DeviceWrapper;
struct DeviceWrapper
{
    %extend {
        FITZEXCEPTION(DeviceWrapper, !result)
        DeviceWrapper(struct Pixmap *pm, PyObject *clip) {
            struct DeviceWrapper *dw = NULL;
            fz_try(gctx) {
                dw = (struct DeviceWrapper *)calloc(1, sizeof(struct DeviceWrapper));
                fz_irect bbox = JM_irect_from_py(clip);
                if (fz_is_infinite_irect(bbox))
                    dw->device = fz_new_draw_device(gctx, fz_identity, (fz_pixmap *) pm);
                else
                    dw->device = fz_new_draw_device_with_bbox(gctx, fz_identity, (fz_pixmap *) pm, &bbox);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return dw;
        }
        DeviceWrapper(struct DisplayList *dl) {
            struct DeviceWrapper *dw = NULL;
            fz_try(gctx) {
                dw = (struct DeviceWrapper *)calloc(1, sizeof(struct DeviceWrapper));
                dw->device = fz_new_list_device(gctx, (fz_display_list *) dl);
                dw->list = (fz_display_list *) dl;
                fz_keep_display_list(gctx, (fz_display_list *) dl);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return dw;
        }
        DeviceWrapper(struct TextPage *tp, int flags = 0) {
            struct DeviceWrapper *dw = NULL;
            fz_try(gctx) {
                dw = (struct DeviceWrapper *)calloc(1, sizeof(struct DeviceWrapper));
                fz_stext_options opts = { 0 };
                opts.flags = flags;
                dw->device = fz_new_stext_device(gctx, (fz_stext_page *) tp, &opts);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return dw;
        }
        ~DeviceWrapper() {
            fz_display_list *list = $self->list;
            DEBUGMSG1("Device");
            fz_close_device(gctx, $self->device);
            fz_drop_device(gctx, $self->device);
            DEBUGMSG2;
            if(list)
            {
                DEBUGMSG1("DisplayList after Device");
                fz_drop_display_list(gctx, list);
                DEBUGMSG2;
            }
        }
    }
};

//------------------------------------------------------------------------
// fz_outline
//------------------------------------------------------------------------
%nodefaultctor;
struct Outline {
    %immutable;
    %extend {
        ~Outline()
        {
            DEBUGMSG1("Outline");
            fz_outline *this_ol = (fz_outline *) $self;
            fz_drop_outline(gctx, this_ol);
            DEBUGMSG2;
        }

        %pythoncode %{@property%}
        PyObject *uri()
        {
            fz_outline *ol = (fz_outline *) $self;
            return JM_UnicodeFromStr(ol->uri);
        }

        /* `%newobject foo;` is equivalent to wrapping C fn in python like:
            ret = _foo()
            ret.thisown=true
            return ret.
        */
        %newobject next;
        %pythoncode %{@property%}
        struct Outline *next()
        {
            fz_outline *ol = (fz_outline *) $self;
            fz_outline *next_ol = ol->next;
            if (!next_ol) return NULL;
            next_ol = fz_keep_outline(gctx, next_ol);
            return (struct Outline *) next_ol;
        }

        %newobject down;
        %pythoncode %{@property%}
        struct Outline *down()
        {
            fz_outline *ol = (fz_outline *) $self;
            fz_outline *down_ol = ol->down;
            if (!down_ol) return NULL;
            down_ol = fz_keep_outline(gctx, down_ol);
            return (struct Outline *) down_ol;
        }

        %pythoncode %{@property%}
        PyObject *is_external()
        {
            fz_outline *ol = (fz_outline *) $self;
            if (!ol->uri) Py_RETURN_FALSE;
            return JM_BOOL(fz_is_external_link(gctx, ol->uri));
        }

        %pythoncode %{@property%}
        int page()
        {
            fz_outline *ol = (fz_outline *) $self;
            return ol->page.page;
        }

        %pythoncode %{@property%}
        float x()
        {
            fz_outline *ol = (fz_outline *) $self;
            return ol->x;
        }

        %pythoncode %{@property%}
        float y()
        {
            fz_outline *ol = (fz_outline *) $self;
            return ol->y;
        }

        %pythoncode %{@property%}
        PyObject *title()
        {
            fz_outline *ol = (fz_outline *) $self;
            return JM_UnicodeFromStr(ol->title);
        }

        %pythoncode %{@property%}
        PyObject *is_open()
        {
            fz_outline *ol = (fz_outline *) $self;
            return JM_BOOL(ol->is_open);
        }

        %pythoncode %{
        @property
        def dest(self):
            '''outline destination details'''
            return linkDest(self, None)

        def __del__(self):
            if not isinstance(self, Outline):
                return
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)
        %}
    }
};
%clearnodefaultctor;


//------------------------------------------------------------------------
// Annotation
//------------------------------------------------------------------------
%nodefaultctor;
struct Annot
{
    %extend
    {
        ~Annot()
        {
            DEBUGMSG1("Annot");
            pdf_annot *this_annot = (pdf_annot *) $self;
            pdf_drop_annot(gctx, this_annot);
            DEBUGMSG2;
        }
        //----------------------------------------------------------------
        // annotation rectangle
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(rect, """annotation rectangle""")
        %pythonappend rect %{
        val = Rect(val)
        val *= self.parent.derotation_matrix
        %}
        PyObject *
        rect()
        {
            fz_rect r = pdf_bound_annot(gctx, (pdf_annot *) $self);
            return JM_py_from_rect(r);
        }

        %pythoncode %{@property%}
        PARENTCHECK(rect_delta, """annotation delta values to rectangle""")
        PyObject *
        rect_delta()
        {
            PyObject *rc=NULL;
            float d;
            fz_try(gctx) {
                pdf_obj *annot_obj = pdf_annot_obj(gctx, (pdf_annot *) $self);
                pdf_obj *arr = pdf_dict_get(gctx, annot_obj, PDF_NAME(RD));
                int i, n = pdf_array_len(gctx, arr);
                if (n != 4) {
                    rc = Py_BuildValue("s", NULL);
                } else {
                    rc = PyTuple_New(4);
                    for (i = 0; i < n; i++) {
                        d = pdf_to_real(gctx, pdf_array_get(gctx, arr, i));
                        if (i == 2 || i == 3) d *= -1;
                        PyTuple_SET_ITEM(rc, i, Py_BuildValue("f", d));
                    }
                }
            }
            fz_catch(gctx) {
                Py_RETURN_NONE;
            }
            return rc;
        }

        //----------------------------------------------------------------
        // annotation xref number
        //----------------------------------------------------------------
        PARENTCHECK(xref, """annotation xref""")
        %pythoncode %{@property%}
        PyObject *xref()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            return Py_BuildValue("i", pdf_to_num(gctx, annot_obj));
        }

        //----------------------------------------------------------------
        // annotation get IRT xref number
        //----------------------------------------------------------------
        PARENTCHECK(irt_xref, """annotation IRT xref""")
        %pythoncode %{@property%}
        PyObject *irt_xref()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_obj *irt = pdf_dict_get(gctx, annot_obj, PDF_NAME(IRT));
            if (!irt) return PyLong_FromLong(0);
            return PyLong_FromLong((long) pdf_to_num(gctx, irt));
        }

        //----------------------------------------------------------------
        // annotation set IRT xref number
        //----------------------------------------------------------------
        FITZEXCEPTION(set_irt_xref, !result)
        PARENTCHECK(set_irt_xref, """Set annotation IRT xref""")
        PyObject *set_irt_xref(int xref)
        {
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_page *page = pdf_annot_page(gctx, annot);
                if (!INRANGE(xref, 1, pdf_xref_len(gctx, page->doc) - 1)) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                pdf_obj *irt = pdf_new_indirect(gctx, page->doc, xref, 0);
                pdf_obj *subt = pdf_dict_get(gctx, irt, PDF_NAME(Subtype));
                int irt_subt = pdf_annot_type_from_string(gctx, pdf_to_name(gctx, subt));
                if (irt_subt < 0) {
                    pdf_drop_obj(gctx, irt);
                    RAISEPY(gctx, MSG_IS_NO_ANNOT, PyExc_ValueError);
                }
                pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(IRT), irt);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // annotation get AP/N Matrix
        //----------------------------------------------------------------
        PARENTCHECK(apn_matrix, """annotation appearance matrix""")
        %pythonappend apn_matrix %{val = Matrix(val)%}
        %pythoncode %{@property%}
        PyObject *
        apn_matrix()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                            PDF_NAME(N), NULL);
            if (!ap)
                return JM_py_from_matrix(fz_identity);
            fz_matrix mat = pdf_dict_get_matrix(gctx, ap, PDF_NAME(Matrix));
            return JM_py_from_matrix(mat);
        }


        //----------------------------------------------------------------
        // annotation get AP/N BBox
        //----------------------------------------------------------------
        PARENTCHECK(apn_bbox, """annotation appearance bbox""")
        %pythonappend apn_bbox %{
        val = Rect(val) * self.parent.transformation_matrix
        val *= self.parent.derotation_matrix%}
        %pythoncode %{@property%}
        PyObject *
        apn_bbox()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                            PDF_NAME(N), NULL);
            if (!ap)
                return JM_py_from_rect(fz_infinite_rect);
            fz_rect rect = pdf_dict_get_rect(gctx, ap, PDF_NAME(BBox));
            return JM_py_from_rect(rect);
        }


        //----------------------------------------------------------------
        // annotation set AP/N Matrix
        //----------------------------------------------------------------
        FITZEXCEPTION(set_apn_matrix, !result)
        PARENTCHECK(set_apn_matrix, """Set annotation appearance matrix.""")
        PyObject *
        set_apn_matrix(PyObject *matrix)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            fz_try(gctx) {
                pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                                                PDF_NAME(N), NULL);
                if (!ap) {
                    RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError);
                }
                fz_matrix mat = JM_matrix_from_py(matrix);
                pdf_dict_put_matrix(gctx, ap, PDF_NAME(Matrix), mat);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation set AP/N BBox
        //----------------------------------------------------------------
        FITZEXCEPTION(set_apn_bbox, !result)
        %pythonprepend set_apn_bbox %{
        """Set annotation appearance bbox."""

        CheckParent(self)
        page = self.parent
        rot = page.rotation_matrix
        mat = page.transformation_matrix
        bbox *= rot * ~mat
        %}
        PyObject *
        set_apn_bbox(PyObject *bbox)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            fz_try(gctx) {
                pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                                                PDF_NAME(N), NULL);
                if (!ap) {
                    RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError);
                }
                fz_rect rect = JM_rect_from_py(bbox);
                pdf_dict_put_rect(gctx, ap, PDF_NAME(BBox), rect);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation show blend mode (/BM)
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(blendmode, """annotation BlendMode""")
        PyObject *blendmode()
        {
            PyObject *blend_mode = NULL;
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_obj *obj, *obj1, *obj2;
                obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(BM));
                if (obj) {
                    blend_mode = JM_UnicodeFromStr(pdf_to_name(gctx, obj));
                    goto finished;
                }
                // loop through the /AP/N/Resources/ExtGState objects
                obj = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                    PDF_NAME(N),
                    PDF_NAME(Resources),
                    PDF_NAME(ExtGState),
                    NULL);

                if (pdf_is_dict(gctx, obj)) {
                    int i, j, m, n = pdf_dict_len(gctx, obj);
                    for (i = 0; i < n; i++) {
                        obj1 = pdf_dict_get_val(gctx, obj, i);
                        if (pdf_is_dict(gctx, obj1)) {
                            m = pdf_dict_len(gctx, obj1);
                            for (j = 0; j < m; j++) {
                                obj2 = pdf_dict_get_key(gctx, obj1, j);
                                if (pdf_objcmp(gctx, obj2, PDF_NAME(BM)) == 0) {
                                    blend_mode = JM_UnicodeFromStr(pdf_to_name(gctx, pdf_dict_get_val(gctx, obj1, j)));
                                    goto finished;
                                }
                            }
                        }
                    }
                }
                finished:;
            }
            fz_catch(gctx) {
                Py_RETURN_NONE;
            }
            if (blend_mode) return blend_mode;
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation set blend mode (/BM)
        //----------------------------------------------------------------
        FITZEXCEPTION(set_blendmode, !result)
        PARENTCHECK(set_blendmode, """Set annotation BlendMode.""")
        PyObject *
        set_blendmode(char *blend_mode)
        {
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_dict_put_name(gctx, annot_obj, PDF_NAME(BM), blend_mode);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation get optional content
        //----------------------------------------------------------------
        FITZEXCEPTION(get_oc, !result)
        PARENTCHECK(get_oc, """Get annotation optional content reference.""")
        PyObject *get_oc()
        {
            int oc = 0;
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(OC));
                if (obj) {
                    oc = pdf_to_num(gctx, obj);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", oc);
        }


        //----------------------------------------------------------------
        // annotation set open
        //----------------------------------------------------------------
        FITZEXCEPTION(set_open, !result)
        PARENTCHECK(set_open, """Set 'open' status of annotation or its Popup.""")
        PyObject *set_open(int is_open)
        {
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_set_annot_is_open(gctx, annot, is_open);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation inquiry: is open
        //----------------------------------------------------------------
        FITZEXCEPTION(is_open, !result)
        PARENTCHECK(is_open, """Get 'open' status of annotation or its Popup.""")
        %pythoncode %{@property%}
        PyObject *
        is_open()
        {
            int is_open;
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                is_open = pdf_annot_is_open(gctx, annot);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_BOOL(is_open);
        }


        //----------------------------------------------------------------
        // annotation inquiry: has Popup
        //----------------------------------------------------------------
        FITZEXCEPTION(has_popup, !result)
        PARENTCHECK(has_popup, """Check if annotation has a Popup.""")
        %pythoncode %{@property%}
        PyObject *
        has_popup()
        {
            int has_popup = 0;
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Popup));
                if (obj) has_popup = 1;
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_BOOL(has_popup);
        }


        //----------------------------------------------------------------
        // annotation set Popup
        //----------------------------------------------------------------
        FITZEXCEPTION(set_popup, !result)
        PARENTCHECK(set_popup, """Create annotation 'Popup' or update rectangle.""")
        PyObject *
        set_popup(PyObject *rect)
        {
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_page *pdfpage = pdf_annot_page(gctx, annot);
                fz_matrix rot = JM_rotate_page_matrix(gctx, pdfpage);
                fz_rect r = fz_transform_rect(JM_rect_from_py(rect), rot);
                pdf_set_annot_popup(gctx, annot, r);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // annotation Popup rectangle
        //----------------------------------------------------------------
        FITZEXCEPTION(popup_rect, !result)
        PARENTCHECK(popup_rect, """annotation 'Popup' rectangle""")
        %pythoncode %{@property%}
        %pythonappend popup_rect %{
        val = Rect(val) * self.parent.transformation_matrix
        val *= self.parent.derotation_matrix%}
        PyObject *
        popup_rect()
        {
            fz_rect rect = fz_infinite_rect;
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Popup));
                if (obj) {
                    rect = pdf_dict_get_rect(gctx, obj, PDF_NAME(Rect));
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_py_from_rect(rect);
        }


        //----------------------------------------------------------------
        // annotation Popup xref
        //----------------------------------------------------------------
        FITZEXCEPTION(popup_xref, !result)
        PARENTCHECK(popup_xref, """annotation 'Popup' xref""")
        %pythoncode %{@property%}
        PyObject *
        popup_xref()
        {
            int xref = 0;
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Popup));
                if (obj) {
                    xref = pdf_to_num(gctx, obj);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xref);
        }


        //----------------------------------------------------------------
        // annotation set optional content
        //----------------------------------------------------------------
        FITZEXCEPTION(set_oc, !result)
        PARENTCHECK(set_oc, """Set / remove annotation OC xref.""")
        PyObject *
        set_oc(int oc=0)
        {
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                if (!oc) {
                    pdf_dict_del(gctx, annot_obj, PDF_NAME(OC));
                } else {
                    JM_add_oc_object(gctx, pdf_get_bound_document(gctx, annot_obj), annot_obj, oc);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        %pythoncode%{@property%}
        %pythonprepend language %{"""annotation language"""%}
        PyObject *language()
        {
            pdf_annot *this_annot = (pdf_annot *) $self;
            fz_text_language lang = pdf_annot_language(gctx, this_annot);
            char buf[8];
            if (lang == FZ_LANG_UNSET) Py_RETURN_NONE;
            return Py_BuildValue("s", fz_string_from_text_language(buf, lang));
        }

        //----------------------------------------------------------------
        // annotation set language (/Lang)
        //----------------------------------------------------------------
        FITZEXCEPTION(set_language, !result)
        PARENTCHECK(set_language, """Set annotation language.""")
        PyObject *set_language(char *language=NULL)
        {
            pdf_annot *this_annot = (pdf_annot *) $self;
            fz_try(gctx) {
                fz_text_language lang;
                if (!language)
                    lang = FZ_LANG_UNSET;
                else
                    lang = fz_text_language_from_string(language);
                pdf_set_annot_language(gctx, this_annot, lang);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation get decompressed appearance stream source
        //----------------------------------------------------------------
        FITZEXCEPTION(_getAP, !result)
        PyObject *
        _getAP()
        {
            PyObject *r = NULL;
            fz_buffer *res = NULL;
            fz_var(res);
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                                              PDF_NAME(N), NULL);

                if (pdf_is_stream(gctx, ap))  res = pdf_load_stream(gctx, ap);
                if (res) {
                    r = JM_BinFromBuffer(gctx, res);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                Py_RETURN_NONE;
            }
            if (!r) Py_RETURN_NONE;
            return r;
        }

        //----------------------------------------------------------------
        // annotation update /AP stream
        //----------------------------------------------------------------
        FITZEXCEPTION(_setAP, !result)
        PyObject *
        _setAP(PyObject *buffer, int rect=0)
        {
            fz_buffer *res = NULL;
            fz_var(res);
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_page *page = pdf_annot_page(gctx, annot);
                pdf_obj *apobj = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                                              PDF_NAME(N), NULL);
                if (!apobj) {
                    RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError);
                }
                if (!pdf_is_stream(gctx, apobj)) {
                    RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError);
                }
                res = JM_BufferFromBytes(gctx, buffer);
                if (!res) {
                    RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_ValueError);
                }
                JM_update_stream(gctx, page->doc, apobj, res, 1);
                if (rect) {
                    fz_rect bbox = pdf_dict_get_rect(gctx, annot_obj, PDF_NAME(Rect));
                    pdf_dict_put_rect(gctx, apobj, PDF_NAME(BBox), bbox);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // redaction annotation get values
        //----------------------------------------------------------------
        FITZEXCEPTION(_get_redact_values, !result)
        %pythonappend _get_redact_values %{
        if not val:
            return val
        val["rect"] = self.rect
        text_color, fontname, fontsize = TOOLS._parse_da(self)
        val["text_color"] = text_color
        val["fontname"] = fontname
        val["fontsize"] = fontsize
        fill = self.colors["fill"]
        val["fill"] = fill

        %}
        PyObject *
        _get_redact_values()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            if (pdf_annot_type(gctx, annot) != PDF_ANNOT_REDACT)
                Py_RETURN_NONE;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            PyObject *values = PyDict_New();
            pdf_obj *obj = NULL;
            const char *text = NULL;
            fz_try(gctx) {
                obj = pdf_dict_gets(gctx, annot_obj, "RO");
                if (obj) {
                    JM_Warning("Ignoring redaction key '/RO'.");
                    int xref = pdf_to_num(gctx, obj);
                    DICT_SETITEM_DROP(values, dictkey_xref, Py_BuildValue("i", xref));
                }
                obj = pdf_dict_gets(gctx, annot_obj, "OverlayText");
                if (obj) {
                    text = pdf_to_text_string(gctx, obj);
                    DICT_SETITEM_DROP(values, dictkey_text, JM_UnicodeFromStr(text));
                } else {
                    DICT_SETITEM_DROP(values, dictkey_text, Py_BuildValue("s", ""));
                }
                obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Q));
                int align = 0;
                if (obj) {
                    align = pdf_to_int(gctx, obj);
                }
                DICT_SETITEM_DROP(values, dictkey_align, Py_BuildValue("i", align));
            }
            fz_catch(gctx) {
                Py_DECREF(values);
                return NULL;
            }
            return values;
        }

        //----------------------------------------------------------------
        // annotation get TextPage
        //----------------------------------------------------------------
        %pythonappend get_textpage %{
            if val:
                val.thisown = True
        %}
        FITZEXCEPTION(get_textpage, !result)
        PARENTCHECK(get_textpage, """Make annotation TextPage.""")
        struct TextPage *
        get_textpage(PyObject *clip=NULL, int flags = 0)
        {
            fz_stext_page *textpage=NULL;
            fz_stext_options options = { 0 };
            options.flags = flags;
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                textpage = pdf_new_stext_page_from_annot(gctx, annot, &options);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct TextPage *) textpage;
        }


        //----------------------------------------------------------------
        // annotation set name
        //----------------------------------------------------------------
        FITZEXCEPTION(set_name, !result)
        PARENTCHECK(set_name, """Set /Name (icon) of annotation.""")
        PyObject *
        set_name(char *name)
        {
            fz_try(gctx) {
                pdf_annot *annot = (pdf_annot *) $self;
                pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
                pdf_dict_put_name(gctx, annot_obj, PDF_NAME(Name), name);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation set rectangle
        //----------------------------------------------------------------
        PARENTCHECK(set_rect, """Set annotation rectangle.""")
        FITZEXCEPTION(set_rect, !result)
        PyObject *
        set_rect(PyObject *rect)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            int type = pdf_annot_type(gctx, annot);
            int err_source = 0;  // what raised the error
            fz_var(err_source);
            fz_try(gctx) {
                pdf_page *pdfpage = pdf_annot_page(gctx, annot);
                fz_matrix rot = JM_rotate_page_matrix(gctx, pdfpage);
                fz_rect r = fz_transform_rect(JM_rect_from_py(rect), rot);
                if (fz_is_empty_rect(r) || fz_is_infinite_rect(r)) {
                    RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError);
                }
                err_source = 1;  // indicate that error was from MuPDF
                pdf_set_annot_rect(gctx, annot, r);
            }
            fz_catch(gctx) {
                if (err_source == 0) {
                    return NULL;
                }
                PySys_WriteStderr("cannot set rect: '%s'\n", fz_caught_message(gctx));
                Py_RETURN_FALSE;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation set rotation
        //----------------------------------------------------------------
        PARENTCHECK(set_rotation, """Set annotation rotation.""")
        PyObject *
        set_rotation(int rotate=0)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            int type = pdf_annot_type(gctx, annot);
            switch (type)
            {
                case PDF_ANNOT_CARET: break;
                case PDF_ANNOT_CIRCLE: break;
                case PDF_ANNOT_FREE_TEXT: break;
                case PDF_ANNOT_FILE_ATTACHMENT: break;
                case PDF_ANNOT_INK: break;
                case PDF_ANNOT_LINE: break;
                case PDF_ANNOT_POLY_LINE: break;
                case PDF_ANNOT_POLYGON: break;
                case PDF_ANNOT_SQUARE: break;
                case PDF_ANNOT_STAMP: break;
                case PDF_ANNOT_TEXT: break;
                default: Py_RETURN_NONE;
            }
            int rot = rotate;
            while (rot < 0) rot += 360;
            while (rot >= 360) rot -= 360;
            if (type == PDF_ANNOT_FREE_TEXT && rot % 90 != 0)
                rot = 0;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Rotate), rot);
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation get rotation
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(rotation, """annotation rotation""")
        int rotation()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_obj *rotation = pdf_dict_get(gctx, annot_obj, PDF_NAME(Rotate));
            if (!rotation) return -1;
            return pdf_to_int(gctx, rotation);
        }


        //----------------------------------------------------------------
        // annotation vertices (for "Line", "Polgon", "Ink", etc.
        //----------------------------------------------------------------
        PARENTCHECK(vertices, """annotation vertex points""")
        %pythoncode %{@property%}
        PyObject *vertices()
        {
            PyObject *res = NULL, *res1 = NULL;
            pdf_obj *o, *o1;
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_page *page = pdf_annot_page(gctx, annot);
            int i, j;
            fz_point point;  // point object to work with
            fz_matrix page_ctm;  // page transformation matrix
            pdf_page_transform(gctx, page, NULL, &page_ctm);
            fz_matrix derot = JM_derotate_page_matrix(gctx, page);
            page_ctm = fz_concat(page_ctm, derot);

            //----------------------------------------------------------------
            // The following objects occur in different annotation types.
            // So we are sure that (!o) occurs at most once.
            // Every pair of floats is one point, that needs to be separately
            // transformed with the page transformation matrix.
            //----------------------------------------------------------------
            o = pdf_dict_get(gctx, annot_obj, PDF_NAME(Vertices));
            if (o) goto weiter;
            o = pdf_dict_get(gctx, annot_obj, PDF_NAME(L));
            if (o) goto weiter;
            o = pdf_dict_get(gctx, annot_obj, PDF_NAME(QuadPoints));
            if (o) goto weiter;
            o = pdf_dict_gets(gctx, annot_obj, "CL");
            if (o) goto weiter;
            o = pdf_dict_get(gctx, annot_obj, PDF_NAME(InkList));
            if (o) goto inklist;
            Py_RETURN_NONE;

            // handle lists with 1-level depth --------------------------------
            weiter:;
            res = PyList_New(0);  // create Python list
            for (i = 0; i < pdf_array_len(gctx, o); i += 2)
            {
                point.x = pdf_to_real(gctx, pdf_array_get(gctx, o, i));
                point.y = pdf_to_real(gctx, pdf_array_get(gctx, o, i+1));
                point = fz_transform_point(point, page_ctm);
                LIST_APPEND_DROP(res, Py_BuildValue("ff", point.x, point.y));
            }
            return res;

            // InkList has 2-level lists --------------------------------------
            inklist:;
            res = PyList_New(0);
            for (i = 0; i < pdf_array_len(gctx, o); i++)
            {
                res1 = PyList_New(0);
                o1 = pdf_array_get(gctx, o, i);
                for (j = 0; j < pdf_array_len(gctx, o1); j += 2)
                {
                    point.x = pdf_to_real(gctx, pdf_array_get(gctx, o1, j));
                    point.y = pdf_to_real(gctx, pdf_array_get(gctx, o1, j+1));
                    point = fz_transform_point(point, page_ctm);
                    LIST_APPEND_DROP(res1, Py_BuildValue("ff", point.x, point.y));
                }
                LIST_APPEND_DROP(res, res1);
            }
            return res;
        }

        //----------------------------------------------------------------
        // annotation colors
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(colors, """Color definitions.""")
        PyObject *colors()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            return JM_annot_colors(gctx, annot_obj);
        }

        //----------------------------------------------------------------
        // annotation update appearance
        //----------------------------------------------------------------
        PyObject *_update_appearance(float opacity=-1,
                    char *blend_mode=NULL,
                    PyObject *fill_color=NULL,
                    int rotate = -1)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_page *page = pdf_annot_page(gctx, annot);
            pdf_document *pdf = page->doc;
            int type = pdf_annot_type(gctx, annot);
            float fcol[4] = {1,1,1,1};  // std fill color: white
            int i, nfcol = 0;  // number of color components
            JM_color_FromSequence(fill_color, &nfcol, fcol);
            fz_try(gctx) {
                // remove fill color from unsupported annots
                // or if so requested
                if ((type != PDF_ANNOT_SQUARE
                    && type != PDF_ANNOT_CIRCLE
                    && type != PDF_ANNOT_LINE
                    && type != PDF_ANNOT_POLY_LINE
                    && type != PDF_ANNOT_POLYGON
                    )
                    || nfcol == 0
                    ) {
                    pdf_dict_del(gctx, annot_obj, PDF_NAME(IC));
                } else if (nfcol > 0) {
                    pdf_set_annot_interior_color(gctx, annot, nfcol, fcol);
                }

                int insert_rot = (rotate >= 0) ? 1 : 0;
                switch (type) {
                    case PDF_ANNOT_CARET:
                    case PDF_ANNOT_CIRCLE:
                    case PDF_ANNOT_FREE_TEXT:
                    case PDF_ANNOT_FILE_ATTACHMENT:
                    case PDF_ANNOT_INK:
                    case PDF_ANNOT_LINE:
                    case PDF_ANNOT_POLY_LINE:
                    case PDF_ANNOT_POLYGON:
                    case PDF_ANNOT_SQUARE:
                    case PDF_ANNOT_STAMP:
                    case PDF_ANNOT_TEXT: break;
                    default: insert_rot = 0;
                }

                if (insert_rot) {
                    pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Rotate), rotate);
                }

                pdf_dirty_annot(gctx, annot);
                pdf_update_annot(gctx, annot);  // let MuPDF update
                pdf->resynth_required = 0;
                // insert fill color
                if (type == PDF_ANNOT_FREE_TEXT) {
                    if (nfcol > 0) {
                        pdf_set_annot_color(gctx, annot, nfcol, fcol);
                    }
                } else if (nfcol > 0) {
                    pdf_obj *col = pdf_new_array(gctx, page->doc, nfcol);
                    for (i = 0; i < nfcol; i++) {
                        pdf_array_push_real(gctx, col, fcol[i]);
                    }
                    pdf_dict_put_drop(gctx,annot_obj, PDF_NAME(IC), col);
                }
            }
            fz_catch(gctx) {
                PySys_WriteStderr("cannot update annot: '%s'\n", fz_caught_message(gctx));
                Py_RETURN_FALSE;
            }

            if ((opacity < 0 || opacity >= 1) && !blend_mode)  // no opacity, no blend_mode
                goto normal_exit;

            fz_try(gctx) {  // create or update /ExtGState
                pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP),
                                        PDF_NAME(N), NULL);
                if (!ap)  { // should never happen
                    RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError);
                }

                pdf_obj *resources = pdf_dict_get(gctx, ap, PDF_NAME(Resources));
                if (!resources) {  // no Resources yet: make one
                    resources = pdf_dict_put_dict(gctx, ap, PDF_NAME(Resources), 2);
                }
                pdf_obj *alp0 = pdf_new_dict(gctx, page->doc, 3);
                if (opacity >= 0 && opacity < 1) {
                    pdf_dict_put_real(gctx, alp0, PDF_NAME(CA), (double) opacity);
                    pdf_dict_put_real(gctx, alp0, PDF_NAME(ca), (double) opacity);
                    pdf_dict_put_real(gctx, annot_obj, PDF_NAME(CA), (double) opacity);
                }
                if (blend_mode) {
                    pdf_dict_put_name(gctx, alp0, PDF_NAME(BM), blend_mode);
                    pdf_dict_put_name(gctx, annot_obj, PDF_NAME(BM), blend_mode);
                }
                pdf_obj *extg = pdf_dict_get(gctx, resources, PDF_NAME(ExtGState));
                if (!extg) {  // no ExtGState yet: make one
                    extg = pdf_dict_put_dict(gctx, resources, PDF_NAME(ExtGState), 2);
                }
                pdf_dict_put_drop(gctx, extg, PDF_NAME(H), alp0);
            }

            fz_catch(gctx) {
                PySys_WriteStderr("cannot set opacity or blend mode\n");
                Py_RETURN_FALSE;
            }
            normal_exit:;
            Py_RETURN_TRUE;
        }


        %pythoncode %{
        def update(self,
                   blend_mode: OptStr =None,
                   opacity: OptFloat =None,
                   fontsize: float =0,
                   fontname: OptStr =None,
                   text_color: OptSeq =None,
                   border_color: OptSeq =None,
                   fill_color: OptSeq =None,
                   cross_out: bool =True,
                   rotate: int =-1,
                   ):

            """Update annot appearance.

            Notes:
                Depending on the annot type, some parameters make no sense,
                while others are only available in this method to achieve the
                desired result. This is especially true for 'FreeText' annots.
            Args:
                blend_mode: set the blend mode, all annotations.
                opacity: set the opacity, all annotations.
                fontsize: set fontsize, 'FreeText' only.
                fontname: set the font, 'FreeText' only.
                border_color: set border color, 'FreeText' only.
                text_color: set text color, 'FreeText' only.
                fill_color: set fill color, all annotations.
                cross_out: draw diagonal lines, 'Redact' only.
                rotate: set rotation, 'FreeText' and some others.
            """
            CheckParent(self)
            def color_string(cs, code):
                """Return valid PDF color operator for a given color sequence.
                """
                cc = ColorCode(cs, code)
                if not cc:
                    return b""
                return (cc + "\n").encode()

            annot_type = self.type[0]  # get the annot type
            dt = self.border.get("dashes", None)  # get the dashes spec
            bwidth = self.border.get("width", -1)  # get border line width
            stroke = self.colors["stroke"]  # get the stroke color
            if fill_color != None:  # change of fill color requested
                fill = fill_color
            else:  # put in current annot value
                fill = self.colors["fill"]

            rect = None  # self.rect  # prevent MuPDF fiddling with it
            apnmat = self.apn_matrix  # prevent MuPDF fiddling with it
            if rotate != -1:  # sanitize rotation value
                while rotate < 0:
                    rotate += 360
                while rotate >= 360:
                    rotate -= 360
                if annot_type == PDF_ANNOT_FREE_TEXT and rotate % 90 != 0:
                    rotate = 0

            #------------------------------------------------------------------
            # handle opacity and blend mode
            #------------------------------------------------------------------
            if blend_mode is None:
                blend_mode = self.blendmode
            if not hasattr(opacity, "__float__"):
                opacity = self.opacity

            if 0 <= opacity < 1 or blend_mode is not None:
                opa_code = "/H gs\n"  # then we must reference this 'gs'
            else:
                opa_code = ""

            if annot_type == PDF_ANNOT_FREE_TEXT:
                CheckColor(border_color)
                CheckColor(text_color)
                CheckColor(fill_color)
                tcol, fname, fsize = TOOLS._parse_da(self)

                # read and update default appearance as necessary
                update_default_appearance = False
                if fsize <= 0:
                    fsize = 12
                    update_default_appearance = True
                if text_color is not None:
                    tcol = text_color
                    update_default_appearance = True
                if fontname is not None:
                    fname = fontname
                    update_default_appearance = True
                if fontsize > 0:
                    fsize = fontsize
                    update_default_appearance = True

                if update_default_appearance:
                    da_str = ""
                    if len(tcol) == 3:
                        fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf"
                    elif len(tcol) == 1:
                        fmt = "{:g} g /{f:s} {s:g} Tf"
                    elif len(tcol) == 4:
                        fmt = "{:g} {:g} {:g} {:g} k /{f:s} {s:g} Tf"
                    da_str = fmt.format(*tcol, f=fname, s=fsize)
                    TOOLS._update_da(self, da_str)

            #------------------------------------------------------------------
            # now invoke MuPDF to update the annot appearance
            #------------------------------------------------------------------
            val = self._update_appearance(
                opacity=opacity,
                blend_mode=blend_mode,
                fill_color=fill,
                rotate=rotate,
            )
            if val == False:
                raise RuntimeError("Error updating annotation.")

            bfill = color_string(fill, "f")
            bstroke = color_string(stroke, "c")

            p_ctm = self.parent.transformation_matrix
            imat = ~p_ctm  # inverse page transf. matrix

            if dt:
                dashes = "[" + " ".join(map(str, dt)) + "] 0 d\n"
                dashes = dashes.encode("utf-8")
            else:
                dashes = None

            if self.line_ends:
                line_end_le, line_end_ri = self.line_ends
            else:
                line_end_le, line_end_ri = 0, 0  # init line end codes

            # read contents as created by MuPDF
            ap = self._getAP()
            ap_tab = ap.splitlines()  # split in single lines
            ap_updated = False  # assume we did nothing

            if annot_type == PDF_ANNOT_REDACT:
                if cross_out:  # create crossed-out rect
                    ap_updated = True
                    ap_tab = ap_tab[:-1]
                    _, LL, LR, UR, UL = ap_tab
                    ap_tab.append(LR)
                    ap_tab.append(LL)
                    ap_tab.append(UR)
                    ap_tab.append(LL)
                    ap_tab.append(UL)
                    ap_tab.append(b"S")

                if bwidth > 0 or bstroke != b"":
                    ap_updated = True
                    ntab = [b"%g w" % bwidth] if bwidth > 0 else []
                    for line in ap_tab:
                        if line.endswith(b"w"):
                            continue
                        if line.endswith(b"RG") and bstroke != b"":
                            line = bstroke[:-1]
                        ntab.append(line)
                    ap_tab = ntab

                ap = b"\n".join(ap_tab)

            if annot_type == PDF_ANNOT_FREE_TEXT:
                BT = ap.find(b"BT")
                ET = ap.find(b"ET") + 2
                ap = ap[BT:ET]
                w, h = self.rect.width, self.rect.height
                if rotate in (90, 270) or not (apnmat.b == apnmat.c == 0):
                    w, h = h, w
                re = b"0 0 %g %g re" % (w, h)
                ap = re + b"\nW\nn\n" + ap
                ope = None
                fill_string = color_string(fill, "f")
                if fill_string:
                    ope = b"f"
                stroke_string = color_string(border_color, "c")
                if stroke_string and bwidth > 0:
                    ope = b"S"
                    bwidth = b"%g w\n" % bwidth
                else:
                    bwidth = stroke_string = b""
                if fill_string and stroke_string:
                    ope = b"B"
                if ope != None:
                    ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap

                if dashes != None:  # handle dashes
                    ap = dashes + b"\n" + ap
                    dashes = None

                ap_updated = True

            if annot_type in (PDF_ANNOT_POLYGON, PDF_ANNOT_POLY_LINE):
                ap = b"\n".join(ap_tab[:-1]) + b"\n"
                ap_updated = True
                if bfill != b"":
                    if annot_type == PDF_ANNOT_POLYGON:
                        ap = ap + bfill + b"b"  # close, fill, and stroke
                    elif annot_type == PDF_ANNOT_POLY_LINE:
                        ap = ap + b"S"  # stroke
                else:
                    if annot_type == PDF_ANNOT_POLYGON:
                        ap = ap + b"s"  # close and stroke
                    elif annot_type == PDF_ANNOT_POLY_LINE:
                        ap = ap + b"S"  # stroke

            if dashes is not None:  # handle dashes
                ap = dashes + ap
                # reset dashing - only applies for LINE annots with line ends given
                ap = ap.replace(b"\nS\n", b"\nS\n[] 0 d\n", 1)
                ap_updated = True

            if opa_code:
                ap = opa_code.encode("utf-8") + ap
                ap_updated = True

            ap = b"q\n" + ap + b"\nQ\n"
            #----------------------------------------------------------------------
            # the following handles line end symbols for 'Polygon' and 'Polyline'
            #----------------------------------------------------------------------
            if line_end_le + line_end_ri > 0 and annot_type in (PDF_ANNOT_POLYGON, PDF_ANNOT_POLY_LINE):

                le_funcs = (None, TOOLS._le_square, TOOLS._le_circle,
                            TOOLS._le_diamond, TOOLS._le_openarrow,
                            TOOLS._le_closedarrow, TOOLS._le_butt,
                            TOOLS._le_ropenarrow, TOOLS._le_rclosedarrow,
                            TOOLS._le_slash)
                le_funcs_range = range(1, len(le_funcs))
                d = 2 * max(1, self.border["width"])
                rect = self.rect + (-d, -d, d, d)
                ap_updated = True
                points = self.vertices
                if line_end_le in le_funcs_range:
                    p1 = Point(points[0]) * imat
                    p2 = Point(points[1]) * imat
                    left = le_funcs[line_end_le](self, p1, p2, False, fill_color)
                    ap += left.encode()
                if line_end_ri in le_funcs_range:
                    p1 = Point(points[-2]) * imat
                    p2 = Point(points[-1]) * imat
                    left = le_funcs[line_end_ri](self, p1, p2, True, fill_color)
                    ap += left.encode()

            if ap_updated:
                if rect:                        # rect modified here?
                    self.set_rect(rect)
                    self._setAP(ap, rect=1)
                else:
                    self._setAP(ap, rect=0)

            #-------------------------------
            # handle annotation rotations
            #-------------------------------
            if annot_type not in (  # only these types are supported
                PDF_ANNOT_CARET,
                PDF_ANNOT_CIRCLE,
                PDF_ANNOT_FILE_ATTACHMENT,
                PDF_ANNOT_INK,
                PDF_ANNOT_LINE,
                PDF_ANNOT_POLY_LINE,
                PDF_ANNOT_POLYGON,
                PDF_ANNOT_SQUARE,
                PDF_ANNOT_STAMP,
                PDF_ANNOT_TEXT,
                ):
                return

            rot = self.rotation  # get value from annot object
            if rot == -1:  # nothing to change
                return

            M = (self.rect.tl + self.rect.br) / 2  # center of annot rect

            if rot == 0:  # undo rotations
                if abs(apnmat - Matrix(1, 1)) < 1e-5:
                    return  # matrix already is a no-op
                quad = self.rect.morph(M, ~apnmat)  # derotate rect
                self.set_rect(quad.rect)
                self.set_apn_matrix(Matrix(1, 1))  # appearance matrix = no-op
                return

            mat = Matrix(rot)
            quad = self.rect.morph(M, mat)
            self.set_rect(quad.rect)
            self.set_apn_matrix(apnmat * mat)
        %}

        //----------------------------------------------------------------
        // annotation set colors
        //----------------------------------------------------------------
        %pythoncode %{
        def set_colors(self, colors=None, stroke=None, fill=None):
            """Set 'stroke' and 'fill' colors.

            Use either a dict or the direct arguments.
            """
            CheckParent(self)
            doc = self.parent.parent
            if type(colors) is not dict:
                colors = {"fill": fill, "stroke": stroke}
            fill = colors.get("fill")
            stroke = colors.get("stroke")
            fill_annots = (PDF_ANNOT_CIRCLE, PDF_ANNOT_SQUARE, PDF_ANNOT_LINE, PDF_ANNOT_POLY_LINE, PDF_ANNOT_POLYGON,
                           PDF_ANNOT_REDACT,)
            if stroke in ([], ()):
                doc.xref_set_key(self.xref, "C", "[]")
            elif stroke is not None:
                if hasattr(stroke, "__float__"):
                    stroke = [float(stroke)]
                CheckColor(stroke)
                if len(stroke) == 1:
                    s = "[%g]" % stroke[0]
                elif len(stroke) == 3:
                    s = "[%g %g %g]" % tuple(stroke)
                else:
                    s = "[%g %g %g %g]" % tuple(stroke)
                doc.xref_set_key(self.xref, "C", s)

            if fill and self.type[0] not in fill_annots:
                print("Warning: fill color ignored for annot type '%s'." % self.type[1])
                return
            if fill in ([], ()):
                doc.xref_set_key(self.xref, "IC", "[]")
            elif fill is not None:
                if hasattr(fill, "__float__"):
                    fill = [float(fill)]
                CheckColor(fill)
                if len(fill) == 1:
                    s = "[%g]" % fill[0]
                elif len(fill) == 3:
                    s = "[%g %g %g]" % tuple(fill)
                else:
                    s = "[%g %g %g %g]" % tuple(fill)
                doc.xref_set_key(self.xref, "IC", s)
        %}


        //----------------------------------------------------------------
        // annotation line_ends
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(line_ends, """Line end codes.""")
        PyObject *
        line_ends()
        {
            pdf_annot *annot = (pdf_annot *) $self;

            // return nothing for invalid annot types
            if (!pdf_annot_has_line_ending_styles(gctx, annot))
                Py_RETURN_NONE;

            int lstart = (int) pdf_annot_line_start_style(gctx, annot);
            int lend = (int) pdf_annot_line_end_style(gctx, annot);
            return Py_BuildValue("ii", lstart, lend);
        }


        //----------------------------------------------------------------
        // annotation set line ends
        //----------------------------------------------------------------
        PARENTCHECK(set_line_ends, """Set line end codes.""")
        void set_line_ends(int start, int end)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            if (pdf_annot_has_line_ending_styles(gctx, annot))
                pdf_set_annot_line_ending_styles(gctx, annot, start, end);
            else
                JM_Warning("bad annot type for line ends");
        }


        //----------------------------------------------------------------
        // annotation type
        //----------------------------------------------------------------
        PARENTCHECK(type, """annotation type""")
        %pythoncode %{@property%}
        PyObject *type()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            int type = pdf_annot_type(gctx, annot);
            const char *c = pdf_string_from_annot_type(gctx, type);
            pdf_obj *o = pdf_dict_gets(gctx, annot_obj, "IT");
            if (!o || !pdf_is_name(gctx, o))
                return Py_BuildValue("is", type, c);         // no IT entry
            const char *it = pdf_to_name(gctx, o);
            return Py_BuildValue("iss", type, c, it);
        }

        //----------------------------------------------------------------
        // annotation opacity
        //----------------------------------------------------------------
        PARENTCHECK(opacity, """Opacity.""")
        %pythoncode %{@property%}
        PyObject *opacity()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            double opy = -1;
            pdf_obj *ca = pdf_dict_get(gctx, annot_obj, PDF_NAME(CA));
            if (pdf_is_number(gctx, ca))
                opy = pdf_to_real(gctx, ca);
            return Py_BuildValue("f", opy);
        }

        //----------------------------------------------------------------
        // annotation set opacity
        //----------------------------------------------------------------
        PARENTCHECK(set_opacity, """Set opacity.""")
        void set_opacity(float opacity)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            if (!INRANGE(opacity, 0.0f, 1.0f))
            {
                pdf_set_annot_opacity(gctx, annot, 1);
                return;
            }
            pdf_set_annot_opacity(gctx, annot, opacity);
            if (opacity < 1.0f)
            {
                pdf_page *page = pdf_annot_page(gctx, annot);
                page->transparency = 1;
            }
        }


        //----------------------------------------------------------------
        // annotation get attached file info
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        FITZEXCEPTION(file_info, !result)
        PARENTCHECK(file_info, """Attached file information.""")
        PyObject *file_info()
        {
            PyObject *res = PyDict_New();  // create Python dict
            char *filename = NULL;
            char *desc = NULL;
            int length = -1, size = -1;
            pdf_obj *stream = NULL, *o = NULL, *fs = NULL;
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            fz_try(gctx) {
                int type = (int) pdf_annot_type(gctx, annot);
                if (type != PDF_ANNOT_FILE_ATTACHMENT) {
                    RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError);
                }
                stream = pdf_dict_getl(gctx, annot_obj, PDF_NAME(FS),
                                   PDF_NAME(EF), PDF_NAME(F), NULL);
                if (!stream) {
                    RAISEPY(gctx, "bad PDF: file entry not found", JM_Exc_FileDataError);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }

            fs = pdf_dict_get(gctx, annot_obj, PDF_NAME(FS));

            o = pdf_dict_get(gctx, fs, PDF_NAME(UF));
            if (o) {
                filename = (char *) pdf_to_text_string(gctx, o);
            } else {
                o = pdf_dict_get(gctx, fs, PDF_NAME(F));
                if (o) filename = (char *) pdf_to_text_string(gctx, o);
            }

            o = pdf_dict_get(gctx, fs, PDF_NAME(Desc));
            if (o) desc = (char *) pdf_to_text_string(gctx, o);

            o = pdf_dict_get(gctx, stream, PDF_NAME(Length));
            if (o) length = pdf_to_int(gctx, o);

            o = pdf_dict_getl(gctx, stream, PDF_NAME(Params),
                                PDF_NAME(Size), NULL);
            if (o) size = pdf_to_int(gctx, o);

            DICT_SETITEM_DROP(res, dictkey_filename, JM_EscapeStrFromStr(filename));
            DICT_SETITEM_DROP(res, dictkey_desc, JM_UnicodeFromStr(desc));
            DICT_SETITEM_DROP(res, dictkey_length, Py_BuildValue("i", length));
            DICT_SETITEM_DROP(res, dictkey_size, Py_BuildValue("i", size));
            return res;
        }


        //----------------------------------------------------------------
        // annotation get attached file content
        //----------------------------------------------------------------
        FITZEXCEPTION(get_file, !result)
        PARENTCHECK(get_file, """Retrieve attached file content.""")
        PyObject *
        get_file()
        {
            PyObject *res = NULL;
            pdf_obj *stream = NULL;
            fz_buffer *buf = NULL;
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            fz_var(buf);
            fz_try(gctx) {
                int type = (int) pdf_annot_type(gctx, annot);
                if (type != PDF_ANNOT_FILE_ATTACHMENT) {
                    RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError);
                }
                stream = pdf_dict_getl(gctx, annot_obj, PDF_NAME(FS),
                                   PDF_NAME(EF), PDF_NAME(F), NULL);
                if (!stream) {
                    RAISEPY(gctx, "bad PDF: file entry not found", JM_Exc_FileDataError);
                }
                buf = pdf_load_stream(gctx, stream);
                res = JM_BinFromBuffer(gctx, buf);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return res;
        }


        //----------------------------------------------------------------
        // annotation get attached sound stream
        //----------------------------------------------------------------
        FITZEXCEPTION(get_sound, !result)
        PARENTCHECK(get_sound, """Retrieve sound stream.""")
        PyObject *
        get_sound()
        {
            PyObject *res = NULL;
            PyObject *stream = NULL;
            fz_buffer *buf = NULL;
            pdf_obj *obj = NULL;
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            fz_var(buf);
            fz_try(gctx) {
                int type = (int) pdf_annot_type(gctx, annot);
                pdf_obj *sound = pdf_dict_get(gctx, annot_obj, PDF_NAME(Sound));
                if (type != PDF_ANNOT_SOUND || !sound) {
                    RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError);
                }
                if (pdf_dict_get(gctx, sound, PDF_NAME(F))) {
                    RAISEPY(gctx, "unsupported sound stream", JM_Exc_FileDataError);
                }
                res = PyDict_New();
                obj = pdf_dict_get(gctx, sound, PDF_NAME(R));
                if (obj) {
                    DICT_SETITEMSTR_DROP(res, "rate",
                            Py_BuildValue("f", pdf_to_real(gctx, obj)));
                }
                obj = pdf_dict_get(gctx, sound, PDF_NAME(C));
                if (obj) {
                    DICT_SETITEMSTR_DROP(res, "channels",
                            Py_BuildValue("i", pdf_to_int(gctx, obj)));
                }
                obj = pdf_dict_get(gctx, sound, PDF_NAME(B));
                if (obj) {
                    DICT_SETITEMSTR_DROP(res, "bps",
                            Py_BuildValue("i", pdf_to_int(gctx, obj)));
                }
                obj = pdf_dict_get(gctx, sound, PDF_NAME(E));
                if (obj) {
                    DICT_SETITEMSTR_DROP(res, "encoding",
                            Py_BuildValue("s", pdf_to_name(gctx, obj)));
                }
                obj = pdf_dict_gets(gctx, sound, "CO");
                if (obj) {
                    DICT_SETITEMSTR_DROP(res, "compression",
                            Py_BuildValue("s", pdf_to_name(gctx, obj)));
                }
                buf = pdf_load_stream(gctx, sound);
                stream = JM_BinFromBuffer(gctx, buf);
                DICT_SETITEMSTR_DROP(res, "stream", stream);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buf);
            }
            fz_catch(gctx) {
                Py_CLEAR(res);
                return NULL;
            }
            return res;
        }


        //----------------------------------------------------------------
        // annotation update attached file
        //----------------------------------------------------------------
        FITZEXCEPTION(update_file, !result)
        %pythonprepend update_file
%{"""Update attached file."""
CheckParent(self)%}

        PyObject *
        update_file(PyObject *buffer=NULL, char *filename=NULL, char *ufilename=NULL, char *desc=NULL)
        {
            pdf_document *pdf = NULL;       // to be filled in
            fz_buffer *res = NULL;          // for compressed content
            pdf_obj *stream = NULL, *fs = NULL;
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            fz_try(gctx) {
                pdf = pdf_get_bound_document(gctx, annot_obj);  // the owning PDF
                int type = (int) pdf_annot_type(gctx, annot);
                if (type != PDF_ANNOT_FILE_ATTACHMENT) {
                    RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError);
                }
                stream = pdf_dict_getl(gctx, annot_obj, PDF_NAME(FS),
                                   PDF_NAME(EF), PDF_NAME(F), NULL);
                // the object for file content
                if (!stream) {
                    RAISEPY(gctx, "bad PDF: no /EF object", JM_Exc_FileDataError);
                }

                fs = pdf_dict_get(gctx, annot_obj, PDF_NAME(FS));

                // file content given
                res = JM_BufferFromBytes(gctx, buffer);
                if (buffer && !res) {
                    RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_ValueError);
                }
                if (res) {
                    JM_update_stream(gctx, pdf, stream, res, 1);
                    // adjust /DL and /Size parameters
                    int64_t len = (int64_t) fz_buffer_storage(gctx, res, NULL);
                    pdf_obj *l = pdf_new_int(gctx, len);
                    pdf_dict_put(gctx, stream, PDF_NAME(DL), l);
                    pdf_dict_putl(gctx, stream, l, PDF_NAME(Params), PDF_NAME(Size), NULL);
                }

                if (filename) {
                    pdf_dict_put_text_string(gctx, stream, PDF_NAME(F), filename);
                    pdf_dict_put_text_string(gctx, fs, PDF_NAME(F), filename);
                    pdf_dict_put_text_string(gctx, stream, PDF_NAME(UF), filename);
                    pdf_dict_put_text_string(gctx, fs, PDF_NAME(UF), filename);
                    pdf_dict_put_text_string(gctx, annot_obj, PDF_NAME(Contents), filename);
                }

                if (ufilename) {
                    pdf_dict_put_text_string(gctx, stream, PDF_NAME(UF), ufilename);
                    pdf_dict_put_text_string(gctx, fs, PDF_NAME(UF), ufilename);
                }

                if (desc) {
                    pdf_dict_put_text_string(gctx, stream, PDF_NAME(Desc), desc);
                    pdf_dict_put_text_string(gctx, fs, PDF_NAME(Desc), desc);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation info
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(info, """Various information details.""")
        PyObject *info()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            PyObject *res = PyDict_New();
            pdf_obj *o;

            DICT_SETITEM_DROP(res, dictkey_content,
                          JM_UnicodeFromStr(pdf_annot_contents(gctx, annot)));

            o = pdf_dict_get(gctx, annot_obj, PDF_NAME(Name));
            DICT_SETITEM_DROP(res, dictkey_name, JM_UnicodeFromStr(pdf_to_name(gctx, o)));

            // Title (= author)
            o = pdf_dict_get(gctx, annot_obj, PDF_NAME(T));
            DICT_SETITEM_DROP(res, dictkey_title, JM_UnicodeFromStr(pdf_to_text_string(gctx, o)));

            // CreationDate
            o = pdf_dict_gets(gctx, annot_obj, "CreationDate");
            DICT_SETITEM_DROP(res, dictkey_creationDate,
                          JM_UnicodeFromStr(pdf_to_text_string(gctx, o)));

            // ModDate
            o = pdf_dict_get(gctx, annot_obj, PDF_NAME(M));
            DICT_SETITEM_DROP(res, dictkey_modDate, JM_UnicodeFromStr(pdf_to_text_string(gctx, o)));

            // Subj
            o = pdf_dict_gets(gctx, annot_obj, "Subj");
            DICT_SETITEM_DROP(res, dictkey_subject,
                          Py_BuildValue("s",pdf_to_text_string(gctx, o)));

            // Identification (PDF key /NM)
            o = pdf_dict_gets(gctx, annot_obj, "NM");
            DICT_SETITEM_DROP(res, dictkey_id,
                          JM_UnicodeFromStr(pdf_to_text_string(gctx, o)));

            return res;
        }

        //----------------------------------------------------------------
        // annotation set information
        //----------------------------------------------------------------
        FITZEXCEPTION(set_info, !result)
        %pythonprepend set_info %{
        """Set various properties."""
        CheckParent(self)
        if type(info) is dict:  # build the args from the dictionary
            content = info.get("content", None)
            title = info.get("title", None)
            creationDate = info.get("creationDate", None)
            modDate = info.get("modDate", None)
            subject = info.get("subject", None)
            info = None
        %}
        PyObject *
        set_info(PyObject *info=NULL, char *content=NULL, char *title=NULL,
                          char *creationDate=NULL, char *modDate=NULL, char *subject=NULL)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            // use this to indicate a 'markup' annot type
            int is_markup = pdf_annot_has_author(gctx, annot);
            fz_try(gctx) {
                // contents
                if (content)
                    pdf_set_annot_contents(gctx, annot, content);

                if (is_markup) {
                    // title (= author)
                    if (title)
                        pdf_set_annot_author(gctx, annot, title);

                    // creation date
                    if (creationDate)
                        pdf_dict_put_text_string(gctx, annot_obj,
                                                 PDF_NAME(CreationDate), creationDate);

                    // mod date
                    if (modDate)
                        pdf_dict_put_text_string(gctx, annot_obj,
                                                 PDF_NAME(M), modDate);

                    // subject
                    if (subject)
                        pdf_dict_puts_drop(gctx, annot_obj, "Subj",
                                           pdf_new_text_string(gctx, subject));
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // annotation border
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        %pythonprepend border %{
        """Border information."""
        CheckParent(self)
        atype = self.type[0]
        if atype not in (PDF_ANNOT_CIRCLE, PDF_ANNOT_FREE_TEXT, PDF_ANNOT_INK, PDF_ANNOT_LINE, PDF_ANNOT_POLY_LINE,PDF_ANNOT_POLYGON, PDF_ANNOT_SQUARE):
            return {}
        %}
        PyObject *border()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            return JM_annot_border(gctx, annot_obj);
        }

        //----------------------------------------------------------------
        // set annotation border
        //----------------------------------------------------------------
        %pythonprepend set_border %{
        """Set border properties.

        Either a dict, or direct arguments width, style, dashes or clouds."""

        CheckParent(self)
        atype, atname = self.type[:2]  # annotation type
        if atype not in (PDF_ANNOT_CIRCLE, PDF_ANNOT_FREE_TEXT, PDF_ANNOT_INK, PDF_ANNOT_LINE, PDF_ANNOT_POLY_LINE,PDF_ANNOT_POLYGON, PDF_ANNOT_SQUARE):
            print(f"Cannot set border for '{atname}'.")
            return None
        if not atype in (PDF_ANNOT_CIRCLE, PDF_ANNOT_FREE_TEXT,PDF_ANNOT_POLYGON, PDF_ANNOT_SQUARE):
            if clouds > 0:
                print(f"Cannot set cloudy border for '{atname}'.")
                clouds = -1  # do not set border effect
        if type(border) is not dict:
            border = {"width": width, "style": style, "dashes": dashes, "clouds": clouds}
        border.setdefault("width", -1)
        border.setdefault("style", None)
        border.setdefault("dashes", None)
        border.setdefault("clouds", -1)
        if border["width"] == None:
            border["width"] = -1
        if border["clouds"] == None:
            border["clouds"] = -1
        if hasattr(border["dashes"], "__getitem__"):  # ensure sequence items are integers
            border["dashes"] = tuple(border["dashes"])
            for item in border["dashes"]:
                if not isinstance(item, int):
                    border["dashes"] = None
                    break
        %}
        PyObject *
        set_border(PyObject *border=NULL, float width=-1, char *style=NULL, PyObject *dashes=NULL, int clouds=-1)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_document *pdf = pdf_get_bound_document(gctx, annot_obj);
            return JM_annot_set_border(gctx, border, pdf, annot_obj);
        }


        //----------------------------------------------------------------
        // annotation flags
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        PARENTCHECK(flags, """Flags field.""")
        int flags()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            return pdf_annot_flags(gctx, annot);
        }

        //----------------------------------------------------------------
        // annotation clean contents
        //----------------------------------------------------------------
        FITZEXCEPTION(clean_contents, !result)
        PARENTCHECK(clean_contents, """Clean appearance contents stream.""")
        PyObject *clean_contents(int sanitize=1)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_document *pdf = pdf_get_bound_document(gctx, pdf_annot_obj(gctx, annot));
            #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22
            pdf_filter_factory list[2] = { 0 };
            pdf_sanitize_filter_options sopts = { 0 };
            pdf_filter_options filter = {
                1,     // recurse: true
                0,     // instance forms
                0,     // do not ascii-escape binary data
                0,     // no_update
                NULL,  // end_page_opaque
                NULL,  // end page
                list,  // filters
                };
            if (sanitize) {
              list[0].filter = pdf_new_sanitize_filter;
              list[0].options = &sopts;
            }
            #else
            pdf_filter_options filter = {
                NULL,  // opaque
                NULL,  // image filter
                NULL,  // text filter
                NULL,  // after text
                NULL,  // end page
                1,     // recurse: true
                1,     // instance forms
                1,     // sanitize,
                0      // do not ascii-escape binary data
                };
            filter.sanitize = sanitize;
            #endif
            fz_try(gctx) {
                pdf_filter_annot_contents(gctx, pdf, annot, &filter);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        //----------------------------------------------------------------
        // set annotation flags
        //----------------------------------------------------------------
        PARENTCHECK(set_flags, """Set annotation flags.""")
        void
        set_flags(int flags)
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_set_annot_flags(gctx, annot, flags);
        }


        //----------------------------------------------------------------
        // annotation delete responses
        //----------------------------------------------------------------
        FITZEXCEPTION(delete_responses, !result)
        PARENTCHECK(delete_responses, """Delete 'Popup' and responding annotations.""")
        PyObject *
        delete_responses()
        {
            pdf_annot *annot = (pdf_annot *) $self;
            pdf_obj *annot_obj = pdf_annot_obj(gctx, annot);
            pdf_page *page = pdf_annot_page(gctx, annot);
            pdf_annot *irt_annot = NULL;
            fz_try(gctx) {
                while (1) {
                    irt_annot = JM_find_annot_irt(gctx, annot);
                    if (!irt_annot)
                        break;
                    pdf_delete_annot(gctx, page, irt_annot);
                }
                pdf_dict_del(gctx, annot_obj, PDF_NAME(Popup));
                
                pdf_obj *annots = pdf_dict_get(gctx, page->obj, PDF_NAME(Annots));
                int i, n = pdf_array_len(gctx, annots), found = 0;
                for (i = n - 1; i >= 0; i--) {
                    pdf_obj *o = pdf_array_get(gctx, annots, i);
                    pdf_obj *p = pdf_dict_get(gctx, o, PDF_NAME(Parent));
                    if (!p)
                        continue;
                    if (!pdf_objcmp(gctx, p, annot_obj)) {
                        pdf_array_delete(gctx, annots, i);
                        found = 1;
                    }
                }
                if (found > 0) {
                    pdf_dict_put(gctx, page->obj, PDF_NAME(Annots), annots);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // next annotation
        //----------------------------------------------------------------
        PARENTCHECK(next, """Next annotation.""")
        %pythonappend next %{
        if not val:
            return None
        val.thisown = True
        val.parent = self.parent  # copy owning page object from previous annot
        val.parent._annot_refs[id(val)] = val

        if val.type[0] == PDF_ANNOT_WIDGET:
            widget = Widget()
            TOOLS._fill_widget(val, widget)
            val = widget
        %}
        %pythoncode %{@property%}
        struct Annot *next()
        {
            pdf_annot *this_annot = (pdf_annot *) $self;
            int type = pdf_annot_type(gctx, this_annot);
            pdf_annot *annot;

            if (type != PDF_ANNOT_WIDGET) {
                annot = pdf_next_annot(gctx, this_annot);
            } else {
                annot = pdf_next_widget(gctx, this_annot);
            }

            if (annot)
                pdf_keep_annot(gctx, annot);
            return (struct Annot *) annot;
        }


        //----------------------------------------------------------------
        // annotation pixmap
        //----------------------------------------------------------------
        FITZEXCEPTION(get_pixmap, !result)
        %pythonprepend get_pixmap
%{"""annotation Pixmap"""

CheckParent(self)
cspaces = {"gray": csGRAY, "rgb": csRGB, "cmyk": csCMYK}
if type(colorspace) is str:
    colorspace = cspaces.get(colorspace.lower(), None)
if dpi:
    matrix = Matrix(dpi / 72, dpi / 72)
%}
        %pythonappend get_pixmap
%{
        val.thisown = True
        if dpi:
            val.set_dpi(dpi, dpi)
%}
        struct Pixmap *
        get_pixmap(PyObject *matrix = NULL, PyObject *dpi=NULL, struct Colorspace *colorspace = NULL, int alpha = 0)
        {
            fz_matrix ctm = JM_matrix_from_py(matrix);
            fz_colorspace *cs = (fz_colorspace *) colorspace;
            fz_pixmap *pix = NULL;
            if (!cs) {
                cs = fz_device_rgb(gctx);
            }

            fz_try(gctx) {
                pix = pdf_new_pixmap_from_annot(gctx, (pdf_annot *) $self, ctm, cs, NULL, alpha);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pix;
        }
        %pythoncode %{
        def _erase(self):
            self.__swig_destroy__(self)
            self.parent = None

        def __str__(self):
            CheckParent(self)
            return "'%s' annotation on %s" % (self.type[1], str(self.parent))

        def __repr__(self):
            CheckParent(self)
            return "'%s' annotation on %s" % (self.type[1], str(self.parent))

        def __del__(self):
            if self.parent is None:
                return
            self._erase()%}
    }
};
%clearnodefaultctor;

//------------------------------------------------------------------------
// fz_link
//------------------------------------------------------------------------
%nodefaultctor;
struct Link
{
    %immutable;
    %extend {
        ~Link() {
            DEBUGMSG1("Link");
            fz_link *this_link = (fz_link *) $self;
            fz_drop_link(gctx, this_link);
            DEBUGMSG2;
        }

        PyObject *_border(struct Document *doc, int xref)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc);
            if (!pdf) Py_RETURN_NONE;
            pdf_obj *link_obj = pdf_new_indirect(gctx, pdf, xref, 0);
            if (!link_obj) Py_RETURN_NONE;
            PyObject *b = JM_annot_border(gctx, link_obj);
            pdf_drop_obj(gctx, link_obj);
            return b;
        }

        PyObject *_setBorder(PyObject *border, struct Document *doc, int xref)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc);
            if (!pdf) Py_RETURN_NONE;
            pdf_obj *link_obj = pdf_new_indirect(gctx, pdf, xref, 0);
            if (!link_obj) Py_RETURN_NONE;
            PyObject *b = JM_annot_set_border(gctx, border, pdf, link_obj);
            pdf_drop_obj(gctx, link_obj);
            return b;
        }

        FITZEXCEPTION(_colors, !result)
        PyObject *_colors(struct Document *doc, int xref)
        {
            pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc);
            if (!pdf) Py_RETURN_NONE;
            PyObject *b = NULL;
            pdf_obj *link_obj;
            fz_try(gctx) {
                link_obj = pdf_new_indirect(gctx, pdf, xref, 0);
                if (!link_obj) {
                    RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError);
                }
                b = JM_annot_colors(gctx, link_obj);
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, link_obj);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return b;
        }


        %pythoncode %{
        @property
        def border(self):
            return self._border(self.parent.parent.this, self.xref)

        @property
        def flags(self)->int:
            CheckParent(self)
            doc = self.parent.parent
            if not doc.is_pdf:
                return 0
            f = doc.xref_get_key(self.xref, "F")
            if f[1] != "null":
                return int(f[1])
            return 0

        def set_flags(self, flags):
            CheckParent(self)
            doc = self.parent.parent
            if not doc.is_pdf:
                raise ValueError("is no PDF")
            if not type(flags) is int:
                raise ValueError("bad 'flags' value")
            doc.xref_set_key(self.xref, "F", str(flags))
            return None

        def set_border(self, border=None, width=0, dashes=None, style=None):
            if type(border) is not dict:
                border = {"width": width, "style": style, "dashes": dashes}
            return self._setBorder(border, self.parent.parent.this, self.xref)

        @property
        def colors(self):
            return self._colors(self.parent.parent.this, self.xref)

        def set_colors(self, colors=None, stroke=None, fill=None):
            """Set border colors."""
            CheckParent(self)
            doc = self.parent.parent
            if type(colors) is not dict:
                colors = {"fill": fill, "stroke": stroke}
            fill = colors.get("fill")
            stroke = colors.get("stroke")
            if fill is not None:
                print("warning: links have no fill color")
            if stroke in ([], ()):
                doc.xref_set_key(self.xref, "C", "[]")
                return
            if hasattr(stroke, "__float__"):
                stroke = [float(stroke)]
            CheckColor(stroke)
            if len(stroke) == 1:
                s = "[%g]" % stroke[0]
            elif len(stroke) == 3:
                s = "[%g %g %g]" % tuple(stroke)
            else:
                s = "[%g %g %g %g]" % tuple(stroke)
            doc.xref_set_key(self.xref, "C", s)
        %}
        %pythoncode %{@property%}
        PARENTCHECK(uri, """Uri string.""")
        PyObject *uri()
        {
            fz_link *this_link = (fz_link *) $self;
            return JM_UnicodeFromStr(this_link->uri);
        }

        %pythoncode %{@property%}
        PARENTCHECK(is_external, """Flag the link as external.""")
        PyObject *is_external()
        {
            fz_link *this_link = (fz_link *) $self;
            if (!this_link->uri) Py_RETURN_FALSE;
            return JM_BOOL(fz_is_external_link(gctx, this_link->uri));
        }

        %pythoncode
        %{
        page = -1
        @property
        def dest(self):
            """Create link destination details."""
            if hasattr(self, "parent") and self.parent is None:
                raise ValueError("orphaned object: parent is None")
            if self.parent.parent.is_closed or self.parent.parent.is_encrypted:
                raise ValueError("document closed or encrypted")
            doc = self.parent.parent

            if self.is_external or self.uri.startswith("#"):
                uri = None
            else:
                uri = doc.resolve_link(self.uri)

            return linkDest(self, uri)
        %}

        PARENTCHECK(rect, """Rectangle ('hot area').""")
        %pythoncode %{@property%}
        %pythonappend rect %{val = Rect(val)%}
        PyObject *rect()
        {
            fz_link *this_link = (fz_link *) $self;
            return JM_py_from_rect(this_link->rect);
        }

        //----------------------------------------------------------------
        // next link
        //----------------------------------------------------------------
        // we need to increase the link refs number
        // so that it will not be freed when the head is dropped
        PARENTCHECK(next, """Next link.""")
        %pythonappend next %{
            if val:
                val.thisown = True
                val.parent = self.parent  # copy owning page from prev link
                val.parent._annot_refs[id(val)] = val
                if self.xref > 0:  # prev link has an xref
                    link_xrefs = [x[0] for x in self.parent.annot_xrefs() if x[1] == PDF_ANNOT_LINK]
                    link_ids = [x[2] for x in self.parent.annot_xrefs() if x[1] == PDF_ANNOT_LINK]
                    idx = link_xrefs.index(self.xref)
                    val.xref = link_xrefs[idx + 1]
                    val.id = link_ids[idx + 1]
                else:
                    val.xref = 0
                    val.id = ""
        %}
        %pythoncode %{@property%}
        struct Link *next()
        {
            fz_link *this_link = (fz_link *) $self;
            fz_link *next_link = this_link->next;
            if (!next_link) return NULL;
            next_link = fz_keep_link(gctx, next_link);
            return (struct Link *) next_link;
        }

        %pythoncode %{
        def _erase(self):
            self.__swig_destroy__(self)
            self.parent = None

        def __str__(self):
            CheckParent(self)
            return "link on " + str(self.parent)

        def __repr__(self):
            CheckParent(self)
            return "link on " + str(self.parent)

        def __del__(self):
            self._erase()%}
    }
};
%clearnodefaultctor;

//------------------------------------------------------------------------
// fz_display_list
//------------------------------------------------------------------------
struct DisplayList {
    %extend
    {
        ~DisplayList() {
            DEBUGMSG1("DisplayList");
            fz_display_list *this_dl = (fz_display_list *) $self;
            fz_drop_display_list(gctx, this_dl);
            DEBUGMSG2;
        }
        FITZEXCEPTION(DisplayList, !result)
        DisplayList(PyObject *mediabox)
        {
            fz_display_list *dl = NULL;
            fz_try(gctx) {
                dl = fz_new_display_list(gctx, JM_rect_from_py(mediabox));
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct DisplayList *) dl;
        }

        FITZEXCEPTION(run, !result)
        PyObject *run(struct DeviceWrapper *dw, PyObject *m, PyObject *area) {
            fz_try(gctx) {
                fz_run_display_list(gctx, (fz_display_list *) $self, dw->device,
                    JM_matrix_from_py(m), JM_rect_from_py(area), NULL);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------------------------------------
        // DisplayList.rect
        //----------------------------------------------------------------
        %pythoncode%{@property%}
        %pythonappend rect %{val = Rect(val)%}
        PyObject *rect()
        {
            return JM_py_from_rect(fz_bound_display_list(gctx, (fz_display_list *) $self));
        }

        //----------------------------------------------------------------
        // DisplayList.get_pixmap
        //----------------------------------------------------------------
        FITZEXCEPTION(get_pixmap, !result)
        %pythonappend get_pixmap %{val.thisown = True%}
        struct Pixmap *get_pixmap(PyObject *matrix=NULL,
                                      struct Colorspace *colorspace=NULL,
                                      int alpha=0,
                                      PyObject *clip=NULL)
        {
            fz_colorspace *cs = NULL;
            fz_pixmap *pix = NULL;

            if (colorspace) cs = (fz_colorspace *) colorspace;
            else cs = fz_device_rgb(gctx);

            fz_try(gctx) {
                pix = JM_pixmap_from_display_list(gctx,
                          (fz_display_list *) $self, matrix, cs,
                           alpha, clip, NULL);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Pixmap *) pix;
        }

        //----------------------------------------------------------------
        // DisplayList.get_textpage
        //----------------------------------------------------------------
        FITZEXCEPTION(get_textpage, !result)
        %pythonappend get_textpage %{val.thisown = True%}
        struct TextPage *get_textpage(int flags = 3)
        {
            fz_display_list *this_dl = (fz_display_list *) $self;
            fz_stext_page *tp = NULL;
            fz_try(gctx) {
                fz_stext_options stext_options = { 0 };
                stext_options.flags = flags;
                tp = fz_new_stext_page_from_display_list(gctx, this_dl, &stext_options);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct TextPage *) tp;
        }
        %pythoncode %{
        def __del__(self):
            if not type(self) is DisplayList:
                return
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)
        %}
    }
};

//------------------------------------------------------------------------
// fz_stext_page
//------------------------------------------------------------------------
struct TextPage {
    %extend {
        ~TextPage()
        {
            DEBUGMSG1("TextPage");
            fz_stext_page *this_tp = (fz_stext_page *) $self;
            fz_drop_stext_page(gctx, this_tp);
            DEBUGMSG2;
        }

        FITZEXCEPTION(TextPage, !result)
        %pythonappend TextPage %{self.thisown=True%}
        TextPage(PyObject *mediabox)
        {
            fz_stext_page *tp = NULL;
            fz_try(gctx) {
                tp = fz_new_stext_page(gctx, JM_rect_from_py(mediabox));
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct TextPage *) tp;
        }

        //----------------------------------------------------------------
        // method search()
        //----------------------------------------------------------------
        FITZEXCEPTION(search, !result)
        %pythonprepend search
        %{"""Locate 'needle' returning rects or quads."""%}
        %pythonappend search %{
        if not val:
            return val
        items = len(val)
        for i in range(items):  # change entries to quads or rects
            q = Quad(val[i])
            if quads:
                val[i] = q
            else:
                val[i] = q.rect
        if quads:
            return val
        i = 0  # join overlapping rects on the same line
        while i < items - 1:
            v1 = val[i]
            v2 = val[i + 1]
            if v1.y1 != v2.y1 or (v1 & v2).is_empty:
                i += 1
                continue  # no overlap on same line
            val[i] = v1 | v2  # join rectangles
            del val[i + 1]  # remove v2
            items -= 1  # reduce item count
        %}
        PyObject *search(const char *needle, int hit_max=0, int quads=1)
        {
            PyObject *liste = NULL;
            fz_try(gctx) {
                liste = JM_search_stext_page(gctx, (fz_stext_page *) $self, needle);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return liste;
        }


        //----------------------------------------------------------------
        // Get list of all blocks with block type and bbox as a Python list
        //----------------------------------------------------------------
        FITZEXCEPTION(_getNewBlockList, !result)
        PyObject *
        _getNewBlockList(PyObject *page_dict, int raw)
        {
            fz_try(gctx) {
                JM_make_textpage_dict(gctx, (fz_stext_page *) $self, page_dict, raw);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode %{
        def _textpage_dict(self, raw=False):
            page_dict = {"width": self.rect.width, "height": self.rect.height}
            self._getNewBlockList(page_dict, raw)
            return page_dict
        %}


        //----------------------------------------------------------------
        // Get image meta information as a Python dictionary
        //----------------------------------------------------------------
        FITZEXCEPTION(extractIMGINFO, !result)
        %pythonprepend extractIMGINFO
        %{"""Return a list with image meta information."""%}
        PyObject *
        extractIMGINFO(int hashes=0)
        {
            fz_stext_block *block;
            int block_n = -1;
            fz_stext_page *this_tpage = (fz_stext_page *) $self;
            PyObject *rc = NULL, *block_dict = NULL;
            fz_pixmap *pix = NULL;
            fz_try(gctx) {
                rc = PyList_New(0);
                for (block = this_tpage->first_block; block; block = block->next) {
                    block_n++;
                    if (block->type == FZ_STEXT_BLOCK_TEXT) {
                        continue;
                    }
                    unsigned char digest[16];
                    fz_image *img = block->u.i.image;
                    Py_ssize_t img_size = 0;
                    fz_compressed_buffer *cbuff = fz_compressed_image_buffer(gctx, img);
                    if (cbuff) {
                        img_size = (Py_ssize_t) cbuff->buffer->len;
                    }
                    if (hashes) {
                        pix = fz_get_pixmap_from_image(gctx, img, NULL, NULL, NULL, NULL);
                        if (img_size == 0) {
                            img_size = (Py_ssize_t) pix->w * pix->h * pix->n;
                        }
                        fz_md5_pixmap(gctx, pix, digest);
                        fz_drop_pixmap(gctx, pix);
                        pix = NULL;
                    }
                    fz_colorspace *cs = img->colorspace;
                    block_dict = PyDict_New();
                    DICT_SETITEM_DROP(block_dict, dictkey_number, Py_BuildValue("i", block_n));
                    DICT_SETITEM_DROP(block_dict, dictkey_bbox,
                                    JM_py_from_rect(block->bbox));
                    DICT_SETITEM_DROP(block_dict, dictkey_matrix,
                                    JM_py_from_matrix(block->u.i.transform));
                    DICT_SETITEM_DROP(block_dict, dictkey_width,
                                    Py_BuildValue("i", img->w));
                    DICT_SETITEM_DROP(block_dict, dictkey_height,
                                    Py_BuildValue("i", img->h));
                    DICT_SETITEM_DROP(block_dict, dictkey_colorspace,
                                    Py_BuildValue("i",
                                    fz_colorspace_n(gctx, cs)));
                    DICT_SETITEM_DROP(block_dict, dictkey_cs_name,
                                    Py_BuildValue("s",
                                    fz_colorspace_name(gctx, cs)));
                    DICT_SETITEM_DROP(block_dict, dictkey_xres,
                                    Py_BuildValue("i", img->xres));
                    DICT_SETITEM_DROP(block_dict, dictkey_yres,
                                    Py_BuildValue("i", img->xres));
                    DICT_SETITEM_DROP(block_dict, dictkey_bpc,
                                    Py_BuildValue("i", (int) img->bpc));
                    DICT_SETITEM_DROP(block_dict, dictkey_size,
                                    Py_BuildValue("n", img_size));
                    if (hashes) {
                        DICT_SETITEMSTR_DROP(block_dict, "digest",
                                    PyBytes_FromStringAndSize(digest, 16));
                    }
                    LIST_APPEND_DROP(rc, block_dict);
                }
            }
            fz_always(gctx) {
            }
            fz_catch(gctx) {
                Py_CLEAR(rc);
                Py_CLEAR(block_dict);
                fz_drop_pixmap(gctx, pix);
                return NULL;
            }
            return rc;
        }


        //----------------------------------------------------------------
        // Get text blocks with their bbox and concatenated lines
        // as a Python list
        //----------------------------------------------------------------
        FITZEXCEPTION(extractBLOCKS, !result)
        %pythonprepend extractBLOCKS
        %{"""Return a list with text block information."""%}
        PyObject *
        extractBLOCKS()
        {
            fz_stext_block *block;
            fz_stext_line *line;
            fz_stext_char *ch;
            int block_n = -1;
            PyObject *text = NULL, *litem;
            fz_buffer *res = NULL;
            fz_var(res);
            fz_stext_page *this_tpage = (fz_stext_page *) $self;
            fz_rect tp_rect = this_tpage->mediabox;
            PyObject *lines = NULL;
            fz_try(gctx) {
                res = fz_new_buffer(gctx, 1024);
                lines = PyList_New(0);
                for (block = this_tpage->first_block; block; block = block->next) {
                    block_n++;
                    fz_rect blockrect = fz_empty_rect;
                    if (block->type == FZ_STEXT_BLOCK_TEXT) {
                        fz_clear_buffer(gctx, res);  // set text buffer to empty
                        int line_n = -1;
                        int last_char = 0;
                        for (line = block->u.t.first_line; line; line = line->next) {
                            line_n++;
                            fz_rect linerect = fz_empty_rect;
                            for (ch = line->first_char; ch; ch = ch->next) {
                                fz_rect cbbox = JM_char_bbox(gctx, line, ch);
                                if (!JM_rects_overlap(tp_rect, cbbox) &&
                                    !fz_is_infinite_rect(tp_rect)) {
                                    continue;
                                }
                                JM_append_rune(gctx, res, ch->c);
                                last_char = ch->c;
                                linerect = fz_union_rect(linerect, cbbox);
                            }
                            if (last_char != 10 && !fz_is_empty_rect(linerect)) {
                                fz_append_byte(gctx, res, 10);
                            }
                            blockrect = fz_union_rect(blockrect, linerect);
                        }
                        text = JM_EscapeStrFromBuffer(gctx, res);
                    } else if (JM_rects_overlap(tp_rect, block->bbox) || fz_is_infinite_rect(tp_rect)) {
                        fz_image *img = block->u.i.image;
                        fz_colorspace *cs = img->colorspace;
                        text = PyUnicode_FromFormat("<image: %s, width: %d, height: %d, bpc: %d>", fz_colorspace_name(gctx, cs), img->w, img->h, img->bpc);
                        blockrect = fz_union_rect(blockrect, block->bbox);
                    }
                    if (!fz_is_empty_rect(blockrect)) {
                        litem = PyTuple_New(7);
                        PyTuple_SET_ITEM(litem, 0, Py_BuildValue("f", blockrect.x0));
                        PyTuple_SET_ITEM(litem, 1, Py_BuildValue("f", blockrect.y0));
                        PyTuple_SET_ITEM(litem, 2, Py_BuildValue("f", blockrect.x1));
                        PyTuple_SET_ITEM(litem, 3, Py_BuildValue("f", blockrect.y1));
                        PyTuple_SET_ITEM(litem, 4, Py_BuildValue("O", text));
                        PyTuple_SET_ITEM(litem, 5, Py_BuildValue("i", block_n));
                        PyTuple_SET_ITEM(litem, 6, Py_BuildValue("i", block->type));
                        LIST_APPEND_DROP(lines, litem);
                    }
                    Py_CLEAR(text);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
                PyErr_Clear();
            }
            fz_catch(gctx) {
                Py_CLEAR(lines);
                return NULL;
            }
            return lines;
        }

        //----------------------------------------------------------------
        // Get text words with their bbox
        //----------------------------------------------------------------
        FITZEXCEPTION(extractWORDS, !result)
        %pythonprepend extractWORDS
        %{"""Return a list with text word information."""%}
        PyObject *
        extractWORDS(PyObject *delimiters=NULL)
        {
            fz_stext_block *block;
            fz_stext_line *line;
            fz_stext_char *ch;
            fz_buffer *buff = NULL;
            fz_var(buff);
            size_t buflen = 0;
            int block_n = -1, line_n, word_n;
            fz_rect wbbox = fz_empty_rect;  // word bbox
            fz_stext_page *this_tpage = (fz_stext_page *) $self;
            fz_rect tp_rect = this_tpage->mediabox;
            int word_delimiter = 0;
            PyObject *lines = NULL;
            fz_try(gctx) {
                buff = fz_new_buffer(gctx, 64);
                lines = PyList_New(0);
                for (block = this_tpage->first_block; block; block = block->next) {
                    block_n++;
                    if (block->type != FZ_STEXT_BLOCK_TEXT) {
                        continue;
                    }
                    line_n = -1;
                    for (line = block->u.t.first_line; line; line = line->next) {
                        line_n++;
                        word_n = 0;                       // word counter per line
                        fz_clear_buffer(gctx, buff);      // reset word buffer
                        buflen = 0;                       // reset char counter
                        for (ch = line->first_char; ch; ch = ch->next) {
                            fz_rect cbbox = JM_char_bbox(gctx, line, ch);
                            if (!JM_rects_overlap(tp_rect, cbbox) &&
                                !fz_is_infinite_rect(tp_rect)) {
                                continue;
                            }
                            word_delimiter = JM_is_word_delimiter(ch->c, delimiters);
                            if (word_delimiter) {
                                if (buflen == 0) continue;  // skip spaces at line start
                                if (!fz_is_empty_rect(wbbox)) {  // output word
                                    word_n = JM_append_word(gctx, lines, buff, &wbbox,
                                                        block_n, line_n, word_n);
                                }
                                fz_clear_buffer(gctx, buff);
                                buflen = 0;  // reset char counter
                                continue;
                            }
                            // append one unicode character to the word
                            JM_append_rune(gctx, buff, ch->c);
                            buflen++;
                            // enlarge word bbox
                            wbbox = fz_union_rect(wbbox, JM_char_bbox(gctx, line, ch));
                        }
                        if (buflen && !fz_is_empty_rect(wbbox)) {
                            word_n = JM_append_word(gctx, lines, buff, &wbbox,
                                                    block_n, line_n, word_n);
                        }
                        fz_clear_buffer(gctx, buff);
                        buflen = 0;
                    }
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buff);
                PyErr_Clear();
            }
            fz_catch(gctx) {
                return NULL;
            }
            return lines;
        }

        //----------------------------------------------------------------
        // TextPage poolsize
        //----------------------------------------------------------------
        %pythonprepend poolsize
        %{"""TextPage current poolsize."""%}
        PyObject *poolsize()
        {
            fz_stext_page *tpage = (fz_stext_page *) $self;
            size_t size = fz_pool_size(gctx, tpage->pool);
            return PyLong_FromSize_t(size);
        }

        //----------------------------------------------------------------
        // TextPage rectangle
        //----------------------------------------------------------------
        %pythoncode %{@property%}
        %pythonprepend rect
        %{"""TextPage rectangle."""%}
        %pythonappend rect %{val = Rect(val)%}
        PyObject *rect()
        {
            fz_stext_page *this_tpage = (fz_stext_page *) $self;
            fz_rect mediabox = this_tpage->mediabox;
            return JM_py_from_rect(mediabox);
        }

        //----------------------------------------------------------------
        // method _extractText()
        //----------------------------------------------------------------
        FITZEXCEPTION(_extractText, !result)
        %newobject _extractText;
        PyObject *_extractText(int format)
        {
            fz_buffer *res = NULL;
            fz_output *out = NULL;
            PyObject *text = NULL;
            fz_var(res);
            fz_var(out);
            fz_stext_page *this_tpage = (fz_stext_page *) $self;
            fz_try(gctx) {
                res = fz_new_buffer(gctx, 1024);
                out = fz_new_output_with_buffer(gctx, res);
                switch(format) {
                    case(1):
                        fz_print_stext_page_as_html(gctx, out, this_tpage, 0);
                        break;
                    case(3):
                        fz_print_stext_page_as_xml(gctx, out, this_tpage, 0);
                        break;
                    case(4):
                        fz_print_stext_page_as_xhtml(gctx, out, this_tpage, 0);
                        break;
                    default:
                        JM_print_stext_page_as_text(gctx, res, this_tpage);
                        break;
                }
                text = JM_EscapeStrFromBuffer(gctx, res);

            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
                fz_drop_output(gctx, out);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return text;
        }


        //----------------------------------------------------------------
        // method extractTextbox()
        //----------------------------------------------------------------
        FITZEXCEPTION(extractTextbox, !result)
        PyObject *extractTextbox(PyObject *rect)
        {
            fz_stext_page *this_tpage = (fz_stext_page *) $self;
            fz_rect area = JM_rect_from_py(rect);
            PyObject *rc = NULL;
            fz_try(gctx) {
                rc = JM_copy_rectangle(gctx, this_tpage, area);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return rc;
        }

        //----------------------------------------------------------------
        // method extractSelection()
        //----------------------------------------------------------------
        PyObject *extractSelection(PyObject *pointa, PyObject *pointb)
        {
            fz_stext_page *this_tpage = (fz_stext_page *) $self;
            fz_point a = JM_point_from_py(pointa);
            fz_point b = JM_point_from_py(pointb);
            char *found = fz_copy_selection(gctx, this_tpage, a, b, 0);
            PyObject *rc = NULL;
            if (found) {
                rc = PyUnicode_FromString(found);
                JM_Free(found);
            } else {
                rc = EMPTY_STRING;
            }
            return rc;
        }

        %pythoncode %{
            def extractText(self, sort=False) -> str:
                """Return simple, bare text on the page."""
                if sort is False:
                    return self._extractText(0)
                blocks = self.extractBLOCKS()[:]
                blocks.sort(key=lambda b: (b[3], b[0]))
                return "".join([b[4] for b in blocks])

            def extractHTML(self) -> str:
                """Return page content as a HTML string."""
                return self._extractText(1)

            def extractJSON(self, cb=None, sort=False) -> str:
                """Return 'extractDICT' converted to JSON format."""
                import base64, json
                val = self._textpage_dict(raw=False)

                class b64encode(json.JSONEncoder):
                    def default(self, s):
                        if type(s) in (bytes, bytearray):
                            return base64.b64encode(s).decode()

                if cb is not None:
                    val["width"] = cb.width
                    val["height"] = cb.height
                if sort is True:
                    blocks = val["blocks"]
                    blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
                    val["blocks"] = blocks
                val = json.dumps(val, separators=(",", ":"), cls=b64encode, indent=1)
                return val

            def extractRAWJSON(self, cb=None, sort=False) -> str:
                """Return 'extractRAWDICT' converted to JSON format."""
                import base64, json
                val = self._textpage_dict(raw=True)

                class b64encode(json.JSONEncoder):
                    def default(self,s):
                        if type(s) in (bytes, bytearray):
                            return base64.b64encode(s).decode()

                if cb is not None:
                    val["width"] = cb.width
                    val["height"] = cb.height
                if sort is True:
                    blocks = val["blocks"]
                    blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
                    val["blocks"] = blocks
                val = json.dumps(val, separators=(",", ":"), cls=b64encode, indent=1)
                return val

            def extractXML(self) -> str:
                """Return page content as a XML string."""
                return self._extractText(3)

            def extractXHTML(self) -> str:
                """Return page content as a XHTML string."""
                return self._extractText(4)

            def extractDICT(self, cb=None, sort=False) -> dict:
                """Return page content as a Python dict of images and text spans."""
                val = self._textpage_dict(raw=False)
                if cb is not None:
                    val["width"] = cb.width
                    val["height"] = cb.height
                if sort is True:
                    blocks = val["blocks"]
                    blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
                    val["blocks"] = blocks
                return val

            def extractRAWDICT(self, cb=None, sort=False) -> dict:
                """Return page content as a Python dict of images and text characters."""
                val =  self._textpage_dict(raw=True)
                if cb is not None:
                    val["width"] = cb.width
                    val["height"] = cb.height
                if sort is True:
                    blocks = val["blocks"]
                    blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0]))
                    val["blocks"] = blocks
                return val

            def __del__(self):
                if not type(self) is TextPage:
                    return
                if getattr(self, "thisown", False):
                    self.__swig_destroy__(self)
        %}
    }
};

//------------------------------------------------------------------------
// Graftmap - only used internally for inter-PDF object copy operations
//------------------------------------------------------------------------
struct Graftmap
{
    %extend
    {
        ~Graftmap()
        {
            DEBUGMSG1("Graftmap");
            pdf_graft_map *this_gm = (pdf_graft_map *) $self;
            pdf_drop_graft_map(gctx, this_gm);
            DEBUGMSG2;
        }

        FITZEXCEPTION(Graftmap, !result)
        Graftmap(struct Document *doc)
        {
            pdf_graft_map *map = NULL;
            fz_try(gctx) {
                pdf_document *dst = pdf_specifics(gctx, (fz_document *) doc);
                ASSERT_PDF(dst);
                map = pdf_new_graft_map(gctx, dst);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Graftmap *) map;
        }

        %pythoncode %{
        def __del__(self):
            if not type(self) is Graftmap:
                return
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)
        %}
    }
};


//------------------------------------------------------------------------
// TextWriter
//------------------------------------------------------------------------
struct TextWriter
{
    %extend {
        ~TextWriter()
        {
            DEBUGMSG1("TextWriter");
            fz_text *this_tw = (fz_text *) $self;
            fz_drop_text(gctx, this_tw);
            DEBUGMSG2;
        }

        FITZEXCEPTION(TextWriter, !result)
        %pythonprepend TextWriter
        %{"""Stores text spans for later output on compatible PDF pages."""%}
        %pythonappend TextWriter %{
        self.opacity = opacity
        self.color = color
        self.rect = Rect(page_rect)
        self.ctm = Matrix(1, 0, 0, -1, 0, self.rect.height)
        self.ictm = ~self.ctm
        self.last_point = Point()
        self.last_point.__doc__ = "Position following last text insertion."
        self.text_rect = Rect()

        self.text_rect.__doc__ = "Accumulated area of text spans."
        self.used_fonts = set()
        self.thisown = True
        %}
        TextWriter(PyObject *page_rect, float opacity=1, PyObject *color=NULL )
        {
            fz_text *text = NULL;
            fz_try(gctx) {
                text = fz_new_text(gctx);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct TextWriter *) text;
        }

        FITZEXCEPTION(append, !result)
        %pythonprepend append %{
        """Store 'text' at point 'pos' using 'font' and 'fontsize'."""

        pos = Point(pos) * self.ictm
        if font is None:
            font = Font("helv")
        if not font.is_writable:
            raise ValueError("Unsupported font '%s'." % font.name)
        if right_to_left:
            text = self.clean_rtl(text)
            text = "".join(reversed(text))
            right_to_left = 0
        %}
        %pythonappend append %{
        self.last_point = Point(val[-2:]) * self.ctm
        self.text_rect = self._bbox * self.ctm
        val = self.text_rect, self.last_point
        if font.flags["mono"] == 1:
            self.used_fonts.add(font)
        %}
        PyObject *
        append(PyObject *pos, char *text, struct Font *font=NULL, float fontsize=11, char *language=NULL, int right_to_left=0, int small_caps=0)
        {
            fz_text_language lang = fz_text_language_from_string(language);
            fz_point p = JM_point_from_py(pos);
            fz_matrix trm = fz_make_matrix(fontsize, 0, 0, fontsize, p.x, p.y);
            int markup_dir = 0, wmode = 0;
            fz_try(gctx) {
                if (small_caps == 0) {
                    trm = fz_show_string(gctx, (fz_text *) $self, (fz_font *) font,
                                trm, text, wmode, right_to_left, markup_dir, lang);
                } else {
                    trm = JM_show_string_cs(gctx, (fz_text *) $self, (fz_font *) font,
                                trm, text, wmode, right_to_left, markup_dir, lang);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_py_from_matrix(trm);
        }

        %pythoncode %{
        def appendv(self, pos, text, font=None, fontsize=11,
            language=None, small_caps=False):
            """Append text in vertical write mode."""
            lheight = fontsize * 1.2
            for c in text:
                self.append(pos, c, font=font, fontsize=fontsize,
                    language=language, small_caps=small_caps)
                pos.y += lheight
            return self.text_rect, self.last_point


        def clean_rtl(self, text):
            """Revert the sequence of Latin text parts.

            Text with right-to-left writing direction (Arabic, Hebrew) often
            contains Latin parts, which are written in left-to-right: numbers, names,
            etc. For output as PDF text we need *everything* in right-to-left.
            E.g. an input like "<arabic> ABCDE FG HIJ <arabic> KL <arabic>" will be
            converted to "<arabic> JIH GF EDCBA <arabic> LK <arabic>". The Arabic
            parts remain untouched.

            Args:
                text: str
            Returns:
                Massaged string.
            """
            if not text:
                return text
            # split into words at space boundaries
            words = text.split(" ")
            idx = []
            for i in range(len(words)):
                w = words[i]
                # revert character sequence for Latin only words
                if not (len(w) < 2 or max([ord(c) for c in w]) > 255):
                    words[i] = "".join(reversed(w))
                    idx.append(i)  # stored index of Latin word

            # adjacent Latin words must revert their sequence, too
            idx2 = []  # store indices of adjacent Latin words
            for i in range(len(idx)):
                if idx2 == []:  # empty yet?
                    idx2.append(idx[i]) # store Latin word number

                elif idx[i] > idx2[-1] + 1:  # large gap to last?
                    if len(idx2) > 1:  # at least two consecutives?
                        words[idx2[0] : idx2[-1] + 1] = reversed(
                            words[idx2[0] : idx2[-1] + 1]
                        )  # revert their sequence
                    idx2 = [idx[i]]  # re-initialize

                elif idx[i] == idx2[-1] + 1:  # new adjacent Latin word
                    idx2.append(idx[i])

            text = " ".join(words)
            return text
        %}


        %pythoncode %{@property%}
        %pythonappend _bbox%{val = Rect(val)%}
        PyObject *_bbox()
        {
            return JM_py_from_rect(fz_bound_text(gctx, (fz_text *) $self, NULL, fz_identity));
        }

        FITZEXCEPTION(write_text, !result)
        %pythonprepend write_text%{
        """Write the text to a PDF page having the TextWriter's page size.

        Args:
            page: a PDF page having same size.
            color: override text color.
            opacity: override transparency.
            overlay: put in foreground or background.
            morph: tuple(Point, Matrix), apply a matrix with a fixpoint.
            matrix: Matrix to be used instead of 'morph' argument.
            render_mode: (int) PDF render mode operator 'Tr'.
        """

        CheckParent(page)
        if abs(self.rect - page.rect) > 1e-3:
            raise ValueError("incompatible page rect")
        if morph != None:
            if (type(morph) not in (tuple, list)
                or type(morph[0]) is not Point
                or type(morph[1]) is not Matrix
                ):
                raise ValueError("morph must be (Point, Matrix) or None")
        if matrix != None and morph != None:
            raise ValueError("only one of matrix, morph is allowed")
        if getattr(opacity, "__float__", None) is None or opacity == -1:
            opacity = self.opacity
        if color is None:
            color = self.color
        %}

        %pythonappend write_text%{
        max_nums = val[0]
        content = val[1]
        max_alp, max_font = max_nums
        old_cont_lines = content.splitlines()

        optcont = page._get_optional_content(oc)
        if optcont != None:
            bdc = "/OC /%s BDC" % optcont
            emc = "EMC"
        else:
            bdc = emc = ""

        new_cont_lines = ["q"]
        if bdc:
            new_cont_lines.append(bdc)

        cb = page.cropbox_position
        if page.rotation in (90, 270):
            delta = page.rect.height - page.rect.width
        else:
            delta = 0
        mb = page.mediabox
        if bool(cb) or mb.y0 != 0 or delta != 0:
            new_cont_lines.append("1 0 0 1 %g %g cm" % (cb.x, cb.y + mb.y0 - delta))

        if morph:
            p = morph[0] * self.ictm
            delta = Matrix(1, 1).pretranslate(p.x, p.y)
            matrix = ~delta * morph[1] * delta
        if morph or matrix:
            new_cont_lines.append("%g %g %g %g %g %g cm" % JM_TUPLE(matrix))

        for line in old_cont_lines:
            if line.endswith(" cm"):
                continue
            if line == "BT":
                new_cont_lines.append(line)
                new_cont_lines.append("%i Tr" % render_mode)
                continue
            if line.endswith(" gs"):
                alp = int(line.split()[0][4:]) + max_alp
                line = "/Alp%i gs" % alp
            elif line.endswith(" Tf"):
                temp = line.split()
                fsize = float(temp[1])
                if render_mode != 0:
                    w = fsize * 0.05
                else:
                    w = 1
                new_cont_lines.append("%g w" % w)
                font = int(temp[0][2:]) + max_font
                line = " ".join(["/F%i" % font] + temp[1:])
            elif line.endswith(" rg"):
                new_cont_lines.append(line.replace("rg", "RG"))
            elif line.endswith(" g"):
                new_cont_lines.append(line.replace(" g", " G"))
            elif line.endswith(" k"):
                new_cont_lines.append(line.replace(" k", " K"))
            new_cont_lines.append(line)
        if emc:
            new_cont_lines.append(emc)
        new_cont_lines.append("Q\n")
        content = "\n".join(new_cont_lines).encode("utf-8")
        TOOLS._insert_contents(page, content, overlay=overlay)
        val = None
        for font in self.used_fonts:
            repair_mono_font(page, font)
        %}
        PyObject *write_text(struct Page *page, PyObject *color=NULL, float opacity=-1, int overlay=1,
                    PyObject *morph=NULL, PyObject *matrix=NULL, int render_mode=0, int oc=0)
        {
            pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page);
            pdf_obj *resources = NULL;
            fz_buffer *contents = NULL;
            fz_device *dev = NULL;
            PyObject *result = NULL, *max_nums, *cont_string;
            float alpha = 1;
            if (opacity >= 0 && opacity < 1)
                alpha = opacity;
            fz_colorspace *colorspace;
            int ncol = 1;
            float dev_color[4] = {0, 0, 0, 0};
            if (EXISTS(color)) {
                JM_color_FromSequence(color, &ncol, dev_color);
            }
            switch(ncol) {
                case 3: colorspace = fz_device_rgb(gctx); break;
                case 4: colorspace = fz_device_cmyk(gctx); break;
                default: colorspace = fz_device_gray(gctx); break;
            }

            fz_var(contents);
            fz_var(resources);
            fz_var(dev);
            fz_try(gctx) {
                ASSERT_PDF(pdfpage);
                resources = pdf_new_dict(gctx, pdfpage->doc, 5);
                contents = fz_new_buffer(gctx, 1024);
                dev = pdf_new_pdf_device(gctx, pdfpage->doc, fz_identity,
                                         resources, contents);
                fz_fill_text(gctx, dev, (fz_text *) $self, fz_identity,
                    colorspace, dev_color, alpha, fz_default_color_params);
                fz_close_device(gctx, dev);

                // copy generated resources into the one of the page
                max_nums = JM_merge_resources(gctx, pdfpage, resources);
                cont_string = JM_EscapeStrFromBuffer(gctx, contents);
                result = Py_BuildValue("OO", max_nums, cont_string);
                Py_DECREF(cont_string);
                Py_DECREF(max_nums);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, contents);
                pdf_drop_obj(gctx, resources);
                fz_drop_device(gctx, dev);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return result;
        }
        %pythoncode %{
        def __del__(self):
            if not type(self) is TextWriter:
                return
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)
        %}
    }
};


//------------------------------------------------------------------------
// Font
//------------------------------------------------------------------------
struct Font
{
    %extend
    {
        ~Font()
        {
            DEBUGMSG1("Font");
            fz_font *this_font = (fz_font *) $self;
            fz_drop_font(gctx, this_font);
            DEBUGMSG2;
        }

        FITZEXCEPTION(Font, !result)
        %pythonprepend Font %{
        if fontbuffer:
            if hasattr(fontbuffer, "getvalue"):
                fontbuffer = fontbuffer.getvalue()
            elif isinstance(fontbuffer, bytearray):
                fontbuffer = bytes(fontbuffer)
            if not isinstance(fontbuffer, bytes):
                raise ValueError("bad type: 'fontbuffer'")

        if isinstance(fontname, str):
            fname_lower = fontname.lower()
            if "/" in fname_lower or "\\" in fname_lower or "." in fname_lower:
                print("Warning: did you mean a fontfile?")

            if fname_lower in ("cjk", "china-t", "china-ts"):
                ordering = 0
            elif fname_lower.startswith("china-s"):
                ordering = 1
            elif fname_lower.startswith("korea"):
                ordering = 3
            elif fname_lower.startswith("japan"):
                ordering = 2
            elif fname_lower in fitz_fontdescriptors.keys():
                import pymupdf_fonts  # optional fonts
                fontbuffer = pymupdf_fonts.myfont(fname_lower)  # make a copy
                fontname = None  # ensure using fontbuffer only
                del pymupdf_fonts  # remove package again

            elif ordering < 0:
                fontname = Base14_fontdict.get(fontname, fontname)
        %}
        %pythonappend Font %{self.thisown = True%}
        Font(char *fontname=NULL, char *fontfile=NULL,
             PyObject *fontbuffer=NULL, int script=0,
             char *language=NULL, int ordering=-1, int is_bold=0,
             int is_italic=0, int is_serif=0, int embed=1)
        {
            fz_font *font = NULL;
            fz_try(gctx) {
                fz_text_language lang = fz_text_language_from_string(language);
                font = JM_get_font(gctx, fontname, fontfile,
                           fontbuffer, script, lang, ordering,
                           is_bold, is_italic, is_serif, embed);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Font *) font;
        }


        %pythonprepend glyph_advance
        %{"""Return the glyph width of a unicode (font size 1)."""%}
        PyObject *glyph_advance(int chr, char *language=NULL, int script=0, int wmode=0, int small_caps=0)
        {
            fz_font *font, *thisfont = (fz_font *) $self;
            int gid;
            fz_text_language lang = fz_text_language_from_string(language);
            if (small_caps) {
                gid = fz_encode_character_sc(gctx, thisfont, chr);
                if (gid >= 0) font = thisfont;
            } else {
                gid = fz_encode_character_with_fallback(gctx, thisfont, chr, script, lang, &font);
            }
            return PyFloat_FromDouble((double) fz_advance_glyph(gctx, font, gid, wmode));
        }
        

        FITZEXCEPTION(text_length, !result)
        %pythonprepend text_length
        %{"""Return length of unicode 'text' under a fontsize."""%}
        PyObject *text_length(PyObject *text, double fontsize=11, char *language=NULL, int script=0, int wmode=0, int small_caps=0)
        {
            fz_font *font=NULL, *thisfont = (fz_font *) $self;
            fz_text_language lang = fz_text_language_from_string(language);
            double rc = 0;
            int gid;
            fz_try(gctx) {
                if (!PyUnicode_Check(text) || PyUnicode_READY(text) != 0) {
                    RAISEPY(gctx, MSG_BAD_TEXT, PyExc_TypeError);
                }
                Py_ssize_t i, len = PyUnicode_GET_LENGTH(text);
                int kind = PyUnicode_KIND(text);
                void *data = PyUnicode_DATA(text);
                for (i = 0; i < len; i++) {
                    int c = PyUnicode_READ(kind, data, i);
                    if (small_caps) {
                        gid = fz_encode_character_sc(gctx, thisfont, c);
                        if (gid >= 0) font = thisfont;
                    } else {
                        gid = fz_encode_character_with_fallback(gctx,thisfont, c, script, lang, &font);
                    }
                    rc += (double) fz_advance_glyph(gctx, font, gid, wmode);
                }
            }
            fz_catch(gctx) {
                PyErr_Clear();
                return NULL;
            }
            rc *= fontsize;
            return PyFloat_FromDouble(rc);
        }


        FITZEXCEPTION(char_lengths, !result)
        %pythonprepend char_lengths
        %{"""Return tuple of char lengths of unicode 'text' under a fontsize."""%}
        PyObject *char_lengths(PyObject *text, double fontsize=11, char *language=NULL, int script=0, int wmode=0, int small_caps=0)
        {
            fz_font *font, *thisfont = (fz_font *) $self;
            fz_text_language lang = fz_text_language_from_string(language);
            PyObject *rc = NULL;
            int gid;
            fz_try(gctx) {
                if (!PyUnicode_Check(text) || PyUnicode_READY(text) != 0) {
                    RAISEPY(gctx, MSG_BAD_TEXT, PyExc_TypeError);
                }
                Py_ssize_t i, len = PyUnicode_GET_LENGTH(text);
                int kind = PyUnicode_KIND(text);
                void *data = PyUnicode_DATA(text);
                rc = PyTuple_New(len);
                for (i = 0; i < len; i++) {
                    int c = PyUnicode_READ(kind, data, i);
                    if (small_caps) {
                        gid = fz_encode_character_sc(gctx, thisfont, c);
                        if (gid >= 0) font = thisfont;
                    } else {
                        gid = fz_encode_character_with_fallback(gctx,thisfont, c, script, lang, &font);
                    }
                    PyTuple_SET_ITEM(rc, i,
                        PyFloat_FromDouble(fontsize * (double) fz_advance_glyph(gctx, font, gid, wmode)));
                }
            }
            fz_catch(gctx) {
                PyErr_Clear();
                Py_CLEAR(rc);
                return NULL;
            }
            return rc;
        }


        %pythonprepend glyph_bbox
        %{"""Return the glyph bbox of a unicode (font size 1)."""%}
        %pythonappend glyph_bbox %{val = Rect(val)%}
        PyObject *glyph_bbox(int chr, char *language=NULL, int script=0, int small_caps=0)
        {
            fz_font *font, *thisfont = (fz_font *) $self;
            int gid;
            fz_text_language lang = fz_text_language_from_string(language);
            if (small_caps) {
                gid = fz_encode_character_sc(gctx, thisfont, chr);
                if (gid >= 0) font = thisfont;
            } else {
                gid = fz_encode_character_with_fallback(gctx, thisfont, chr, script, lang, &font);
            }
            return JM_py_from_rect(fz_bound_glyph(gctx, font, gid, fz_identity));
        }

        %pythonprepend has_glyph
        %{"""Check whether font has a glyph for this unicode."""%}
        PyObject *has_glyph(int chr, char *language=NULL, int script=0, int fallback=0, int small_caps=0)
        {
            fz_font *font, *thisfont = (fz_font *) $self;
            fz_text_language lang;
            int gid = 0;
            if (fallback) {
                lang = fz_text_language_from_string(language);
                gid = fz_encode_character_with_fallback(gctx, (fz_font *) $self, chr, script, lang, &font);
            } else {
                if (!small_caps) {
                    gid = fz_encode_character(gctx, thisfont, chr);
                } else {
                    gid = fz_encode_character_sc(gctx, thisfont, chr);
                }
            }
            return Py_BuildValue("i", gid);
        }


        %pythoncode %{
        def valid_codepoints(self):
            from array import array
            gc = self.glyph_count
            cp = array("l", (0,) * gc)
            arr = cp.buffer_info()
            self._valid_unicodes(arr)
            return array("l", sorted(set(cp))[1:])
        %}
        void _valid_unicodes(PyObject *arr)
        {
            fz_font *font = (fz_font *) $self;
            PyObject *temp = PySequence_ITEM(arr, 0);
            void *ptr = PyLong_AsVoidPtr(temp);
            JM_valid_chars(gctx, font, ptr);
            Py_DECREF(temp);
        }


        %pythoncode %{@property%}
        PyObject *flags()
        {
            fz_font_flags_t *f = fz_font_flags((fz_font *) $self);
            if (!f) Py_RETURN_NONE;
            return Py_BuildValue(
                "{s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N"
                #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22
                    ",s:N,s:N"
                #endif
                "}",
                "mono", JM_BOOL(f->is_mono),
                "serif", JM_BOOL(f->is_serif),
                "bold", JM_BOOL(f->is_bold),
                "italic", JM_BOOL(f->is_italic),
                "substitute", JM_BOOL(f->ft_substitute),
                "stretch", JM_BOOL(f->ft_stretch),
                "fake-bold", JM_BOOL(f->fake_bold),
                "fake-italic", JM_BOOL(f->fake_italic),
                "opentype", JM_BOOL(f->has_opentype),
                "invalid-bbox", JM_BOOL(f->invalid_bbox),
                "cjk", JM_BOOL(f->cjk),
                "cjk-lang", (f->cjk ? PyLong_FromUnsignedLong((unsigned long) f->cjk_lang) : Py_BuildValue("s",NULL))
                #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22
                ,
                "embed", JM_BOOL(f->embed),
                "never-embed", JM_BOOL(f->never_embed)
                #endif
            );

        }


        %pythoncode %{@property%}
        PyObject *is_bold()
        {
            fz_font *font = (fz_font *) $self;
            if (fz_font_is_bold(gctx,font)) {
                Py_RETURN_TRUE;
            }
            Py_RETURN_FALSE;
        }


        %pythoncode %{@property%}
        PyObject *is_serif()
        {
            fz_font *font = (fz_font *) $self;
            if (fz_font_is_serif(gctx,font)) {
                Py_RETURN_TRUE;
            }
            Py_RETURN_FALSE;
        }


        %pythoncode %{@property%}
        PyObject *is_italic()
        {
            fz_font *font = (fz_font *) $self;
            if (fz_font_is_italic(gctx,font)) {
                Py_RETURN_TRUE;
            }
            Py_RETURN_FALSE;
        }


        %pythoncode %{@property%}
        PyObject *is_monospaced()
        {
            fz_font *font = (fz_font *) $self;
            if (fz_font_is_monospaced(gctx,font)) {
                Py_RETURN_TRUE;
            }
            Py_RETURN_FALSE;
        }


        /* temporarily disabled
        * PyObject *is_writable()
        * {
        *     fz_font *font = (fz_font *) $self;
        *     if (fz_font_t3_procs(gctx, font) ||
        *         fz_font_flags(font)->ft_substitute ||
        *         !pdf_font_writing_supported(font)) {
        *         Py_RETURN_FALSE;
        *     }
        *     Py_RETURN_TRUE;
        * }
        */

        %pythoncode %{@property%}
        PyObject *name()
        {
            return JM_UnicodeFromStr(fz_font_name(gctx, (fz_font *) $self));
        }

        %pythoncode %{@property%}
        int glyph_count()
        {
            fz_font *this_font = (fz_font *) $self;
            return this_font->glyph_count;
        }

        %pythoncode %{@property%}
        PyObject *buffer()
        {
            fz_font *this_font = (fz_font *) $self;
            unsigned char *data = NULL;
            size_t len = fz_buffer_storage(gctx, this_font->buffer, &data);
            return JM_BinFromCharSize(data, len);
        }

        %pythoncode %{@property%}
        %pythonappend bbox%{val = Rect(val)%}
        PyObject *bbox()
        {
            fz_font *this_font = (fz_font *) $self;
            return JM_py_from_rect(fz_font_bbox(gctx, this_font));
        }

        %pythoncode %{@property%}
        %pythonprepend ascender
        %{"""Return the glyph ascender value."""%}
        float ascender()
        {
            return fz_font_ascender(gctx, (fz_font *) $self);
        }


        %pythoncode %{@property%}
        %pythonprepend descender
        %{"""Return the glyph descender value."""%}
        float descender()
        {
            return fz_font_descender(gctx, (fz_font *) $self);
        }


        %pythoncode %{

            @property
            def is_writable(self):
                return True

            def glyph_name_to_unicode(self, name):
                """Return the unicode for a glyph name."""
                return glyph_name_to_unicode(name)

            def unicode_to_glyph_name(self, ch):
                """Return the glyph name for a unicode."""
                return unicode_to_glyph_name(ch)

            def __repr__(self):
                return "Font('%s')" % self.name

            def __del__(self):
                if not type(self) is Font:
                    return
                if getattr(self, "thisown", False):
                    self.__swig_destroy__(self)
        %}
    }
};


//------------------------------------------------------------------------
// DocumentWriter
//------------------------------------------------------------------------

struct DocumentWriter
{
    %extend
    {
        ~DocumentWriter()
        {
            // need this structure to free any fz_output the writer may have
            typedef struct { // copied from pdf_write.c
                fz_document_writer super;
                pdf_document *pdf;
                pdf_write_options opts;
                fz_output *out;
                fz_rect mediabox;
                pdf_obj *resources;
                fz_buffer *contents;
            } pdf_writer;

            fz_document_writer *writer_fz = (fz_document_writer *) $self;
            fz_output *out = NULL;
            pdf_writer *writer_pdf = (pdf_writer *) writer_fz;
            if (writer_pdf) {
                out = writer_pdf->out;
                if (out) {
                    DEBUGMSG1("Output of DocumentWriter");
                    fz_drop_output(gctx, out);
                    writer_pdf->out = NULL;
                    DEBUGMSG2;
                }
            }
            DEBUGMSG1("DocumentWriter");
            fz_drop_document_writer( gctx, writer_fz);
            DEBUGMSG2;
        }
        
        FITZEXCEPTION(DocumentWriter, !result)
        %pythonprepend DocumentWriter
        %{
            if type(path) is str:
                pass
            elif hasattr(path, "absolute"):
                path = str(path)
            elif hasattr(path, "name"):
                path = path.name
            if options==None:
                options=""
        %}
        %pythonappend DocumentWriter
        %{
        %}
        DocumentWriter( PyObject* path, const char* options=NULL)
        {
            fz_output *out = NULL;
            fz_document_writer* ret=NULL;
            fz_try(gctx) {
            if (PyUnicode_Check(path)) {
                ret = fz_new_pdf_writer( gctx, PyUnicode_AsUTF8(path), options);
            } else {
                out = JM_new_output_fileptr(gctx, path);
                ret = fz_new_pdf_writer_with_output(gctx, out, options);
            }
            }

            fz_catch(gctx) {
                return NULL;
            }
            return (struct DocumentWriter*) ret;
        }
        
        struct DeviceWrapper* begin_page( PyObject* mediabox)
        {
            fz_rect mediabox2 = JM_rect_from_py(mediabox);
            fz_device* device = fz_begin_page( gctx, (fz_document_writer*) $self, mediabox2);
            struct DeviceWrapper* device_wrapper
                = (struct DeviceWrapper*) calloc(1, sizeof(struct DeviceWrapper))
                ;
            device_wrapper->device = device;
            device_wrapper->list = NULL;
            return device_wrapper;
        }
        
        void end_page()
        {
            fz_end_page( gctx, (fz_document_writer*) $self);
        }
        
        void close()
        {
            fz_document_writer *writer = (fz_document_writer*) $self;
            fz_close_document_writer( gctx, writer);
        }
        %pythoncode
        %{
            def __del__(self):
                if not type(self) is DocumentWriter:
                    return
                if getattr(self, "thisown", False):
                    self.__swig_destroy__(self)

            def __enter__(self):
                return self

            def __exit__(self, *args):
                self.close()
        %}
    }
};

//------------------------------------------------------------------------
// Archive
//------------------------------------------------------------------------
struct Archive
{
    %extend
    {
        ~Archive()
        {
            DEBUGMSG1("Archive");
            fz_drop_archive( gctx, (fz_archive *) $self);
            DEBUGMSG2;
        }
        FITZEXCEPTION(Archive, !result)
        %pythonprepend Archive %{
        self._subarchives = []
        %}
        %pythonappend Archive %{
        self.thisown = True
        if args != ():
            self.add(*args)
        %}

        //---------------------------------------
        // new empty archive
        //---------------------------------------
        Archive(struct Archive *a0=NULL, const char *path=NULL)
        {
            fz_archive *arch=NULL;
            fz_try(gctx) {
                arch = fz_new_multi_archive(gctx);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Archive *) arch;
        }

        Archive(PyObject *a0=NULL, const char *path=NULL)
        {
            fz_archive *arch=NULL;
            fz_try(gctx) {
                arch = fz_new_multi_archive(gctx);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Archive *) arch;
        }

        FITZEXCEPTION(has_entry, !result)
        PyObject *has_entry(const char *name)
        {
            fz_archive *arch = (fz_archive *) $self;
            int ret = 0;
            fz_try(gctx) {
                ret = fz_has_archive_entry(gctx, arch, name);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_BOOL(ret);
        }

        FITZEXCEPTION(read_entry, !result)
        PyObject *read_entry(const char *name)
        {
            fz_archive *arch = (fz_archive *) $self;
            PyObject *ret = NULL;
            fz_buffer *buff = NULL;
            fz_try(gctx) {
                buff = fz_read_archive_entry(gctx, arch, name);
                ret = JM_BinFromBuffer(gctx, buff);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buff);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return ret;
        }

        //--------------------------------------
        // add dir
        //--------------------------------------
        FITZEXCEPTION(_add_dir, !result)
        PyObject *_add_dir(const char *folder, const char *path=NULL)
        {
            fz_archive *arch = (fz_archive *) $self;
            fz_archive *sub = NULL;
            fz_try(gctx) {
                sub = fz_open_directory(gctx, folder);
                fz_mount_multi_archive(gctx, arch, sub, path);
            }
            fz_always(gctx) {
                fz_drop_archive(gctx, sub);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------
        // add archive
        //----------------------------------
        FITZEXCEPTION(_add_arch, !result)
        PyObject *_add_arch(struct Archive *subarch, const char *path=NULL)
        {
            fz_archive *arch = (fz_archive *) $self;
            fz_archive *sub = (fz_archive *) subarch;
            fz_try(gctx) {
                fz_mount_multi_archive(gctx, arch, sub, path);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------
        // add ZIP/TAR from file
        //----------------------------------
        FITZEXCEPTION(_add_ziptarfile, !result)
        PyObject *_add_ziptarfile(const char *filepath, int type, const char *path=NULL)
        {
            fz_archive *arch = (fz_archive *) $self;
            fz_archive *sub = NULL;
            fz_try(gctx) {
                if (type==1) {
                    sub = fz_open_zip_archive(gctx, filepath);
                } else {
                    sub = fz_open_tar_archive(gctx, filepath);
                }
                fz_mount_multi_archive(gctx, arch, sub, path);
            }
            fz_always(gctx) {
                fz_drop_archive(gctx, sub);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------
        // add ZIP/TAR from memory
        //----------------------------------
        FITZEXCEPTION(_add_ziptarmemory, !result)
        PyObject *_add_ziptarmemory(PyObject *memory, int type, const char *path=NULL)
        {
            fz_archive *arch = (fz_archive *) $self;
            fz_archive *sub = NULL;
            fz_stream *stream = NULL;
            fz_buffer *buff = NULL;
            fz_try(gctx) {
                buff = JM_BufferFromBytes(gctx, memory);
                stream = fz_open_buffer(gctx, buff);
                if (type==1) {
                    sub = fz_open_zip_archive_with_stream(gctx, stream);
                } else {
                    sub = fz_open_tar_archive_with_stream(gctx, stream);
                }
                fz_mount_multi_archive(gctx, arch, sub, path);
            }
            fz_always(gctx) {
                fz_drop_stream(gctx, stream);
                fz_drop_buffer(gctx, buff);
                fz_drop_archive(gctx, sub);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        //----------------------------------
        // add "tree" item
        //----------------------------------
        FITZEXCEPTION(_add_treeitem, !result)
        PyObject *_add_treeitem(PyObject *memory, const char *name, const char *path=NULL)
        {
            fz_archive *arch = (fz_archive *) $self;
            fz_archive *sub = NULL;
            fz_buffer *buff = NULL;
            int drop_sub = 0;
            fz_try(gctx) {
                buff = JM_BufferFromBytes(gctx, memory);
                sub = JM_last_tree(gctx, arch, path);
                if (!sub) {
                    sub = fz_new_tree_archive(gctx, NULL);
                    drop_sub = 1;
                }
                fz_tree_archive_add_buffer(gctx, sub, name, buff);
                if (drop_sub) {
                    fz_mount_multi_archive(gctx, arch, sub, path);
                }
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buff);
                if (drop_sub) {
                    fz_drop_archive(gctx, sub);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode %{
        def add(self, content, path=None):
            """Add a sub-archive.

            Args:
                content: content to be added. May be one of Archive, folder
                     name, file name, raw bytes (bytes, bytearray), zipfile,
                     tarfile, or a sequence of any of these types.
                path: (str) a "virtual" path name, under which the elements
                    of content can be retrieved. Use it to e.g. cope with
                    duplicate element names.
            """
            bin_ok = lambda x: isinstance(x, (bytes, bytearray, io.BytesIO))

            entries = []
            mount = None
            fmt = None

            def make_subarch():
                subarch = {"fmt": fmt, "entries": entries, "path": mount}
                if fmt != "tree" or self._subarchives == []:
                    self._subarchives.append(subarch)
                else:
                    ltree = self._subarchives[-1]
                    if ltree["fmt"] != "tree" or ltree["path"] != subarch["path"]:
                        self._subarchives.append(subarch)
                    else:
                        ltree["entries"].extend(subarch["entries"])
                        self._subarchives[-1] = ltree
                return

            if isinstance(content, zipfile.ZipFile):
                fmt = "zip"
                entries = content.namelist()
                mount = path
                filename = getattr(content, "filename", None)
                fp = getattr(content, "fp", None)
                if filename:
                    self._add_ziptarfile(filename, 1, path)
                else:
                    self._add_ziptarmemory(fp.getvalue(), 1, path)
                return make_subarch()
            
            if isinstance(content, tarfile.TarFile):
                fmt = "tar"
                entries = content.getnames()
                mount = path
                filename = getattr(content.fileobj, "name", None)
                fp = content.fileobj
                if not isinstance(fp, io.BytesIO) and not filename:
                    fp = fp.fileobj
                if filename:
                    self._add_ziptarfile(filename, 0, path)
                else:
                    self._add_ziptarmemory(fp.getvalue(), 0, path)
                return make_subarch()

            if isinstance(content, Archive):
                fmt = "multi"
                mount = path
                self._add_arch(content, path)
                return make_subarch()

            if bin_ok(content):
                if not (path and type(path) is str):
                    raise ValueError("need name for binary content")
                fmt = "tree"
                mount = None
                entries = [path]
                self._add_treeitem(content, path)
                return make_subarch()

            if hasattr(content, "name"):
                content = content.name
            elif isinstance(content, pathlib.Path):
                content = str(content)
            
            if os.path.isdir(str(content)):
                a0 = str(content)
                fmt = "dir"
                mount = path
                entries = os.listdir(a0)
                self._add_dir(a0, path)
                return make_subarch()
            
            if os.path.isfile(str(content)):
                if not (path and type(path) is str):
                    raise ValueError("need name for binary content")
                a0 = str(content)
                _ = open(a0, "rb")
                ff = _.read()
                _.close()
                fmt = "tree"
                mount = None
                entries = [path]
                self._add_treeitem(ff, path)
                return make_subarch()
            
            if type(content) is str or not getattr(content, "__getitem__", None):
                raise ValueError("bad archive content")

            #----------------------------------------
            # handling sequence types here
            #----------------------------------------

            if len(content) == 2: # covers the tree item plus path
                data, name = content
                if bin_ok(data) or os.path.isfile(str(data)):
                    if not type(name) is str:
                        raise ValueError(f"bad item name {name}")
                    mount = path
                    fmt = "tree"
                    if bin_ok(data):
                        self._add_treeitem(data, name, path=mount)
                    else:
                        _ = open(str(data), "rb")
                        ff = _.read()
                        _.close()
                        seld._add_treeitem(ff, name, path=mount)
                    entries = [name]
                    return make_subarch()

            # deal with sequence of disparate items
            for item in content:
                self.add(item, path)

        __doc__ = """Archive(dirname [, path]) - from folder
        Archive(file [, path]) - from file name or object
        Archive(data, name) - from memory item
        Archive() - empty archive
        Archive(archive [, path]) - from archive
        """

        @property
        def entry_list(self):
            """List of sub archives."""
            return self._subarchives

        def __repr__(self):
            return f"Archive, sub-archives: {len(self._subarchives)}"

        def __del__(self):
            if not type(self) is Archive:
                return
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)
        %}
    }
};
//------------------------------------------------------------------------
// Xml
//------------------------------------------------------------------------
struct Xml
{
    %extend
    {
        ~Xml()
        {
            DEBUGMSG1("Xml");
            fz_drop_xml( gctx, (fz_xml*) $self);
            DEBUGMSG2;
        }
        
        FITZEXCEPTION(Xml, !result)
        Xml(fz_xml* xml)
        {
            fz_keep_xml( gctx, xml);
            return (struct Xml*) xml;
        }

        Xml(const char *html)
        {
            fz_buffer *buff = NULL;
            fz_xml *ret = NULL;
            fz_try(gctx) {
                buff = fz_new_buffer_from_copied_data(gctx, html, strlen(html)+1);
                ret = fz_parse_xml_from_html5(gctx, buff);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, buff);
            }
            fz_catch(gctx) {
                return NULL;
            }
            fz_keep_xml(gctx, ret);
            return (struct Xml*) ret;
        }

        %pythoncode %{@property%}
        FITZEXCEPTION (root, !result)
        struct Xml* root()
        {
            fz_xml* ret = NULL;
            fz_try(gctx) {
                ret = fz_xml_root((fz_xml_doc *) $self);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Xml*) ret;
        }

        FITZEXCEPTION (bodytag, !result)
        struct Xml* bodytag()
        {
            fz_xml* ret = NULL;
            fz_try(gctx) {
                ret = fz_keep_xml( gctx, fz_dom_body( gctx, (fz_xml *) $self));
            }
            fz_catch(gctx) {
                return NULL;
            }
            return (struct Xml*) ret;
        }

        FITZEXCEPTION (append_child, !result)
        PyObject *append_child( struct Xml* child)
        {
            fz_try(gctx) {
                fz_dom_append_child( gctx, (fz_xml *) $self, (fz_xml *) child);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION (create_text_node, !result)
        struct Xml* create_text_node( const char *text)
        {
            fz_xml* ret = NULL;
            fz_try(gctx) {
                ret = fz_dom_create_text_node( gctx,(fz_xml *) $self, text);
            }
            fz_catch(gctx) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        FITZEXCEPTION (create_element, !result)
        struct Xml* create_element( const char *tag)
        {
            fz_xml* ret = NULL;
            fz_try(gctx) {
                ret = fz_dom_create_element( gctx, (fz_xml *)$self, tag);
            }
            fz_catch(gctx) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        struct Xml *find(const char *tag, const char *att, const char *match)
        {
            fz_xml* ret=NULL;
            ret = fz_dom_find( gctx, (fz_xml *)$self, tag, att, match);
            if (!ret) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        struct Xml *find_next( const char *tag, const char *att, const char *match)
        {
            fz_xml* ret=NULL;
            ret = fz_dom_find_next( gctx, (fz_xml *)$self, tag, att, match);
            if (!ret) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        %pythoncode %{@property%}
        struct Xml *next()
        {
            fz_xml* ret=NULL;
            ret = fz_dom_next( gctx, (fz_xml *)$self);
            if (!ret) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        %pythoncode %{@property%}
        struct Xml *previous()
        {
            fz_xml* ret=NULL;
            ret = fz_dom_previous( gctx, (fz_xml *)$self);
            if (!ret) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        FITZEXCEPTION (set_attribute, !result)
        PyObject *set_attribute(const char *key, const char *value)
        {
            fz_try(gctx) {
                if (strlen(key)==0) {
                    RAISEPY(gctx, "key must not be empty", PyExc_ValueError);
                }
                fz_dom_add_attribute(gctx, (fz_xml *)$self, key, value);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION (remove_attribute, !result)
        PyObject *remove_attribute(const char *key)
        {
            fz_try(gctx) {
                if (strlen(key)==0) {
                    RAISEPY(gctx, "key must not be empty", PyExc_ValueError);
                }
                fz_xml *elt = (fz_xml *)$self;
                fz_dom_remove_attribute(gctx, elt, key);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION (get_attribute_value, !result)
        PyObject *get_attribute_value(const char *key)
        {
            const char *ret=NULL;
            fz_try(gctx) {
                if (strlen(key)==0) {
                    RAISEPY(gctx, "key must not be empty", PyExc_ValueError);
                }
                fz_xml *elt = (fz_xml *)$self;
                ret=fz_dom_attribute(gctx, elt, key);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("s", ret);
        }


        FITZEXCEPTION (get_attributes, !result)
        PyObject *get_attributes()
        {
            fz_xml *this = (fz_xml *) $self;
            if (fz_xml_text(this)) { // text node has none
                Py_RETURN_NONE;
            }
            PyObject *result=PyDict_New();
            fz_try(gctx) {
                int i=0;
                const char *key=NULL;
                const char *val=NULL;
                while (1) {
                    val = fz_dom_get_attribute(gctx, this, i, &key);
                    if (!val || !key) {
                        break;
                    }
                    PyObject *temp = Py_BuildValue("s",val);
                    PyDict_SetItemString(result, key, temp);
                    Py_DECREF(temp);
                    i += 1;
                }
            }
            fz_catch(gctx) {
                Py_DECREF(result);
                return NULL;
            }
            return result;
        }


        FITZEXCEPTION (insert_before, !result)
        PyObject *insert_before(struct Xml *node)
        {
            fz_xml *existing = (fz_xml *) $self;
            fz_xml *what = (fz_xml *) node;
            fz_try(gctx)
            {
                fz_dom_insert_before(gctx, existing, what);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION (insert_after, !result)
        PyObject *insert_after(struct Xml *node)
        {
            fz_xml *existing = (fz_xml *) $self;
            fz_xml *what = (fz_xml *) node;
            fz_try(gctx)
            {
                fz_dom_insert_after(gctx, existing, what);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION (clone, !result)
        struct Xml* clone()
        {
            fz_xml* ret = NULL;
            fz_try(gctx) {
                ret = fz_dom_clone( gctx, (fz_xml *)$self);
            }
            fz_catch(gctx) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        %pythoncode %{@property%}
        struct Xml *parent()
        {
            fz_xml* ret = NULL;
            ret = fz_dom_parent( gctx, (fz_xml *)$self);
            if (!ret) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }

        %pythoncode %{@property%}
        struct Xml *first_child()
        {
            fz_xml* ret = NULL;
            fz_xml *this = (fz_xml *)$self;
            if (fz_xml_text(this)) { // a text node has no child
                return NULL;
            }
            ret = fz_dom_first_child( gctx, (fz_xml *)$self);
            if (!ret) {
                return NULL;
            }
            fz_keep_xml( gctx, ret);
            return (struct Xml*) ret;
        }


        FITZEXCEPTION (remove, !result)
        PyObject *remove()
        {
            fz_try(gctx) {
                fz_dom_remove( gctx, (fz_xml *)$self);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode %{@property%}
        PyObject *text()
        {
            return Py_BuildValue("s", fz_xml_text((fz_xml *)$self));
        }

        %pythoncode %{@property%}
        PyObject *tagname()
        {
            return Py_BuildValue("s", fz_xml_tag((fz_xml *)$self));
        }


        %pythoncode %{
        def _get_node_tree(self):
            def show_node(node, items, shift):
                while node != None:
                    if node.is_text:
                        items.append((shift, f'"{node.text}"'))
                        node = node.next
                        continue
                    items.append((shift, f"({node.tagname}"))
                    for k, v in node.get_attributes().items():
                        items.append((shift, f"={k} '{v}'"))
                    child = node.first_child
                    if child:
                        items = show_node(child, items, shift + 1)
                    items.append((shift, f"){node.tagname}"))
                    node = node.next
                return items

            shift = 0
            items = []
            items = show_node(self, items, shift)
            return items

        def debug(self):
            """Print a list of the node tree below self."""
            items = self._get_node_tree()
            for item in items:
                print("  " * item[0] + item[1].replace("\n", "\\n"))

        @property
        def is_text(self):
            """Check if this is a text node."""
            return self.text != None

        @property
        def last_child(self):
            """Return last child node."""
            child = self.first_child
            if child==None:
                return None
            while True:
                if child.next == None:
                    return child
                child = child.next

        @staticmethod
        def color_text(color):
            if type(color) is str:
                return color
            if type(color) is int:
                return f"rgb({sRGB_to_rgb(color)})"
            if type(color) in (tuple, list):
                return f"rgb{tuple(color)}"
            return color

        def add_number_list(self, start=1, numtype=None):
            """Add numbered list ("ol" tag)"""
            child = self.create_element("ol")
            if start > 1:
                child.set_attribute("start", str(start))
            if numtype != None:
                child.set_attribute("type", numtype)
            self.append_child(child)
            return child

        def add_description_list(self):
            """Add description list ("dl" tag)"""
            child = self.create_element("dl")
            self.append_child(child)
            return child

        def add_image(self, name, width=None, height=None, imgfloat=None, align=None):
            """Add image node (tag "img")."""
            child = self.create_element("img")
            if width != None:
                child.set_attribute("width", f"{width}")
            if height != None:
                child.set_attribute("height", f"{height}")
            if imgfloat != None:
                child.set_attribute("style", f"float: {imgfloat}")
            if align != None:
                child.set_attribute("align", f"{align}")
            child.set_attribute("src", f"{name}")
            self.append_child(child)
            return child

        def add_bullet_list(self):
            """Add bulleted list ("ul" tag)"""
            child = self.create_element("ul")
            self.append_child(child)
            return child

        def add_list_item(self):
            """Add item ("li" tag) under a (numbered or bulleted) list."""
            if self.tagname not in ("ol", "ul"):
                raise ValueError("cannot add list item to", self.tagname)
            child = self.create_element("li")
            self.append_child(child)
            return child

        def add_span(self):
            child = self.create_element("span")
            self.append_child(child)
            return child

        def add_paragraph(self):
            """Add "p" tag"""
            child = self.create_element("p")
            if self.tagname != "p":
                self.append_child(child)
            else:
                self.parent.append_child(child)
            return child

        def add_header(self, level=1):
            """Add header tag"""
            if level not in range(1, 7):
                raise ValueError("Header level must be in [1, 6]")
            this_tag = self.tagname
            new_tag = f"h{level}"
            child = self.create_element(new_tag)
            prev = self
            if this_tag not in ("h1", "h2", "h3", "h4", "h5", "h6", "p"):
                self.append_child(child)
                return child
            self.parent.append_child(child)
            return child

        def add_division(self):
            """Add "div" tag"""
            child = self.create_element("div")
            self.append_child(child)
            return child

        def add_horizontal_line(self):
            """Add horizontal line ("hr" tag)"""
            child = self.create_element("hr")
            self.append_child(child)
            return child

        def add_link(self, href, text=None):
            """Add a hyperlink ("a" tag)"""
            child = self.create_element("a")
            if not isinstance(text, str):
                text = href
            child.set_attribute("href", href)
            child.append_child(self.create_text_node(text)) 
            prev = self.span_bottom()
            if prev == None:
                prev = self
            prev.append_child(child)
            return self

        def add_code(self, text=None):
            """Add a "code" tag"""
            child = self.create_element("code")
            if type(text) is str:
               child.append_child(self.create_text_node(text)) 
            prev = self.span_bottom()
            if prev == None:
                prev = self
            prev.append_child(child)
            return self

        add_var = add_code
        add_samp = add_code
        add_kbd = add_code

        def add_superscript(self, text=None):
            """Add a superscript ("sup" tag)"""
            child = self.create_element("sup")
            if type(text) is str:
               child.append_child(self.create_text_node(text)) 
            prev = self.span_bottom()
            if prev == None:
                prev = self
            prev.append_child(child)
            return self

        def add_subscript(self, text=None):
            """Add a subscript ("sub" tag)"""
            child = self.create_element("sub")
            if type(text) is str:
               child.append_child(self.create_text_node(text)) 
            prev = self.span_bottom()
            if prev == None:
                prev = self
            prev.append_child(child)
            return self

        def add_codeblock(self):
            """Add monospaced lines ("pre" node)"""
            child = self.create_element("pre")
            self.append_child(child)
            return child

        def span_bottom(self):
            """Find deepest level in stacked spans."""
            parent = self
            child = self.last_child
            if child == None:
                return None
            while child.is_text:
                child = child.previous
                if child == None:
                    break
            if child == None or child.tagname != "span":
                return None

            while True:
                if child == None:
                    return parent
                if child.tagname in ("a", "sub","sup","body") or child.is_text:
                    child = child.next
                    continue
                if child.tagname == "span":
                    parent = child
                    child = child.first_child
                else:
                    return parent

        def append_styled_span(self, style):
            span = self.create_element("span")
            span.add_style(style)
            prev = self.span_bottom()
            if prev == None:
                prev = self
            prev.append_child(span)
            return prev

        def set_margins(self, val):
            """Set margin values via CSS style"""
            text = "margins: %s" % val
            self.append_styled_span(text)
            return self

        def set_font(self, font):
            """Set font-family name via CSS style"""
            text = "font-family: %s" % font
            self.append_styled_span(text)
            return self

        def set_color(self, color):
            """Set text color via CSS style"""
            text = f"color: %s" % self.color_text(color)
            self.append_styled_span(text)
            return self

        def set_columns(self, cols):
            """Set number of text columns via CSS style"""
            text = f"columns: {cols}"
            self.append_styled_span(text)
            return self

        def set_bgcolor(self, color):
            """Set background color via CSS style"""
            text = f"background-color: %s" % self.color_text(color)
            self.add_style(text)  # does not work on span level
            return self

        def set_opacity(self, opacity):
            """Set opacity via CSS style"""
            text = f"opacity: {opacity}"
            self.append_styled_span(text)
            return self

        def set_align(self, align):
            """Set text alignment via CSS style"""
            text = "text-align: %s"
            if isinstance( align, str):
                t = align
            elif align == TEXT_ALIGN_LEFT:
                t = "left"
            elif align == TEXT_ALIGN_CENTER:
                t = "center"
            elif align == TEXT_ALIGN_RIGHT:
                t = "right"
            elif align == TEXT_ALIGN_JUSTIFY:
                t = "justify"
            else:
                raise ValueError(f"Unrecognised align={align}")
            text = text % t
            self.add_style(text)
            return self

        def set_underline(self, val="underline"):
            text = "text-decoration: %s" % val
            self.append_styled_span(text)
            return self

        def set_pagebreak_before(self):
            """Insert a page break before this node."""
            text = "page-break-before: always"
            self.add_style(text)
            return self

        def set_pagebreak_after(self):
            """Insert a page break after this node."""
            text = "page-break-after: always"
            self.add_style(text)
            return self

        def set_fontsize(self, fontsize):
            """Set font size name via CSS style"""
            if type(fontsize) is str:
                px=""
            else:
                px="px"
            text = f"font-size: {fontsize}{px}"
            self.append_styled_span(text)
            return self

        def set_lineheight(self, lineheight):
            """Set line height name via CSS style - block-level only."""
            text = f"line-height: {lineheight}"
            self.add_style(text)
            return self

        def set_leading(self, leading):
            """Set inter-line spacing value via CSS style - block-level only."""
            text = f"-mupdf-leading: {leading}"
            self.add_style(text)
            return self

        def set_word_spacing(self, spacing):
            """Set inter-word spacing value via CSS style"""
            text = f"word-spacing: {spacing}"
            self.append_styled_span(text)
            return self

        def set_letter_spacing(self, spacing):
            """Set inter-letter spacing value via CSS style"""
            text = f"letter-spacing: {spacing}"
            self.append_styled_span(text)
            return self

        def set_text_indent(self, indent):
            """Set text indentation name via CSS style - block-level only."""
            text = f"text-indent: {indent}"
            self.add_style(text)
            return self

        def set_bold(self, val=True):
            """Set bold on / off via CSS style"""
            if val:
                val="bold"
            else:
                val="normal"
            text = "font-weight: %s" % val
            self.append_styled_span(text)
            return self

        def set_italic(self, val=True):
            """Set italic on / off via CSS style"""
            if val:
                val="italic"
            else:
                val="normal"
            text = "font-style: %s" % val
            self.append_styled_span(text)
            return self

        def set_properties(
            self,
            align=None,
            bgcolor=None,
            bold=None,
            color=None,
            columns=None,
            font=None,
            fontsize=None,
            indent=None,
            italic=None,
            leading=None,
            letter_spacing=None,
            lineheight=None,
            margins=None,
            pagebreak_after=None,
            pagebreak_before=None,
            word_spacing=None,
            unqid=None,
            cls=None,
        ):
            """Set any or all properties of a node.
            
            To be used for existing nodes preferrably.
            """
            root = self.root
            temp = root.add_division()
            if align is not None:
                temp.set_align(align)
            if bgcolor is not None:
                temp.set_bgcolor(bgcolor)
            if bold is not None:
                temp.set_bold(bold)
            if color is not None:
                temp.set_color(color)
            if columns is not None:
                temp.set_columns(columns)
            if font is not None:
                temp.set_font(font)
            if fontsize is not None:
                temp.set_fontsize(fontsize)
            if indent is not None:
                temp.set_text_indent(indent)
            if italic is not None:
                temp.set_italic(italic)
            if leading is not None:
                temp.set_leading(leading)
            if letter_spacing is not None:
                temp.set_letter_spacing(letter_spacing)
            if lineheight is not None:
                temp.set_lineheight(lineheight)
            if margins is not None:
                temp.set_margins(margins)
            if pagebreak_after is not None:
                temp.set_pagebreak_after()
            if pagebreak_before is not None:
                temp.set_pagebreak_before()
            if word_spacing is not None:
                temp.set_word_spacing(word_spacing)
            if unqid is not None:
                self.set_id(unqid)
            if cls is not None:
                self.add_class(cls)

            styles = []
            top_style = temp.get_attribute_value("style")
            if top_style is not None:
                styles.append(top_style)
            child = temp.first_child
            while child:
                styles.append(child.get_attribute_value("style"))
                child = child.first_child
            self.set_attribute("style", ";".join(styles))
            temp.remove()
            return self

        def set_id(self, unique):
            """Set a unique id."""
            # check uniqueness
            tagname = self.tagname
            root = self.root
            if root.find(None, "id", unique):
                raise ValueError(f"id '{unique}' already exists")
            self.set_attribute("id", unique)
            return self

        def add_text(self, text):
            """Add text. Line breaks are honored."""
            lines = text.splitlines()
            line_count = len(lines)
            prev = self.span_bottom()
            if prev == None:
                prev = self

            for i, line in enumerate(lines):
                prev.append_child(self.create_text_node(line))
                if i < line_count - 1:
                    prev.append_child(self.create_element("br"))
            return self

        def add_style(self, text):
            """Set some style via CSS style. Replaces complete style spec."""
            style = self.get_attribute_value("style")
            if style != None and text in style:
                return self
            self.remove_attribute("style")
            if style == None:
                style = text
            else:
                style += ";" + text
            self.set_attribute("style", style)
            return self

        def add_class(self, text):
            """Set some class via CSS. Replaces complete class spec."""
            cls = self.get_attribute_value("class")
            if cls != None and text in cls:
                return self
            self.remove_attribute("class")
            if cls == None:
                cls = text
            else:
                cls += " " + text
            self.set_attribute("class", cls)
            return self

        def insert_text(self, text):
            lines = text.splitlines()
            line_count = len(lines)
            for i, line in enumerate(lines):
                self.append_child(self.create_text_node(line))
                if i < line_count - 1:
                    self.append_child(self.create_element("br"))
            return self

        def __enter__(self):
            return self

        def __exit__(self, *args):
            pass

        def __del__(self):
            if not type(self) is Xml:
                return
            if getattr(self, "thisown", False):
                self.__swig_destroy__(self)
        %}
    }
};

//------------------------------------------------------------------------
// Story
//------------------------------------------------------------------------
struct Story
{
    %extend
    {
        ~Story()
        {
            DEBUGMSG1("Story");
            fz_story *this_story = (fz_story *) $self;
            fz_drop_story(gctx, this_story);
            DEBUGMSG2;
        }

        FITZEXCEPTION(Story, !result)
        %pythonprepend Story %{
        if archive != None and isinstance(archive, Archive) == False:
            archive = Archive(archive)
        %}
        Story(const char* html=NULL, const char *user_css=NULL, double em=12, struct Archive *archive=NULL)
        {
            fz_story* story = NULL;
            fz_buffer *buffer = NULL;
            fz_archive* arch = NULL;
            fz_var(story);
            fz_var(buffer);
            const char *html2="";
            if (html) {
                html2=html;
            }

            fz_try(gctx)
            {
                buffer = fz_new_buffer_from_copied_data(gctx, html2, strlen(html2)+1);
                if (archive) {
                    arch = (fz_archive *) archive;
                }
                story = fz_new_story(gctx, buffer, user_css, em, arch);
            }
            fz_always(gctx)
            {
                fz_drop_buffer(gctx, buffer);
            }
            fz_catch(gctx)
            {
                return NULL;
            }
            struct Story* ret = (struct Story *) story;
            return ret;
        }
        
        FITZEXCEPTION(reset, !result)
        PyObject* reset()
        {
            fz_try(gctx)
            {
                fz_reset_story(gctx, (fz_story *)$self);
            }
            fz_catch(gctx)
            {
                return NULL;
            }
            Py_RETURN_NONE;
        }
        
        FITZEXCEPTION(place, !result)
        PyObject* place( PyObject* where)
        {
            PyObject* ret = NULL;
            fz_try(gctx)
            {
                fz_rect where2 = JM_rect_from_py(where);
                fz_rect filled;
                int more = fz_place_story( gctx, (fz_story*) $self, where2, &filled);
                ret = PyTuple_New(2);
                PyTuple_SET_ITEM( ret, 0, Py_BuildValue( "i", more));
                PyTuple_SET_ITEM( ret, 1, JM_py_from_rect( filled));
            }
            fz_catch(gctx)
            {
                return NULL;
            }
            return ret;
        }

        FITZEXCEPTION(draw, !result)
        PyObject* draw( struct DeviceWrapper* device, PyObject* matrix=NULL)
        {
            fz_try(gctx)
            {
                fz_matrix ctm2 = JM_matrix_from_py( matrix);
                fz_device *dev = (device) ? device->device : NULL;
                fz_draw_story( gctx, (fz_story*) $self, dev, ctm2);
            }
            fz_catch(gctx)
            {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        FITZEXCEPTION(document, !result)
        struct Xml* document()
        {
            fz_xml* dom=NULL;
            fz_try(gctx) {
                dom = fz_story_document( gctx, (fz_story*) $self);
            }
            fz_catch(gctx) {
                return NULL;
            }
            fz_keep_xml( gctx, dom);
            return (struct Xml*) dom;
        }

        FITZEXCEPTION(element_positions, !result)
        %pythonprepend element_positions %{
        """Trigger a callback function to record where items have been placed.
        
        Args:
            function: a function accepting exactly one argument.
            args: an optional dictionary for passing additional data.
        """
        if type(args) is dict:
            for k in args.keys():
                if not (type(k) is str and k.isidentifier()):
                    raise ValueError(f"invalid key '{k}'")
        else:
            args = {}
        if not callable(function) or function.__code__.co_argcount != 1:
            raise ValueError("callback 'function' must be a callable with exactly one argument")
        %}
        PyObject* element_positions(PyObject *function, PyObject *args)
        {
            PyObject *callarg=NULL;
            fz_try(gctx) {
                callarg = Py_BuildValue("OO", function, args);
                fz_story_positions(gctx, (fz_story *) $self, Story_Callback, callarg);
            }
            fz_always(gctx) {
                Py_CLEAR(callarg);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        %pythoncode
        %{
            def write(self, writer, rectfn, positionfn=None, pagefn=None):
                dev = None
                page_num = 0
                rect_num = 0
                filled = Rect(0, 0, 0, 0)
                while 1:
                    mediabox, rect, ctm = rectfn(rect_num, filled)
                    rect_num += 1
                    if mediabox:
                        # new page.
                        page_num += 1
                    more, filled = self.place( rect)
                    #print(f"write(): positionfn={positionfn}")
                    if positionfn:
                        def positionfn2(position):
                            # We add a `.page_num` member to the
                            # `ElementPosition` instance.
                            position.page_num = page_num
                            #print(f"write(): position={position}")
                            positionfn(position)
                        self.element_positions(positionfn2, {})
                    if writer:
                        if mediabox:
                            # new page.
                            if dev:
                                if pagefn:
                                    pagefn(page_num, medibox, dev, 1)
                                writer.end_page()
                            dev = writer.begin_page( mediabox)
                            if pagefn:
                                pagefn(page_num, mediabox, dev, 0)
                        self.draw( dev, ctm)
                        if not more:
                            if pagefn:
                                pagefn( page_num, mediabox, dev, 1)
                            writer.end_page()
                    else:
                        self.draw(None, ctm)
                    if not more:
                        break

            @staticmethod
            def write_stabilized(writer, contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True):
                positions = list()
                content = None
                # Iterate until stable.
                while 1:
                    content_prev = content
                    content = contentfn( positions)
                    stable = False
                    if content == content_prev:
                        stable = True
                    content2 = content
                    story = Story(content2, user_css, em, archive)

                    if add_header_ids:
                        story.add_header_ids()

                    positions = list()
                    def positionfn2(position):
                        #print(f"write_stabilized(): stable={stable} positionfn={positionfn} position={position}")
                        positions.append(position)
                        if stable and positionfn:
                            positionfn(position)
                    story.write(
                            writer if stable else None,
                            rectfn,
                            positionfn2,
                            pagefn,
                            )
                    if stable:
                        break

            def add_header_ids(self):
                '''
                Look for `<h1..6>` items in `self` and adds unique `id`
                attributes if not already present.
                '''
                dom = self.body
                i = 0
                x = dom.find(None, None, None)
                while x:
                    name = x.tagname
                    if len(name) == 2 and name[0]=="h" and name[1] in "123456":
                        attr = x.get_attribute_value("id")
                        if not attr:
                            id_ = f"h_id_{i}"
                            #print(f"name={name}: setting id={id_}")
                            x.set_attribute("id", id_)
                            i += 1
                    x = x.find_next(None, None, None)

            def write_with_links(self, rectfn, positionfn=None, pagefn=None):
                #print("write_with_links()")
                stream = io.BytesIO()
                writer = DocumentWriter(stream)
                positions = []
                def positionfn2(position):
                    #print(f"write_with_links(): position={position}")
                    positions.append(position)
                    if positionfn:
                        positionfn(position)
                self.write(writer, rectfn, positionfn=positionfn2, pagefn=pagefn)
                writer.close()
                stream.seek(0)
                return Story.add_pdf_links(stream, positions)

            @staticmethod
            def write_stabilized_with_links(contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True):
                #print("write_stabilized_with_links()")
                stream = io.BytesIO()
                writer = DocumentWriter(stream)
                positions = []
                def positionfn2(position):
                    #print(f"write_stabilized_with_links(): position={position}")
                    positions.append(position)
                    if positionfn:
                        positionfn(position)
                Story.write_stabilized(writer, contentfn, rectfn, user_css, em, positionfn2, pagefn, archive, add_header_ids)
                writer.close()
                stream.seek(0)
                return Story.add_pdf_links(stream, positions)

            @staticmethod
            def add_pdf_links(document_or_stream, positions):
                """
                Adds links to PDF document.
                Args:
                    document_or_stream:
                        A PDF `Document` or raw PDF content, for example an
                        `io.BytesIO` instance.
                    positions:
                        List of `ElementPosition`'s for `document_or_stream`,
                        typically from Story.element_positions(). We raise an
                        exception if two or more positions have same id.
                Returns:
                    `document_or_stream` if a `Document` instance, otherwise a
                    new `Document` instance.
                We raise an exception if an `href` in `positions` refers to an
                internal position `#<name>` but no item in `postions` has `id =
                name`.
                """
                if isinstance(document_or_stream, Document):
                    document = document_or_stream
                else:
                    document = Document("pdf", document_or_stream)

                # Create dict from id to position, which we will use to find
                # link destinations.
                #
                id_to_position = dict()
                #print(f"positions: {positions}")
                for position in positions:
                    #print(f"add_pdf_links(): position: {position}")
                    if (position.open_close & 1) and position.id:
                        #print(f"add_pdf_links(): position with id: {position}")
                        if position.id in id_to_position:
                            #print(f"Ignoring duplicate positions with id={position.id!r}")
                            pass
                        else:
                            id_to_position[ position.id] = position

                # Insert links for all positions that have an `href` starting
                # with '#'.
                #
                for position_from in positions:
                    if ((position_from.open_close & 1)
                            and position_from.href
                            and position_from.href.startswith("#")
                            ):
                        # This is a `<a href="#...">...</a>` internal link.
                        #print(f"add_pdf_links(): position with href: {position}")
                        target_id = position_from.href[1:]
                        try:
                            position_to = id_to_position[ target_id]
                        except Exception as e:
                            raise RuntimeError(f"No destination with id={target_id}, required by position_from: {position_from}")
                        # Make link from `position_from`'s rect to top-left of
                        # `position_to`'s rect.
                        if 0:
                            print(f"add_pdf_links(): making link from:")
                            print(f"add_pdf_links():    {position_from}")
                            print(f"add_pdf_links(): to:")
                            print(f"add_pdf_links():    {position_to}")
                        link = dict()
                        link["kind"] = LINK_GOTO
                        link["from"] = Rect(position_from.rect)
                        x0, y0, x1, y1 = position_to.rect
                        # This appears to work well with viewers which scroll
                        # to make destination point top-left of window.
                        link["to"] = Point(x0, y0)
                        link["page"] = position_to.page_num - 1
                        document[position_from.page_num - 1].insert_link(link)
                return document

            @property
            def body(self):
                dom = self.document()
                return dom.bodytag()

            def __del__(self):
                if not type(self) is Story:
                    return
                if getattr(self, "thisown", False):
                    self.__swig_destroy__(self)
        %}
    }
};


//------------------------------------------------------------------------
// Tools - a collection of tools and utilities
//------------------------------------------------------------------------
struct Tools
{
    %extend
    {
        Tools()
        {
            /* It looks like global objects are never destructed when running
            with SWIG, so we use Memento_startLeaking()/Memento_stopLeaking().
            */
            Memento_startLeaking();
            void* p = malloc( sizeof(struct Tools));
            Memento_stopLeaking();
            //fprintf(stderr, "Tools constructor p=%p\n", p);
            return (struct Tools*) p;
        }

        ~Tools()
        {
            /* This is not called. */
            struct Tools* p = (struct Tools*) $self;
            //fprintf(stderr, "~Tools() p=%p\n", p);
            free(p);
        }

        %pythonprepend gen_id
        %{"""Return a unique positive integer."""%}
        PyObject *gen_id()
        {
            JM_UNIQUE_ID += 1;
            if (JM_UNIQUE_ID < 0) JM_UNIQUE_ID = 1;
            return Py_BuildValue("i", JM_UNIQUE_ID);
        }


        FITZEXCEPTION(set_icc, !result)
        %pythonprepend set_icc
        %{"""Set ICC color handling on or off."""%}
        PyObject *set_icc(int on=0)
        {
            fz_try(gctx) {
                if (on) {
                    if (FZ_ENABLE_ICC)
                        fz_enable_icc(gctx);
                    else {
                        RAISEPY(gctx, "MuPDF built w/o ICC support",PyExc_ValueError);
                    }
                } else if (FZ_ENABLE_ICC) {
                    fz_disable_icc(gctx);
                }
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        %pythonprepend set_annot_stem
        %{"""Get / set id prefix for annotations."""%}
        char *set_annot_stem(char *stem=NULL)
        {
            if (!stem) {
                return JM_annot_id_stem;
            }
            size_t len = strlen(stem) + 1;
            if (len > 50) len = 50;
            memcpy(&JM_annot_id_stem, stem, len);
            return JM_annot_id_stem;
        }


        %pythonprepend set_small_glyph_heights
        %{"""Set / unset small glyph heights."""%}
        PyObject *set_small_glyph_heights(PyObject *on=NULL)
        {
            if (!on || on == Py_None) {
                return JM_BOOL(small_glyph_heights);
            }
            if (PyObject_IsTrue(on)) {
                small_glyph_heights = 1;
            } else {
                small_glyph_heights = 0;
            }
            return JM_BOOL(small_glyph_heights);
        }


        %pythonprepend set_subset_fontnames
        %{"""Set / unset returning fontnames with their subset prefix."""%}
        PyObject *set_subset_fontnames(PyObject *on=NULL)
        {
            if (!on || on == Py_None) {
                return JM_BOOL(subset_fontnames);
            }
            if (PyObject_IsTrue(on)) {
                subset_fontnames = 1;
            } else {
                subset_fontnames = 0;
            }
            return JM_BOOL(subset_fontnames);
        }


        %pythonprepend set_low_memory
        %{"""Set / unset MuPDF device caching."""%}
        PyObject *set_low_memory(PyObject *on=NULL)
        {
            if (!on || on == Py_None) {
                return JM_BOOL(no_device_caching);
            }
            if (PyObject_IsTrue(on)) {
                no_device_caching = 1;
            } else {
                no_device_caching = 0;
            }
            return JM_BOOL(no_device_caching);
        }


        %pythonprepend unset_quad_corrections
        %{"""Set ascender / descender corrections on or off."""%}
        PyObject *unset_quad_corrections(PyObject *on=NULL)
        {
            if (!on || on == Py_None) {
                return JM_BOOL(skip_quad_corrections);
            }
            if (PyObject_IsTrue(on)) {
                skip_quad_corrections = 1;
            } else {
                skip_quad_corrections = 0;
            }
            return JM_BOOL(skip_quad_corrections);
        }


        %pythonprepend store_shrink
        %{"""Free 'percent' of current store size."""%}
        PyObject *store_shrink(int percent)
        {
            if (percent >= 100) {
                fz_empty_store(gctx);
                return Py_BuildValue("i", 0);
            }
            if (percent > 0) fz_shrink_store(gctx, 100 - percent);
            return Py_BuildValue("i", (int) gctx->store->size);
        }


        %pythoncode%{@property%}
        %pythonprepend store_size
        %{"""MuPDF current store size."""%}
        PyObject *store_size()
        {
            return Py_BuildValue("i", (int) gctx->store->size);
        }


        %pythoncode%{@property%}
        %pythonprepend store_maxsize
        %{"""MuPDF store size limit."""%}
        PyObject *store_maxsize()
        {
            return Py_BuildValue("i", (int) gctx->store->max);
        }


        %pythonprepend show_aa_level
        %{"""Show anti-aliasing values."""%}
        %pythonappend show_aa_level %{
        temp = {"graphics": val[0], "text": val[1], "graphics_min_line_width": val[2]}
        val = temp%}
        PyObject *show_aa_level()
        {
            return Py_BuildValue("iif",
                fz_graphics_aa_level(gctx),
                fz_text_aa_level(gctx),
                fz_graphics_min_line_width(gctx));
        }


        %pythonprepend set_aa_level
        %{"""Set anti-aliasing level."""%}
        void set_aa_level(int level)
        {
            fz_set_aa_level(gctx, level);
        }


        %pythonprepend set_graphics_min_line_width
        %{"""Set the graphics minimum line width."""%}
        void set_graphics_min_line_width(float min_line_width)
        {
            fz_set_graphics_min_line_width(gctx, min_line_width);
        }


        FITZEXCEPTION(image_profile, !result)
        %pythonprepend image_profile
        %{"""Metadata of an image binary stream."""%}
        PyObject *image_profile(PyObject *stream, int keep_image=0)
        {
            PyObject *rc = NULL;
            fz_try(gctx) {
                rc = JM_image_profile(gctx, stream, keep_image);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return rc;
        }


        PyObject *_rotate_matrix(struct Page *page)
        {
            pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page);
            if (!pdfpage) return JM_py_from_matrix(fz_identity);
            return JM_py_from_matrix(JM_rotate_page_matrix(gctx, pdfpage));
        }


        PyObject *_derotate_matrix(struct Page *page)
        {
            pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page);
            if (!pdfpage) return JM_py_from_matrix(fz_identity);
            return JM_py_from_matrix(JM_derotate_page_matrix(gctx, pdfpage));
        }


        %pythoncode%{@property%}
        %pythonprepend fitz_config
        %{"""PyMuPDF configuration parameters."""%}
        PyObject *fitz_config()
        {
            return JM_fitz_config();
        }


        %pythonprepend glyph_cache_empty
        %{"""Empty the glyph cache."""%}
        void glyph_cache_empty()
        {
            fz_purge_glyph_cache(gctx);
        }


        FITZEXCEPTION(_fill_widget, !result)
        %pythonappend _fill_widget %{
            widget.rect = Rect(annot.rect)
            widget.xref = annot.xref
            widget.parent = annot.parent
            widget._annot = annot  # backpointer to annot object
            if not widget.script:
                widget.script = None
            if not widget.script_stroke:
                widget.script_stroke = None
            if not widget.script_format:
                widget.script_format = None
            if not widget.script_change:
                widget.script_change = None
            if not widget.script_calc:
                widget.script_calc = None
            if not widget.script_blur:
                widget.script_blur = None
            if not widget.script_focus:
                widget.script_focus = None
        %}
        PyObject *_fill_widget(struct Annot *annot, PyObject *widget)
        {
            fz_try(gctx) {
                JM_get_widget_properties(gctx, (pdf_annot *) annot, widget);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(_save_widget, !result)
        PyObject *_save_widget(struct Annot *annot, PyObject *widget)
        {
            fz_try(gctx) {
                JM_set_widget_properties(gctx, (pdf_annot *) annot, widget);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(_reset_widget, !result)
        PyObject *_reset_widget(struct Annot *annot)
        {
            fz_try(gctx) {
                pdf_annot *this_annot = (pdf_annot *) annot;
                pdf_obj *this_annot_obj = pdf_annot_obj(gctx, this_annot);
                pdf_document *pdf = pdf_get_bound_document(gctx, this_annot_obj);
                pdf_field_reset(gctx, pdf, this_annot_obj);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }

        // Ensure that widgets with a /AA/C JavaScript are in AcroForm/CO
        FITZEXCEPTION(_ensure_widget_calc, !result)
        PyObject *_ensure_widget_calc(struct Annot *annot)
        {
            pdf_obj *PDFNAME_CO=NULL;
            fz_try(gctx) {
                pdf_obj *annot_obj = pdf_annot_obj(gctx, (pdf_annot *) annot);
                pdf_document *pdf = pdf_get_bound_document(gctx, annot_obj);
                PDFNAME_CO = pdf_new_name(gctx, "CO");  // = PDF_NAME(CO)
                pdf_obj *acro = pdf_dict_getl(gctx,  // get AcroForm dict
                                pdf_trailer(gctx, pdf),
                                PDF_NAME(Root),
                                PDF_NAME(AcroForm),
                                NULL);

                pdf_obj *CO = pdf_dict_get(gctx, acro, PDFNAME_CO);  // = AcroForm/CO
                if (!CO) {
                    CO = pdf_dict_put_array(gctx, acro, PDFNAME_CO, 2);
                }
                int i, n = pdf_array_len(gctx, CO);
                int xref, nxref, found = 0;
                xref = pdf_to_num(gctx, annot_obj);
                for (i = 0; i < n; i++) {
                    nxref = pdf_to_num(gctx, pdf_array_get(gctx, CO, i));
                    if (xref == nxref) {
                        found = 1;
                        break;
                    }
                }
                if (!found) {
                    pdf_array_push_drop(gctx, CO, pdf_new_indirect(gctx, pdf, xref, 0));
                }
            }
            fz_always(gctx) {
                pdf_drop_obj(gctx, PDFNAME_CO);
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(_parse_da, !result)
        %pythonappend _parse_da %{
        if not val:
            return ((0,), "", 0)
        font = "Helv"
        fsize = 12
        col = (0, 0, 0)
        dat = val.split()  # split on any whitespace
        for i, item in enumerate(dat):
            if item == "Tf":
                font = dat[i - 2][1:]
                fsize = float(dat[i - 1])
                dat[i] = dat[i-1] = dat[i-2] = ""
                continue
            if item == "g":            # unicolor text
                col = [(float(dat[i - 1]))]
                dat[i] = dat[i-1] = ""
                continue
            if item == "rg":           # RGB colored text
                col = [float(f) for f in dat[i - 3:i]]
                dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = ""
                continue
            if item == "k":           # CMYK colored text
                col = [float(f) for f in dat[i - 4:i]]
                dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = dat[i-4] = ""
                continue

        val = (col, font, fsize)
        %}
        PyObject *_parse_da(struct Annot *annot)
        {
            char *da_str = NULL;
            pdf_annot *this_annot = (pdf_annot *) annot;
            pdf_obj *this_annot_obj = pdf_annot_obj(gctx, this_annot);
            pdf_document *pdf = pdf_get_bound_document(gctx, this_annot_obj);
            fz_try(gctx) {
                pdf_obj *da = pdf_dict_get_inheritable(gctx, this_annot_obj,
                                                       PDF_NAME(DA));
                if (!da) {
                    pdf_obj *trailer = pdf_trailer(gctx, pdf);
                    da = pdf_dict_getl(gctx, trailer, PDF_NAME(Root),
                                       PDF_NAME(AcroForm),
                                       PDF_NAME(DA),
                                       NULL);
                }
                da_str = (char *) pdf_to_text_string(gctx, da);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return JM_UnicodeFromStr(da_str);
        }


        FITZEXCEPTION(_update_da, !result)
        PyObject *_update_da(struct Annot *annot, char *da_str)
        {
            fz_try(gctx) {
                pdf_annot *this_annot = (pdf_annot *) annot;
                pdf_obj *this_annot_obj = pdf_annot_obj(gctx, this_annot);
                pdf_dict_put_text_string(gctx, this_annot_obj, PDF_NAME(DA), da_str);
                pdf_dict_del(gctx, this_annot_obj, PDF_NAME(DS)); /* not supported */
                pdf_dict_del(gctx, this_annot_obj, PDF_NAME(RC)); /* not supported */
            }
            fz_catch(gctx) {
                return NULL;
            }
            Py_RETURN_NONE;
        }


        FITZEXCEPTION(_get_all_contents, !result)
        %pythonprepend _get_all_contents
        %{"""Concatenate all /Contents objects of a page into a bytes object."""%}
        PyObject *_get_all_contents(struct Page *fzpage)
        {
            pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) fzpage);
            fz_buffer *res = NULL;
            PyObject *result = NULL;
            fz_try(gctx) {
                ASSERT_PDF(page);
                res = JM_read_contents(gctx, page->obj);
                result = JM_BinFromBuffer(gctx, res);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, res);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return result;
        }


        FITZEXCEPTION(_insert_contents, !result)
        %pythonprepend _insert_contents
        %{"""Add bytes as a new /Contents object for a page, and return its xref."""%}
        PyObject *_insert_contents(struct Page *page, PyObject *newcont, int overlay=1)
        {
            fz_buffer *contbuf = NULL;
            int xref = 0;
            pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page);
            fz_try(gctx) {
                ASSERT_PDF(pdfpage);
                ENSURE_OPERATION(gctx, pdfpage->doc);
                contbuf = JM_BufferFromBytes(gctx, newcont);
                xref = JM_insert_contents(gctx, pdfpage->doc, pdfpage->obj, contbuf, overlay);
            }
            fz_always(gctx) {
                fz_drop_buffer(gctx, contbuf);
            }
            fz_catch(gctx) {
                return NULL;
            }
            return Py_BuildValue("i", xref);
        }

        %pythonprepend mupdf_version
        %{"""Get version of MuPDF binary build."""%}
        PyObject *mupdf_version()
        {
            return Py_BuildValue("s", FZ_VERSION);
        }

        %pythonprepend mupdf_warnings
        %{"""Get the MuPDF warnings/errors with optional reset (default)."""%}
        %pythonappend mupdf_warnings %{
        val = "\n".join(val)
        if reset:
            self.reset_mupdf_warnings()%}
        PyObject *mupdf_warnings(int reset=1)
        {
            Py_INCREF(JM_mupdf_warnings_store);
            return JM_mupdf_warnings_store;
        }

        int _int_from_language(char *language)
        {
            return fz_text_language_from_string(language);
        }

        %pythonprepend reset_mupdf_warnings
        %{"""Empty the MuPDF warnings/errors store."""%}
        void reset_mupdf_warnings()
        {
            Py_CLEAR(JM_mupdf_warnings_store);
            JM_mupdf_warnings_store = PyList_New(0);
        }

        %pythonprepend mupdf_display_errors
        %{"""Set MuPDF error display to True or False."""%}
        PyObject *mupdf_display_errors(PyObject *on=NULL)
        {
            if (!on || on == Py_None) {
                return JM_BOOL(JM_mupdf_show_errors);
            }
            if (PyObject_IsTrue(on)) {
                JM_mupdf_show_errors = 1;
            } else {
                JM_mupdf_show_errors = 0;
            }
            return JM_BOOL(JM_mupdf_show_errors);
        }

        %pythonprepend mupdf_display_warnings
        %{"""Set MuPDF warnings display to True or False."""%}
        PyObject *mupdf_display_warnings(PyObject *on=NULL)
        {
            if (!on || on == Py_None) {
                return JM_BOOL(JM_mupdf_show_warnings);
            }
            if (PyObject_IsTrue(on)) {
                JM_mupdf_show_warnings = 1;
            } else {
                JM_mupdf_show_warnings = 0;
            }
            return JM_BOOL(JM_mupdf_show_warnings);
        }

        %pythoncode %{
def _le_annot_parms(self, annot, p1, p2, fill_color):
    """Get common parameters for making annot line end symbols.

    Returns:
        m: matrix that maps p1, p2 to points L, P on the x-axis
        im: its inverse
        L, P: transformed p1, p2
        w: line width
        scol: stroke color string
        fcol: fill color store_shrink
        opacity: opacity string (gs command)
    """
    w = annot.border["width"]  # line width
    sc = annot.colors["stroke"]  # stroke color
    if not sc:  # black if missing
        sc = (0,0,0)
    scol = " ".join(map(str, sc)) + " RG\n"
    if fill_color:
        fc = fill_color
    else:
        fc = annot.colors["fill"]  # fill color
    if not fc:
        fc = (1,1,1)  # white if missing
    fcol = " ".join(map(str, fc)) + " rg\n"
    # nr = annot.rect
    np1 = p1                   # point coord relative to annot rect
    np2 = p2                   # point coord relative to annot rect
    m = Matrix(util_hor_matrix(np1, np2))  # matrix makes the line horizontal
    im = ~m                            # inverted matrix
    L = np1 * m                        # converted start (left) point
    R = np2 * m                        # converted end (right) point
    if 0 <= annot.opacity < 1:
        opacity = "/H gs\n"
    else:
        opacity = ""
    return m, im, L, R, w, scol, fcol, opacity

def _oval_string(self, p1, p2, p3, p4):
    """Return /AP string defining an oval within a 4-polygon provided as points
    """
    def bezier(p, q, r):
        f = "%f %f %f %f %f %f c\n"
        return f % (p.x, p.y, q.x, q.y, r.x, r.y)

    kappa = 0.55228474983              # magic number
    ml = p1 + (p4 - p1) * 0.5          # middle points ...
    mo = p1 + (p2 - p1) * 0.5          # for each ...
    mr = p2 + (p3 - p2) * 0.5          # polygon ...
    mu = p4 + (p3 - p4) * 0.5          # side
    ol1 = ml + (p1 - ml) * kappa       # the 8 bezier
    ol2 = mo + (p1 - mo) * kappa       # helper points
    or1 = mo + (p2 - mo) * kappa
    or2 = mr + (p2 - mr) * kappa
    ur1 = mr + (p3 - mr) * kappa
    ur2 = mu + (p3 - mu) * kappa
    ul1 = mu + (p4 - mu) * kappa
    ul2 = ml + (p4 - ml) * kappa
    # now draw, starting from middle point of left side
    ap = "%f %f m\n" % (ml.x, ml.y)
    ap += bezier(ol1, ol2, mo)
    ap += bezier(or1, or2, mr)
    ap += bezier(ur1, ur2, mu)
    ap += bezier(ul1, ul2, ml)
    return ap

def _le_diamond(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for diamond line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 2.5             # 2*shift*width = length of square edge
    d = shift * max(1, w)
    M = R - (d/2., 0) if lr else L + (d/2., 0)
    r = Rect(M, M) + (-d, -d, d, d)         # the square
    # the square makes line longer by (2*shift - 1)*width
    p = (r.tl + (r.bl - r.tl) * 0.5) * im
    ap = "q\n%s%f %f m\n" % (opacity, p.x, p.y)
    p = (r.tl + (r.tr - r.tl) * 0.5) * im
    ap += "%f %f l\n"   % (p.x, p.y)
    p = (r.tr + (r.br - r.tr) * 0.5) * im
    ap += "%f %f l\n"   % (p.x, p.y)
    p = (r.br + (r.bl - r.br) * 0.5) * im
    ap += "%f %f l\n"   % (p.x, p.y)
    ap += "%g w\n" % w
    ap += scol + fcol + "b\nQ\n"
    return ap

def _le_square(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for square line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 2.5             # 2*shift*width = length of square edge
    d = shift * max(1, w)
    M = R - (d/2., 0) if lr else L + (d/2., 0)
    r = Rect(M, M) + (-d, -d, d, d)         # the square
    # the square makes line longer by (2*shift - 1)*width
    p = r.tl * im
    ap = "q\n%s%f %f m\n" % (opacity, p.x, p.y)
    p = r.tr * im
    ap += "%f %f l\n"   % (p.x, p.y)
    p = r.br * im
    ap += "%f %f l\n"   % (p.x, p.y)
    p = r.bl * im
    ap += "%f %f l\n"   % (p.x, p.y)
    ap += "%g w\n" % w
    ap += scol + fcol + "b\nQ\n"
    return ap

def _le_circle(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for circle line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 2.5             # 2*shift*width = length of square edge
    d = shift * max(1, w)
    M = R - (d/2., 0) if lr else L + (d/2., 0)
    r = Rect(M, M) + (-d, -d, d, d)         # the square
    ap = "q\n" + opacity + self._oval_string(r.tl * im, r.tr * im, r.br * im, r.bl * im)
    ap += "%g w\n" % w
    ap += scol + fcol + "b\nQ\n"
    return ap

def _le_butt(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for butt line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 3
    d = shift * max(1, w)
    M = R if lr else L
    top = (M + (0, -d/2.)) * im
    bot = (M + (0, d/2.)) * im
    ap = "\nq\n%s%f %f m\n" % (opacity, top.x, top.y)
    ap += "%f %f l\n" % (bot.x, bot.y)
    ap += "%g w\n" % w
    ap += scol + "s\nQ\n"
    return ap

def _le_slash(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for slash line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    rw = 1.1547 * max(1, w) * 1.0         # makes rect diagonal a 30 deg inclination
    M = R if lr else L
    r = Rect(M.x - rw, M.y - 2 * w, M.x + rw, M.y + 2 * w)
    top = r.tl * im
    bot = r.br * im
    ap = "\nq\n%s%f %f m\n" % (opacity, top.x, top.y)
    ap += "%f %f l\n" % (bot.x, bot.y)
    ap += "%g w\n" % w
    ap += scol + "s\nQ\n"
    return ap

def _le_openarrow(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for open arrow line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 2.5
    d = shift * max(1, w)
    p2 = R + (d/2., 0) if lr else L - (d/2., 0)
    p1 = p2 + (-2*d, -d) if lr else p2 + (2*d, -d)
    p3 = p2 + (-2*d, d) if lr else p2 + (2*d, d)
    p1 *= im
    p2 *= im
    p3 *= im
    ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
    ap += "%f %f l\n" % (p2.x, p2.y)
    ap += "%f %f l\n" % (p3.x, p3.y)
    ap += "%g w\n" % w
    ap += scol + "S\nQ\n"
    return ap

def _le_closedarrow(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for closed arrow line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 2.5
    d = shift * max(1, w)
    p2 = R + (d/2., 0) if lr else L - (d/2., 0)
    p1 = p2 + (-2*d, -d) if lr else p2 + (2*d, -d)
    p3 = p2 + (-2*d, d) if lr else p2 + (2*d, d)
    p1 *= im
    p2 *= im
    p3 *= im
    ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
    ap += "%f %f l\n" % (p2.x, p2.y)
    ap += "%f %f l\n" % (p3.x, p3.y)
    ap += "%g w\n" % w
    ap += scol + fcol + "b\nQ\n"
    return ap

def _le_ropenarrow(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for right open arrow line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 2.5
    d = shift * max(1, w)
    p2 = R - (d/3., 0) if lr else L + (d/3., 0)
    p1 = p2 + (2*d, -d) if lr else p2 + (-2*d, -d)
    p3 = p2 + (2*d, d) if lr else p2 + (-2*d, d)
    p1 *= im
    p2 *= im
    p3 *= im
    ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
    ap += "%f %f l\n" % (p2.x, p2.y)
    ap += "%f %f l\n" % (p3.x, p3.y)
    ap += "%g w\n" % w
    ap += scol + fcol + "S\nQ\n"
    return ap

def _le_rclosedarrow(self, annot, p1, p2, lr, fill_color):
    """Make stream commands for right closed arrow line end symbol. "lr" denotes left (False) or right point.
    """
    m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color)
    shift = 2.5
    d = shift * max(1, w)
    p2 = R - (2*d, 0) if lr else L + (2*d, 0)
    p1 = p2 + (2*d, -d) if lr else p2 + (-2*d, -d)
    p3 = p2 + (2*d, d) if lr else p2 + (-2*d, d)
    p1 *= im
    p2 *= im
    p3 *= im
    ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y)
    ap += "%f %f l\n" % (p2.x, p2.y)
    ap += "%f %f l\n" % (p3.x, p3.y)
    ap += "%g w\n" % w
    ap += scol + fcol + "b\nQ\n"
    return ap

def __del__(self):
    if not type(self) is Tools:
        return
    if getattr(self, "thisown", False):
        self.__swig_destroy__(self)
        %}
    }
};