Exemple #1
0
    def functions_cache_populate(self, tu):
        if tu in self.functions_cache:
            return
        fns = dict()
        global_data = dict()
        enums = dict()

        for cursor in tu.cursor.get_children():
            if cursor.kind == clang.cindex.CursorKind.ENUM_DECL:
                #jlib.log('ENUM_DECL: {cursor.spelling=}')
                enum_values = list()
                for cursor2 in cursor.get_children():
                    #jlib.log('    {cursor2.spelling=}')
                    name = cursor2.spelling
                    #if name.startswith('PDF_ENUM_NAME_'):
                    enum_values.append(name)
                enums[cursor.type.get_canonical().spelling] = enum_values
            if (cursor.linkage == clang.cindex.LinkageKind.EXTERNAL or
                    cursor.is_definition()  # Picks up static inline functions.
                ):
                if cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL:
                    fnname = cursor.mangled_name
                    if self.show_details(fnname):
                        jlib.log('Looking at {fnname=}')
                    if fnname not in omit_fns:
                        fns[fnname] = cursor
                else:
                    global_data[cursor.mangled_name] = cursor

        self.functions_cache[tu] = fns
        self.global_data[tu] = global_data
        self.enums[tu] = enums
        jlib.log('Have populated fns and global_data. {len(enums)=}')
Exemple #2
0
def main():

    argv = sys.argv
    if len( sys.argv) < 2:

        # Use test args.
        for zlib_pdf in (
                os.path.expanduser( '~/mupdf/thirdparty/zlib/zlib.3.pdf'),
                os.path.expanduser( '~/artifex/mupdf/thirdparty/zlib/zlib.3.pdf'),
                ):
            if os.path.isfile( zlib_pdf):
                break
        else:
            raise Exception( 'cannot find zlib.3.pdf')
        for command in [
                f'trace {zlib_pdf}',
                f'convert -o zlib.3.pdf-%d.png {zlib_pdf}',
                f'draw -o zlib.3.pdf-%d.png -s tmf -v -y l -w 150 -R 30 -h 200 {zlib_pdf}',
                f'draw -o zlib.png -R 10 {zlib_pdf}',
                ]:
            if 0:
                # This breaks - looks like <colorspace> gets dropped and *m_internal is freed?
                main2( [None] + command.split())
            else:
                jlib.log( 'running with test command: {command}')
                command = f'{argv[0]} {command}'
                jlib.log( f'running: {command}')
                e = os.system( f'{command}')
                assert not e, f'command failed: {command}'
    else:
        main2( sys.argv)
Exemple #3
0
    def set_dir_so(self, dir_so):
        '''
        Sets self.dir_so and also updates self.cpp_flags etc.
        '''
        dir_so = abspath(dir_so)
        self.dir_so = dir_so

        if 0: pass  # lgtm [py/unreachable-statement]
        elif '-debug' in dir_so: self.cpp_flags = '-g'
        elif '-release' in dir_so: self.cpp_flags = '-O2 -DNDEBUG'
        elif '-memento' in dir_so: self.cpp_flags = '-g -DMEMENTO'
        else:
            self.cpp_flags = None
            jlib.log(
                'Warning: unrecognised {dir_so=}, so cannot determine cpp_flags'
            )

        # Set self.cpu and self.python_version.
        if state_.windows:
            # Infer from self.dir_so.
            m = re.match('shared-([a-z]+)(-(x[0-9]+))?(-py([0-9.]+))?$',
                         os.path.basename(self.dir_so))
            #log(f'self.dir_so={self.dir_so} {os.path.basename(self.dir_so)} m={m}')
            assert m, f'Failed to parse dir_so={self.dir_so!r} - should be *-x32|x64-pyA.B'
            self.cpu = Cpu(m.group(3))
            self.python_version = m.group(5)
            #log('{self.cpu=} {self.python_version=} {dir_so=}')
        else:
            # Use Python we are running under.
            self.cpu = Cpu(cpu_name())
            self.python_version = python_version()
Exemple #4
0
def dump_ast(cursor, depth=0):
    indent = depth * 4 * ' '
    for cursor2 in cursor.get_children():
        jlib.log(
            indent * ' ' +
            '{cursor2.kind=} {cursor2.mangled_name=} {cursor2.displayname=} {cursor2.spelling=}'
        )
        dump_ast(cursor2, depth + 1)
Exemple #5
0
def main2( argv):
    arg1 = argv[1]
    fn = getattr( sys.modules[__name__], arg1, None)
    if not fn:
        print( f'cannot find {arg1}')
        usage()
        sys.exit(1)

    fn( argv[2:])
    jlib.log( 'finished')
Exemple #6
0
 def method( self, structname, fnname):
     if structname.startswith( 'fz_'):
         ret = clip( fnname, ('fz_', 'pdf_'))
         if ret in ('stdin', 'stdout', 'stderr'):
             jlib.log( 'appending underscore to {ret=}')
             ret += '_'
         return ret
     if structname.startswith( 'pdf_'):
         return clip( fnname, ('fz_', 'pdf_'))
     assert 0, f'unrecognised structname={structname}'
Exemple #7
0
def test(test_command, package_name, wheels, abis, pypi, pypi_test, py):
    '''
    If on Windows and <py> is false, we run test() with a python for
    each wheel in <wheels>.

    test_command:
        .
    package_name:
        Can be None if we are using <wheels>.
    wheels:
        List of wheels, used if pypi is false.
    pypi:
        If true, install from pypi.org or test.pypi.org if pypi_test is true.
    pypi_test:
        If true, pypi installs from test.pypi.org.
    py:
        Python command. If None, on Windows we test ABI from each wheel,
        otherwise we use native python.
    '''
    if not py and windows():
        if wheels:
            # Test with each wheel.
            log('Testing for python implied by each wheel.')
            for wheel in wheels:
                name, version, py, none, cpu = parse_wheel(wheel)
                pyv = f'{py[2]}.{py[3]}'
                cpu_bits = 64 if cpu == 'win_amd64' else 32
                py = f'py -{pyv}-{cpu_bits}'
                if package_name is None:
                    package_name = name
                if pypi:
                    test_pypi(test_command, package_name, pypi_test, py)
                else:
                    test_local(test_command, wheels, py)
        else:
            assert pypi, f'No wheels specified; need to use *pypi.org.'
            assert package_name, f'No wheels specified; need package_name.'
            # Test with each ABI.
            for abi in abis:
                cpu, python_version, py = windows_python_from_abi(abi)
                jlib.log('Testing with {package_name=} {pypi_test=} {py=}')
                test_pypi(test_command, package_name, pypi_test, py)
    else:
        if not py:
            if windows():
                log('Using default python "py".')
                py = 'py'
            else:
                log('Using default python {sys.executable=}.')
                py = sys.executable
        if pypi:
            test_pypi(test_command, package_name, pypi_test, py)
        else:
            test_local(test_command, wheels, py)
Exemple #8
0
def find_wrappable_function_with_arg0_type(tu, structname):
    '''
    Return list of fz_*() function names which could be wrapped as a method of
    our wrapper class for <structname>.

    The functions whose names we return, satisfy all of the following:

        First non-context param is <structname> (by reference, pointer or value).

        If return type is a fz_* struc (by reference, pointer or value), the
        corresponding wrapper class has a raw constructor.
    '''
    find_wrappable_function_with_arg0_type_cache_populate(tu)

    ret = find_wrappable_function_with_arg0_type_cache.get(structname, [])
    if state.state_.show_details(structname):
        jlib.log('{structname=}: {len(ret)=}:')
        for i in ret:
            jlib.log('    {i}')
    return ret
Exemple #9
0
def update_file_regress(text, filename, check_regression):
    '''
    Behaves like jlib.update_file(), but if check_regression is true and
    <filename> already exists with different content from <text>, we show a
    diff and raise an exception.
    '''
    text_old = jlib.update_file(text, filename, check_regression)
    if text_old:
        jlib.log(
            'jlib.update_file() => {len(text_old)=}. {filename=} {check_regression}'
        )
    if check_regression:
        if text_old is not None:
            # Existing content differs and <check_regression> is true.
            with open(f'{filename}-2', 'w') as f:
                f.write(text)
            jlib.log('Output would have changed: {filename}')
            jlib.system(f'diff -u {filename} {filename}-2',
                        verbose=True,
                        raise_errors=False,
                        prefix=f'diff {os.path.relpath(filename)}: ',
                        out='log')
            return Exception(f'Output would have changed: {filename}')
        else:
            jlib.log('Generated file unchanged: {filename}')
def test_internal(test_command, package_name, pip_install_arg, py):
    if test_command == '.':
        test_command = ''

    jlib.log('Testing {package_name=} {pip_install_arg=} {py=}')
    if not test_command:
        test_command = 'pypackage_test.py'
        code = (f'import {package_name}\n'
                f'print("Successfully imported {package_name}")\n')
        log(f'Testing default code for package_name={package_name}:\n{code}')
        with open(test_command, 'w') as f:
            f.write(code)

    venv_run(
        [
            f'pip install {pip_install_arg}',
            f'python {test_command}',
        ],
        venv='pypackage-venv-test',
        py=py,
        clean=True,
    )
Exemple #11
0
def is_pointer_to(type_, destination, verbose=False):
    '''
    Returns true if <type> is a pointer to <destination>.

    We do this using text for <destination>, rather than a clang.cindex.Type
    or clang.cindex.Cursor, so that we can represent base types such as int or
    char without having clang parse system headers. This involves stripping any
    initial 'struct ' text.

    Also, clang's representation of mupdf's varying use of typedef, struct and
    forward-declarations is rather difficult to work with directly.

    type_:
        A clang.cindex.Type.
    destination:
        Text typename.
    '''
    # Use cache - reduces time from 0.6s to 0.2.
    #
    key = type_.spelling, destination
    ret = is_pointer_to_cache.get(key)
    if ret is None:
        assert isinstance(type_, clang.cindex.Type)
        ret = None
        destination = util.clip(destination, 'struct ')
        if verbose:
            jlib.log('{type_.kind=}')
        if type_.kind == clang.cindex.TypeKind.POINTER:
            pointee = type_.get_pointee().get_canonical()
            d = cpp.declaration_text(pointee, '')
            d = util.clip(d, 'const ')
            d = util.clip(d, 'struct ')
            if verbose:
                jlib.log('{destination!r=} {d!r=}')
            ret = d == f'{destination} ' or d == f'const {destination} '
        is_pointer_to_cache[key] = ret

    return ret
Exemple #12
0
def trace( argv):

    password = ''
    layout_w = mupdf.FZ_DEFAULT_LAYOUT_W
    layout_h = mupdf.FZ_DEFAULT_LAYOUT_H
    layout_em = mupdf.FZ_DEFAULT_LAYOUT_EM
    layout_css = None
    layout_use_doc_css = 1

    use_display_list = 0

    argv_i = 0
    while 1:
        jlib.log( '{argv_i=} {argv[ argv_i]=}')
        arg = argv[ argv_i]
        if arg == '-p':
            password = next( opt)
        elif arg == '-W':
            argv_i += 1
            layout_w = float( argv[argv_i])
        elif arg == '-H':
            argv_i += 1
            layout_h = float( argv[argv_i])
        elif arg == '-S':
            argv_i += 1
            layout_em = float( argv[argv_i])
        elif arg == '-U':
            argv_i += 1
            layout_css = argv[argv_i]
        elif arg == '-X':
            layout_use_doc_css = 0
        elif arg == '-d':
            use_display_list = 1
        else:
            break
        argv_i += 1

    #jlib.log( '{argv_i=} {len( argv)=}')
    if argv_i == len( argv):
        trace_usage()

    if layout_css:
        buffer_ = mupdf.Buffer( layout_css)
        mupdf.mupdf_set_user_css( buffer_.string_from_buffer())

    mupdf.set_use_document_css( layout_use_doc_css)

    for argv_i in range( argv_i, len( argv)):
        arg = argv[ argv_i]
        doc = mupdf.Document( arg)
        if doc.needs_password():
            doc.authenticate_password( password)
        doc.layout_document( layout_w, layout_h, layout_em)
        print( f'<document filename="{arg}">')
        count = doc.count_pages()
        if argv_i + 1 < len( argv) and mupdf.is_page_range( argv[ argv_i+1]):
            argv_i += 1
            trace_runrange( use_display_list, doc, count, argv[ argv_i])
        else:
            trace_runrange( use_display_list, doc, count, '1-N')
        print( '<document>')
Exemple #13
0
def build_swig(
    state_,
    build_dirs,
    generated,
    language='python',
    swig_command='swig',
    check_regress=False,
    force_rebuild=False,
):
    '''
    Builds python or C# wrappers for all mupdf_* functions and classes, by
    creating a .i file that #include's our generated C++ header files and
    running swig.

    build_dirs
        A BuildDirs instance.
    generated.
        A Generated instance.
    language
        The output language, must be 'python' or 'csharp'.
    swig
        Location of swig binary.
    check_regress
        If true, we fail with error if generated .i file already exists and
        differs from our new content.
    '''
    assert isinstance(state_, state.State)
    jlib.log('{=build_dirs type(build_dirs)}')
    assert isinstance(build_dirs, state.BuildDirs), type(build_dirs)
    assert isinstance(generated, cpp.Generated), type(generated)
    assert language in ('python', 'csharp')
    # Find version of swig. (We use quotes around <swig> to make things work on
    # Windows.)
    try:
        t = jlib.system(f'"{swig_command}" -version', out='return')
    except Exception as e:
        if state_.windows:
            raise Exception(
                'swig failed; on Windows swig can be auto-installed with: --swig-windows-auto'
            ) from e
        else:
            raise
    m = re.search('SWIG Version ([0-9]+)[.]([0-9]+)[.]([0-9]+)', t)
    assert m
    swig_major = int(m.group(1))

    # Create a .i file for SWIG.
    #
    common = f'''
            #include <stdexcept>

            #include "mupdf/functions.h"
            #include "mupdf/classes.h"
            #include "mupdf/classes2.h"
            '''
    if language == 'csharp':
        common += textwrap.dedent(f'''
                /* This is required otherwise compiling the resulting C++ code
                fails with:
                    error: use of undeclared identifier 'SWIG_fail'

                But no idea whether it is the 'correct' thing to do; seems odd
                that SWIG doesn't define SWIG_fail itself.
                */
                #define SWIG_fail throw std::runtime_error( e.what());
                ''')

    if language == 'python':
        common += textwrap.dedent(f'''
                /* Support for extracting buffer data into a Python bytes. */
                PyObject* buffer_extract_bytes(fz_buffer* buffer)
                {{
                    unsigned char* c = NULL;
                    /* We mimic the affects of fz_buffer_extract(), which leaves
                    the buffer with zero capacity. */
                    size_t len = mupdf::buffer_storage(buffer, &c);
                    PyObject* ret = PyBytes_FromStringAndSize((const char*) c, (Py_ssize_t) len);
                    if (ret) {{
                        mupdf::clear_buffer(buffer);
                        mupdf::trim_buffer(buffer);
                    }}
                    return ret;
                }}

                /* Creates Python bytes from copy of raw data. */
                PyObject* raw_to_python_bytes(const unsigned char* c, size_t len)
                {{
                    return PyBytes_FromStringAndSize((const char*) c, (Py_ssize_t) len);
                }}

                /* Creates Python bytes from copy of raw data. */
                PyObject* raw_to_python_bytes(const void* c, size_t len)
                {{
                    return PyBytes_FromStringAndSize((const char*) c, (Py_ssize_t) len);
                }}

                /* The SWIG wrapper for this function returns a SWIG proxy for
                a 'const unsigned char*' pointing to the raw data of a python
                bytes. This proxy can then be passed from Python to functions
                that take a 'const unsigned char*'.

                For example to create a MuPDF fz_buffer* from a copy of a
                Python bytes instance:
                    bs = b'qwerty'
                    buffer_ = mupdf.new_buffer_from_copied_data(mupdf.python_bytes_data(bs), len(bs))
                */
                const unsigned char* python_bytes_data(const unsigned char* PYTHON_BYTES_DATA, size_t PYTHON_BYTES_SIZE)
                {{
                    return PYTHON_BYTES_DATA;
                }}

                /* 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;
                }}

                /* SWIG-friendly alternative to ppdf_set_annot_color(). */
                void ppdf_set_annot_color2(pdf_annot *annot, int n, float color0, float color1, float color2, float color3)
                {{
                    float color[] = {{ color0, color1, color2, color3 }};
                    return mupdf::ppdf_set_annot_color(annot, n, color);
                }}


                /* SWIG-friendly alternative to ppdf_set_annot_color(). */
                void ppdf_set_annot_interior_color2(pdf_annot *annot, int n, float color0, float color1, float color2, float color3)
                {{
                    float color[] = {{ color0, color1, color2, color3 }};
                    return mupdf::ppdf_set_annot_color(annot, n, color);
                }}

                /* SWIG-friendly alternative to mfz_fill_text(). */
                void mfz_fill_text2(
                        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 mfz_fill_text(dev, text, ctm, colorspace, color, alpha, color_params);
                }}

                std::vector<unsigned char> mfz_memrnd2(int length)
                {{
                    std::vector<unsigned char>  ret(length);
                    mupdf::mfz_memrnd(&ret[0], length);
                    return ret;
                }}
                ''')

    common += textwrap.dedent(f'''
            /* SWIG-friendly alternative to fz_runetochar(). */
            std::vector<unsigned char> runetochar2(int rune)
            {{
                std::vector<unsigned char>  buffer(10);
                int n = mupdf::runetochar((char*) &buffer[0], rune);
                assert(n < sizeof(buffer));
                buffer.resize(n);
                return buffer;
            }}

            /* SWIG-friendly alternatives to fz_make_bookmark() and
            fz_lookup_bookmark(), using long long instead of fz_bookmark
            because SWIG appears to treat fz_bookmark as an int despite it
            being a typedef for intptr_t, so ends up slicing. */
            long long unsigned make_bookmark2(fz_document* doc, fz_location loc)
            {{
                fz_bookmark bm = mupdf::make_bookmark(doc, loc);
                return (long long unsigned) bm;
            }}
            long long unsigned mfz_make_bookmark2(fz_document* doc, fz_location loc)
            {{
                return make_bookmark2(doc, loc);
            }}

            fz_location lookup_bookmark2(fz_document *doc, long long unsigned mark)
            {{
                return mupdf::lookup_bookmark(doc, (fz_bookmark) mark);
            }}
            fz_location mfz_lookup_bookmark2(fz_document *doc, long long unsigned mark)
            {{
                return lookup_bookmark2(doc, mark);
            }}

            struct convert_color2_dv
            {{
                float dv0;
                float dv1;
                float dv2;
                float dv3;
            }};

            /* SWIG-friendly alternative for fz_convert_color(). */
            void convert_color2(
                    fz_colorspace *ss,
                    const float *sv,
                    fz_colorspace *ds,
                    convert_color2_dv* dv,
                    fz_colorspace *is,
                    fz_color_params params
                    )
            {{
                mupdf::convert_color(ss, sv, ds, &dv->dv0, is, params);
            }}

            /* SWIG-friendly support for fz_set_warning_callback() and
            fz_set_error_callback(). */

            struct SetWarningCallback
            {{
                SetWarningCallback( void* user=NULL)
                {{
                    this->user = user;
                    mupdf::set_warning_callback( s_print, this);
                }}
                virtual void print( const char* message)
                {{
                }}
                static void s_print( void* self0, const char* message)
                {{
                    SetWarningCallback* self = (SetWarningCallback*) self0;
                    return self->print( message);
                }}
                void* user;
            }};

            struct SetErrorCallback
            {{
                SetErrorCallback( void* user=NULL)
                {{
                    this->user = user;
                    mupdf::set_error_callback( s_print, this);
                }}
                virtual void print( const char* message)
                {{
                }}
                static void s_print( void* self0, const char* message)
                {{
                    SetErrorCallback* self = (SetErrorCallback*) self0;
                    return self->print( message);
                }}
                void* user;
            }};
            ''')

    common += generated.swig_cpp
    common += translate_ucdn_macros(build_dirs)

    text = ''

    if state_.windows:
        # 2022-02-24: Director classes break Windows builds at the moment.
        pass
    else:
        text += '%module(directors="1") mupdf\n'
        for i in generated.virtual_fnptrs:
            text += f'%feature("director") {i};\n'

        text += f'%feature("director") SetWarningCallback;\n'
        text += f'%feature("director") SetErrorCallback;\n'

        text += textwrap.dedent('''
                %feature("director:except")
                {
                  if ($error != NULL)
                  {
                    throw Swig::DirectorMethodException();
                  }
                }
                ''')
    for fnname in generated.c_functions:
        if fnname in ('pdf_annot_type', 'pdf_widget_type'):
            # These are also enums which we don't want to ignore. SWIGing the
            # functions is hopefully harmless.
            pass
        elif 0 and fnname == 'pdf_string_from_annot_type':  # causes duplicate symbol with classes2.cpp and python.
            pass
        else:
            text += f'%ignore {fnname};\n'

    for i in (
            'fz_append_vprintf',
            'fz_error_stack_slot',
            'fz_format_string',
            'fz_vsnprintf',
            'fz_vthrow',
            'fz_vwarn',
            'fz_write_vprintf',
    ):
        text += f'%ignore {i};\n'
        text += f'%ignore m{i};\n'

    text += textwrap.dedent(f'''
            // Not implemented in mupdf.so: fz_colorspace_name_process_colorants
            %ignore fz_colorspace_name_process_colorants;

            %ignore fz_open_file_w;

            %ignore {util.rename.function('fz_append_vprintf')};
            %ignore {util.rename.function('fz_error_stack_slot_s')};
            %ignore {util.rename.function('fz_format_string')};
            %ignore {util.rename.function('fz_vsnprintf')};
            %ignore {util.rename.function('fz_vthrow')};
            %ignore {util.rename.function('fz_vwarn')};
            %ignore {util.rename.function('fz_write_vprintf')};
            %ignore {util.rename.function('fz_vsnprintf')};
            %ignore {util.rename.function('fz_vthrow')};
            %ignore {util.rename.function('fz_vwarn')};
            %ignore {util.rename.function('fz_append_vprintf')};
            %ignore {util.rename.function('fz_write_vprintf')};
            %ignore {util.rename.function('fz_format_string')};
            %ignore {util.rename.function('fz_open_file_w')};

            // SWIG can't handle this because it uses a valist.
            %ignore {util.rename.function('Memento_vasprintf')};

            // asprintf() isn't available on windows, so exclude Memento_asprintf because
            // it is #define-d to asprintf.
            %ignore {util.rename.function('Memento_asprintf')};

            // Might prefer to #include mupdf/exceptions.h and make the
            // %exception block below handle all the different exception types,
            // but swig-3 cannot parse 'throw()' in mupdf/exceptions.h.
            //
            // So for now we just #include <stdexcept> and handle
            // std::exception only.

            %include "typemaps.i"
            %include "cpointer.i"

            // This appears to allow python to call fns taking an int64_t.
            %include "stdint.i"

            %{{
            {common}
            %}}

            %include exception.i
            %include std_string.i
            %include carrays.i
            %include cdata.i
            %include std_vector.i
            {"%include argcargv.i" if language=="python" else ""}

            %array_class(unsigned char, uchar_array);

            %include <cstring.i>
            %cstring_output_allocate(char **OUTPUT, free($1));

            namespace std
            {{
                %template(vectoruc) vector<unsigned char>;
                %template(vectori) vector<int>;
                %template(vectors) vector<std::string>;
                %template(vectorq) vector<mupdf::{util.rename.class_("fz_quad")}>;
            }};

            // Make sure that operator++() gets converted to __next__().
            //
            // Note that swig already seems to do:
            //
            //     operator* => __ref__
            //     operator== => __eq__
            //     operator!= => __ne__
            //     operator-> => __deref__
            //
            // Just need to add this method to containers that already have
            // begin() and end():
            //     def __iter__( self):
            //         return CppIterator( self)
            //

            %rename(__increment__) *::operator++;


            %array_functions(unsigned char, bytes);
            ''')

    text += textwrap.dedent(f'''
            %exception {{
                try {{
                    $action
                }}
            ''')
    if not state_.windows:  # Directors not currently supported on Windows.
        text += textwrap.dedent(f'''
                catch (Swig::DirectorException &e) {{
                    SWIG_fail;
                }}
                ''')
    text += textwrap.dedent(f'''
            catch(std::exception& e) {{
                SWIG_exception(SWIG_RuntimeError, e.what());
            }}
            catch(...) {{
                    SWIG_exception(SWIG_RuntimeError, "Unknown exception");
                }}
            }}
            ''')

    text += textwrap.dedent(f'''
            // Ensure SWIG handles OUTPUT params.
            //
            %include "cpointer.i"

            // Don't wrap raw fz_*() functions.
            %rename("$ignore", regexmatch$name="^fz_", %$isfunction, %$not %$ismember) "";
            ''')

    if swig_major < 4:
        text += textwrap.dedent(f'''
                // SWIG version is less than 4 so swig is not able to copy
                // across comments from header file into generated code. The
                // next best thing is to use autodoc to make swig at least show
                // some generic information about arg types.
                //
                %feature("autodoc", "3");
                ''')

    text += textwrap.dedent(f'''
            // Tell swig about pdf_clean_file()'s (int,argv)-style args:
            %apply (int ARGC, char **ARGV) {{ (int retainlen, char *retainlist[]) }}
            ''')

    if language == 'python':
        text += textwrap.dedent('''
                %include pybuffer.i

                /* Convert Python bytes to (const unsigned char*, size_t) pair
                for python_bytes_data(). */
                %pybuffer_binary(const unsigned char* PYTHON_BYTES_DATA, size_t PYTHON_BYTES_SIZE);
                ''')

    text += common

    if language == 'python':
        text += textwrap.dedent(f'''
                %pointer_functions(int, pint);

                %pythoncode %{{

                def Document_lookup_metadata(self, key):
                    """
                    Python implementation override of Document.lookup_metadata().

                    Returns string or None if not found.
                    """
                    e = new_pint()
                    ret = lookup_metadata(self.m_internal, key, e)
                    e = pint_value(e)
                    if e < 0:
                        return None
                    return ret

                Document.lookup_metadata = Document_lookup_metadata

                def PdfDocument_lookup_metadata(self, key):
                    """
                    Python implementation override of PdfDocument.lookup_metadata().

                    Returns string or None if not found.
                    """
                    e = new_pint()
                    ret = ppdf_lookup_metadata(self.m_internal, key, e)
                    e = pint_value(e)
                    if e < 0:
                        return None
                    return ret

                PdfDocument.lookup_metadata = PdfDocument_lookup_metadata
                ''')

    if language == 'python':
        # Make some additions to the generated Python module.
        #
        # E.g. python wrappers for functions that take out-params should return
        # tuples.
        #
        text += generated.swig_python
        text += textwrap.dedent('''
                import re

                # Wrap parse_page_range() to fix SWIG bug where a NULL return
                # value seems to mess up the returned list - we end up with ret
                # containing two elements rather than three, e.g. [0, 2]. This
                # occurs with SWIG-3.0; maybe fixed in SWIG-4?
                #
                w_parse_page_range = parse_page_range
                def parse_page_range(s, n):
                    ret = w_parse_page_range(s, n)
                    if len(ret) == 2:
                        return None, 0, 0
                    else:
                        return ret[0], ret[1], ret[2]

                # Provide native python implementation of format_output_path() (->
                # fz_format_output_path).
                #
                def format_output_path( 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

                class IteratorWrap:
                    """
                    This is a Python iterator for containers that have C++-style
                    begin() and end() methods that return iterators.

                    Iterators must have the following methods:

                        __increment__(): move to next item in the container.
                        __ref__(): return reference to item in the container.

                    Must also be able to compare two iterators for equality.

                    """
                    def __init__( self, container):
                        self.container = container
                        self.pos = None
                        self.end = container.end()
                    def __iter__( self):
                        return self
                    def __next__( self):    # for python2.
                        if self.pos is None:
                            self.pos = self.container.begin()
                        else:
                            self.pos.__increment__()
                        if self.pos == self.end:
                            raise StopIteration()
                        return self.pos.__ref__()
                    def next( self):    # for python3.
                        return self.__next__()

                # The auto-generated Python class method Buffer.buffer_extract()
                # returns (size, data).
                #
                # But these raw values aren't particularly useful to Python code so
                # we change the method to return a Python bytes instance instead,
                # using the special C function buffer_storage_bytes() defined
                # above.
                #
                # We make the original method available as
                # Buffer.buffer_extract_raw(); this can be used to create a
                # mupdf.Stream by passing the raw values back to C++ with:
                #
                #   data, size = buffer_.buffer_extract_raw()
                #   stream = mupdf.Stream(data, size))
                #
                # We don't provide a similar wrapper for Buffer.buffer_storage()
                # because we can't create a Python bytes object that
                # points into the buffer's storage. We still provide
                # Buffer.buffer_storage_raw() just in case there is a need for
                # Python code that can pass the raw (data, size) back in to C.
                #

                Buffer.buffer_extract_raw = Buffer.buffer_extract

                def Buffer_buffer_extract(self):
                    """
                    Returns buffer data as a Python bytes instance, leaving the
                    buffer empty. Note that this will make a copy of the underlying
                    data.
                    """
                    return buffer_extract_bytes(self.m_internal)

                Buffer.buffer_extract = Buffer_buffer_extract

                Buffer.buffer_storage_raw = Buffer.buffer_storage
                #delattr(Buffer, 'buffer_storage')
                def Buffer_buffer_storage(self):
                    raise Exception("Buffer.buffer_storage() is not available; use Buffer.buffer_storage_raw() to get (size, data) where <data> is SWIG wrapper for buffer's 'unsigned char*' storage")
                Buffer.buffer_storage = Buffer_buffer_storage


                # Overwrite Buffer.new_buffer_from_copied_data() to take Python Bytes instance.
                #
                def Buffer_new_buffer_from_copied_data(bytes_):
                    buffer_ = new_buffer_from_copied_data(python_bytes_data(bytes_), len(bytes_))
                    return Buffer(buffer_)
                Buffer.new_buffer_from_copied_data = Buffer_new_buffer_from_copied_data


                def mpdf_dict_getl(obj, *tail):
                    """
                    Python implementation of pdf_dict_getl(fz_context *ctx,
                    pdf_obj *obj, ...), because SWIG doesn't handle variadic
                    args.
                    """
                    for key in tail:
                        if not obj.m_internal:
                            break
                        obj = obj.dict_get(key)
                    assert isinstance(obj, PdfObj)
                    return obj
                PdfObj.dict_getl = mpdf_dict_getl

                def mpdf_dict_putl(obj, val, *tail):
                    """
                    Python implementation of pdf_dict_putl(fz_context *ctx,
                    pdf_obj *obj, pdf_obj *val, ...) because SWIG doesn't
                    handle variadic args.
                    """
                    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)
                PdfObj.dict_putl = mpdf_dict_putl

                def mpdf_dict_putl_drop(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()')
                PdfObj.dict_putl_drop = mpdf_dict_putl_drop

                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}')

                # Override PdfAnnot.set_annot_color() to use the above.
                def mpdf_set_annot_color(self, color):
                    return ppdf_set_annot_color(self.m_internal, color)
                PdfAnnot.set_annot_color = mpdf_set_annot_color

                def ppdf_set_annot_interior_color(annot, color):
                    """
                    Python version of pdf_set_annot_color() using
                    ppdf_set_annot_color2().
                    """
                    if isinstance(color, float):
                        ppdf_set_annot_interior_color2(annot, 1, color, 0, 0, 0)
                    elif len(color) == 1:
                        ppdf_set_annot_interior_color2(annot, 1, color[0], 0, 0, 0)
                    elif len(color) == 2:
                        ppdf_set_annot_interior_color2(annot, 2, color[0], color[1], 0, 0)
                    elif len(color) == 3:
                        ppdf_set_annot_interior_color2(annot, 3, color[0], color[1], color[2], 0)
                    elif len(color) == 4:
                        ppdf_set_annot_interior_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}')

                # Override PdfAnnot.set_interiorannot_color() to use the above.
                def mpdf_set_annot_interior_color(self, color):
                    return ppdf_set_annot_interior_color(self.m_internal, color)
                PdfAnnot.set_annot_interior_color = mpdf_set_annot_interior_color

                # Override mfz_fill_text() to handle color as a Python tuple/list.
                def mfz_fill_text(dev, text, ctm, colorspace, color, alpha, color_params):
                    """
                    Python version of mfz_fill_text() using mfz_fill_text2().
                    """
                    color = tuple(color) + (0,) * (4-len(color))
                    assert len(color) == 4, f'color not len 4: len={len(color)}: {color}'
                    return mfz_fill_text2(dev, text, ctm, colorspace, *color, alpha, color_params)

                Device.fill_text = mfz_fill_text

                # Override set_warning_callback() and set_error_callback() to
                # use Python classes derived from our SWIG Director classes
                # SetWarningCallback and SetErrorCallback (defined in C), so
                # that fnptrs can call Python code.
                #
                set_warning_callback_s = None
                set_error_callback_s = None

                def set_warning_callback2( printfn):
                    class Callback( SetWarningCallback):
                        def print( self, message):
                            printfn( message)
                    global set_warning_callback_s
                    set_warning_callback_s = Callback()

                # Override set_error_callback().
                def set_error_callback2( printfn):
                    class Callback( SetErrorCallback):
                        def print( self, message):
                            printfn( message)
                    global set_error_callback_s
                    set_error_callback_s = Callback()

                set_warning_callback = set_warning_callback2
                set_error_callback = set_error_callback2
                ''')

        # Add __iter__() methods for all classes with begin() and end() methods.
        #
        for classname in generated.container_classnames:
            text += f'{classname}.__iter__ = lambda self: IteratorWrap( self)\n'

        # For all wrapper classes with a to_string() method, add a __str__()
        # method to the underlying struct's Python class, which calls
        # to_string_<structname>().
        #
        # E.g. this allows Python code to print a mupdf.fz_rect instance.
        #
        # [We could instead call our generated to_string() and rely on overloading,
        # but this will end up switching on the type in the SWIG code.]
        #
        for struct_name in generated.to_string_structnames:
            text += f'{struct_name}.__str__ = lambda s: to_string_{struct_name}(s)\n'

        # For all wrapper classes with a to_string() method, add a __str__() method
        # to the Python wrapper class, which calls the class's to_string() method.
        #
        # E.g. this allows Python code to print a mupdf.Rect instance.
        #
        for struct_name in generated.to_string_structnames:
            text += f'{util.rename.class_(struct_name)}.__str__ = lambda self: self.to_string()\n'

        text += '%}\n'

    if 1:  # lgtm [py/constant-conditional-expression]
        # This is a horrible hack to avoid swig failing because
        # include/mupdf/pdf/object.h defines an enum which contains a #include.
        #
        # Would like to pre-process files in advance so that swig doesn't see
        # the #include, but this breaks swig in a different way - swig cannot
        # cope with some code in system headers.
        #
        # So instead we copy include/mupdf/pdf/object.h into
        # {build_dirs.dir_mupdf}/platform/python/include/mupdf/pdf/object.h,
        # manually expanding the #include using a Python .replace() call. Then
        # we specify {build_dirs.dir_mupdf}/platform/python/include as the
        # first include path so that our modified mupdf/pdf/object.h will get
        # included in preference to the original.
        #
        os.makedirs(
            f'{build_dirs.dir_mupdf}/platform/python/include/mupdf/pdf',
            exist_ok=True)
        with open(f'{build_dirs.dir_mupdf}/include/mupdf/pdf/object.h') as f:
            o = f.read()
        with open(
                f'{build_dirs.dir_mupdf}/include/mupdf/pdf/name-table.h') as f:
            name_table_h = f.read()
        oo = o.replace('#include "mupdf/pdf/name-table.h"\n', name_table_h)
        assert oo != o
        jlib.update_file(
            oo,
            f'{build_dirs.dir_mupdf}/platform/python/include/mupdf/pdf/object.h'
        )

    swig_i = f'{build_dirs.dir_mupdf}/platform/{language}/mupdfcpp_swig.i'
    include1 = f'{build_dirs.dir_mupdf}/include/'
    include2 = f'{build_dirs.dir_mupdf}/platform/c++/include'
    swig_cpp = f'{build_dirs.dir_mupdf}/platform/{language}/mupdfcpp_swig.cpp'
    swig_py = f'{build_dirs.dir_so}/mupdf.py'

    os.makedirs(f'{build_dirs.dir_mupdf}/platform/{language}', exist_ok=True)
    os.makedirs(f'{build_dirs.dir_so}', exist_ok=True)
    util.update_file_regress(text, swig_i, check_regress)

    # Try to disable some unhelpful SWIG warnings;. unfortunately this doesn't
    # seem to have any effect.
    disable_swig_warnings = [
        201,  # Warning 201: Unable to find 'stddef.h'
        314,  # Warning 314: 'print' is a python keyword, renaming to '_print'
        312,  # Warning 312: Nested union not currently supported (ignored).
        321,  # Warning 321: 'max' conflicts with a built-in name in python
        362,  # Warning 362: operator= ignored
        451,  # Warning 451: Setting a const char * variable may leak memory.
        503,  # Warning 503: Can't wrap 'operator <<' unless renamed to a valid identifier.
        512,  # Warning 512: Overloaded method mupdf::DrawOptions::internal() const ignored, using non-const method mupdf::DrawOptions::internal() instead.
    ]
    disable_swig_warnings = map(str, disable_swig_warnings)
    disable_swig_warnings = '-w' + ','.join(disable_swig_warnings)

    if language == 'python':
        # Need -D_WIN32 on Windows because as of 2022-03-17, C++ code for
        # SWIG Directors support doesn't work on Windows so is inside #ifndef
        # _WIN32...#endif.
        #
        # Maybe use '^' on windows as equivalent to unix '\\' for multiline
        # ending?
        command = (textwrap.dedent(f'''
                "{swig_command}"
                    {"-D_WIN32" if state_.windows else ""}
                    -Wall
                    -c++
                    {"-doxygen" if swig_major >= 4 else ""}
                    -python
                    {disable_swig_warnings}
                    -module mupdf
                    -outdir {os.path.relpath(build_dirs.dir_so)}
                    -o {os.path.relpath(swig_cpp)}
                    -includeall
                    -I{os.path.relpath(build_dirs.dir_mupdf)}/platform/python/include
                    -I{os.path.relpath(include1)}
                    -I{os.path.relpath(include2)}
                    -ignoremissing
                    {os.path.relpath(swig_i)}
                ''').strip().replace('\n', "" if state_.windows else "\\\n"))
        rebuilt = jlib.build(
            (swig_i, include1, include2),
            (swig_cpp, swig_py),
            command,
            force_rebuild,
        )
        jlib.log('{rebuilt=}')
        if rebuilt:
            swig_py_tmp = f'{swig_py}-'
            jlib.remove(swig_py_tmp)
            os.rename(swig_py, swig_py_tmp)
            with open(swig_py_tmp) as f:
                swig_py_content = f.read()

            if state_.openbsd:
                # Write Python code that will automatically load the required
                # .so's when mupdf.py is imported. Unfortunately this doesn't
                # work on Linux.
                prefix = textwrap.dedent(f'''
                        import ctypes
                        import os
                        import importlib

                        # The required .so's are in the same directory as this
                        # Python file. On OpenBSD we can explicitly load these
                        # .so's here using ctypes.cdll.LoadLibrary(), which
                        # avoids the need for LD_LIBRARY_PATH to be defined.
                        #
                        # Unfortunately this doesn't work on Linux.
                        #
                        for leaf in ('libmupdf.so', 'libmupdfcpp.so', '_mupdf.so'):
                            path = os.path.abspath(f'{{__file__}}/../{{leaf}}')
                            #print(f'path={{path}}')
                            #print(f'exists={{os.path.exists(path)}}')
                            ctypes.cdll.LoadLibrary( path)
                            #print(f'have loaded {{path}}')
                        ''')
                swig_py_content = prefix + swig_py_content

            elif state_.windows:
                jlib.log('Adding prefix to {swig_cpp=}')
                prefix = ''
                postfix = ''
                with open(swig_cpp) as f:
                    swig_py_content = prefix + swig_py_content + postfix

            # Change all our PDF_ENUM_NAME_* enums so that they are actually
            # PdfObj instances so that they can be used like any other PdfObj.
            #
            jlib.log('{len(generated.c_enums)=}')
            for enum_type, enum_names in generated.c_enums.items():
                for enum_name in enum_names:
                    if enum_name.startswith('PDF_ENUM_NAME_'):
                        swig_py_content += f'{enum_name} = PdfObj( obj_enum_to_obj( {enum_name}))\n'

            with open(swig_py_tmp, 'w') as f:
                f.write(swig_py_content)
            os.rename(swig_py_tmp, swig_py)

    elif language == 'csharp':
        outdir = os.path.relpath(f'{build_dirs.dir_mupdf}/platform/csharp')
        os.makedirs(outdir, exist_ok=True)
        # Looks like swig comes up with 'mupdfcpp_swig_wrap.cxx' leafname.
        #
        # We include platform/python/include in order to pick up the modified
        # include/mupdf/pdf/object.h that we generate elsewhere.
        dllimport = 'mupdfcsharp.so'
        if state_.windows:
            # Would like to specify relative path to .dll with:
            #   dllimport = os.path.relpath( f'{build_dirs.dir_so}/mupdfcsharp.dll')
            # but Windows/.NET doesn't seem to support this, despite
            # https://stackoverflow.com/questions/31807289 "how can i add a
            # swig generated c dll reference to a c sharp project".
            #
            dllimport = 'mupdfcsharp.dll'
        command = (textwrap.dedent(f'''
                "{swig_command}"
                    {"-D_WIN32" if state_.windows else ""}
                    -Wall
                    -c++
                    -csharp
                    {disable_swig_warnings}
                    -module mupdf
                    -namespace mupdf
                    -dllimport {dllimport}
                    -outdir {outdir}
                    -outfile mupdf.cs
                    -o {os.path.relpath(swig_cpp)}
                    -includeall
                    -I{os.path.relpath(build_dirs.dir_mupdf)}/platform/python/include
                    -I{os.path.relpath(include1)}
                    -I{os.path.relpath(include2)}
                    -ignoremissing
                    {os.path.relpath(swig_i)}
                ''').strip().replace('\n', "" if state_.windows else "\\\n"))
        rebuilt = jlib.build(
            (swig_i, include1, include2),
            (f'{outdir}/mupdf.cs', os.path.relpath(swig_cpp)),
            command,
            force_rebuild,
        )
        # fixme: use <rebuilt> line with language=='python' to avoid multiple
        # modifications to unchanged mupdf.cs?
        #
        # For classes that have our to_string() method, override C#'s
        # ToString() to call to_string().
        with open(f'{outdir}/mupdf.cs') as f:
            cs = f.read()
        cs2 = re.sub(
            '(( *)public string to_string[(][)])',
            '\\2public override string ToString() { return to_string(); }\n\\1',
            cs,
        )
        jlib.log('{len(cs)=}')
        jlib.log('{len(cs2)=}')
        assert cs2 != cs, f'Failed to add toString() methods.'
        jlib.log('{len(generated.swig_csharp)=}')
        assert len(generated.swig_csharp)
        cs2 += generated.swig_csharp
        jlib.update_file(cs2, f'{build_dirs.dir_so}/mupdf.cs')
        #jlib.copy(f'{outdir}/mupdf.cs', f'{build_dirs.dir_so}/mupdf.cs')
        jlib.log('{rebuilt=}')

    else:
        assert 0
Exemple #14
0
def main():

    sdist = None
    wheels = []
    pypi_test = 1
    abis = None
    outdir = 'pypackage-out'
    remote = None
    if windows():
        abis = ['x32-38', 'x32-39', 'x64-38', 'x64-39']
    else:
        abis = ['37', '38', '39']
        manylinux_container_name = None
        manylinux_install_docker = False
        manylinux_docker_image = None
        manylinux_pull_docker_image = None

    parser = jlib.Arg(
        '',
        required=1,
        help=__doc__,
        subargs=[
            jlib.Arg(
                'abis <abis>',
                help=f'''
                        Set ABIs to build, comma-separated. Default is {abis}.
                        ''',
            ),
            jlib.Arg(
                'build',
                help='Build wheels.',
                multi=True,
                subargs=[
                    jlib.Arg(
                        '-r <uri>',
                        help='''
                                    Build on specified remote machine
                                    [<user>@]<host>:[<directory>] and copy them
                                    back to local machine.
                                    ''',
                    ),
                    jlib.Arg(
                        '-a <abis>',
                        help='Set ABIs to build remotely, comma-separated.'),
                    jlib.Arg(
                        '-t',
                        help='''
                                    Run basic "test ." import test. On Windows
                                    this is is done for each wheel we have
                                    built by running "py <abi>"; otherwise we
                                    only test with native python.
                                    ''',
                    ),
                ],
            ),
            jlib.Arg(
                'pypi-test <test>',
                help='Whether to use test.pypi.org.',
            ),
            jlib.Arg(
                'remote',
                multi=True,
                help='''
                        Sync to and run pypackage.py on remote machine.
                        ''',
                subargs=[
                    jlib.Arg(
                        '-s <files>',
                        help='''
                                    Comma-separated files to sync to remote
                                    machine. (pypackage.py is always synced.)
                                    ''',
                    ),
                    jlib.Arg(
                        '-a <args>',
                        help='''
                                    Args to pass to pypackage.py on remote
                                    machine.
                                    ''',
                    ),
                    jlib.Arg(
                        '<uri>',
                        required=1,
                        help='''
                                    The remote machine:
                                    [<user>@]<host>:[<directory>]
                                    ''',
                    ),
                ],
            ),
            jlib.Arg(
                'sdist <path>',
                help='''
                        Name of preexisting sdist file to use or Python package
                        directory (e.g. containing setup.py) in which to build
                        a new sdist.
                        ''',
            ),
            jlib.Arg('tag', help='Internal use only.'),
            jlib.Arg(
                'test',
                help='''
                        Run test programme. If <command> is '.' or '', we
                        instead run a temporary test programme that imports the
                        package. Uses list of wheels from "wheels <pattern>"
                        or "build ...", otherwise uses ABIs (default or as
                        specified with 'abis ...').
                        ''',
                multi=True,
                subargs=[
                    jlib.Arg(
                        '-p <python>',
                        help='''
                                    Set python to run. If not specified, on
                                    Windows we test with each wheel/abi's
                                    matching python, otherwise we use default
                                    python.
                                    ''',
                    ),
                    jlib.Arg(
                        '--pypi <package-name>',
                        help='''
                                    Install specified package from pypi before
                                    running test; otherwise we install from
                                    local wheels.
                                    ''',
                    ),
                    jlib.Arg(
                        '<command>',
                        required=1,
                        help='The test command to run.',
                    ),
                ],
            ),
            jlib.Arg('upload', help='Upload sdist and wheels to pypi.'),
            jlib.Arg(
                'wheels <pattern>',
                help='''
                        Specify pre-existing wheels using glob pattern. A "*"
                        is appended if does not end with ".whl".
                        ''',
            ),
        ],
    )

    args = parser.parse(sys.argv[1:])

    # Need to handle args in particular order because some depend on others.
    #
    if args.abis:
        abis_prev = abis
        abis = args.abis.split(',')
        log(f'Changing abis from {abis_prev} to {abis}')

    if args.sdist:
        if os.path.isfile(args.sdist.path):
            parse_sdist(args.sdist.path)
            sdist = args.sdist.path
        else:
            package_directory = args.sdist.path
            sdist = make_sdist(package_directory, outdir)

    if args.pypi_test:
        pypi_test = int(args.pypi_test.test)

    for build in args.build:
        assert sdist, f'build requires sdist'
        if build.r:
            # Remote build.
            user, host, directory = parse_remote(build.r.uri)
            local_dir = os.path.dirname(__file__)
            command = (
                ''
                f'rsync -aP {local_dir}/pypackage.py {local_dir}/jlib.py {sdist} {user}{host}:{directory}'
                f' && echo rsync finished'
                f' && ssh {user}{host} '
                f'"'
                f'{"cd "+directory if directory else "true"}'
                f' && ./pypackage.py sdist {os.path.basename(sdist)}')
            if build.a:
                command += f' abis {build.a.abis}'

            command += (f' build')

            if build.t:
                # Also run basic import test.
                command += " -t"

            # Add a command to rsync remote wheels back to local machine.
            #
            #log( '{sdist=}')
            sdist_prefix = os.path.basename(sdist)
            sdist_suffix = '.tar.gz'
            assert sdist_prefix.endswith(sdist_suffix)
            sdist_prefix = sdist_prefix[:-len(sdist_suffix)]
            command += (
                f'"'
                f' && rsync -ai \'{user}{host}:{directory}pypackage-out/{sdist_prefix}*\' {outdir}/'
            )

            system(command, prefix=f'{user}{host}:{directory}: ')

        else:
            # Local build.
            if windows():
                wheels = make_windows(sdist, abis, outdir)
            else:
                wheels = make_unix(
                    sdist,
                    abis,
                    outdir,
                    test_direct_install=False,
                    install_docker=manylinux_install_docker,
                    docker_image=manylinux_docker_image,
                    pull_docker_image=manylinux_pull_docker_image,
                    container_name=manylinux_container_name,
                )
            if build.t:
                # Run basic import test.
                package_name, _ = parse_sdist(sdist)
                test('',
                     package_name,
                     wheels,
                     abis,
                     pypi=False,
                     pypi_test=None,
                     py=None)

            log(f'sdist: {sdist}')
            for wheel in wheels:
                log(f'    wheel: {wheel}')

    if args.tag:
        print(f'tag: {make_tag()}')

    if args.wheels:
        pattern = args.wheels.pattern
        if not pattern.endswith('.whl'):
            pattern += '*'
            log(f'Have appended "*" to get pattern={pattern!r}')
        wheels_raw = glob.glob(pattern)
        if not wheels_raw:
            log(f'Warning: no matches found for wheels pattern {pattern!r}.')
        wheels = []
        for wheel in wheels_raw:
            if wheel.endswith('.whl'):
                wheels.append(wheel)
        log(f'Found {len(wheels)} wheels with pattern {pattern}:')
        for wheel in wheels:
            log(f'    {wheel}')

    if args.test:
        for test_ in args.test:
            pypi = False
            package_name = None
            python = test_.p.python if test_.p else None
            if test_.pypi:
                pypi = True
                package_name = test_.pypi.package_name
            test(test_.command, package_name, wheels, abis, pypi, pypi_test,
                 python)

    if args.upload:
        assert sdist, f'Cannot upload because no sdist specified; use "sdist ...".'
        wheels = wheels_for_sdist(sdist, outdir)
        log(f'Uploading wheels ({len(wheels)} for sdist: {sdist!r}')
        for wheel in wheels:
            log(f'    {wheel}')
        # We repeated on error, in case user enters the wrong password.
        while 1:
            try:
                venv_run(
                    [
                        f'pip install twine',
                        f'python -m twine upload --disable-progress-bar {"--repository testpypi" if pypi_test else ""} {sdist} {" ".join(wheels)}',
                    ],
                    bufsize=0,  # So we see login/password prompts.
                    raise_errors=True,
                )
            except Exception as e:
                jlib.log('Failed to upload: {e=}')
                input(jlib.log_text('Press <enter> to retry... ').strip())
            else:
                break

    for remote in args.remote:
        user, host, directory = parse_remote(remote.uri)
        local_dir = os.path.dirname(__file__)
        sync_files = f'{local_dir}/pypackage.py,{local_dir}/jlib.py'
        if remote.s:
            sync_files += f',{remote.s.files}'
        system(
            f'rsync -ai {sync_files.replace(",", " ")} {sdist if sdist else ""} {user}{host}:{directory}',
            prefix=f'rsync to {user}{host}:{directory}: ',
        )
        if remote.a:
            remote_args = f'pypi-test {pypi_test} {remote.a.args}'
            system(
                f'ssh {user}{host} '
                f'"'
                f'{"cd "+directory+" && " if directory else ""}'
                f'./pypackage.py {remote_args}'
                f'"',
                prefix=f'{user}{host}:{directory}: ',
            )
Exemple #15
0
    def _try_init_clang(self, version):
        if state_.openbsd:
            clang_bin = glob.glob(f'/usr/local/bin/clang-{version}')
            if not clang_bin:
                jlib.log('Cannot find {clang_bin=}', 1)
                return
            clang_bin = clang_bin[0]
            self.clang_version = version
            libclang_so = glob.glob(f'/usr/local/lib/libclang.so*')
            assert len(libclang_so) == 1
            self.libclang_so = libclang_so[0]
            self.resource_dir = jlib.system(
                f'{clang_bin} -print-resource-dir',
                out='return',
            ).strip()
            self.include_path = os.path.join(self.resource_dir, 'include')
            #logx('{self.libclang_so=} {self.resource_dir=} {self.include_path=}')
            if os.environ.get('VIRTUAL_ENV'):
                clang.cindex.Config.set_library_file(self.libclang_so)
            return True

        for p in os.environ.get('PATH').split(':'):
            clang_bins = glob.glob(os.path.join(p, f'clang-{version}*'))
            if not clang_bins:
                continue
            clang_bins.sort()
            for clang_bin in clang_bins:
                e, clang_search_dirs = jlib.system(
                    f'{clang_bin} -print-search-dirs',
                    #verbose=log,
                    out='return',
                    raise_errors=False,
                )
                if e:
                    jlib.log('[could not find {clang_bin}: {e=}]')
                    return
                if version == 10:
                    m = re.search('\nlibraries: =(.+)\n', clang_search_dirs)
                    assert m
                    clang_search_dirs = m.group(1)
                clang_search_dirs = clang_search_dirs.strip().split(':')
                for i in ['/usr/lib', '/usr/local/lib'] + clang_search_dirs:
                    for leaf in f'libclang-{version}.*so*', f'libclang.so.{version}.*':
                        p = os.path.join(i, leaf)
                        p = os.path.abspath(p)
                        jlib.log('{p=}')
                        libclang_so = glob.glob(p)
                        if not libclang_so:
                            continue

                        # We have found libclang.so.
                        self.libclang_so = libclang_so[0]
                        jlib.log('Using {self.libclang_so=}')
                        clang.cindex.Config.set_library_file(self.libclang_so)
                        self.resource_dir = jlib.system(
                            f'{clang_bin} -print-resource-dir',
                            out='return',
                        ).strip()
                        self.include_path = os.path.join(
                            self.resource_dir, 'include')
                        self.clang_version = version
                        return True
Exemple #16
0
def draw(argv):
    global alphabits_graphics
    global alphabits_text
    global band_height
    global colorspace
    global files
    global fir
    global format_
    global gamma_value
    global height
    global ignore_errors
    global invert
    global layer_config
    global layout_css
    global layout_em
    global layout_h
    global layout_use_doc_css
    global layout_w
    global lowmemory
    global min_line_width
    global no_icc
    global num_workers
    global num_workers
    global out
    global out_cs
    global output
    global output_file_per_page
    global output_format
    global password
    global proof_filename
    global quiet
    global resolution
    global res_specified
    global rotation
    global showfeatures
    global showmd5
    global showmemory
    global showtime
    global spots
    global uselist
    global width

    info = trace_info()

    quiet = 0

    items, argv = getopt.getopt(
        argv, 'qp:o:F:R:r:w:h:fB:c:e:G:Is:A:DiW:H:S:T:U:XLvPl:y:NO:')
    for option, value in items:
        if 0: pass
        elif option == '-q': quiet = 1
        elif option == '-p': password = value
        elif option == '-o': output = value
        elif option == '-F': format_ = value
        elif option == '-R': rotation = float(value)
        elif option == '-r':
            resolution = float(output)
            res_specified = 1
        elif option == '-w':
            width = float(value)
        elif option == '-h':
            height = float(value)
        elif option == '-f':
            fir = 1
        elif option == '-B':
            band_height = int(value)
        elif option == '-c':
            out_cs = parse_colorspace(value)
        elif option == '-e':
            proof_filename = value
        elif option == '-G':
            gamma_value = float(value)
        elif option == '-I':
            invert += 1
        elif option == '-W':
            layout_w = float(value)
        elif option == '-H':
            layout_h = float(value)
        elif option == '-S':
            layout_em = float(value)
        elif option == '-U':
            layout_css = value
        elif option == '-X':
            layout_use_doc_css = 0
        elif option == '-O':
            spots = float(value)
            if not mupdf.FZ_ENABLE_SPOT_RENDERING:
                jlib.log(
                    'Spot rendering/Overprint/Overprint simulation not enabled in this build'
                )
                spots = SPOTS_NONE
        elif option == '-s':
            if 't' in value: showtime += 1
            if 'm' in value: showmemory += 1
            if 'f' in value: showfeatures += 1
            if '5' in value: showmd5 += 1

        elif option == '-A':
            alphabits_graphics = int(value)
            sep = value.find('/')
            if sep >= 0:
                alphabits_text = int(value[sep + 1:])
            else:
                alphabits_text = alphabits_graphics
        elif option == '-D':
            uselist = 0
        elif option == '-l':
            min_line_width = float(value)
        elif option == '-i':
            ignore_errors = 1
        elif option == '-N':
            no_icc = 1

        elif option == '-T':
            num_workers = int(value)
        elif option == '-L':
            lowmemory = 1
        elif option == '-P':
            bgprint.active = 1
        elif option == '-y':
            layer_config = value

        elif option == '-v':
            print(f'mudraw version {mupdf.FZ_VERSION}')

    if not argv:
        usage()

    if num_workers > 0:
        if uselist == 0:
            printf('cannot use multiple threads without using display list')
            sys.exit(1)

        if band_height == 0:
            printf('Using multiple threads without banding is pointless')

    if bgprint.active:
        if uselist == 0:
            printf('cannot bgprint without using display list')
            sys.exit(1)

    if proof_filename:
        proof_buffer = mupdf.Buffer(proof_filename)
        proof_cs = mupdf.Colorspace(FZ_COLORSPACE_NONE, 0, None, proof_buffer)

    mupdf.set_text_aa_level(alphabits_text)
    mupdf.set_graphics_aa_level(alphabits_graphics)
    mupdf.set_graphics_min_line_width(min_line_width)
    if no_icc:
        mupdf.disable_icc()
    else:
        mupdf.enable_icc()

    if layout_css:
        buf = mupdf.Buffer(layout_css)
        mupdf.set_user_css(buf.string_from_buffer())

    mupdf.set_use_document_css(layout_use_doc_css)

    # Determine output type
    if band_height < 0:
        print('Bandheight must be > 0')
        sys.exit(1)

    output_format = OUT_PNG
    if format_:
        jlib.log('{format=}')
        for i in range(len(suffix_table)):
            if format_ == suffix_table[i].suffix[1:]:
                output_format = suffix_table[i].format
                if spots == SPOTS_FULL and suffix_table[i].spots == 0:
                    print(
                        f'Output format {suffix_table[i].suffix[1:]} does not support spot rendering.'
                    )
                    print('Doing overprint simulation instead.')
                    spots = SPOTS_OVERPRINT_SIM
                break
        else:
            print(f'Unknown output format {format}')
            sys.exit(1)
    elif output:
        suffix = output
        i = 0
        while 1:
            if i == len(suffix_table):
                break
            s = suffix.find(suffix_table[i].suffix)
            if s != -1:
                suffix = suffix_table[i].suffix[s + 1:]
                output_format = suffix_table[i].format_
                if spots == SPOTS_FULL and suffix_table[i].spots == 0:
                    print(
                        'Output format {suffix_table[i].suffix[1:]} does not support spot rendering'
                    )
                    print('Doing overprint simulation instead.')
                    spots = SPOTS_OVERPRINT_SIM
                i = 0
            else:
                i += 1

    if band_height:
        if output_format not in (OUT_PAM, OUT_PGM, OUT_PPM, OUT_PNM, OUT_PNG,
                                 OUT_PBM, OUT_PKM, OUT_PCL, OUT_PCLM, OUT_PS,
                                 OUT_PSD):
            print(
                'Banded operation only possible with PxM, PCL, PCLM, PS, PSD, and PNG outputs'
            )
            sys.exit(1)
        if showmd5:
            print('Banded operation not compatible with MD5')
            sys.exit(1)

    for i in range(len(format_cs_table)):
        if format_cs_table[i].format_ == output_format:
            if out_cs == CS_UNSET:
                out_cs = format_cs_table[i].default_cs
            for j in range(len(format_cs_table[i].permitted_cs)):
                if format_cs_table[i].permitted_cs[j] == out_cs:
                    break
            else:
                print('Unsupported colorspace for this format')
                sys.exit(1)

    alpha = 1
    if out_cs in (CS_MONO, CS_GRAY, CS_GRAY_ALPHA):
        colorspace = mupdf.Colorspace(mupdf.Colorspace.Fixed_GRAY)
        alpha = (out_cs == CS_GRAY_ALPHA)
    elif out_cs in (CS_RGB, CS_RGB_ALPHA):
        colorspace = mupdf.Colorspace(mupdf.Colorspace.Fixed_RGB)
        alpha = (out_cs == CS_RGB_ALPHA)
    elif out_cs in (CS_CMYK, CS_CMYK_ALPHA):
        colorspace = mupdf.Colorspace(mupdf.Colorspace.Fixed_CMYK)
        alpha = (out_cs == CS_CMYK_ALPHA)
    elif out_cs == CS_ICC:
        icc_buffer = mupdf.Buffer(icc_filename)
        colorspace = Colorspace(mupdf.FZ_COLORSPACE_NONE, 0, None, icc_buffer)
        alpha = 0
    else:
        print('Unknown colorspace!')
        sys.exit(1)

    if out_cs != CS_ICC:
        colorspace = mupdf.Colorspace(colorspace)
    else:
        # Check to make sure this icc profile is ok with the output format */
        okay = 0
        for i in range(len(format_cs_table)):
            if format_cs_table[i].format == output_format:
                for j in range(len(format_cs_table[i].permitted_cs)):
                    x = format_cs_table[i].permitted_cs[j]
                    if x in (CS_MONO, CS_GRAY, CS_GRAY_ALPHA):
                        if colorspace.colorspace_is_gray():
                            okay = 1
                        elif x in (CS_RGB, CS_RGB_ALPHA):
                            if colorspace.colorspace_is_rgb():
                                okay = 1
                        elif x in (CS_CMYK, CS_CMYK_ALPHA):
                            if colorspace.colorspace_is_cmyk():
                                okay = 1

        if not okay:
            print(
                'ICC profile uses a colorspace that cannot be used for this format'
            )
            sys.exit(1)

    if output_format == OUT_SVG:
        # SVG files are always opened for each page. Do not open "output".
        pass
    elif output and (not output.startswith('-')
                     or len(output) >= 2) and len(output) >= 1:
        if has_percent_d(output):
            output_file_per_page = 1
        else:
            out = mupdf.Output(output, 0)
    else:
        quiet = 1
        # automatically be quiet if printing to stdout
        if 0:
            # Windows specific code to make stdout binary.
            if output_format not in (OUT_TEXT, OUT_STEXT, OUT_HTML, OUT_XHTML,
                                     OUT_TRACE):
                setmode(fileno(stdout), O_BINARY)
        out = mupdf.Output(mupdf.Output.Fixed_STDOUT)

    filename = argv[0]
    if not output_file_per_page:
        file_level_headers()

    timing.count = 0
    timing.total = 0
    timing.min = 1 << 30
    timing.max = 0
    timing.mininterp = 1 << 30
    timing.maxinterp = 0
    timing.minpage = 0
    timing.maxpage = 0
    timing.minfilename = ""
    timing.maxfilename = ""
    if showtime and bgprint.active:
        timing.total = gettime()

    fz_optind = 0
    while fz_optind < len(argv):

        filename = argv[fz_optind]
        fz_optind += 1

        files += 1

        doc = mupdf.Document(filename)

        if doc.needs_password():
            if not doc.authenticate_password(password):
                raise Exception(f'cannot authenticate password: {filename}')

        # Once document is open check for output intent colorspace
        oi = doc.document_output_intent()
        if oi:  # todo: fz_document_output_intent() can return NULL, and we don't handle this.
            # See if we had explicitly set a profile to render
            if out_cs != CS_ICC:
                # In this case, we want to render to the output intent
                # color space if the number of channels is the same
                if oi.colorspace_n() == colorspace.colorspace_n():
                    colorspace = oi

        doc.layout_document(layout_w, layout_h, layout_em)

        if layer_config:
            apply_layer_config(doc, layer_config)

        if fz_optind == len(argv) or not mupdf.is_page_range(argv[fz_optind]):
            drawrange(doc, "1-N")
        if fz_optind < len(argv) and mupdf.is_page_range(argv[fz_optind]):
            drawrange(doc, argv[fz_optind])
            fz_optind += 1

        bgprint_flush()

    if not output_file_per_page:
        file_level_trailers()

    if out:
        out.close_output()
    out = None

    if showtime and timing.count > 0:
        if bgprint.active:
            timing.total = gettime() - timing.total

        if files == 1:
            print(
                f'total {timing.total}ms / {timing.count} pages for an average of {timing.total / timing.count}ms'
            )
            if bgprint.active:
                printf(
                    f'fastest page {timing.minpage}: {timing.mininterp}ms (interpretation) {timing.min - timing.mininterp}ms (rendering) {timing.min}ms(total)'
                )
                print(
                    f'slowest page {timing.maxpage}: {timing.maxinterp}ms (interpretation) {timing.max - timing.maxinterp}ms (rendering) {timing.max}ms(total)'
                )
            else:
                print(f'fastest page {timing.minpage}: {timing.min}ms')
                print(f'slowest page {timing.maxpage}: {timing.max}ms')
        else:
            print(
                f'total {timing.total}ms / {timing.count} pages for an average of {timing.total / timing.count}ms in {files} files'
            )
            print(
                f'fastest page {timing.minpage}: {timing.min}ms ({timing.minfilename})'
            )
            print(
                f'slowest page {timing.maxpage}: {timing.max}ms ({timing.maxfilename})'
            )

    if showmemory:
        print(
            f'Memory use total={info.total} peak={info.peak} current={info.current}'
        )

    return errored != 0
Exemple #17
0
def has_refs(tu, type_):
    '''
    Returns (offset, bits) if <type_> has a 'refs' member, otherwise False.
        offset:
            Byte offset of 'refs' or name of 'refs' for use with offsetof(),
            e.g. 'super.refs'.
        bits:
            Size of 'refs' in bits.
    '''
    type0 = type_
    type_ = type_.get_canonical()

    key = type_.spelling
    key = util.clip(key, 'struct ')
    ret = has_refs_cache.get(key, None)
    if ret is None:
        ret = False
        #jlib.log( 'Analysing {type0.spelling=} {type_.spelling=} {key=}')

        for prefix in (
                'fz_',
                'pdf_',
        ):
            #jlib.log( '{type_.spelling=} {prefix=}')
            if key.startswith(prefix):
                #jlib.log( 'Type is a fz_ or pdf_ struct: {key=}')
                keep_name = f'{prefix}keep_{key[len(prefix):]}'
                keep_fn_cursor = state.state_.find_function(tu,
                                                            keep_name,
                                                            method=False)
                #jlib.log( '{keep_name=} {keep_fn_cursor=}')
                if keep_fn_cursor:
                    #jlib.log( 'There is a keep() fn for this type so it uses reference counting: {keep_name=}')
                    base_type_cursor = get_base_type(type_).get_declaration()
                    if base_type_cursor.is_definition():
                        #jlib.log( 'Type definition is available so we look for .refs member: {key=}')
                        for cursor in type_.get_fields():
                            name = cursor.spelling
                            type2 = cursor.type.get_canonical()
                            #jlib.log( '{name=} {type2.spelling=}')
                            if name == 'refs' and type2.spelling == 'int':
                                ret = 'refs', 32
                                break
                            if name == 'storable' and type2.spelling == 'struct fz_storable':
                                ret = 'storable.refs', 32
                                break
                    else:
                        #jlib.log('Definition is not available for {key=}')
                        pass

                    if not ret:
                        if 0:
                            jlib.log(
                                'Cannot find .refs member or we only have forward'
                                ' declaration, so have to hard-code the size and offset'
                                ' of the refs member.')
                        if base_type_cursor.is_definition():
                            if key == 'pdf_document':
                                ret = 'super.refs', 32
                            elif key == 'fz_pixmap':
                                ret = 'storable.refs', 32
                            elif key in (
                                    'fz_colorspace',
                                    'fz_image',
                            ):
                                return 'key_storable.storable.refs', 32
                            elif key == 'pdf_cmap':
                                return 'storable.refs', 32
                        else:
                            #jlib.log( 'No defintion available, i.e. forward decl only.')
                            if key == 'pdf_obj':
                                ret = 0, 16
                            elif key == 'fz_path':
                                ret = 0, 8
                            elif key in (
                                    'fz_separations',
                                    'fz_halftone',
                                    'pdf_annot',
                                    'pdf_graft_map',
                            ):
                                # Forward decl, first member is 'int regs;'.
                                return 0, 32
                            elif key in (
                                    'fz_display_list',
                                    'fz_glyph',
                                    'fz_jbig2_globals',
                                    'pdf_function',
                            ):
                                # Forward decl, first member is 'fz_storable storable;'.
                                return 0, 32

                        if ret is None:
                            # Need to hard-code info for this type.
                            assert 0, jlib.expand_nv(
                                '{key=} has {keep_name}() fn but is forward decl or we cannot find .regs,'
                                ' and we have no hard-coded info about size and offset of .regs.'
                                ' {type0.spelling=} {type_.spelling=} {base_type_cursor.spelling}'
                            )
                    assert ret, f'{key} has {keep_name}() but have not found size/location of .refs member.'

        if type_.spelling in (
                'struct fz_document',
                'struct fz_buffer',
        ):
            assert ret
        #jlib.log('Populating has_refs_cache with {key=} {ret=}')
        has_refs_cache[key] = ret
    return ret
Exemple #18
0
    try:
        import clang.cindex
    except ModuleNotFoundError as e:

        # On devuan, clang-python isn't on python3's path, but python2's
        # clang-python works fine with python3, so we deviously get the path by
        # running some python 2.
        #
        e, clang_path = jlib.system(
            'python2 -c "import clang; print clang.__path__[0]"',
            out='return',
            raise_errors=0)

        if e == 0:
            jlib.log(
                'Retrying import of clang using info from python2 {clang_path=}'
            )
            sys.path.append(os.path.dirname(clang_path))
            import clang.cindex
        else:
            raise

except Exception as e:
    jlib.log(
        'Warning: failed to import clang.cindex: {e=}\n'
        f'We need Clang Python to build MuPDF python.\n'
        f'Install with "pip install libclang" or use the --venv option, or:\n'
        f'    OpenBSD: pkg_add py3-llvm\n'
        f'    Linux:debian/devuan: apt install python-clang\n')
    clang = None
Exemple #19
0
def get_args(tu,
             cursor,
             include_fz_context=False,
             skip_first_alt=False,
             verbose=False):
    '''
    Yields Arg instance for each arg of the function at <cursor>.

    Args:
        tu:
            A clang.cindex.TranslationUnit instance.
        cursor:
            Clang cursor for the function.
        include_fz_context:
            If false, we skip args that are 'struct fz_context*'
        skip_first_alt:
            If true, we skip the first arg with .alt set.
        verbose:
            .
    '''
    # We are called a few times for each function, and the calculations we do
    # are slow, so we cache the returned items. E.g. this reduces total time of
    # --build 0 from 3.5s to 2.1s.
    #
    key = tu, cursor.location.file, cursor.location.line, include_fz_context, skip_first_alt
    ret = get_args_cache.get(key)
    if not verbose and state.state_.show_details(cursor.spelling):
        verbose = True
        #jlib.log('Verbose because {cursor.spelling=}')
    if ret is None:
        ret = []
        i = 0
        i_alt = 0
        separator = ''
        for arg_cursor in cursor.get_arguments():
            assert arg_cursor.kind == clang.cindex.CursorKind.PARM_DECL
            if not include_fz_context and is_pointer_to(
                    arg_cursor.type, 'fz_context'):
                # Omit this arg because our generated mupdf_*() wrapping functions
                # use internalContextGet() to get a context.
                continue
            name = arg_cursor.mangled_name or f'arg_{i}'
            if 0 and name == 'stmofsp':
                verbose = True
            alt = None
            out_param = False
            base_type_cursor, base_typename, extras = get_extras(
                tu, arg_cursor.type)
            if verbose:
                jlib.log('Looking at arg. {extras=}')
            if extras:
                if verbose:
                    jlib.log(
                        '{extras.opaque=} {base_type_cursor.kind=} {base_type_cursor.is_definition()=}'
                    )
                if extras.opaque:
                    # E.g. we don't have access to defintion of fz_separation,
                    # but it is marked in classes.classextras with opaque=true,
                    # so there will be a wrapper class.
                    alt = base_type_cursor
                elif (1 and base_type_cursor.kind
                      == clang.cindex.CursorKind.STRUCT_DECL
                      #and base_type_cursor.is_definition()
                      ):
                    alt = base_type_cursor
            if verbose:
                jlib.log(
                    '{arg_cursor.type.spelling=} {base_typename=} {arg_cursor.type.kind=} {get_base_typename(arg_cursor.type)=}'
                )
            if alt:
                if is_double_pointer(arg_cursor.type):
                    out_param = True
            elif get_base_typename(
                    arg_cursor.type) in ('char', 'unsigned char',
                                         'signed char', 'void', 'FILE'):
                if is_double_pointer(arg_cursor.type):
                    if verbose:
                        jlib.log(
                            'setting outparam: {cursor.spelling=} {arg_cursor.type=}'
                        )
                    if cursor.spelling == 'pdf_clean_file':
                        # Don't mark char** argv as out-param, which will also
                        # allow us to tell swig to convert python lists into
                        # (argc,char**) pair.
                        pass
                    else:
                        if verbose:
                            jlib.log('setting out_param to true')
                        out_param = True
            elif base_typename.startswith(('fz_', 'pdf_')):
                # Pointer to fz_ struct is not usually an out-param.
                if verbose:
                    jlib.log(
                        'not out-param because arg is: {arg_cursor.displayname=} {base_type.spelling=} {extras}'
                    )
            elif arg_cursor.type.kind == clang.cindex.TypeKind.POINTER:
                pointee = arg_cursor.type.get_pointee()
                if verbose:
                    jlib.log('clang.cindex.TypeKind.POINTER')
                if pointee.get_canonical(
                ).kind == clang.cindex.TypeKind.FUNCTIONPROTO:
                    # Don't mark function-pointer args as out-params.
                    if verbose:
                        jlib.log('clang.cindex.TypeKind.FUNCTIONPROTO')
                elif pointee.is_const_qualified():
                    if verbose:
                        jlib.log('is_const_qualified()')
                elif pointee.spelling == 'FILE':
                    pass
                else:
                    if verbose:
                        jlib.log('setting out_param = True')
                    out_param = True
            if alt:
                i_alt += 1
            i += 1
            if alt and skip_first_alt and i_alt == 1:
                continue
            arg = Arg(arg_cursor, name, separator, alt, out_param)
            ret.append(arg)
            if verbose:
                jlib.log('Appending {arg=}')
            separator = ', '

        get_args_cache[key] = ret

    for arg in ret:
        yield arg
Exemple #20
0
def make_outparam_helper_csharp(
    tu,
    cursor,
    fnname,
    fnname_wrapper,
    generated,
    main_name,
):
    '''
    Write C# code for a convenient tuple-returning wrapper for MuPDF
    function that has out-params. We use the C# wrapper for our generated
    {main_name}_outparams() function.

    We don't attempt to handle functions that take unsigned char* args
    because these generally indicate sized binary data and cannot be handled
    generically.
    '''
    def write(text):
        generated.swig_csharp.write(text)

    main_name = util.rename.function(cursor.mangled_name)
    return_void = cursor.result_type.spelling == 'void'

    if fnname == 'fz_buffer_extract':
        # Write custom wrapper that returns the binary data as a C# bytes
        # array, using the C# wrapper for buffer_extract_outparams_fn(fz_buffer
        # buf, buffer_extract_outparams outparams).
        #
        write('\n')
        write('// Custom C# wrapper for fz_buffer_extract().\n')
        write('public static class mupdf_Buffer_extract\n')
        write('{\n')
        write(
            '    public static byte[] buffer_extract(this mupdf.Buffer buffer)\n'
        )
        write('    {\n')
        write(
            '        var outparams = new mupdf.buffer_storage_outparams();\n')
        write(
            '        uint n = mupdf.mupdf.buffer_storage_outparams_fn(buffer.m_internal, outparams);\n'
        )
        write(
            '        var raw1 = mupdf.SWIGTYPE_p_unsigned_char.getCPtr(outparams.datap);\n'
        )
        write(
            '        System.IntPtr raw2 = System.Runtime.InteropServices.HandleRef.ToIntPtr(raw1);\n'
        )
        write('        byte[] ret = new byte[n];\n')
        write(
            '        // Marshal.Copy() raises exception if <raw2> is null even if <n> is zero.\n'
        )
        write('        if (n == 0) return ret;\n')
        write(
            '        System.Runtime.InteropServices.Marshal.Copy(raw2, ret, 0, (int) n);\n'
        )
        write('        buffer.clear_buffer();\n')
        write('        buffer.trim_buffer();\n')
        write('        return ret;\n')
        write('    }\n')
        write('}\n')
        write('\n')

        return

    # We don't attempt to generate wrappers for fns that take or return
    # 'unsigned char*' - swig does not treat these as zero-terminated strings,
    # and they are generally binary data so cannot be handled generically.
    #
    if parse.is_pointer_to(cursor.result_type, 'unsigned char'):
        jlib.log(
            f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because it returns unsigned char*.',
            1)
        return
    for arg in parse.get_args(tu, cursor):
        if parse.is_pointer_to(arg.cursor.type, 'unsigned char'):
            jlib.log(
                f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has unsigned char* arg.',
                1)
            return
        if parse.is_pointer_to_pointer_to(arg.cursor.type, 'unsigned char'):
            jlib.log(
                f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has unsigned char** arg.',
                1)
            return
        if arg.cursor.type.get_array_size() >= 0:
            jlib.log(
                f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has array arg.',
                1)
            return
        if arg.cursor.type.kind == state.clang.cindex.TypeKind.POINTER:
            pointee = arg.cursor.type.get_pointee().get_canonical()
            if pointee.kind == state.clang.cindex.TypeKind.ENUM:
                jlib.log(
                    f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has enum out-param arg.',
                    1)
                return
            if pointee.kind == state.clang.cindex.TypeKind.FUNCTIONPROTO:
                jlib.log(
                    f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has fn-ptr arg.',
                    1)
                return
            if pointee.is_const_qualified():
                # Not an out-param.
                jlib.log(
                    f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has pointer-to-const arg.',
                    1)
                return
            if arg.cursor.type.get_pointee().spelling == 'FILE':
                jlib.log(
                    f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has FILE* arg.',
                    1)
                return
            if pointee.spelling == 'void':
                jlib.log(
                    f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has void* arg.',
                    1)
                return

    num_return_values = 0 if return_void else 1
    for arg in parse.get_args(tu, cursor):
        if arg.out_param:
            num_return_values += 1
    assert num_return_values

    if num_return_values > 7:
        # On linux, mono-csc can fail with:
        #   System.NotImplementedException: tuples > 7
        #
        jlib.log(
            f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because would require > 7-tuple.'
        )
        return

    # Write C# wrapper.
    arg0, _ = parse.get_first_arg(tu, cursor)
    if not arg0.alt:
        return

    write(f'\n')
    write(
        f'// Out-params extension method for C# class {util.rename.class_(arg0.alt.type.spelling)} (wrapper for MuPDF {arg0.alt.type.spelling}),\n'
    )
    write(
        f'// adding class method {fnname_wrapper}() (wrapper for {fnname}())\n'
    )
    write(f'// which returns out-params directly.\n')
    write(f'//\n')
    write(f'public static class mupdf_{main_name}_outparams_helper\n')
    write(f'{{\n')
    write(f'    public static ')

    def write_type(alt, type_):
        if alt:
            write(f'mupdf.{util.rename.class_(alt.type.spelling)}')
        elif parse.is_pointer_to(type_, 'char'):
            write(f'string')
        else:
            text = cpp.declaration_text(type_, '').strip()
            if text == 'int16_t': text = 'short'
            elif text == 'int64_t': text = 'long'
            elif text == 'size_t': text = 'ulong'
            elif text == 'unsigned int': text = 'uint'
            write(f'{text}')

    # Generate the returned tuple.
    #
    if num_return_values > 1:
        write('(')

    sep = ''

    # Returned param, if any.
    if not return_void:
        return_alt = None
        base_type_cursor, base_typename, extras = parse.get_extras(
            tu, cursor.result_type)
        if extras:
            if extras.opaque:
                # E.g. we don't have access to defintion of fz_separation,
                # but it is marked in classextras with opaque=true, so
                # there will be a wrapper class.
                return_alt = base_type_cursor
            elif base_type_cursor.kind == state.clang.cindex.CursorKind.STRUCT_DECL:
                return_alt = base_type_cursor
        write_type(return_alt, cursor.result_type)
        sep = ', '

    # Out-params.
    for arg in parse.get_args(tu, cursor):
        if arg.out_param:
            write(sep)
            write_type(arg.alt, arg.cursor.type.get_pointee())
            if num_return_values > 1:
                write(f' {arg.name_csharp}')
            sep = ', '

    if num_return_values > 1:
        write(')')

    # Generate function name and params. If first arg is a wrapper class we
    # use C#'s 'this' keyword to make this a member function of the wrapper
    # class.
    #jlib.log('outputs fn {fnname=}: is member: {"yes" if arg0.alt else "no"}')
    write(f' ')
    write(fnname_wrapper if arg0.alt else 'fn')
    write(f'(')
    if arg0.alt: write('this ')
    sep = ''
    for arg in parse.get_args(tu, cursor):
        if arg.out_param:
            continue
        write(sep)
        if arg.alt:
            # E.g. 'Document doc'.
            write(
                f'mupdf.{util.rename.class_(arg.alt.type.spelling)} {arg.name_csharp}'
            )
        elif parse.is_pointer_to(arg.cursor.type, 'char'):
            write(f'string {arg.name_csharp}')
        else:
            text = cpp.declaration_text(arg.cursor.type,
                                        arg.name_csharp).strip()
            text = util.clip(text, 'const ')
            text = text.replace('int16_t ', 'short ')
            text = text.replace('int64_t ', 'long ')
            text = text.replace('size_t ', 'uint ')
            text = text.replace('unsigned int ', 'uint ')
            write(text)
        sep = ', '
    write(f')\n')

    # Function body.
    #
    write(f'    {{\n')

    # Create local outparams struct.
    write(f'        var outparams = new mupdf.{main_name}_outparams();\n')
    write(f'        ')

    # Generate function call.
    #
    # The C# *_outparams_fn() generated by swig is inside namespace mupdf {
    # class mupdf { ... } }, so we access it using the rather clumsy prefix
    # 'mupdf.mupdf.'. It will have been generated from a C++ function
    # (generate by us) which is in top-level namespace mupdf, but swig
    # appears to generate the same code even if the C++ function is not in
    # a namespace.
    #
    if not return_void:
        write(f'var ret = ')
    write(f'mupdf.mupdf.{main_name}_outparams_fn(')
    sep = ''
    for arg in parse.get_args(tu, cursor):
        if arg.out_param:
            continue
        write(f'{sep}{arg.name_csharp}')
        if arg.alt:
            extras = parse.get_fz_extras(tu, arg.alt.type.spelling)
            assert extras.pod != 'none' \
                    'Cannot pass wrapper for {type_.spelling} as arg because pod is "none" so we cannot recover struct.'
            write('.internal_()' if extras.pod else '.m_internal')
        sep = ', '
    write(f'{sep}outparams);\n')

    # Generate return of tuple.
    write(f'        return ')
    if num_return_values > 1:
        write(f'(')
    sep = ''
    if not return_void:
        if return_alt:
            write(
                f'new mupdf.{util.rename.class_(return_alt.type.spelling)}(ret)'
            )
        else:
            write(f'ret')
        sep = ', '
    for arg in parse.get_args(tu, cursor):
        if arg.out_param:
            write(f'{sep}')
            type_ = arg.cursor.type.get_pointee()
            if arg.alt:
                write(
                    f'new mupdf.{util.rename.class_(arg.alt.type.spelling)}(outparams.{arg.name_csharp})'
                )
            elif 0 and parse.is_pointer_to(type_, 'char'):
                # This was intended to convert char* to string, but swig
                # will have already done that when making a C# version of
                # the C++ struct, and modern csc on Windows doesn't like
                # creating a string from a string for some reason.
                write(f'new string(outparams.{arg.name_csharp})')
            else:
                write(f'outparams.{arg.name_csharp}')
            sep = ', '
    if num_return_values > 1:
        write(')')
    write(';\n')
    write(f'    }}\n')
    write(f'}}\n')
Exemple #21
0
def find_wrappable_function_with_arg0_type_cache_populate(tu):
    '''
    Populates caches with wrappable functions.
    '''
    global find_wrappable_function_with_arg0_type_cache
    global find_wrappable_function_with_arg0_type_excluded_cache

    if find_wrappable_function_with_arg0_type_cache:
        return

    t0 = time.time()

    find_wrappable_function_with_arg0_type_cache = dict()
    find_wrappable_function_with_arg0_type_excluded_cache = dict()

    for fnname, cursor in state.state_.find_functions_starting_with(
            tu, ('fz_', 'pdf_'), method=True):

        exclude_reasons = []

        if fnname.startswith('fz_drop_') or fnname.startswith('fz_keep_'):
            continue
        if fnname.startswith('pdf_drop_') or fnname.startswith('pdf_keep_'):
            continue

        if cursor.type.is_function_variadic():
            exclude_reasons.append((
                MethodExcludeReason_VARIADIC,
                'function is variadic',
            ))

        # Look at resulttype.
        #
        result_type = cursor.type.get_result().get_canonical()
        if result_type.kind == clang.cindex.TypeKind.POINTER:
            result_type = result_type.get_pointee().get_canonical()
        result_type = util.clip(result_type.spelling, 'struct ')
        if result_type.startswith(('fz_', 'pdf_')):
            result_type_extras = get_fz_extras(tu, result_type)
            if not result_type_extras:
                exclude_reasons.append((
                    MethodExcludeReason_NO_EXTRAS,
                    f'no extras defined for result_type={result_type}',
                ))
            else:
                if not result_type_extras.constructor_raw:
                    exclude_reasons.append((
                        MethodExcludeReason_NO_RAW_CONSTRUCTOR,
                        f'wrapper for result_type={result_type} does not have raw constructor.',
                    ))
                if not result_type_extras.copyable:
                    exclude_reasons.append((
                        MethodExcludeReason_NOT_COPYABLE,
                        f'wrapper for result_type={result_type} is not copyable.',
                    ))

        # Look at args
        #
        i = 0
        arg0_cursor = None
        for arg in get_args(tu, cursor):

            base_typename = get_base_typename(arg.cursor.type)
            if not arg.alt and base_typename.startswith(('fz_', 'pdf_')):
                if arg.cursor.type.get_canonical(
                ).kind == clang.cindex.TypeKind.ENUM:
                    # We don't (yet) wrap fz_* enums, but for now at least we
                    # still wrap functions that take fz_* enum parameters -
                    # callers will have to use the fz_* type.
                    #
                    # For example this is required by mutool_draw.py because
                    # mudraw.c calls fz_set_separation_behavior().
                    #
                    jlib.logx(
                        'not excluding {fnname=} with enum fz_ param : {arg.cursor.spelling=} {arg.cursor.type.kind} {arg.cursor.type.get_canonical().kind=}'
                    )
                else:
                    exclude_reasons.append((
                        MethodExcludeReason_NO_WRAPPER_CLASS,
                        f'no wrapper class for arg i={i}: {arg.cursor.type.get_canonical().spelling} {arg.cursor.type.get_canonical().kind}',
                    ))
            if i == 0:
                if arg.alt:
                    arg0_cursor = arg.alt
                else:
                    exclude_reasons.append((
                        MethodExcludeReason_FIRST_ARG_NOT_STRUCT,
                        'first arg is not fz_* struct',
                    ))
            i += 1

        if exclude_reasons:
            find_wrappable_function_with_arg0_type_excluded_cache[
                fnname] = exclude_reasons
            #if fnname == 'fz_load_outline':   # lgtm [py/unreachable-statement]
            if state.state_.show_details(fnname):
                jlib.log(
                    'Excluding {fnname=} from possible class methods because:')
                for i in exclude_reasons:
                    jlib.log('    {i}')
        else:
            if i > 0:
                # <fnname> is ok to wrap.
                arg0 = arg0_cursor.type.get_canonical().spelling
                arg0 = util.clip(arg0, 'struct ')

                items = find_wrappable_function_with_arg0_type_cache.setdefault(
                    arg0, [])
                items.append(fnname)

    jlib.logx(
        f'populating find_wrappable_function_with_arg0_type_cache took {time.time()-t0}s'
    )