diff mupdf-source/scripts/wrap/make_cppyy.py @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/scripts/wrap/make_cppyy.py	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,902 @@
+import jlib
+
+import textwrap
+
+def make_cppyy(
+        state_,
+        build_dirs,
+        generated,
+        ):
+    path = f'{build_dirs.dir_so}/mupdf_cppyy.py'
+    jlib.log( 'Updating {path}')
+
+    text = ''
+
+    text += textwrap.dedent( """
+            '''
+            MuPDF Python bindings using cppyy: https://cppyy.readthedocs.io
+
+            Cppyy generates bindings at runtime, so we don't need to build a .so like SWIG.
+
+            However we still need the mupdf.so (MuPDF C API) and mupdfcpp.so (MuPDF C++
+            API) libraries to be present and accessible via LD_LIBRARY_PATH.
+
+            Usage:
+                import mupdf_cppyy
+                mupdf = mupdf_cppyy.cppyy.gbl.mupdf
+
+                document = mupdf.Document(...)
+
+            Requirements:
+                Install cppyy; for example:
+                    python -m pip install cppyy
+            '''
+
+            import ctypes
+            import inspect
+            import os
+            import re
+            import sys
+
+            import cppyy
+            import cppyy.ll
+
+            try:
+                import jlib
+            except ModuleNotFoundError:
+                class jlib:
+                    @staticmethod
+                    def log( text):
+                        sys.stderr.write( f'{text}\\n')
+            mupdf_dir = os.path.abspath( f'{__file__}/../../..')
+
+            # pdf_annot_type is both an enum and a function (that returns
+            # the enum type!).
+            with open( f'{mupdf_dir}/include/mupdf/pdf/annot.h') as f:
+                text = f.read()
+            text, n = re.subn(
+                    '(enum pdf_annot_type pdf_annot_type[(]fz_context [*]ctx, pdf_annot [*]annot[)];)',
+                    '/*\\1*/',
+                    text,
+                    )
+            assert n == 1, f'n={n}'
+
+            # libmupdf and libmupdf.so also work here.
+            if 0:
+                print( f'$LD_LIBRARY_PATH={os.environ["LD_LIBRARY_PATH"]}', file=sys.stderr)
+            ret = cppyy.load_library('mupdf')
+            #jlib.log( 'after loading "mupdf": ret={ret=}')
+            cppyy.load_library('mupdfcpp')
+
+            cppyy.add_include_path( f'{mupdf_dir}/include')
+            cppyy.add_include_path( f'{mupdf_dir}/platform/c++/include')
+
+            # pdf_annot_type is both an enum and a function (that returns
+            # the enum type!).
+            with open( f'{mupdf_dir}/include/mupdf/pdf/annot.h') as f:
+                text1 = f.read()
+            text1, n = re.subn(
+                    '(enum pdf_annot_type pdf_annot_type[(]fz_context [*]ctx, pdf_annot [*]annot[)];)',
+                    '/* \\\\1 */',
+                    text1,
+                    )
+            assert n == 1, f'n={n}'
+            with open( 'foo_text1.h', 'w') as f:
+                f.write( text1)
+
+            # pdf_widget_type is both an enum and a function (that returns
+            # the enum type!).
+            with open( f'{mupdf_dir}/include/mupdf/pdf/form.h') as f:
+                text2 = f.read()
+            text2, n = re.subn(
+                    '(enum pdf_widget_type pdf_widget_type[(]fz_context [*]ctx, pdf_annot [*]widget[)];)',
+                    '/* \\\\1 */',
+                    text2,
+                    )
+            assert n == 1, f'n={n}'
+            with open( 'foo_text2.h', 'w') as f:
+                f.write( text2)
+
+            # Not sure why we need '#define FZ_ENABLE_ICC 1', but
+            # otherwise classes.h doesn't see a definition of
+            # fz_icc_profile. Presumably something to do with us manually
+            # including our modified copy of include/mupdf/pdf/annot.h.
+            #
+            cppyy.cppdef( f'''
+                    #undef NDEBUG
+                    #define FZ_ENABLE_ICC 1
+                    {text1}
+                    {text2}
+                    #ifndef MUPDF_PDF_ANNOT_H
+                        #error MUPDF_PDF_ANNOT_H not defined
+                    #endif
+
+                    #include "mupdf/fitz/version.h"
+                    #include "mupdf/classes.h"
+                    #include "mupdf/classes2.h"
+                    #include "mupdf/functions.h"
+                    #include "mupdf/fitz.h"
+                    #include "mupdf/pdf.h"
+                    ''')
+
+            cppyy.cppdef( f'''
+                    #ifndef MUPDF_PDF_ANNOT_H
+                        #error MUPDF_PDF_ANNOT_H not defined
+                    #endif
+                    ''')
+
+            if os.environ.get( 'MUPDF_cppyy_sig_exceptions') == '1':
+                jlib.log( 'calling cppyy.ll.set_signals_as_exception(True)')
+                cppyy.ll.set_signals_as_exception(True)
+
+                if 0:
+                    # Do some checks.
+                    try:
+                        cppyy.gbl.abort()
+                    except Exception as e:
+                        print( f'Ignoring test exception from abort(): {e}', file=sys.stderr)
+                    else:
+                        assert 0, 'No exception from cppyy.gbl.abort()'
+
+                    cppyy.cppdef('''
+                            void mupdf_check_assert()
+                            {
+                                assert( 0);
+                            }
+                            ''')
+                    cppyy.ll.set_signals_as_exception(True)
+                    print( 'Testing assert failure', file=sys.stderr)
+                    try:
+                        cppyy.gbl.mupdf_check_assert()
+                    except Exception as e:
+                        print( f'Ignoring test exception from assert(0): {e}', file=sys.stderr)
+
+                    print( 'Testing rect creation from null', file=sys.stderr)
+                    try:
+                        r = cppyy.gbl.mupdf.Rect( 0)
+                    except Exception as e:
+                        print( f'Ignoring exception from test rect creation from null e={e}', file=sys.stderr)
+                    except:
+                        print( '*** Non-Exception exception', file=sys.stderr)
+                        traceback.print_exc()
+                    else:
+                        print( f'*** No exception from test rect creation from null', file=sys.stderr)
+                    print( 'Finished testing rect creation from null', file=sys.stderr)
+
+                    #try:
+                    #    cppyy.gbl.raise( SIGABRT)
+                    #except:
+                    #    traceback.print_exc()
+
+
+            #
+            # Would be convenient to do:
+            #
+            #   from cppyy.gbl.mupdf import *
+            #
+            # - but unfortunately this is not possible, e.g. see:
+            #
+            #   https://cppyy.readthedocs.io/en/latest/misc.html#reduced-typing
+            #
+            # So instead it is suggested that users of this api do:
+            #
+            #   import mupdf
+            #   mupdf = mupdf.cppyy.gbl.mupdf
+            #
+            # If access to mupdf.cppyy.gbl is required (e.g. to see globals that are not in
+            # the C++ mupdf namespace), caller can additionally do:
+            #
+            #   import cppyy
+            #   cppyy.gbl.std...
+            #
+
+            # We make various modifications of cppyy.gbl.mupdf to simplify usage.
+            #
+
+            #print( f'len(dir(cppyy.gbl))={len(dir(cppyy.gbl))}')
+            #print( f'len(dir(cppyy.gbl.mupdf))={len(dir(cppyy.gbl.mupdf))}')
+
+            # Find macros and import into cppyy.gbl.mupdf.
+            paths = (
+                    f'{mupdf_dir}/include/mupdf/fitz/version.h',
+                    f'{mupdf_dir}/include/mupdf/ucdn.h',
+                    f'{mupdf_dir}/pdf/object.h',
+                    )
+            for path in (
+                    f'{mupdf_dir}/include/mupdf/fitz/version.h',
+                    f'{mupdf_dir}/include/mupdf/ucdn.h',
+                    ):
+                with open( path) as f:
+                    for line in f:
+                        m = re.match('^#define\\\\s([a-zA-Z_][a-zA-Z_0-9]+)\\\\s+([^\\\\s]*)\\\\s*$', line)
+                        if m:
+                            name = m.group(1)
+                            value = m.group(2)
+                            if value == '':
+                                value = 1
+                            else:
+                                value = eval( value)
+                            #print( f'mupdf_cppyy.py: Setting {name}={value!r}')
+                            setattr( cppyy.gbl.mupdf, name, value)
+
+            # MuPDF enums are defined as C so are not in the mupdf
+            # namespace. To mimic the SWIG mupdf bindings, we explicitly
+            # copy them into cppyy.gbl.mupdf.
+            #
+            """)
+
+    # Copy enums into mupdf namespace. We use generated.c_enums for this
+    # because cppyy has a bug where enums are not visible for iteration in a
+    # namespace - see: https://github.com/wlav/cppyy/issues/45
+    #
+    for enum_type, enum_names in generated.c_enums.items():
+        for enum_name in enum_names:
+            text += f'cppyy.gbl.mupdf.{enum_name} = cppyy.gbl.{enum_name}\n'
+
+    # Add code for converting small integers into MuPDF's special pdf_obj*
+    # values, and add these special enums to the mupdf namespace.
+    text += textwrap.dedent( """
+            cppyy.cppdef('''
+                    #include "mupdf/fitz.h"
+                    /* Casts an integer to a pdf_obj*. Used to convert SWIG's int
+                    values for PDF_ENUM_NAME_* into PdfObj's. */
+                    pdf_obj* obj_enum_to_obj(int n)
+                    {
+                        return (pdf_obj*) (intptr_t) n;
+                    }
+                    ''')
+            """)
+    for enum_type, enum_names in generated.c_enums.items():
+        for enum_name in enum_names:
+            if enum_name.startswith( 'PDF_ENUM_NAME_'):
+                text += f'cppyy.gbl.mupdf.{enum_name} = cppyy.gbl.mupdf.PdfObj( cppyy.gbl.obj_enum_to_obj( cppyy.gbl.mupdf.{enum_name}))\n'
+    # Auto-generated out-param wrappers.
+    text += generated.cppyy_extra
+
+    # Misc processing can be done directly in Python code.
+    #
+    text += textwrap.dedent( """
+
+            # Import selected basic types into mupdf namespace.
+            #
+            cppyy.gbl.mupdf.fz_point = cppyy.gbl.fz_point
+            cppyy.gbl.mupdf.fz_rect = cppyy.gbl.fz_rect
+            cppyy.gbl.mupdf.fz_matrix = cppyy.gbl.fz_matrix
+            cppyy.gbl.mupdf.fz_font_flags_t = cppyy.gbl.fz_font_flags_t
+            cppyy.gbl.mupdf.fz_default_color_params = cppyy.gbl.fz_default_color_params
+
+            # Override various functions so that, for example, functions with
+            # out-parameters instead return tuples.
+            #
+
+            # cppyy doesn't like interpreting char name[32] as a string?
+            cppyy.cppdef('''
+                    std::string mupdf_font_name(fz_font* font)
+                    {
+                        //std::cerr << __FUNCTION__ << ": font=" << font << " font->name=" << font->name << "\\\\n";
+                        return font->name;
+                    }
+                    ''')
+
+            class getattr_path_raise: pass
+            def getattr_path( path, default=getattr_path_raise):
+                '''
+                Like getattr() but resolves string path, splitting at '.'
+                characters.
+                '''
+                if isinstance( path, str):
+                    path = path.split( '.')
+                # Maybe we should use out caller's module?
+                ret = sys.modules[ __name__]
+                try:
+                    for subname in path:
+                        ret = getattr( ret, subname)
+                except AttributeError:
+                    if default is getattr_path_raise:
+                        raise
+                    return default
+                return ret
+
+            def setattr_path( path, value):
+                '''
+                Like getattr() but resolves string path, splitting at '.'
+                characters.
+                '''
+                if isinstance( path, str):
+                    path = path.split( '.')
+                ns = getattr_path( path[:-1])
+                setattr( ns, path[-1], value)
+
+            assert getattr_path( 'cppyy') == cppyy
+            assert getattr_path( 'cppyy.gbl.mupdf') == cppyy.gbl.mupdf
+
+            def insert( *paths):
+                '''
+                Returns a decorator that copies the function into the specified
+                name(s). We assert that each item in <path> does not already
+                exist.
+                '''
+                class Anon: pass
+                for path in paths:
+                    assert getattr_path( path, Anon) is Anon, f'path={path} already exists.'
+                def decorator( fn):
+                    for path in paths:
+                        setattr_path( path, fn)
+                return decorator
+
+            def replace( *paths):
+                '''
+                Decorator that inserts a function into namespace(s), replacing
+                the existing function(s). We assert that the namespace(s)
+                already contains the specified name,
+                '''
+                def decorator( fn):
+                    class Anon: pass
+                    for path in paths:
+                        assert getattr_path( path, Anon) is not Anon, f'path does not exist: {path}'
+                    for path in paths:
+                        setattr_path( path, fn)
+                return decorator
+
+            def override( path, *paths_extra):
+                '''
+                Returns a decorator for <fn> which sets <path> and each item
+                in <paths_extra> to <fn>. When <fn> is called, it is passed an
+                additional <_original> arg set to the original <path>.
+                '''
+                def decorator( fn):
+                    fn_original = getattr_path( path)
+                    def fn2( *args, **kwargs):
+                        '''
+                        Call <fn>, passing additional <_original> arg.
+                        '''
+                        assert '_original' not in kwargs
+                        kwargs[ '_original'] = fn_original
+                        return fn( *args, **kwargs)
+                    setattr_path( path, fn2)
+                    for p in paths_extra:
+                        setattr_path( p, fn2)
+                    return fn2
+                return decorator
+
+            # A C++ fn that returns fz_buffer::data; our returned value seems
+            # to work better than direct access in Python.
+            #
+            cppyy.cppdef(f'''
+                    namespace mupdf
+                    {{
+                        void* Buffer_data( fz_buffer* buffer)
+                        {{
+                            return buffer->data;
+                        }}
+                    }}
+                    ''')
+
+            @replace( 'cppyy.gbl.mupdf.Buffer.buffer_storage', 'cppyy.gbl.mupdf.mfz_buffer_storage')
+            def _( buffer):
+                assert isinstance( buffer, cppyy.gbl.mupdf.Buffer)
+                assert buffer.m_internal
+
+                # Getting buffer.m_internal.data via Buffer_data() appears
+                # to work better than using buffer.m_internal.data
+                # directly. E.g. the latter fails when passed to
+                # mfz_recognize_image_format().
+                #
+                d = cppyy.gbl.mupdf.Buffer_data( buffer.m_internal)
+                return buffer.m_internal.len, d
+
+            cppyy.cppdef('''
+                    std::string mupdf_raw_to_python_bytes( void* data, size_t size)
+                    {
+                        return std::string( (char*) data, size);
+                    }
+                    ''')
+
+            @insert( 'cppyy.gbl.mupdf.raw_to_python_bytes')
+            def _( data, size):
+                '''
+                Need to explicitly convert cppyy's std::string wrapper into
+                a bytes, otherwise it defaults to a Python str.
+                '''
+                ret = cppyy.gbl.mupdf_raw_to_python_bytes( data, size)
+                ret = bytes( ret)
+                return ret
+
+            # Support for converting a fz_buffer's contents into a Python
+            # bytes.
+            #
+            # We do this by creating a std::string in C++, then in Python
+            # converting the resulting class cppyy.gbl.std.string into a bytes.
+            #
+            # Not sure whether this conversion to bytes involves a second copy
+            # of the data.
+            #
+            cppyy.cppdef( f'''
+                    namespace mupdf
+                    {{
+                        /* Returns std::string containing copy of buffer contents. */
+                        std::string buffer_to_string( const Buffer& buffer, bool clear)
+                        {{
+                            unsigned char* datap;
+                            size_t len = mupdf::mfz_buffer_storage( buffer, &datap);
+                            std::string ret = std::string( (char*) datap, len);
+                            if (clear)
+                            {{
+                                mupdf::mfz_clear_buffer(buffer);
+                                mupdf::mfz_trim_buffer(buffer);
+                            }}
+                            return ret;
+                        }}
+                    }}
+                    ''')
+
+            @replace( 'cppyy.gbl.mupdf.mfz_buffer_extract', 'cppyy.gbl.mupdf.Buffer.buffer_extract')
+            def _( buffer):
+                s = cppyy.gbl.mupdf.buffer_to_string( buffer, clear=True)
+                b = bytes( s)
+                return b
+
+            @insert( 'cppyy.gbl.mupdf.mfz_buffer_extract_copy', 'cppyy.gbl.mupdf.Buffer.buffer_extract_copy')
+            def _( buffer):
+                s = cppyy.gbl.mupdf.buffer_to_string( buffer, clear=False)
+                b = bytes( s)
+                return b
+
+            # Python-friendly mfz_new_buffer_from_copied_data() taking a str.
+            #
+            cppyy.cppdef('''
+                    namespace mupdf
+                    {
+                        Buffer mfz_new_buffer_from_copied_data( const std::string& data)
+                        {
+                            /* Constructing a mupdf::Buffer from a char* ends
+                            up using fz_new_buffer_from_base64(). We want to
+                            use fz_new_buffer_from_data() which can be done by
+                            passing an unsigned char*. */
+                            return mupdf::mfz_new_buffer_from_copied_data(
+                                    (const unsigned char*) data.c_str(),
+                                    data.size()
+                                    );
+                        }
+                    }
+                    ''')
+            cppyy.gbl.mupdf.Buffer.new_buffer_from_copied_data = cppyy.gbl.mupdf.mfz_new_buffer_from_copied_data
+
+
+            # Python-friendly alternative to ppdf_set_annot_color(), taking up
+            # to 4 explicit color args.
+            #
+            cppyy.cppdef('''
+                    void mupdf_pdf_set_annot_color(
+                            mupdf::PdfAnnot& self,
+                            int n,
+                            float color0,
+                            float color1,
+                            float color2,
+                            float color3
+                            )
+                    {
+                        float color[] = { color0, color1, color2, color3 };
+                        return self.set_annot_color( n, color);
+                    }
+                    void mupdf_pdf_set_annot_interior_color(
+                            mupdf::PdfAnnot& self,
+                            int n,
+                            float color0,
+                            float color1,
+                            float color2,
+                            float color3
+                            )
+                    {
+                        float color[] = { color0, color1, color2, color3 };
+                        self.set_annot_interior_color( n, color);
+                    }
+                    void mupdf_mfz_fill_text(
+                            const mupdf::Device& dev,
+                            const mupdf::Text& text,
+                            mupdf::Matrix& ctm,
+                            const mupdf::Colorspace& colorspace,
+                            float color0,
+                            float color1,
+                            float color2,
+                            float color3,
+                            float alpha,
+                            mupdf::ColorParams& color_params
+                            )
+                    {
+                        float color[] = { color0, color1, color2, color3 };
+                        return mupdf::mfz_fill_text( dev, text, ctm, colorspace, color, alpha, color_params);
+                    }
+                    ''')
+            def mupdf_make_colors( color):
+                '''
+                Returns (n, colors) where <colors> is a tuple with 4 items,
+                the first <n> of which are from <color> and the rest are
+                zero.
+                '''
+                if isinstance(color, float):
+                    color = color,
+                assert isinstance( color, ( tuple, list))
+                n = len( color)
+                ret = tuple(color) + (4-n)*(0,)
+                assert len( ret) == 4
+                return n, ret
+
+            @replace( 'cppyy.gbl.mupdf.mpdf_set_annot_color', 'cppyy.gbl.mupdf.PdfAnnot.set_annot_color')
+            def _( pdf_annot, color):
+                n, colors = mupdf_make_colors( color)
+                return cppyy.gbl.mupdf_pdf_set_annot_color( pdf_annot, n, *colors)
+
+            @replace( 'cppyy.gbl.mupdf.mpdf_set_annot_interior_color', 'cppyy.gbl.mupdf.PdfAnnot.set_annot_interior_color')
+            def _( pdf_annot, color):
+                n, colors = mupdf_make_colors( color)
+                cppyy.gbl.mupdf_pdf_set_annot_interior_color( pdf_annot, n, *colors)
+
+            @replace( 'cppyy.gbl.mupdf.mfz_fill_text', 'cppyy.gbl.mupdf.Device.fill_text')
+            def _( dev, text, ctm, colorspace, color, alpha, color_params):
+                _, colors = mupdf_make_colors( color)
+                return cppyy.gbl.mupdf_mfz_fill_text( dev, text, ctm, colorspace, *colors, alpha, color_params)
+
+            # Override cppyy.gbl.mupdf.Document.lookup_metadata() to return a
+            # string or None if not found.
+            #
+            @override( 'cppyy.gbl.mupdf.lookup_metadata', 'cppyy.gbl.mupdf.Document.lookup_metadata')
+            def _(self, key, _original):
+                e = ctypes.c_int(0)
+                ret = _original(self.m_internal, key, e)
+                e = e.value
+                if e < 0:
+                    return None
+                # <ret> will be a cppyy.gbl.std.string, for which str()
+                # returns something that looks like a 'bytes', so
+                # explicitly convert to 'str'.
+                ret = str( ret)
+                return ret
+
+            # Override cppyy.gbl.mupdf.parse_page_range() to distinguish
+            # between returned const char* being null or empty string
+            # - cppyy converts both to an empty string, which means
+            # we can't distinguish between the last range (where
+            # fz_parse_page_range() returns '') and beyond the last range
+            # (where fz_parse_page_range() returns null).
+            #
+            # fz_parse_page_range() leaves the out-params unchanged when it
+            # returns null, so we can detect whether null was returned by
+            # initializing the out-params with special values that would never
+            # be ordinarily be returned.
+            #
+            @override( 'cppyy.gbl.mupdf.parse_page_range', 'cppyy.gbl.mupdf.mfz_parse_page_range')
+            def _(s, n, _original):
+                a = ctypes.c_int(-1)
+                b = ctypes.c_int(-1)
+                s = _original(s, a, b, n)
+                if a.value == -1 and b.value == -1:
+                    s = None
+                return s, a.value, b.value
+
+            # Provide native python implementation of cppyy.gbl.mupdf.format_output_path()
+            # (-> fz_format_output_path). (The underlying C/C++ functions take a fixed-size
+            # buffer for the output string so isn't useful for Python code.)
+            #
+            @replace( 'cppyy.gbl.mupdf.format_output_path', 'cppyy.gbl.mupdf.mfz_format_output_path')
+            def _(format, page):
+                m = re.search( '(%[0-9]*d)', format)
+                if m:
+                    ret = format[ :m.start(1)] + str(page) + format[ m.end(1):]
+                else:
+                    dot = format.rfind( '.')
+                    if dot < 0:
+                        dot = len( format)
+                    ret = format[:dot] + str(page) + format[dot:]
+                return ret
+
+            # Override cppyy.gbl.mupdf.Pixmap.n and cppyy.gbl.mupdf.Pixmap.alpha so
+            # that they return int. (The underlying C++ functions return unsigned char
+            # so cppyy's default bindings end up returning a python string which isn't
+            # useful.)
+            #
+            @override( 'cppyy.gbl.mupdf.Pixmap.n')
+            def _( self, _original):
+                return ord( _original( self))
+
+            @override( 'cppyy.gbl.mupdf.Pixmap.alpha')
+            def _(self, _original):
+                return ord( _original( self))
+
+            # Override cppyy.gbl.mupdf.ppdf_clean_file() so that it takes a Python
+            # container instead of (argc, argv).
+            #
+            @override( 'cppyy.gbl.mupdf.ppdf_clean_file', 'cppyy.gbl.mupdf.mpdf_clean_file')
+            def _(infile, outfile, password, opts, argv, _original):
+                a = 0
+                if argv:
+                    a = (ctypes.c_char_p * len(argv))(*argv)
+                    a = ctypes.pointer(a)
+                _original(infile, outfile, password, opts, len(argv), a)
+
+            # Add cppyy.gbl.mupdf.mpdf_dict_getl() with Python variadic args.
+            #
+            @insert( 'cppyy.gbl.mupdf.mpdf_dict_getl', 'cppyy.gbl.mupdf.PdfObj.dict_getl')
+            def _(obj, *tail):
+                for key in tail:
+                    if not obj.m_internal:
+                        break
+                    obj = obj.dict_get(key)
+                assert isinstance(obj, cppyy.gbl.mupdf.PdfObj)
+                return obj
+
+            # Add cppyy.gbl.mupdf.mpdf_dict_getl() with Python variadic args.
+            #
+            @insert( 'cppyy.gbl.mupdf.mpdf_dict_putl', 'cppyy.gbl.mupdf.PdfObj.dict_putl')
+            def _(obj, val, *tail):
+                if obj.is_indirect():
+                    obj = obj.resolve_indirect_chain()
+                if not obj.is_dict():
+                    raise Exception(f'not a dict: {obj}')
+                if not tail:
+                    return
+                doc = obj.get_bound_document()
+                for key in tail[:-1]:
+                    next_obj = obj.dict_get(key)
+                    if not next_obj.m_internal:
+                        # We have to create entries
+                        next_obj = doc.new_dict(1)
+                        obj.dict_put(key, next_obj)
+                    obj = next_obj
+                key = tail[-1]
+                obj.dict_put(key, val)
+
+            # Raise exception if an attempt is made to call mpdf_dict_putl_drop.
+            #
+            @insert( 'cppyy.gbl.mpdf_dict_putl_drop', 'cppyy.gbl.mupdf.PdfObj.dict_putl_drop')
+            def _(obj, *tail):
+                raise Exception(
+                        'mupdf.PdfObj.dict_putl_drop() is unsupported and unnecessary'
+                        ' in Python because reference counting is automatic.'
+                        ' Instead use mupdf.PdfObj.dict_putl()'
+                        )
+
+            def ppdf_set_annot_color(annot, color):
+                '''
+                Python implementation of pdf_set_annot_color() using
+                ppdf_set_annot_color2().
+                '''
+                if isinstance(color, float):
+                    ppdf_set_annot_color2(annot, 1, color, 0, 0, 0)
+                elif len(color) == 1:
+                    ppdf_set_annot_color2(annot, 1, color[0], 0, 0, 0)
+                elif len(color) == 2:
+                    ppdf_set_annot_color2(annot, 2, color[0], color[1], 0, 0)
+                elif len(color) == 3:
+                    ppdf_set_annot_color2(annot, 3, color[0], color[1], color[2], 0)
+                elif len(color) == 4:
+                    ppdf_set_annot_color2(annot, 4, color[0], color[1], color[2], color[3])
+                else:
+                    raise Exception( f'Unexpected color should be float or list of 1-4 floats: {color}')
+
+            # Python-friendly alternative to fz_runetochar().
+            #
+            cppyy.cppdef(f'''
+                std::vector<unsigned char> mupdf_runetochar2(int rune)
+                {{
+                    std::vector<unsigned char>  buffer(10);
+                    int n = mupdf::runetochar((char*) &buffer[0], rune);
+                    assert(n < sizeof(buffer));
+                    buffer.resize(n);
+                    if (0)
+                    {{
+                        std::cerr << __FUNCTION__ << ": rune=" << rune << ":";
+                        for (auto i: buffer)
+                        {{
+                            std::cerr << ' ' << (int) i;
+                        }}
+                        std::cerr << "\\\\n";
+                    }}
+                    return buffer;
+                }}
+                ''')
+            @insert( 'cppyy.gbl.mupdf.runetochar2', 'cppyy.gbl.mupdf.mfz_runetochar2')
+            def mupdf_runetochar2( rune):
+                vuc = cppyy.gbl.mupdf_runetochar2( rune)
+                ret = bytearray()
+                #jlib.log( '{vuc!r=}')
+                for uc in vuc:
+                    #jlib.log( '{uc!r=}')
+                    ret.append( ord( uc))
+                #jlib.log( '{ret!r=}')
+                return ret
+
+            # Patch mfz_text_language_from_string() to treat str=None as nullptr.
+            #
+            @override( 'cppyy.gbl.mupdf.mfz_text_language_from_string')
+            def _( s, _original):
+                if s is None:
+                    s = ctypes.c_char_p()
+                return _original( s)
+
+            # Python-friendly versions of fz_convert_color(), returning (dv0,
+            # dv1, dv2, dv3).
+            #
+            cppyy.cppdef(f'''
+                    struct mupdf_convert_color2_v
+                    {{
+                        float v0;
+                        float v1;
+                        float v2;
+                        float v3;
+                    }};
+                    void mupdf_convert_color2(
+                            fz_colorspace* ss,
+                            const float* sv,
+                            fz_colorspace* ds,
+                            mupdf_convert_color2_v* dv,
+                            fz_colorspace* is,
+                            fz_color_params params
+                            )
+                    {{
+                        mupdf::convert_color(ss, sv, ds, &dv->v0, is, params);
+                    }}
+                    ''')
+            @replace( 'cppyy.gbl.mupdf.convert_color')
+            def _convert_color( ss, sv, ds, is_, params):
+                # Note that <sv> should be a cppyy representation of a float*.
+                dv = cppyy.gbl.mupdf_convert_color2_v()
+                if is_ is None:
+                    is_ = cppyy.ll.cast[ 'fz_colorspace*']( 0)
+                cppyy.gbl.mupdf_convert_color2( ss, sv, ds, dv, is_, params)
+                return dv.v0, dv.v1, dv.v2, dv.v3
+
+            cppyy.cppdef(f'''
+                    namespace mupdf
+                    {{
+                        std::vector<int> mfz_memrnd2(int length)
+                        {{
+                            std::vector<unsigned char>  ret(length);
+                            mupdf::mfz_memrnd(&ret[0], length);
+                            /* Unlike SWIG, cppyy converts
+                            std::vector<unsigned char> into a string, not a
+                            list of integers. */
+                            std::vector<int>    ret2( ret.begin(), ret.end());
+                            return ret2;
+                        }}
+                    }}
+                    ''')
+
+            # Provide an overload for mfz_recognize_image_format(), because
+            # the default unsigned char p[8] causes problems.
+            #
+            cppyy.cppdef(f'''
+                    namespace mupdf
+                    {{
+                        int mfz_recognize_image_format(const void* p)
+                        {{
+                            int ret = mfz_recognize_image_format( (unsigned char*) p);
+                            return ret;
+                        }}
+                    }}
+                    ''')
+
+            # Wrap mupdf::Pixmap::md5_pixmap() and mupdf::Md5::md5_final2
+            # to make them return a Python 'bytes' instance. The
+            # C++ code returns std::vector<unsigned char> which
+            # SWIG converts into something that can be trivially
+            # converted to a Python 'bytes', but with cppyy it is a
+            # cppyy.gbl.std.vector['unsigned char'] which gives error
+            # "TypeError: 'str' object cannot be interpreted as an integer"
+            # if used to construct a Python 'bytes'.
+            #
+            @override( 'cppyy.gbl.mupdf.Pixmap.md5_pixmap')
+            def _( pixmap, _original):
+                r = _original( pixmap)
+                assert isinstance(r, cppyy.gbl.std.vector['unsigned char'])
+                r = bytes( str( r), 'latin')
+                return r
+
+            @override( 'cppyy.gbl.mupdf.Md5.md5_final2')
+            def _( md5, _original):
+                r = _original( md5)
+                assert isinstance(r, cppyy.gbl.std.vector['unsigned char'])
+                r = bytes( str( r), 'latin')
+                return r
+
+            # Allow cppyy.gbl.mupdf.mfz_md5_update() to be called with a buffer.
+            def mupdf_mfz_md5_update_buffer( md5, buffer):
+                len_, data = buffer.buffer_storage()
+                # <data> will be a void*.
+                data = cppyy.ll.cast[ 'const unsigned char*']( data)
+                return cppyy.gbl.mupdf.mfz_md5_update( md5, data, len_)
+            cppyy.gbl.mupdf.mfz_md5_update_buffer = mupdf_mfz_md5_update_buffer
+
+            # Make a version of mpdf_to_name() that returns a std::string
+            # so that cppyy can wrap it, and make Python wrappers for
+            # mupdf::mpdf_to_name() and mupdf::PdfObj::to_name() use this
+            # new version.
+            #
+            # Otherwise cppyy fails with curious error "TypeError: function
+            # takes exactly 5 arguments (1 given)".
+            #
+            cppyy.cppdef(f'''
+                    namespace mupdf
+                    {{
+                        std::string mpdf_to_name2(const PdfObj& obj)
+                        {{
+                            /* Convert const char* to std::string. */
+                            return mpdf_to_name( obj);
+                        }}
+                    }}
+                    ''')
+
+            def mpdf_to_name( obj):
+                return str( cppyy.gbl.mupdf.mpdf_to_name2( obj))
+            cppyy.gbl.mupdf.mpdf_to_name = mpdf_to_name
+            cppyy.gbl.mupdf.PdfObj.to_name = mpdf_to_name
+
+            # Wrap mfz_new_font_from_*() to convert name=None to name=(const
+            # char*) nullptr.
+            #
+            @override( 'cppyy.gbl.mupdf.mfz_new_font_from_buffer')
+            def _( name, fontfile, index, use_glyph_bbox, _original):
+                if name is None:
+                    name = ctypes.c_char_p()
+                return _original( name, fontfile, index, use_glyph_bbox)
+
+            @override( 'cppyy.gbl.mupdf.mfz_new_font_from_file')
+            def _( name, fontfile, index, use_glyph_bbox, _original):
+                if name is None:
+                    name = ctypes.c_char_p()
+                return _original( name, fontfile, index, use_glyph_bbox)
+
+            @override( 'cppyy.gbl.mupdf.mfz_new_font_from_memory')
+            def _( name, data, len, index, use_glyph_bbox, _original):
+                if name is None:
+                    name = ctypes.c_char_p()
+                return _original( name, data, len, index, use_glyph_bbox)
+
+            # String representation of a fz_font_flags_t, for debugging.
+            #
+            cppyy.cppdef(f'''
+                    std::string mupdf_mfz_font_flags_string( const fz_font_flags_t& ff)
+                    {{
+                        std::stringstream   out;
+                        out << "{{"
+                                << " is_mono=" << ff.is_mono
+                                << " is_serif=" << ff.is_serif
+                                << " is_bold=" << ff.is_bold
+                                << " is_italic=" << ff.is_italic
+                                << " ft_substitute=" << ff.ft_substitute
+                                << " ft_stretch=" << ff.ft_stretch
+                                << " fake_bold=" << ff.fake_bold
+                                << " fake_italic=" << ff.fake_italic
+                                << " has_opentype=" << ff.has_opentype
+                                << " invalid_bbox=" << ff.invalid_bbox
+                                << " cjk=" << ff.cjk
+                                << " cjk_lang=" << ff.cjk_lang
+                                << "}}";
+                        return out.str();
+                    }}
+                    ''')
+
+            # Direct access to fz_font_flags_t::ft_substitute for mupdfpy,
+            # while cppyy doesn't handle bitfields correctly.
+            #
+            cppyy.cppdef(f'''
+                    int mupdf_mfz_font_flags_ft_substitute( const fz_font_flags_t& ff)
+                    {{
+                        return ff.ft_substitute;
+                    }}
+                    ''')
+
+            # Allow mupdfpy to work - requires make_bookmark2() due to SWIG weirdness.
+            #
+            cppyy.gbl.mupdf.make_bookmark2 = cppyy.gbl.mupdf.make_bookmark
+            cppyy.gbl.mupdf.lookup_bookmark2 = cppyy.gbl.mupdf.lookup_bookmark
+
+            """)
+
+    # Add auto-generate out-param wrappers - these modify fn wrappers to return
+    # out-params as tuples.
+    #
+    #text += generated.cppyy_extra
+
+    jlib.fs_ensure_parent_dir( path)
+    jlib.fs_update( text, path)