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

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
comparison
equal deleted inserted replaced
1:1d09e1dec1d9 2:b50eed0cc0ef
1 import jlib
2
3 import textwrap
4
5 def make_cppyy(
6 state_,
7 build_dirs,
8 generated,
9 ):
10 path = f'{build_dirs.dir_so}/mupdf_cppyy.py'
11 jlib.log( 'Updating {path}')
12
13 text = ''
14
15 text += textwrap.dedent( """
16 '''
17 MuPDF Python bindings using cppyy: https://cppyy.readthedocs.io
18
19 Cppyy generates bindings at runtime, so we don't need to build a .so like SWIG.
20
21 However we still need the mupdf.so (MuPDF C API) and mupdfcpp.so (MuPDF C++
22 API) libraries to be present and accessible via LD_LIBRARY_PATH.
23
24 Usage:
25 import mupdf_cppyy
26 mupdf = mupdf_cppyy.cppyy.gbl.mupdf
27
28 document = mupdf.Document(...)
29
30 Requirements:
31 Install cppyy; for example:
32 python -m pip install cppyy
33 '''
34
35 import ctypes
36 import inspect
37 import os
38 import re
39 import sys
40
41 import cppyy
42 import cppyy.ll
43
44 try:
45 import jlib
46 except ModuleNotFoundError:
47 class jlib:
48 @staticmethod
49 def log( text):
50 sys.stderr.write( f'{text}\\n')
51 mupdf_dir = os.path.abspath( f'{__file__}/../../..')
52
53 # pdf_annot_type is both an enum and a function (that returns
54 # the enum type!).
55 with open( f'{mupdf_dir}/include/mupdf/pdf/annot.h') as f:
56 text = f.read()
57 text, n = re.subn(
58 '(enum pdf_annot_type pdf_annot_type[(]fz_context [*]ctx, pdf_annot [*]annot[)];)',
59 '/*\\1*/',
60 text,
61 )
62 assert n == 1, f'n={n}'
63
64 # libmupdf and libmupdf.so also work here.
65 if 0:
66 print( f'$LD_LIBRARY_PATH={os.environ["LD_LIBRARY_PATH"]}', file=sys.stderr)
67 ret = cppyy.load_library('mupdf')
68 #jlib.log( 'after loading "mupdf": ret={ret=}')
69 cppyy.load_library('mupdfcpp')
70
71 cppyy.add_include_path( f'{mupdf_dir}/include')
72 cppyy.add_include_path( f'{mupdf_dir}/platform/c++/include')
73
74 # pdf_annot_type is both an enum and a function (that returns
75 # the enum type!).
76 with open( f'{mupdf_dir}/include/mupdf/pdf/annot.h') as f:
77 text1 = f.read()
78 text1, n = re.subn(
79 '(enum pdf_annot_type pdf_annot_type[(]fz_context [*]ctx, pdf_annot [*]annot[)];)',
80 '/* \\\\1 */',
81 text1,
82 )
83 assert n == 1, f'n={n}'
84 with open( 'foo_text1.h', 'w') as f:
85 f.write( text1)
86
87 # pdf_widget_type is both an enum and a function (that returns
88 # the enum type!).
89 with open( f'{mupdf_dir}/include/mupdf/pdf/form.h') as f:
90 text2 = f.read()
91 text2, n = re.subn(
92 '(enum pdf_widget_type pdf_widget_type[(]fz_context [*]ctx, pdf_annot [*]widget[)];)',
93 '/* \\\\1 */',
94 text2,
95 )
96 assert n == 1, f'n={n}'
97 with open( 'foo_text2.h', 'w') as f:
98 f.write( text2)
99
100 # Not sure why we need '#define FZ_ENABLE_ICC 1', but
101 # otherwise classes.h doesn't see a definition of
102 # fz_icc_profile. Presumably something to do with us manually
103 # including our modified copy of include/mupdf/pdf/annot.h.
104 #
105 cppyy.cppdef( f'''
106 #undef NDEBUG
107 #define FZ_ENABLE_ICC 1
108 {text1}
109 {text2}
110 #ifndef MUPDF_PDF_ANNOT_H
111 #error MUPDF_PDF_ANNOT_H not defined
112 #endif
113
114 #include "mupdf/fitz/version.h"
115 #include "mupdf/classes.h"
116 #include "mupdf/classes2.h"
117 #include "mupdf/functions.h"
118 #include "mupdf/fitz.h"
119 #include "mupdf/pdf.h"
120 ''')
121
122 cppyy.cppdef( f'''
123 #ifndef MUPDF_PDF_ANNOT_H
124 #error MUPDF_PDF_ANNOT_H not defined
125 #endif
126 ''')
127
128 if os.environ.get( 'MUPDF_cppyy_sig_exceptions') == '1':
129 jlib.log( 'calling cppyy.ll.set_signals_as_exception(True)')
130 cppyy.ll.set_signals_as_exception(True)
131
132 if 0:
133 # Do some checks.
134 try:
135 cppyy.gbl.abort()
136 except Exception as e:
137 print( f'Ignoring test exception from abort(): {e}', file=sys.stderr)
138 else:
139 assert 0, 'No exception from cppyy.gbl.abort()'
140
141 cppyy.cppdef('''
142 void mupdf_check_assert()
143 {
144 assert( 0);
145 }
146 ''')
147 cppyy.ll.set_signals_as_exception(True)
148 print( 'Testing assert failure', file=sys.stderr)
149 try:
150 cppyy.gbl.mupdf_check_assert()
151 except Exception as e:
152 print( f'Ignoring test exception from assert(0): {e}', file=sys.stderr)
153
154 print( 'Testing rect creation from null', file=sys.stderr)
155 try:
156 r = cppyy.gbl.mupdf.Rect( 0)
157 except Exception as e:
158 print( f'Ignoring exception from test rect creation from null e={e}', file=sys.stderr)
159 except:
160 print( '*** Non-Exception exception', file=sys.stderr)
161 traceback.print_exc()
162 else:
163 print( f'*** No exception from test rect creation from null', file=sys.stderr)
164 print( 'Finished testing rect creation from null', file=sys.stderr)
165
166 #try:
167 # cppyy.gbl.raise( SIGABRT)
168 #except:
169 # traceback.print_exc()
170
171
172 #
173 # Would be convenient to do:
174 #
175 # from cppyy.gbl.mupdf import *
176 #
177 # - but unfortunately this is not possible, e.g. see:
178 #
179 # https://cppyy.readthedocs.io/en/latest/misc.html#reduced-typing
180 #
181 # So instead it is suggested that users of this api do:
182 #
183 # import mupdf
184 # mupdf = mupdf.cppyy.gbl.mupdf
185 #
186 # If access to mupdf.cppyy.gbl is required (e.g. to see globals that are not in
187 # the C++ mupdf namespace), caller can additionally do:
188 #
189 # import cppyy
190 # cppyy.gbl.std...
191 #
192
193 # We make various modifications of cppyy.gbl.mupdf to simplify usage.
194 #
195
196 #print( f'len(dir(cppyy.gbl))={len(dir(cppyy.gbl))}')
197 #print( f'len(dir(cppyy.gbl.mupdf))={len(dir(cppyy.gbl.mupdf))}')
198
199 # Find macros and import into cppyy.gbl.mupdf.
200 paths = (
201 f'{mupdf_dir}/include/mupdf/fitz/version.h',
202 f'{mupdf_dir}/include/mupdf/ucdn.h',
203 f'{mupdf_dir}/pdf/object.h',
204 )
205 for path in (
206 f'{mupdf_dir}/include/mupdf/fitz/version.h',
207 f'{mupdf_dir}/include/mupdf/ucdn.h',
208 ):
209 with open( path) as f:
210 for line in f:
211 m = re.match('^#define\\\\s([a-zA-Z_][a-zA-Z_0-9]+)\\\\s+([^\\\\s]*)\\\\s*$', line)
212 if m:
213 name = m.group(1)
214 value = m.group(2)
215 if value == '':
216 value = 1
217 else:
218 value = eval( value)
219 #print( f'mupdf_cppyy.py: Setting {name}={value!r}')
220 setattr( cppyy.gbl.mupdf, name, value)
221
222 # MuPDF enums are defined as C so are not in the mupdf
223 # namespace. To mimic the SWIG mupdf bindings, we explicitly
224 # copy them into cppyy.gbl.mupdf.
225 #
226 """)
227
228 # Copy enums into mupdf namespace. We use generated.c_enums for this
229 # because cppyy has a bug where enums are not visible for iteration in a
230 # namespace - see: https://github.com/wlav/cppyy/issues/45
231 #
232 for enum_type, enum_names in generated.c_enums.items():
233 for enum_name in enum_names:
234 text += f'cppyy.gbl.mupdf.{enum_name} = cppyy.gbl.{enum_name}\n'
235
236 # Add code for converting small integers into MuPDF's special pdf_obj*
237 # values, and add these special enums to the mupdf namespace.
238 text += textwrap.dedent( """
239 cppyy.cppdef('''
240 #include "mupdf/fitz.h"
241 /* Casts an integer to a pdf_obj*. Used to convert SWIG's int
242 values for PDF_ENUM_NAME_* into PdfObj's. */
243 pdf_obj* obj_enum_to_obj(int n)
244 {
245 return (pdf_obj*) (intptr_t) n;
246 }
247 ''')
248 """)
249 for enum_type, enum_names in generated.c_enums.items():
250 for enum_name in enum_names:
251 if enum_name.startswith( 'PDF_ENUM_NAME_'):
252 text += f'cppyy.gbl.mupdf.{enum_name} = cppyy.gbl.mupdf.PdfObj( cppyy.gbl.obj_enum_to_obj( cppyy.gbl.mupdf.{enum_name}))\n'
253 # Auto-generated out-param wrappers.
254 text += generated.cppyy_extra
255
256 # Misc processing can be done directly in Python code.
257 #
258 text += textwrap.dedent( """
259
260 # Import selected basic types into mupdf namespace.
261 #
262 cppyy.gbl.mupdf.fz_point = cppyy.gbl.fz_point
263 cppyy.gbl.mupdf.fz_rect = cppyy.gbl.fz_rect
264 cppyy.gbl.mupdf.fz_matrix = cppyy.gbl.fz_matrix
265 cppyy.gbl.mupdf.fz_font_flags_t = cppyy.gbl.fz_font_flags_t
266 cppyy.gbl.mupdf.fz_default_color_params = cppyy.gbl.fz_default_color_params
267
268 # Override various functions so that, for example, functions with
269 # out-parameters instead return tuples.
270 #
271
272 # cppyy doesn't like interpreting char name[32] as a string?
273 cppyy.cppdef('''
274 std::string mupdf_font_name(fz_font* font)
275 {
276 //std::cerr << __FUNCTION__ << ": font=" << font << " font->name=" << font->name << "\\\\n";
277 return font->name;
278 }
279 ''')
280
281 class getattr_path_raise: pass
282 def getattr_path( path, default=getattr_path_raise):
283 '''
284 Like getattr() but resolves string path, splitting at '.'
285 characters.
286 '''
287 if isinstance( path, str):
288 path = path.split( '.')
289 # Maybe we should use out caller's module?
290 ret = sys.modules[ __name__]
291 try:
292 for subname in path:
293 ret = getattr( ret, subname)
294 except AttributeError:
295 if default is getattr_path_raise:
296 raise
297 return default
298 return ret
299
300 def setattr_path( path, value):
301 '''
302 Like getattr() but resolves string path, splitting at '.'
303 characters.
304 '''
305 if isinstance( path, str):
306 path = path.split( '.')
307 ns = getattr_path( path[:-1])
308 setattr( ns, path[-1], value)
309
310 assert getattr_path( 'cppyy') == cppyy
311 assert getattr_path( 'cppyy.gbl.mupdf') == cppyy.gbl.mupdf
312
313 def insert( *paths):
314 '''
315 Returns a decorator that copies the function into the specified
316 name(s). We assert that each item in <path> does not already
317 exist.
318 '''
319 class Anon: pass
320 for path in paths:
321 assert getattr_path( path, Anon) is Anon, f'path={path} already exists.'
322 def decorator( fn):
323 for path in paths:
324 setattr_path( path, fn)
325 return decorator
326
327 def replace( *paths):
328 '''
329 Decorator that inserts a function into namespace(s), replacing
330 the existing function(s). We assert that the namespace(s)
331 already contains the specified name,
332 '''
333 def decorator( fn):
334 class Anon: pass
335 for path in paths:
336 assert getattr_path( path, Anon) is not Anon, f'path does not exist: {path}'
337 for path in paths:
338 setattr_path( path, fn)
339 return decorator
340
341 def override( path, *paths_extra):
342 '''
343 Returns a decorator for <fn> which sets <path> and each item
344 in <paths_extra> to <fn>. When <fn> is called, it is passed an
345 additional <_original> arg set to the original <path>.
346 '''
347 def decorator( fn):
348 fn_original = getattr_path( path)
349 def fn2( *args, **kwargs):
350 '''
351 Call <fn>, passing additional <_original> arg.
352 '''
353 assert '_original' not in kwargs
354 kwargs[ '_original'] = fn_original
355 return fn( *args, **kwargs)
356 setattr_path( path, fn2)
357 for p in paths_extra:
358 setattr_path( p, fn2)
359 return fn2
360 return decorator
361
362 # A C++ fn that returns fz_buffer::data; our returned value seems
363 # to work better than direct access in Python.
364 #
365 cppyy.cppdef(f'''
366 namespace mupdf
367 {{
368 void* Buffer_data( fz_buffer* buffer)
369 {{
370 return buffer->data;
371 }}
372 }}
373 ''')
374
375 @replace( 'cppyy.gbl.mupdf.Buffer.buffer_storage', 'cppyy.gbl.mupdf.mfz_buffer_storage')
376 def _( buffer):
377 assert isinstance( buffer, cppyy.gbl.mupdf.Buffer)
378 assert buffer.m_internal
379
380 # Getting buffer.m_internal.data via Buffer_data() appears
381 # to work better than using buffer.m_internal.data
382 # directly. E.g. the latter fails when passed to
383 # mfz_recognize_image_format().
384 #
385 d = cppyy.gbl.mupdf.Buffer_data( buffer.m_internal)
386 return buffer.m_internal.len, d
387
388 cppyy.cppdef('''
389 std::string mupdf_raw_to_python_bytes( void* data, size_t size)
390 {
391 return std::string( (char*) data, size);
392 }
393 ''')
394
395 @insert( 'cppyy.gbl.mupdf.raw_to_python_bytes')
396 def _( data, size):
397 '''
398 Need to explicitly convert cppyy's std::string wrapper into
399 a bytes, otherwise it defaults to a Python str.
400 '''
401 ret = cppyy.gbl.mupdf_raw_to_python_bytes( data, size)
402 ret = bytes( ret)
403 return ret
404
405 # Support for converting a fz_buffer's contents into a Python
406 # bytes.
407 #
408 # We do this by creating a std::string in C++, then in Python
409 # converting the resulting class cppyy.gbl.std.string into a bytes.
410 #
411 # Not sure whether this conversion to bytes involves a second copy
412 # of the data.
413 #
414 cppyy.cppdef( f'''
415 namespace mupdf
416 {{
417 /* Returns std::string containing copy of buffer contents. */
418 std::string buffer_to_string( const Buffer& buffer, bool clear)
419 {{
420 unsigned char* datap;
421 size_t len = mupdf::mfz_buffer_storage( buffer, &datap);
422 std::string ret = std::string( (char*) datap, len);
423 if (clear)
424 {{
425 mupdf::mfz_clear_buffer(buffer);
426 mupdf::mfz_trim_buffer(buffer);
427 }}
428 return ret;
429 }}
430 }}
431 ''')
432
433 @replace( 'cppyy.gbl.mupdf.mfz_buffer_extract', 'cppyy.gbl.mupdf.Buffer.buffer_extract')
434 def _( buffer):
435 s = cppyy.gbl.mupdf.buffer_to_string( buffer, clear=True)
436 b = bytes( s)
437 return b
438
439 @insert( 'cppyy.gbl.mupdf.mfz_buffer_extract_copy', 'cppyy.gbl.mupdf.Buffer.buffer_extract_copy')
440 def _( buffer):
441 s = cppyy.gbl.mupdf.buffer_to_string( buffer, clear=False)
442 b = bytes( s)
443 return b
444
445 # Python-friendly mfz_new_buffer_from_copied_data() taking a str.
446 #
447 cppyy.cppdef('''
448 namespace mupdf
449 {
450 Buffer mfz_new_buffer_from_copied_data( const std::string& data)
451 {
452 /* Constructing a mupdf::Buffer from a char* ends
453 up using fz_new_buffer_from_base64(). We want to
454 use fz_new_buffer_from_data() which can be done by
455 passing an unsigned char*. */
456 return mupdf::mfz_new_buffer_from_copied_data(
457 (const unsigned char*) data.c_str(),
458 data.size()
459 );
460 }
461 }
462 ''')
463 cppyy.gbl.mupdf.Buffer.new_buffer_from_copied_data = cppyy.gbl.mupdf.mfz_new_buffer_from_copied_data
464
465
466 # Python-friendly alternative to ppdf_set_annot_color(), taking up
467 # to 4 explicit color args.
468 #
469 cppyy.cppdef('''
470 void mupdf_pdf_set_annot_color(
471 mupdf::PdfAnnot& self,
472 int n,
473 float color0,
474 float color1,
475 float color2,
476 float color3
477 )
478 {
479 float color[] = { color0, color1, color2, color3 };
480 return self.set_annot_color( n, color);
481 }
482 void mupdf_pdf_set_annot_interior_color(
483 mupdf::PdfAnnot& self,
484 int n,
485 float color0,
486 float color1,
487 float color2,
488 float color3
489 )
490 {
491 float color[] = { color0, color1, color2, color3 };
492 self.set_annot_interior_color( n, color);
493 }
494 void mupdf_mfz_fill_text(
495 const mupdf::Device& dev,
496 const mupdf::Text& text,
497 mupdf::Matrix& ctm,
498 const mupdf::Colorspace& colorspace,
499 float color0,
500 float color1,
501 float color2,
502 float color3,
503 float alpha,
504 mupdf::ColorParams& color_params
505 )
506 {
507 float color[] = { color0, color1, color2, color3 };
508 return mupdf::mfz_fill_text( dev, text, ctm, colorspace, color, alpha, color_params);
509 }
510 ''')
511 def mupdf_make_colors( color):
512 '''
513 Returns (n, colors) where <colors> is a tuple with 4 items,
514 the first <n> of which are from <color> and the rest are
515 zero.
516 '''
517 if isinstance(color, float):
518 color = color,
519 assert isinstance( color, ( tuple, list))
520 n = len( color)
521 ret = tuple(color) + (4-n)*(0,)
522 assert len( ret) == 4
523 return n, ret
524
525 @replace( 'cppyy.gbl.mupdf.mpdf_set_annot_color', 'cppyy.gbl.mupdf.PdfAnnot.set_annot_color')
526 def _( pdf_annot, color):
527 n, colors = mupdf_make_colors( color)
528 return cppyy.gbl.mupdf_pdf_set_annot_color( pdf_annot, n, *colors)
529
530 @replace( 'cppyy.gbl.mupdf.mpdf_set_annot_interior_color', 'cppyy.gbl.mupdf.PdfAnnot.set_annot_interior_color')
531 def _( pdf_annot, color):
532 n, colors = mupdf_make_colors( color)
533 cppyy.gbl.mupdf_pdf_set_annot_interior_color( pdf_annot, n, *colors)
534
535 @replace( 'cppyy.gbl.mupdf.mfz_fill_text', 'cppyy.gbl.mupdf.Device.fill_text')
536 def _( dev, text, ctm, colorspace, color, alpha, color_params):
537 _, colors = mupdf_make_colors( color)
538 return cppyy.gbl.mupdf_mfz_fill_text( dev, text, ctm, colorspace, *colors, alpha, color_params)
539
540 # Override cppyy.gbl.mupdf.Document.lookup_metadata() to return a
541 # string or None if not found.
542 #
543 @override( 'cppyy.gbl.mupdf.lookup_metadata', 'cppyy.gbl.mupdf.Document.lookup_metadata')
544 def _(self, key, _original):
545 e = ctypes.c_int(0)
546 ret = _original(self.m_internal, key, e)
547 e = e.value
548 if e < 0:
549 return None
550 # <ret> will be a cppyy.gbl.std.string, for which str()
551 # returns something that looks like a 'bytes', so
552 # explicitly convert to 'str'.
553 ret = str( ret)
554 return ret
555
556 # Override cppyy.gbl.mupdf.parse_page_range() to distinguish
557 # between returned const char* being null or empty string
558 # - cppyy converts both to an empty string, which means
559 # we can't distinguish between the last range (where
560 # fz_parse_page_range() returns '') and beyond the last range
561 # (where fz_parse_page_range() returns null).
562 #
563 # fz_parse_page_range() leaves the out-params unchanged when it
564 # returns null, so we can detect whether null was returned by
565 # initializing the out-params with special values that would never
566 # be ordinarily be returned.
567 #
568 @override( 'cppyy.gbl.mupdf.parse_page_range', 'cppyy.gbl.mupdf.mfz_parse_page_range')
569 def _(s, n, _original):
570 a = ctypes.c_int(-1)
571 b = ctypes.c_int(-1)
572 s = _original(s, a, b, n)
573 if a.value == -1 and b.value == -1:
574 s = None
575 return s, a.value, b.value
576
577 # Provide native python implementation of cppyy.gbl.mupdf.format_output_path()
578 # (-> fz_format_output_path). (The underlying C/C++ functions take a fixed-size
579 # buffer for the output string so isn't useful for Python code.)
580 #
581 @replace( 'cppyy.gbl.mupdf.format_output_path', 'cppyy.gbl.mupdf.mfz_format_output_path')
582 def _(format, page):
583 m = re.search( '(%[0-9]*d)', format)
584 if m:
585 ret = format[ :m.start(1)] + str(page) + format[ m.end(1):]
586 else:
587 dot = format.rfind( '.')
588 if dot < 0:
589 dot = len( format)
590 ret = format[:dot] + str(page) + format[dot:]
591 return ret
592
593 # Override cppyy.gbl.mupdf.Pixmap.n and cppyy.gbl.mupdf.Pixmap.alpha so
594 # that they return int. (The underlying C++ functions return unsigned char
595 # so cppyy's default bindings end up returning a python string which isn't
596 # useful.)
597 #
598 @override( 'cppyy.gbl.mupdf.Pixmap.n')
599 def _( self, _original):
600 return ord( _original( self))
601
602 @override( 'cppyy.gbl.mupdf.Pixmap.alpha')
603 def _(self, _original):
604 return ord( _original( self))
605
606 # Override cppyy.gbl.mupdf.ppdf_clean_file() so that it takes a Python
607 # container instead of (argc, argv).
608 #
609 @override( 'cppyy.gbl.mupdf.ppdf_clean_file', 'cppyy.gbl.mupdf.mpdf_clean_file')
610 def _(infile, outfile, password, opts, argv, _original):
611 a = 0
612 if argv:
613 a = (ctypes.c_char_p * len(argv))(*argv)
614 a = ctypes.pointer(a)
615 _original(infile, outfile, password, opts, len(argv), a)
616
617 # Add cppyy.gbl.mupdf.mpdf_dict_getl() with Python variadic args.
618 #
619 @insert( 'cppyy.gbl.mupdf.mpdf_dict_getl', 'cppyy.gbl.mupdf.PdfObj.dict_getl')
620 def _(obj, *tail):
621 for key in tail:
622 if not obj.m_internal:
623 break
624 obj = obj.dict_get(key)
625 assert isinstance(obj, cppyy.gbl.mupdf.PdfObj)
626 return obj
627
628 # Add cppyy.gbl.mupdf.mpdf_dict_getl() with Python variadic args.
629 #
630 @insert( 'cppyy.gbl.mupdf.mpdf_dict_putl', 'cppyy.gbl.mupdf.PdfObj.dict_putl')
631 def _(obj, val, *tail):
632 if obj.is_indirect():
633 obj = obj.resolve_indirect_chain()
634 if not obj.is_dict():
635 raise Exception(f'not a dict: {obj}')
636 if not tail:
637 return
638 doc = obj.get_bound_document()
639 for key in tail[:-1]:
640 next_obj = obj.dict_get(key)
641 if not next_obj.m_internal:
642 # We have to create entries
643 next_obj = doc.new_dict(1)
644 obj.dict_put(key, next_obj)
645 obj = next_obj
646 key = tail[-1]
647 obj.dict_put(key, val)
648
649 # Raise exception if an attempt is made to call mpdf_dict_putl_drop.
650 #
651 @insert( 'cppyy.gbl.mpdf_dict_putl_drop', 'cppyy.gbl.mupdf.PdfObj.dict_putl_drop')
652 def _(obj, *tail):
653 raise Exception(
654 'mupdf.PdfObj.dict_putl_drop() is unsupported and unnecessary'
655 ' in Python because reference counting is automatic.'
656 ' Instead use mupdf.PdfObj.dict_putl()'
657 )
658
659 def ppdf_set_annot_color(annot, color):
660 '''
661 Python implementation of pdf_set_annot_color() using
662 ppdf_set_annot_color2().
663 '''
664 if isinstance(color, float):
665 ppdf_set_annot_color2(annot, 1, color, 0, 0, 0)
666 elif len(color) == 1:
667 ppdf_set_annot_color2(annot, 1, color[0], 0, 0, 0)
668 elif len(color) == 2:
669 ppdf_set_annot_color2(annot, 2, color[0], color[1], 0, 0)
670 elif len(color) == 3:
671 ppdf_set_annot_color2(annot, 3, color[0], color[1], color[2], 0)
672 elif len(color) == 4:
673 ppdf_set_annot_color2(annot, 4, color[0], color[1], color[2], color[3])
674 else:
675 raise Exception( f'Unexpected color should be float or list of 1-4 floats: {color}')
676
677 # Python-friendly alternative to fz_runetochar().
678 #
679 cppyy.cppdef(f'''
680 std::vector<unsigned char> mupdf_runetochar2(int rune)
681 {{
682 std::vector<unsigned char> buffer(10);
683 int n = mupdf::runetochar((char*) &buffer[0], rune);
684 assert(n < sizeof(buffer));
685 buffer.resize(n);
686 if (0)
687 {{
688 std::cerr << __FUNCTION__ << ": rune=" << rune << ":";
689 for (auto i: buffer)
690 {{
691 std::cerr << ' ' << (int) i;
692 }}
693 std::cerr << "\\\\n";
694 }}
695 return buffer;
696 }}
697 ''')
698 @insert( 'cppyy.gbl.mupdf.runetochar2', 'cppyy.gbl.mupdf.mfz_runetochar2')
699 def mupdf_runetochar2( rune):
700 vuc = cppyy.gbl.mupdf_runetochar2( rune)
701 ret = bytearray()
702 #jlib.log( '{vuc!r=}')
703 for uc in vuc:
704 #jlib.log( '{uc!r=}')
705 ret.append( ord( uc))
706 #jlib.log( '{ret!r=}')
707 return ret
708
709 # Patch mfz_text_language_from_string() to treat str=None as nullptr.
710 #
711 @override( 'cppyy.gbl.mupdf.mfz_text_language_from_string')
712 def _( s, _original):
713 if s is None:
714 s = ctypes.c_char_p()
715 return _original( s)
716
717 # Python-friendly versions of fz_convert_color(), returning (dv0,
718 # dv1, dv2, dv3).
719 #
720 cppyy.cppdef(f'''
721 struct mupdf_convert_color2_v
722 {{
723 float v0;
724 float v1;
725 float v2;
726 float v3;
727 }};
728 void mupdf_convert_color2(
729 fz_colorspace* ss,
730 const float* sv,
731 fz_colorspace* ds,
732 mupdf_convert_color2_v* dv,
733 fz_colorspace* is,
734 fz_color_params params
735 )
736 {{
737 mupdf::convert_color(ss, sv, ds, &dv->v0, is, params);
738 }}
739 ''')
740 @replace( 'cppyy.gbl.mupdf.convert_color')
741 def _convert_color( ss, sv, ds, is_, params):
742 # Note that <sv> should be a cppyy representation of a float*.
743 dv = cppyy.gbl.mupdf_convert_color2_v()
744 if is_ is None:
745 is_ = cppyy.ll.cast[ 'fz_colorspace*']( 0)
746 cppyy.gbl.mupdf_convert_color2( ss, sv, ds, dv, is_, params)
747 return dv.v0, dv.v1, dv.v2, dv.v3
748
749 cppyy.cppdef(f'''
750 namespace mupdf
751 {{
752 std::vector<int> mfz_memrnd2(int length)
753 {{
754 std::vector<unsigned char> ret(length);
755 mupdf::mfz_memrnd(&ret[0], length);
756 /* Unlike SWIG, cppyy converts
757 std::vector<unsigned char> into a string, not a
758 list of integers. */
759 std::vector<int> ret2( ret.begin(), ret.end());
760 return ret2;
761 }}
762 }}
763 ''')
764
765 # Provide an overload for mfz_recognize_image_format(), because
766 # the default unsigned char p[8] causes problems.
767 #
768 cppyy.cppdef(f'''
769 namespace mupdf
770 {{
771 int mfz_recognize_image_format(const void* p)
772 {{
773 int ret = mfz_recognize_image_format( (unsigned char*) p);
774 return ret;
775 }}
776 }}
777 ''')
778
779 # Wrap mupdf::Pixmap::md5_pixmap() and mupdf::Md5::md5_final2
780 # to make them return a Python 'bytes' instance. The
781 # C++ code returns std::vector<unsigned char> which
782 # SWIG converts into something that can be trivially
783 # converted to a Python 'bytes', but with cppyy it is a
784 # cppyy.gbl.std.vector['unsigned char'] which gives error
785 # "TypeError: 'str' object cannot be interpreted as an integer"
786 # if used to construct a Python 'bytes'.
787 #
788 @override( 'cppyy.gbl.mupdf.Pixmap.md5_pixmap')
789 def _( pixmap, _original):
790 r = _original( pixmap)
791 assert isinstance(r, cppyy.gbl.std.vector['unsigned char'])
792 r = bytes( str( r), 'latin')
793 return r
794
795 @override( 'cppyy.gbl.mupdf.Md5.md5_final2')
796 def _( md5, _original):
797 r = _original( md5)
798 assert isinstance(r, cppyy.gbl.std.vector['unsigned char'])
799 r = bytes( str( r), 'latin')
800 return r
801
802 # Allow cppyy.gbl.mupdf.mfz_md5_update() to be called with a buffer.
803 def mupdf_mfz_md5_update_buffer( md5, buffer):
804 len_, data = buffer.buffer_storage()
805 # <data> will be a void*.
806 data = cppyy.ll.cast[ 'const unsigned char*']( data)
807 return cppyy.gbl.mupdf.mfz_md5_update( md5, data, len_)
808 cppyy.gbl.mupdf.mfz_md5_update_buffer = mupdf_mfz_md5_update_buffer
809
810 # Make a version of mpdf_to_name() that returns a std::string
811 # so that cppyy can wrap it, and make Python wrappers for
812 # mupdf::mpdf_to_name() and mupdf::PdfObj::to_name() use this
813 # new version.
814 #
815 # Otherwise cppyy fails with curious error "TypeError: function
816 # takes exactly 5 arguments (1 given)".
817 #
818 cppyy.cppdef(f'''
819 namespace mupdf
820 {{
821 std::string mpdf_to_name2(const PdfObj& obj)
822 {{
823 /* Convert const char* to std::string. */
824 return mpdf_to_name( obj);
825 }}
826 }}
827 ''')
828
829 def mpdf_to_name( obj):
830 return str( cppyy.gbl.mupdf.mpdf_to_name2( obj))
831 cppyy.gbl.mupdf.mpdf_to_name = mpdf_to_name
832 cppyy.gbl.mupdf.PdfObj.to_name = mpdf_to_name
833
834 # Wrap mfz_new_font_from_*() to convert name=None to name=(const
835 # char*) nullptr.
836 #
837 @override( 'cppyy.gbl.mupdf.mfz_new_font_from_buffer')
838 def _( name, fontfile, index, use_glyph_bbox, _original):
839 if name is None:
840 name = ctypes.c_char_p()
841 return _original( name, fontfile, index, use_glyph_bbox)
842
843 @override( 'cppyy.gbl.mupdf.mfz_new_font_from_file')
844 def _( name, fontfile, index, use_glyph_bbox, _original):
845 if name is None:
846 name = ctypes.c_char_p()
847 return _original( name, fontfile, index, use_glyph_bbox)
848
849 @override( 'cppyy.gbl.mupdf.mfz_new_font_from_memory')
850 def _( name, data, len, index, use_glyph_bbox, _original):
851 if name is None:
852 name = ctypes.c_char_p()
853 return _original( name, data, len, index, use_glyph_bbox)
854
855 # String representation of a fz_font_flags_t, for debugging.
856 #
857 cppyy.cppdef(f'''
858 std::string mupdf_mfz_font_flags_string( const fz_font_flags_t& ff)
859 {{
860 std::stringstream out;
861 out << "{{"
862 << " is_mono=" << ff.is_mono
863 << " is_serif=" << ff.is_serif
864 << " is_bold=" << ff.is_bold
865 << " is_italic=" << ff.is_italic
866 << " ft_substitute=" << ff.ft_substitute
867 << " ft_stretch=" << ff.ft_stretch
868 << " fake_bold=" << ff.fake_bold
869 << " fake_italic=" << ff.fake_italic
870 << " has_opentype=" << ff.has_opentype
871 << " invalid_bbox=" << ff.invalid_bbox
872 << " cjk=" << ff.cjk
873 << " cjk_lang=" << ff.cjk_lang
874 << "}}";
875 return out.str();
876 }}
877 ''')
878
879 # Direct access to fz_font_flags_t::ft_substitute for mupdfpy,
880 # while cppyy doesn't handle bitfields correctly.
881 #
882 cppyy.cppdef(f'''
883 int mupdf_mfz_font_flags_ft_substitute( const fz_font_flags_t& ff)
884 {{
885 return ff.ft_substitute;
886 }}
887 ''')
888
889 # Allow mupdfpy to work - requires make_bookmark2() due to SWIG weirdness.
890 #
891 cppyy.gbl.mupdf.make_bookmark2 = cppyy.gbl.mupdf.make_bookmark
892 cppyy.gbl.mupdf.lookup_bookmark2 = cppyy.gbl.mupdf.lookup_bookmark
893
894 """)
895
896 # Add auto-generate out-param wrappers - these modify fn wrappers to return
897 # out-params as tuples.
898 #
899 #text += generated.cppyy_extra
900
901 jlib.fs_ensure_parent_dir( path)
902 jlib.fs_update( text, path)