Mercurial > hgrepos > Python2 > PyMuPDF
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) |
