comparison mupdf-source/scripts/wrap/swig.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 '''
2 Support for using SWIG to generate language bindings from the C++ bindings.
3 '''
4
5 import inspect
6 import io
7 import os
8 import re
9 import textwrap
10
11 import jlib
12
13 from . import cpp
14 from . import csharp
15 from . import rename
16 from . import state
17 from . import util
18
19
20 def translate_ucdn_macros( build_dirs):
21 '''
22 Returns string containing UCDN_* macros represented as enums.
23 '''
24 out = io.StringIO()
25 with open( f'{build_dirs.dir_mupdf}/include/mupdf/ucdn.h') as f:
26 text = f.read()
27 out.write( '\n')
28 out.write( '\n')
29 out.write( 'enum\n')
30 out.write( '{\n')
31 n = 0
32 for m in re.finditer('\n#define (UCDN_[A-Z0-9_]+) +([^\n]+)', text):
33 out.write(f' {m.group(1)} = {m.group(2)},\n')
34 n += 1
35 out.write( '};\n')
36 out.write( '\n')
37 assert n
38 return out.getvalue()
39
40 def _csharp_unicode_prefix():
41 '''
42 Returns typemaps that automatically convert C# strings (which are utf16)
43 into utf8 when calling MuPDF, and convert strings returned by MuPDF (which
44 are utf8) into utf16.
45
46 We return empty string if not on Windows, because Mono appears to already
47 work.
48 '''
49 if not state.state_.windows:
50 # Mono on Linux already seems to use utf8.
51 return ''
52
53 text = textwrap.dedent('''
54 // This ensures that our code below overrides whatever is defined
55 // in std_string.i and any later `%include "std_string.i"` is
56 // ignored.
57 %include "std_string.i"
58
59 // See https://github.com/swig/swig/pull/2364. We also add typemaps
60 // for `const char*`.
61
62 %{
63 #include <string>
64 %}
65
66 namespace std
67 {
68 %typemap(imtype,
69 inattributes="[global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
70 outattributes="[return: global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
71 directorinattributes="[global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
72 directoroutattributes="[return: global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]"
73 ) string "string"
74
75
76 %typemap(imtype,
77 inattributes="[global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
78 outattributes="[return: global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
79 directorinattributes="[global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
80 directoroutattributes="[return: global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]"
81 ) const string & "string"
82
83 %typemap(imtype,
84 inattributes="[global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
85 outattributes="[return: global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
86 directorinattributes="[global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]",
87 directoroutattributes="[return: global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.LPUTF8Str)]"
88 ) const char* "string"
89 }
90 ''')
91 return text
92
93
94 def build_swig(
95 state_: state.State,
96 build_dirs: state.BuildDirs,
97 generated,
98 language='python',
99 swig_command='swig',
100 check_regress=False,
101 force_rebuild=False,
102 ):
103 '''
104 Builds python or C# wrappers for all mupdf_* functions and classes, by
105 creating a .i file that #include's our generated C++ header files and
106 running swig.
107
108 build_dirs
109 A BuildDirs instance.
110 generated.
111 A Generated instance.
112 language
113 The output language, must be 'python' or 'csharp'.
114 swig
115 Location of swig binary.
116 check_regress
117 If true, we fail with error if generated .i file already exists and
118 differs from our new content.
119 '''
120 assert isinstance( state_, state.State)
121 assert isinstance(build_dirs, state.BuildDirs), type(build_dirs)
122 assert isinstance(generated, cpp.Generated), type(generated)
123 assert language in ('python', 'csharp')
124 # Find version of swig. (We use quotes around <swig> to make things work on
125 # Windows.)
126 e, swig_location = jlib.system( f'which "{swig_command}"', raise_errors=0, out='return', verbose=0)
127 if e == 0:
128 jlib.log(f'{swig_location=}')
129 t = jlib.system( f'"{swig_command}" -version', out='return', verbose=0)
130 jlib.log1('SWIG version info:\n========\n{t}\n========')
131 m = re.search( 'SWIG Version ([0-9]+)[.]([0-9]+)[.]([0-9]+)', t)
132 assert m
133 swig_major = int( m.group(1))
134 jlib.log(f'{m.group()}')
135
136 # Create a .i file for SWIG.
137 #
138 common = textwrap.dedent(f'''
139 #include <stdexcept>
140
141 #include "mupdf/functions.h"
142 #include "mupdf/classes.h"
143 #include "mupdf/classes2.h"
144 #include "mupdf/internal.h"
145 #include "mupdf/exceptions.h"
146 #include "mupdf/extra.h"
147
148 #ifdef NDEBUG
149 static bool g_mupdf_trace_director = false;
150 static bool g_mupdf_trace_exceptions = false;
151 #else
152 static bool g_mupdf_trace_director = mupdf::internal_env_flag("MUPDF_trace_director");
153 static bool g_mupdf_trace_exceptions = mupdf::internal_env_flag("MUPDF_trace_exceptions");
154 #endif
155
156 '''
157 )
158 if language == 'csharp':
159 common += textwrap.dedent(f'''
160 /* This is required otherwise compiling the resulting C++ code
161 fails with:
162 error: use of undeclared identifier 'SWIG_fail'
163
164 But no idea whether it is the 'correct' thing to do; seems odd
165 that SWIG doesn't define SWIG_fail itself.
166 */
167 #define SWIG_fail throw std::runtime_error( e.what());
168 ''')
169
170 if language == 'python':
171 common += textwrap.dedent(f'''
172
173 static std::string to_stdstring(PyObject* s)
174 {{
175 PyObject* repr_str = PyUnicode_AsEncodedString(s, "utf-8", "~E~");
176 #ifdef Py_LIMITED_API
177 const char* repr_str_s = PyBytes_AsString(repr_str);
178 #else
179 const char* repr_str_s = PyBytes_AS_STRING(repr_str);
180 #endif
181 std::string ret = repr_str_s;
182 Py_DECREF(repr_str);
183 Py_DECREF(s);
184 return ret;
185 }}
186
187 static std::string py_repr(PyObject* x)
188 {{
189 if (!x) return "<C_nullptr>";
190 PyObject* s = PyObject_Repr(x);
191 return to_stdstring(s);
192 }}
193
194 static std::string py_str(PyObject* x)
195 {{
196 if (!x) return "<C_nullptr>";
197 PyObject* s = PyObject_Str(x);
198 return to_stdstring(s);
199 }}
200
201 /* Returns a Python `bytes` containing a copy of a `fz_buffer`'s
202 data. If <clear> is true we also clear and trim the buffer. */
203 PyObject* ll_fz_buffer_to_bytes_internal(fz_buffer* buffer, int clear)
204 {{
205 unsigned char* c = NULL;
206 size_t len = {rename.namespace_ll_fn('fz_buffer_storage')}(buffer, &c);
207 PyObject* ret = PyBytes_FromStringAndSize((const char*) c, (Py_ssize_t) len);
208 if (clear)
209 {{
210 /* We mimic the affects of fz_buffer_extract(), which
211 leaves the buffer with zero capacity. */
212 {rename.namespace_ll_fn('fz_clear_buffer')}(buffer);
213 {rename.namespace_ll_fn('fz_trim_buffer')}(buffer);
214 }}
215 return ret;
216 }}
217
218 /* Returns a Python `memoryview` for specified memory. */
219 PyObject* python_memoryview_from_memory( void* data, size_t size, int writable)
220 {{
221 return PyMemoryView_FromMemory(
222 (char*) data,
223 (Py_ssize_t) size,
224 writable ? PyBUF_WRITE : PyBUF_READ
225 );
226 }}
227
228 /* Returns a Python `memoryview` for a `fz_buffer`'s data. */
229 PyObject* ll_fz_buffer_storage_memoryview(fz_buffer* buffer, int writable)
230 {{
231 unsigned char* data = NULL;
232 size_t len = {rename.namespace_ll_fn('fz_buffer_storage')}(buffer, &data);
233 return python_memoryview_from_memory( data, len, writable);
234 }}
235
236 /* Creates Python bytes from copy of raw data. */
237 PyObject* raw_to_python_bytes(const unsigned char* c, size_t len)
238 {{
239 return PyBytes_FromStringAndSize((const char*) c, (Py_ssize_t) len);
240 }}
241
242 /* Creates Python bytes from copy of raw data. */
243 PyObject* raw_to_python_bytes(const void* c, size_t len)
244 {{
245 return PyBytes_FromStringAndSize((const char*) c, (Py_ssize_t) len);
246 }}
247
248 /* The SWIG wrapper for this function returns a SWIG proxy for
249 a 'const unsigned char*' pointing to the raw data of a python
250 bytes. This proxy can then be passed from Python to functions
251 that take a 'const unsigned char*'.
252
253 For example to create a MuPDF fz_buffer* from a copy of a
254 Python bytes instance:
255 bs = b'qwerty'
256 buffer_ = mupdf.fz_new_buffer_from_copied_data(mupdf.python_buffer_data(bs), len(bs))
257 */
258 const unsigned char* python_buffer_data(
259 const unsigned char* PYTHON_BUFFER_DATA,
260 size_t PYTHON_BUFFER_SIZE
261 )
262 {{
263 return PYTHON_BUFFER_DATA;
264 }}
265
266 unsigned char* python_mutable_buffer_data(
267 unsigned char* PYTHON_BUFFER_MUTABLE_DATA,
268 size_t PYTHON_BUFFER_MUTABLE_SIZE
269 )
270 {{
271 return PYTHON_BUFFER_MUTABLE_DATA;
272 }}
273
274 /* Casts an integer to a pdf_obj*. Used to convert SWIG's int
275 values for PDF_ENUM_NAME_* into {rename.class_('pdf_obj')}'s. */
276 pdf_obj* obj_enum_to_obj(int n)
277 {{
278 return (pdf_obj*) (intptr_t) n;
279 }}
280
281 /* SWIG-friendly alternative to {rename.ll_fn('pdf_set_annot_color')}(). */
282 void {rename.ll_fn('pdf_set_annot_color2')}(pdf_annot *annot, int n, float color0, float color1, float color2, float color3)
283 {{
284 float color[] = {{ color0, color1, color2, color3 }};
285 return {rename.namespace_ll_fn('pdf_set_annot_color')}(annot, n, color);
286 }}
287
288
289 /* SWIG-friendly alternative to {rename.ll_fn('pdf_set_annot_interior_color')}(). */
290 void {rename.ll_fn('pdf_set_annot_interior_color2')}(pdf_annot *annot, int n, float color0, float color1, float color2, float color3)
291 {{
292 float color[] = {{ color0, color1, color2, color3 }};
293 return {rename.namespace_ll_fn('pdf_set_annot_interior_color')}(annot, n, color);
294 }}
295
296 /* SWIG-friendly alternative to `fz_fill_text()`. */
297 void ll_fz_fill_text2(
298 fz_device* dev,
299 const fz_text* text,
300 fz_matrix ctm,
301 fz_colorspace* colorspace,
302 float color0,
303 float color1,
304 float color2,
305 float color3,
306 float alpha,
307 fz_color_params color_params
308 )
309 {{
310 float color[] = {{color0, color1, color2, color3}};
311 return {rename.namespace_ll_fn( 'fz_fill_text')}(dev, text, ctm, colorspace, color, alpha, color_params);
312 }}
313
314 std::vector<unsigned char> {rename.fn('fz_memrnd2')}(int length)
315 {{
316 std::vector<unsigned char> ret(length);
317 {rename.namespace_fn('fz_memrnd')}(&ret[0], length);
318 return ret;
319 }}
320
321 /* mupdfpy optimisation for copying raw data into pixmap. `samples` must
322 have enough data to fill the pixmap. */
323 void ll_fz_pixmap_copy_raw( fz_pixmap* pm, const void* samples)
324 {{
325 memcpy(pm->samples, samples, pm->stride * pm->h);
326 }}
327 ''')
328
329 common += textwrap.dedent(f'''
330 /* SWIG-friendly alternative to fz_runetochar(). */
331 std::vector<unsigned char> {rename.fn('fz_runetochar2')}(int rune)
332 {{
333 std::vector<unsigned char> buffer(10);
334 int n = {rename.namespace_ll_fn('fz_runetochar')}((char*) &buffer[0], rune);
335 assert(n < sizeof(buffer));
336 buffer.resize(n);
337 return buffer;
338 }}
339
340 /* SWIG-friendly alternatives to fz_make_bookmark() and
341 {rename.fn('fz_lookup_bookmark')}(), using long long instead of fz_bookmark
342 because SWIG appears to treat fz_bookmark as an int despite it
343 being a typedef for intptr_t, so ends up slicing. */
344 long long unsigned {rename.ll_fn('fz_make_bookmark2')}(fz_document* doc, fz_location loc)
345 {{
346 fz_bookmark bm = {rename.namespace_ll_fn('fz_make_bookmark')}(doc, loc);
347 return (long long unsigned) bm;
348 }}
349
350 fz_location {rename.ll_fn('fz_lookup_bookmark2')}(fz_document *doc, long long unsigned mark)
351 {{
352 return {rename.namespace_ll_fn('fz_lookup_bookmark')}(doc, (fz_bookmark) mark);
353 }}
354 {rename.namespace_class('fz_location')} {rename.fn('fz_lookup_bookmark2')}( {rename.namespace_class('fz_document')} doc, long long unsigned mark)
355 {{
356 return {rename.namespace_class('fz_location')}( {rename.ll_fn('fz_lookup_bookmark2')}(doc.m_internal, mark));
357 }}
358
359 struct {rename.fn('fz_convert_color2_v')}
360 {{
361 float v0;
362 float v1;
363 float v2;
364 float v3;
365 }};
366
367 /* SWIG-friendly alternative for
368 {rename.ll_fn('fz_convert_color')}(), taking `float* sv`. */
369 void {rename.ll_fn('fz_convert_color2')}(
370 fz_colorspace *ss,
371 float* sv,
372 fz_colorspace *ds,
373 {rename.fn('fz_convert_color2_v')}* dv,
374 fz_colorspace *is,
375 fz_color_params params
376 )
377 {{
378 //float sv[] = {{ sv0, sv1, sv2, sv3}};
379 {rename.namespace_ll_fn('fz_convert_color')}(ss, sv, ds, &dv->v0, is, params);
380 }}
381
382 /* SWIG-friendly alternative for
383 {rename.ll_fn('fz_convert_color')}(), taking four explicit `float`
384 values for `sv`. */
385 void {rename.ll_fn('fz_convert_color2')}(
386 fz_colorspace *ss,
387 float sv0,
388 float sv1,
389 float sv2,
390 float sv3,
391 fz_colorspace *ds,
392 {rename.fn('fz_convert_color2_v')}* dv,
393 fz_colorspace *is,
394 fz_color_params params
395 )
396 {{
397 float sv[] = {{ sv0, sv1, sv2, sv3}};
398 {rename.namespace_ll_fn('fz_convert_color')}(ss, sv, ds, &dv->v0, is, params);
399 }}
400
401 /* SWIG- Director class to allow fz_set_warning_callback() and
402 fz_set_error_callback() to be used with Python callbacks. Note that
403 we rename print() to _print() to match what SWIG does. */
404 struct DiagnosticCallback
405 {{
406 /* `description` must be "error" or "warning". */
407 DiagnosticCallback(const char* description)
408 :
409 m_description(description)
410 {{
411 #ifndef NDEBUG
412 if (g_mupdf_trace_director)
413 {{
414 std::cerr
415 << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ":"
416 << " DiagnosticCallback[" << m_description << "]() constructor."
417 << "\\n";
418 }}
419 #endif
420 if (m_description == "warning")
421 {{
422 mupdf::ll_fz_set_warning_callback( s_print, this);
423 }}
424 else if (m_description == "error")
425 {{
426 mupdf::ll_fz_set_error_callback( s_print, this);
427 }}
428 else
429 {{
430 std::cerr
431 << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ":"
432 << " DiagnosticCallback() constructor"
433 << " Unrecognised description: " << m_description
434 << "\\n";
435 assert(0);
436 }}
437 }}
438 virtual void _print( const char* message)
439 {{
440 #ifndef NDEBUG
441 if (g_mupdf_trace_director)
442 {{
443 std::cerr
444 << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ":"
445 << " DiagnosticCallback[" << m_description << "]::_print()"
446 << " called (no derived class?)" << " message: " << message
447 << "\\n";
448 }}
449 #endif
450 }}
451 virtual ~DiagnosticCallback()
452 {{
453 #ifndef NDEBUG
454 if (g_mupdf_trace_director)
455 {{
456 std::cerr
457 << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ":"
458 << " ~DiagnosticCallback[" << m_description << "]() destructor called"
459 << " this=" << this
460 << "\\n";
461 }}
462 #endif
463 }}
464 static void s_print( void* self0, const char* message)
465 {{
466 DiagnosticCallback* self = (DiagnosticCallback*) self0;
467 try
468 {{
469 return self->_print( message);
470 }}
471 catch (std::exception& e)
472 {{
473 /* It's important to swallow any exception from
474 self->_print() because fz_set_warning_callback() and
475 fz_set_error_callback() specifically require that
476 the callback does not throw. But we always output a
477 diagnostic. */
478 std::cerr
479 << "DiagnosticCallback[" << self->m_description << "]::s_print()"
480 << " ignoring exception from _print(): "
481 << e.what()
482 << "\\n";
483 }}
484 }}
485 std::string m_description;
486 }};
487
488 struct StoryPositionsCallback
489 {{
490 StoryPositionsCallback()
491 {{
492 //printf( "StoryPositionsCallback() constructor\\n");
493 }}
494
495 virtual void call( const fz_story_element_position* position) = 0;
496
497 static void s_call( fz_context* ctx, void* self0, const fz_story_element_position* position)
498 {{
499 //printf( "StoryPositionsCallback::s_call()\\n");
500 (void) ctx;
501 StoryPositionsCallback* self = (StoryPositionsCallback*) self0;
502 self->call( position);
503 }}
504
505 virtual ~StoryPositionsCallback()
506 {{
507 //printf( "StoryPositionsCallback() destructor\\n");
508 }}
509 }};
510
511 void ll_fz_story_positions_director( fz_story *story, StoryPositionsCallback* cb)
512 {{
513 //printf( "ll_fz_story_positions_director()\\n");
514 {rename.namespace_ll_fn('fz_story_positions')}(
515 story,
516 StoryPositionsCallback::s_call,
517 cb
518 );
519 }}
520
521 void Pixmap_set_alpha_helper(
522 int balen,
523 int n,
524 int data_len,
525 int zero_out,
526 unsigned char* data,
527 fz_pixmap* pix,
528 int premultiply,
529 int bground,
530 const std::vector<int>& colors,
531 const std::vector<int>& bgcolor
532 )
533 {{
534 int i = 0;
535 int j = 0;
536 int k = 0;
537 int data_fix = 255;
538 while (i < balen) {{
539 unsigned char alpha = data[k];
540 if (zero_out) {{
541 for (j = i; j < i+n; j++) {{
542 if (pix->samples[j] != (unsigned char) colors[j - i]) {{
543 data_fix = 255;
544 break;
545 }} else {{
546 data_fix = 0;
547 }}
548 }}
549 }}
550 if (data_len) {{
551 if (data_fix == 0) {{
552 pix->samples[i+n] = 0;
553 }} else {{
554 pix->samples[i+n] = alpha;
555 }}
556 if (premultiply && !bground) {{
557 for (j = i; j < i+n; j++) {{
558 pix->samples[j] = fz_mul255(pix->samples[j], alpha);
559 }}
560 }} else if (bground) {{
561 for (j = i; j < i+n; j++) {{
562 int m = (unsigned char) bgcolor[j - i];
563 pix->samples[j] = m + fz_mul255((pix->samples[j] - m), alpha);
564 }}
565 }}
566 }} else {{
567 pix->samples[i+n] = data_fix;
568 }}
569 i += n+1;
570 k += 1;
571 }}
572 }}
573
574 void page_merge_helper(
575 {rename.namespace_class('pdf_obj')}& old_annots,
576 {rename.namespace_class('pdf_graft_map')}& graft_map,
577 {rename.namespace_class('pdf_document')}& doc_des,
578 {rename.namespace_class('pdf_obj')}& new_annots,
579 int n
580 )
581 {{
582 #define PDF_NAME2(X) {rename.namespace_class('pdf_obj')}(PDF_NAME(X))
583 for ( int i=0; i<n; ++i)
584 {{
585 {rename.namespace_class('pdf_obj')} o = {rename.namespace_fn('pdf_array_get')}( old_annots, i);
586 if ({rename.namespace_fn('pdf_dict_gets')}( o, "IRT").m_internal)
587 continue;
588 {rename.namespace_class('pdf_obj')} subtype = {rename.namespace_fn('pdf_dict_get')}( o, PDF_NAME2(Subtype));
589 if ( {rename.namespace_fn('pdf_name_eq')}( subtype, PDF_NAME2(Link)))
590 continue;
591 if ( {rename.namespace_fn('pdf_name_eq')}( subtype, PDF_NAME2(Popup)))
592 continue;
593 if ( {rename.namespace_fn('pdf_name_eq')}( subtype, PDF_NAME2(Widget)))
594 {{
595 /* fixme: C++ API doesn't yet wrap fz_warn() - it
596 excludes all variadic fns. */
597 //mupdf::fz_warn( "skipping widget annotation");
598 continue;
599 }}
600 {rename.namespace_fn('pdf_dict_del')}( o, PDF_NAME2(Popup));
601 {rename.namespace_fn('pdf_dict_del')}( o, PDF_NAME2(P));
602 {rename.namespace_class('pdf_obj')} copy_o = {rename.namespace_fn('pdf_graft_mapped_object')}( graft_map, o);
603 {rename.namespace_class('pdf_obj')} annot = {rename.namespace_fn('pdf_new_indirect')}( doc_des, {rename.namespace_fn('pdf_to_num')}( copy_o), 0);
604 {rename.namespace_fn('pdf_array_push')}( new_annots, annot);
605 }}
606 #undef PDF_NAME2
607 }}
608 ''')
609
610 common += generated.swig_cpp
611 common += translate_ucdn_macros( build_dirs)
612
613 text = ''
614
615 text += '%module(directors="1") mupdf\n'
616
617 jlib.log(f'{build_dirs.Py_LIMITED_API=}')
618
619 text += f'%begin %{{\n'
620
621 if build_dirs.Py_LIMITED_API: # e.g. 0x03080000
622 text += textwrap.dedent(f'''
623 /* Use Python Stable ABI with earliest Python version that we
624 support. */
625 #define Py_LIMITED_API {build_dirs.Py_LIMITED_API}
626
627 /* These seem to be mistakenly undefined when Py_LIMITED_API
628 is defined, so we force the values from Python.h. Also see
629 https://github.com/python/cpython/issues/98680. */
630 #ifndef PyBUF_READ
631 #define PyBUF_READ 0x100
632 #endif
633 #ifndef PyBUF_WRITE
634 #define PyBUF_WRITE 0x200
635 #endif
636 ''')
637
638 text += textwrap.dedent(f'''
639 /* This seems to be necessary on some Windows machines with
640 Py_LIMITED_API, otherwise compilation can fail because free()
641 and malloc() are not declared. */
642 #include <stdlib.h>
643 ''')
644
645 text += f'%}}\n'
646
647 # https://www.mono-project.com/docs/advanced/pinvoke/
648 #
649 # > Mono on all platforms currently uses UTF-8 encoding for all string
650 # > marshaling operations.
651 #
652 if language == 'csharp':
653 text += _csharp_unicode_prefix()
654
655 for i in generated.virtual_fnptrs:
656 text += f'%feature("director") {i};\n'
657
658 text += f'%feature("director") DiagnosticCallback;\n'
659 text += f'%feature("director") StoryPositionsCallback;\n'
660
661 text += textwrap.dedent(
662 '''
663 %feature("director:except")
664 {
665 if ($error != NULL)
666 {
667 /*
668 This is how we can end up here:
669
670 1. Python code calls a function in the Python `mupdf` module.
671 2. - which calls SWIG C++ code.
672 3. - which calls MuPDF C++ API wrapper function.
673 4. - which calls MuPDF C code which calls an MuPDF struct's function pointer.
674 5. - which calls MuPDF C++ API Director wrapper (e.g. mupdf::FzDevice2) virtual function.
675 6. - which calls SWIG Director C++ code.
676 7. - which calls Python derived class's method, which raises a Python exception.
677
678 The exception propagates back up the above stack, being converted
679 into different exception representations as it goes:
680
681 6. SWIG Director C++ code (here). We raise a C++ exception.
682 5. MuPDF C++ API Director wrapper converts the C++ exception into a MuPDF fz_try/catch exception.
683 4. MuPDF C code allows the exception to propagate or catches and rethrows or throws a new fz_try/catch exception.
684 3. MuPDF C++ API wrapper function converts the fz_try/catch exception into a C++ exception.
685 2. SWIG C++ code converts the C++ exception into a Python exception.
686 1. Python code receives the Python exception.
687
688 So the exception changes from a Python exception, to a C++
689 exception, to a fz_try/catch exception, to a C++ exception, and
690 finally back into a Python exception.
691
692 Each of these stages is necessary. In particular we cannot let the
693 first C++ exception propagate directly through MuPDF C code without
694 being a fz_try/catch exception, because it would mess up MuPDF C
695 code's fz_try/catch exception stack.
696
697 Unfortuntately MuPDF fz_try/catch exception strings are limited to
698 256 characters so some or all of our detailed backtrace information
699 is lost.
700 */
701
702 /* Get text description of the Python exception. */
703 PyObject* etype;
704 PyObject* obj;
705 PyObject* trace;
706 PyErr_Fetch( &etype, &obj, &trace);
707
708 /* Looks like PyErr_GetExcInfo() fails here, returning NULL.*/
709 /*
710 PyErr_GetExcInfo( &etype, &obj, &trace);
711 std::cerr << "PyErr_GetExcInfo(): etype: " << py_str(etype) << "\\n";
712 std::cerr << "PyErr_GetExcInfo(): obj: " << py_str(obj) << "\\n";
713 std::cerr << "PyErr_GetExcInfo(): trace: " << py_str(trace) << "\\n";
714 */
715
716 std::string message = "Director error: " + py_str(etype) + ": " + py_str(obj) + "\\n";
717
718 if (g_mupdf_trace_director)
719 {
720 /* __FILE__ and __LINE__ are not useful here because SWIG makes
721 them point to the generic .i code. */
722 std::cerr << "========\\n";
723 std::cerr << "g_mupdf_trace_director set: Converting Python error into C++ exception:" << "\\n";
724 #ifndef _WIN32
725 std::cerr << " function: " << __PRETTY_FUNCTION__ << "\\n";
726 #endif
727 std::cerr << " etype: " << py_str(etype) << "\\n";
728 std::cerr << " obj: " << py_str(obj) << "\\n";
729 std::cerr << " trace: " << py_str(trace) << "\\n";
730 std::cerr << "========\\n";
731 }
732
733 PyObject* traceback = PyImport_ImportModule("traceback");
734 if (traceback)
735 {
736 /* Use traceback.format_tb() to get backtrace. */
737 if (0)
738 {
739 message += "Traceback (from traceback.format_tb()):\\n";
740 PyObject* traceback_dict = PyModule_GetDict(traceback);
741 PyObject* format_tb = PyDict_GetItem(traceback_dict, PyString_FromString("format_tb"));
742 PyObject* ret = PyObject_CallFunctionObjArgs(format_tb, trace, NULL);
743 PyObject* iter = PyObject_GetIter(ret);
744 for(;;)
745 {
746 PyObject* item = PyIter_Next(iter);
747 if (!item) break;
748 message += py_str(item);
749 Py_DECREF(item);
750 }
751 /* `format_tb` and `traceback_dict` are borrowed references.
752 */
753 Py_XDECREF(iter);
754 Py_XDECREF(ret);
755 Py_XDECREF(traceback);
756 }
757
758 /* Use exception_info() (copied from mupdf/scripts/jlib.py) to get
759 detailed backtrace. */
760 if (1)
761 {
762 PyObject* globals = PyEval_GetGlobals();
763 PyObject* exception_info = PyDict_GetItemString(globals, "exception_info");
764 PyObject* string_return = PyUnicode_FromString("return");
765 PyObject* ret = PyObject_CallFunctionObjArgs(
766 exception_info,
767 trace,
768 Py_None,
769 string_return,
770 NULL
771 );
772 Py_XDECREF(string_return);
773 message += py_str(ret);
774 Py_XDECREF(ret);
775 }
776 }
777 else
778 {
779 message += "[No backtrace available.]\\n";
780 }
781
782 Py_XDECREF(etype);
783 Py_XDECREF(obj);
784 Py_XDECREF(trace);
785
786 message += "Exception was from C++/Python callback:\\n";
787 message += " ";
788 #ifdef _WIN32
789 message += __FUNCTION__;
790 #else
791 message += __PRETTY_FUNCTION__;
792 #endif
793 message += "\\n";
794
795 if (1 || g_mupdf_trace_director)
796 {
797 std::cerr << "========\\n";
798 std::cerr << "Director exception handler, message is:\\n" << message << "\\n";
799 std::cerr << "========\\n";
800 }
801
802 /* SWIG 4.1 documentation talks about throwing a
803 Swig::DirectorMethodException here, but this doesn't work for us
804 because it sets Python's error state again, which makes the
805 next SWIG call of a C/C++ function appear to fail.
806 //throw Swig::DirectorMethodException();
807 */
808 throw std::runtime_error( message.c_str());
809 }
810 }
811 ''')
812
813 # Ignore all C MuPDF functions; SWIG will still look at the C++ API in
814 # namespace mudf.
815 for fnname in generated.c_functions:
816 if fnname in (
817 'pdf_annot_type',
818 'pdf_widget_type',
819 'pdf_zugferd_profile',
820 ):
821 # These are also enums which we don't want to ignore. SWIGing the
822 # functions is hopefully harmless.
823 pass
824 elif fnname in ('x0', 'y0', 'x1', 'y1'):
825 # Windows appears to have functions called y0() and y1() e.g. in:
826 #
827 # C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.18362.0\\ucrt\\corecrt_math.h
828 #
829 # If we use `%ignore` with these, e.g. `%ignore ::y0`, swig
830 # unhelpfully seems to also ignore any member variables called `y0`
831 # or `y1`.
832 #
833 jlib.log('Not ignoring {fnname=} because breaks wrapping of fz_rect.')
834 pass
835 else:
836 text += f'%ignore ::{fnname};\n'
837
838 # Attempt to move C structs out of the way to allow wrapper classes to have
839 # the same name as the struct they wrap. Unfortunately this causes a small
840 # number of obscure errors from SWIG.
841 if 0:
842 for name in generated.c_structs:
843 text += f'%rename(lll_{name}) ::{name};\n'
844
845 for i in (
846 'fz_append_vprintf',
847 'fz_error_stack_slot',
848 'fz_format_string',
849 'fz_vsnprintf',
850 'fz_vthrow',
851 'fz_vwarn',
852 'fz_write_vprintf',
853 'fz_vlog_error_printf',
854
855 'fz_utf8_from_wchar',
856 'fz_wchar_from_utf8',
857 'fz_fopen_utf8',
858 'fz_remove_utf8',
859 'fz_argv_from_wargv',
860 'fz_free_argv',
861 'fz_stdods',
862 ):
863 text += f'%ignore {i};\n'
864 text += f'%ignore {rename.method(None, i)};\n'
865
866 text += textwrap.dedent(f'''
867 // Not implemented in mupdf.so: fz_colorspace_name_process_colorants
868 %ignore fz_colorspace_name_process_colorants;
869 %ignore fz_argv_from_wargv;
870
871 %ignore fz_open_file_w;
872
873 %ignore {rename.ll_fn('fz_append_vprintf')};
874 %ignore {rename.ll_fn('fz_error_stack_slot_s')};
875 %ignore {rename.ll_fn('fz_format_string')};
876 %ignore {rename.ll_fn('fz_vsnprintf')};
877 %ignore {rename.ll_fn('fz_vthrow')};
878 %ignore {rename.ll_fn('fz_vwarn')};
879 %ignore {rename.ll_fn('fz_write_vprintf')};
880 %ignore {rename.ll_fn('fz_vlog_error_printf')};
881 %ignore {rename.ll_fn('fz_open_file_w')};
882
883 // Ignore custom C++ variadic fns.
884 %ignore {rename.ll_fn('pdf_dict_getlv')};
885 %ignore {rename.ll_fn('pdf_dict_getl')};
886 %ignore {rename.fn('pdf_dict_getlv')};
887 %ignore {rename.fn('pdf_dict_getl')};
888
889 // SWIG can't handle this because it uses a valist.
890 %ignore {rename.ll_fn('Memento_vasprintf')};
891 %ignore {rename.fn('Memento_vasprintf')};
892
893 // These appear to be not present in Windows debug builds.
894 %ignore fz_assert_lock_held;
895 %ignore fz_assert_lock_not_held;
896 %ignore fz_lock_debug_lock;
897 %ignore fz_lock_debug_unlock;
898
899 %ignore Memento_cpp_new;
900 %ignore Memento_cpp_delete;
901 %ignore Memento_cpp_new_array;
902 %ignore Memento_cpp_delete_array;
903 %ignore Memento_showHash;
904
905 // asprintf() isn't available on Windows, so exclude Memento_asprintf because
906 // it is #define-d to asprintf.
907 %ignore {rename.ll_fn('Memento_asprintf')};
908 %ignore {rename.fn('Memento_asprintf')};
909
910 // Might prefer to #include mupdf/exceptions.h and make the
911 // %exception block below handle all the different exception types,
912 // but swig-3 cannot parse 'throw()' in mupdf/exceptions.h.
913 //
914 // So for now we just #include <stdexcept> and handle
915 // std::exception only.
916
917 %include "typemaps.i"
918 %include "cpointer.i"
919
920 // This appears to allow python to call fns taking an int64_t.
921 %include "stdint.i"
922
923 /*
924 This is only documented for Ruby, but is mentioned for Python at
925 https://sourceforge.net/p/swig/mailman/message/4867286/.
926
927 It makes the Python wrapper for `FzErrorBase` inherit Python's
928 `Exception` instead of `object`, which in turn means it can be
929 caught in Python with `except Exception as e: ...` or similar.
930
931 Note that while it will have the underlying C++ class's `what()`
932 method, this is not used by the `__str__()` and `__repr__()`
933 methods. Instead:
934
935 `__str__()` appears to return a tuple of the constructor args
936 that were originally used to create the exception object with
937 `PyObject_CallObject(class_, args)`.
938
939 `__repr__()` returns a SWIG-style string such as
940 `<texcept.MyError; proxy of <Swig Object of type 'MyError *' at
941 0xb61ebfabc00> >`.
942
943 We explicitly overwrite `__str__()` to call `what()`.
944 */
945 %feature("exceptionclass") FzErrorBase;
946
947 %{{
948 ''')
949
950 text += common
951
952 text += textwrap.dedent(f'''
953 %}}
954
955 %include exception.i
956 %include std_string.i
957 %include carrays.i
958 %include cdata.i
959 %include std_vector.i
960 %include std_map.i
961
962 {"%include argcargv.i" if language=="python" else ""}
963
964 %array_class(unsigned char, uchar_array);
965
966 %include <cstring.i>
967
968 namespace std
969 {{
970 %template(vectoruc) vector<unsigned char>;
971 %template(vectori) vector<int>;
972 %template(vectorf) vector<float>;
973 %template(vectord) vector<double>;
974 %template(vectors) vector<std::string>;
975 %template(map_string_int) map<std::string, int>;
976 %template(vectorq) vector<{rename.namespace_class("fz_quad")}>;
977 %template(vector_search_page2_hit) vector<fz_search_page2_hit>;
978 %template(vector_fz_font_ucs_gid) vector<fz_font_ucs_gid>;
979 %template(vector_fz_point) vector<fz_point>;
980 }};
981
982 // Make sure that operator++() gets converted to __next__().
983 //
984 // Note that swig already seems to do:
985 //
986 // operator* => __ref__
987 // operator== => __eq__
988 // operator!= => __ne__
989 // operator-> => __deref__
990 //
991 // Just need to add this method to containers that already have
992 // begin() and end():
993 // def __iter__( self):
994 // return CppIterator( self)
995 //
996
997 %rename(__increment__) *::operator++;
998
999 // Create fns that give access to arrays of some basic types, e.g. bytes_getitem().
1000 //
1001 %array_functions(unsigned char, bytes);
1002
1003 // Useful for fz_stroke_state::dash_list[].
1004 %array_functions(float, floats);
1005 ''')
1006
1007 if language == 'python':
1008 text += generated.swig_python_exceptions.getvalue()
1009
1010 text += textwrap.dedent(f'''
1011 // Ensure SWIG handles OUTPUT params.
1012 //
1013 %include "cpointer.i"
1014 ''')
1015
1016 if swig_major < 4:
1017 text += textwrap.dedent(f'''
1018 // SWIG version is less than 4 so swig is not able to copy
1019 // across comments from header file into generated code. The
1020 // next best thing is to use autodoc to make swig at least show
1021 // some generic information about arg types.
1022 //
1023 %feature("autodoc", "3");
1024 ''')
1025
1026 text += textwrap.dedent(f'''
1027 // Tell swig about pdf_clean_file()'s (int,argv)-style args:
1028 %apply (int ARGC, char **ARGV) {{ (int retainlen, char *retainlist[]) }}
1029 ''')
1030
1031 if language == 'python':
1032 text += textwrap.dedent( '''
1033 %include pybuffer.i
1034
1035 /* Convert Python buffer to (const unsigned char*, size_t) pair
1036 for python_buffer_data(). */
1037 %pybuffer_binary(
1038 const unsigned char* PYTHON_BUFFER_DATA,
1039 size_t PYTHON_BUFFER_SIZE
1040 );
1041 /* Convert Python buffer to (unsigned char*, size_t) pair for
1042 python_mutable_bytes_data(). */
1043 %pybuffer_mutable_binary(
1044 unsigned char* PYTHON_BUFFER_MUTABLE_DATA,
1045 size_t PYTHON_BUFFER_MUTABLE_SIZE
1046 );
1047 '''
1048 )
1049
1050 text += common
1051
1052 if language == 'python':
1053
1054 text += textwrap.dedent(f'''
1055 %pointer_functions(int, pint);
1056
1057 %pythoncode %{{
1058
1059 import inspect
1060 import os
1061 import re
1062 import sys
1063 import traceback
1064
1065 def log( text):
1066 print( text, file=sys.stderr)
1067
1068 g_mupdf_trace_director = (os.environ.get('MUPDF_trace_director') == '1')
1069
1070 def fz_lookup_metadata(document, key):
1071 """
1072 Like fz_lookup_metadata2() but returns None on error
1073 instead of raising exception.
1074 """
1075 try:
1076 return fz_lookup_metadata2(document, key)
1077 except Exception:
1078 return
1079 {rename.class_('fz_document')}.{rename.method('fz_document', 'fz_lookup_metadata')} \
1080 = fz_lookup_metadata
1081
1082 def pdf_lookup_metadata(document, key):
1083 """
1084 Likepsd_lookup_metadata2() but returns None on error
1085 instead of raising exception.
1086 """
1087 try:
1088 return pdf_lookup_metadata2(document, key)
1089 except Exception:
1090 return
1091 {rename.class_('pdf_document')}.{rename.method('pdf_document', 'pdf_lookup_metadata')} \
1092 = pdf_lookup_metadata
1093
1094 ''')
1095
1096 exception_info_text = inspect.getsource(jlib.exception_info)
1097 text += 'import inspect\n'
1098 text += 'import io\n'
1099 text += 'import os\n'
1100 text += 'import sys\n'
1101 text += 'import traceback\n'
1102 text += 'import types\n'
1103 text += exception_info_text
1104
1105 if language == 'python':
1106 # Make some additions to the generated Python module.
1107 #
1108 # E.g. python wrappers for functions that take out-params should return
1109 # tuples.
1110 #
1111 text += generated.swig_python
1112 text += generated.swig_python_set_error_classes.getvalue()
1113
1114 def set_class_method(struct, fn):
1115 return f'{rename.class_(struct)}.{rename.method(struct, fn)} = {fn}'
1116
1117 text += textwrap.dedent(f'''
1118
1119 # Wrap fz_parse_page_range() to fix SWIG bug where a NULL return
1120 # value seems to mess up the returned list - we end up with ret
1121 # containing two elements rather than three, e.g. [0, 2]. This
1122 # occurs with SWIG-3.0; maybe fixed in SWIG-4?
1123 #
1124 ll_fz_parse_page_range_orig = ll_fz_parse_page_range
1125 def ll_fz_parse_page_range(s, n):
1126 ret = ll_fz_parse_page_range_orig(s, n)
1127 if len(ret) == 2:
1128 return None, 0, 0
1129 else:
1130 return ret[0], ret[1], ret[2]
1131 fz_parse_page_range = ll_fz_parse_page_range
1132
1133 # Provide native python implementation of format_output_path() (->
1134 # fz_format_output_path).
1135 #
1136 def ll_fz_format_output_path( format, page):
1137 m = re.search( '(%[0-9]*d)', format)
1138 if m:
1139 ret = format[ :m.start(1)] + str(page) + format[ m.end(1):]
1140 else:
1141 dot = format.rfind( '.')
1142 if dot < 0:
1143 dot = len( format)
1144 ret = format[:dot] + str(page) + format[dot:]
1145 return ret
1146 fz_format_output_path = ll_fz_format_output_path
1147
1148 class IteratorWrap:
1149 """
1150 This is a Python iterator for containers that have C++-style
1151 begin() and end() methods that return iterators.
1152
1153 Iterators must have the following methods:
1154
1155 __increment__(): move to next item in the container.
1156 __ref__(): return reference to item in the container.
1157
1158 Must also be able to compare two iterators for equality.
1159
1160 """
1161 def __init__( self, container):
1162 self.container = container
1163 self.pos = None
1164 self.end = container.end()
1165 def __iter__( self):
1166 return self
1167 def __next__( self): # for python2.
1168 if self.pos is None:
1169 self.pos = self.container.begin()
1170 else:
1171 self.pos.__increment__()
1172 if self.pos == self.end:
1173 raise StopIteration()
1174 return self.pos.__ref__()
1175 def next( self): # for python3.
1176 return self.__next__()
1177
1178 # The auto-generated Python class method
1179 # {rename.class_('fz_buffer')}.{rename.method('fz_buffer', 'fz_buffer_extract')}() returns (size, data).
1180 #
1181 # But these raw values aren't particularly useful to
1182 # Python code so we change the method to return a Python
1183 # bytes instance instead, using the special C function
1184 # buffer_extract_bytes() defined above.
1185 #
1186 # The raw values for a buffer are available via
1187 # fz_buffer_storage().
1188
1189 def ll_fz_buffer_extract(buffer):
1190 """
1191 Returns buffer data as a Python bytes instance, leaving the
1192 buffer empty.
1193 """
1194 assert isinstance( buffer, fz_buffer)
1195 return ll_fz_buffer_to_bytes_internal(buffer, clear=1)
1196 def fz_buffer_extract(buffer):
1197 """
1198 Returns buffer data as a Python bytes instance, leaving the
1199 buffer empty.
1200 """
1201 assert isinstance( buffer, FzBuffer)
1202 return ll_fz_buffer_extract(buffer.m_internal)
1203 {set_class_method('fz_buffer', 'fz_buffer_extract')}
1204
1205 def ll_fz_buffer_extract_copy( buffer):
1206 """
1207 Returns buffer data as a Python bytes instance, leaving the
1208 buffer unchanged.
1209 """
1210 assert isinstance( buffer, fz_buffer)
1211 return ll_fz_buffer_to_bytes_internal(buffer, clear=0)
1212 def fz_buffer_extract_copy( buffer):
1213 """
1214 Returns buffer data as a Python bytes instance, leaving the
1215 buffer unchanged.
1216 """
1217 assert isinstance( buffer, FzBuffer)
1218 return ll_fz_buffer_extract_copy(buffer.m_internal)
1219 {set_class_method('fz_buffer', 'fz_buffer_extract_copy')}
1220
1221 # [ll_fz_buffer_storage_memoryview() is implemented in C.]
1222 def fz_buffer_storage_memoryview( buffer, writable=False):
1223 """
1224 Returns a read-only or writable Python `memoryview` onto
1225 `fz_buffer` data. This relies on `buffer` existing and
1226 not changing size while the `memoryview` is used.
1227 """
1228 assert isinstance( buffer, FzBuffer)
1229 return ll_fz_buffer_storage_memoryview( buffer.m_internal, writable)
1230 {set_class_method('fz_buffer', 'fz_buffer_storage_memoryview')}
1231
1232 # Overwrite wrappers for fz_new_buffer_from_copied_data() to
1233 # take Python buffer.
1234 #
1235 ll_fz_new_buffer_from_copied_data_orig = ll_fz_new_buffer_from_copied_data
1236 def ll_fz_new_buffer_from_copied_data(data):
1237 """
1238 Returns fz_buffer containing copy of `data`, which should
1239 be a `bytes` or similar Python buffer instance.
1240 """
1241 buffer_ = ll_fz_new_buffer_from_copied_data_orig(python_buffer_data(data), len(data))
1242 return buffer_
1243 def fz_new_buffer_from_copied_data(data):
1244 """
1245 Returns FzBuffer containing copy of `data`, which should be
1246 a `bytes` or similar Python buffer instance.
1247 """
1248 return FzBuffer( ll_fz_new_buffer_from_copied_data( data))
1249 {set_class_method('fz_buffer', 'fz_new_buffer_from_copied_data')}
1250
1251 def ll_pdf_dict_getl(obj, *tail):
1252 """
1253 Python implementation of ll_pdf_dict_getl(), because SWIG
1254 doesn't handle variadic args. Each item in `tail` should be
1255 `mupdf.pdf_obj`.
1256 """
1257 for key in tail:
1258 if not obj:
1259 break
1260 obj = ll_pdf_dict_get(obj, key)
1261 assert isinstance(obj, pdf_obj)
1262 return obj
1263 def pdf_dict_getl(obj, *tail):
1264 """
1265 Python implementation of pdf_dict_getl(), because SWIG
1266 doesn't handle variadic args. Each item in `tail` should be
1267 a `mupdf.PdfObj`.
1268 """
1269 for key in tail:
1270 if not obj.m_internal:
1271 break
1272 obj = pdf_dict_get(obj, key)
1273 assert isinstance(obj, PdfObj)
1274 return obj
1275 {set_class_method('pdf_obj', 'pdf_dict_getl')}
1276
1277 def ll_pdf_dict_putl(obj, val, *tail):
1278 """
1279 Python implementation of ll_pdf_dict_putl() because SWIG
1280 doesn't handle variadic args. Each item in `tail` should
1281 be a SWIG wrapper for a `pdf_obj`.
1282 """
1283 if ll_pdf_is_indirect( obj):
1284 obj = ll_pdf_resolve_indirect_chain( obj)
1285 if not pdf_is_dict( obj):
1286 raise Exception(f'not a dict: {{obj}}')
1287 if not tail:
1288 return
1289 doc = ll_pdf_get_bound_document( obj)
1290 for i, key in enumerate( tail[:-1]):
1291 assert isinstance( key, PdfObj), f'Item {{i}} in `tail` should be a pdf_obj but is a {{type(key)}}.'
1292 next_obj = ll_pdf_dict_get( obj, key)
1293 if not next_obj:
1294 # We have to create entries
1295 next_obj = ll_pdf_new_dict( doc, 1)
1296 ll_pdf_dict_put( obj, key, next_obj)
1297 obj = next_obj
1298 key = tail[-1]
1299 ll_pdf_dict_put( obj, key, val)
1300 def pdf_dict_putl(obj, val, *tail):
1301 """
1302 Python implementation of pdf_dict_putl(fz_context *ctx,
1303 pdf_obj *obj, pdf_obj *val, ...) because SWIG doesn't
1304 handle variadic args. Each item in `tail` should
1305 be a SWIG wrapper for a `PdfObj`.
1306 """
1307 if pdf_is_indirect( obj):
1308 obj = pdf_resolve_indirect_chain( obj)
1309 if not pdf_is_dict( obj):
1310 raise Exception(f'not a dict: {{obj}}')
1311 if not tail:
1312 return
1313 doc = pdf_get_bound_document( obj)
1314 for i, key in enumerate( tail[:-1]):
1315 assert isinstance( key, PdfObj), f'item {{i}} in `tail` should be a PdfObj but is a {{type(key)}}.'
1316 next_obj = pdf_dict_get( obj, key)
1317 if not next_obj.m_internal:
1318 # We have to create entries
1319 next_obj = pdf_new_dict( doc, 1)
1320 pdf_dict_put( obj, key, next_obj)
1321 obj = next_obj
1322 key = tail[-1]
1323 pdf_dict_put( obj, key, val)
1324 {set_class_method('pdf_obj', 'pdf_dict_putl')}
1325
1326 def pdf_dict_putl_drop(obj, *tail):
1327 raise Exception('mupdf.pdf_dict_putl_drop() is unsupported and unnecessary in Python because reference counting is automatic. Instead use mupdf.pdf_dict_putl().')
1328 {set_class_method('pdf_obj', 'pdf_dict_putl_drop')}
1329
1330 def ll_pdf_set_annot_color(annot, color):
1331 """
1332 Low-level Python implementation of pdf_set_annot_color()
1333 using ll_pdf_set_annot_color2().
1334 """
1335 if isinstance(color, float):
1336 ll_pdf_set_annot_color2(annot, 1, color, 0, 0, 0)
1337 elif len(color) == 1:
1338 ll_pdf_set_annot_color2(annot, 1, color[0], 0, 0, 0)
1339 elif len(color) == 2:
1340 ll_pdf_set_annot_color2(annot, 2, color[0], color[1], 0, 0)
1341 elif len(color) == 3:
1342 ll_pdf_set_annot_color2(annot, 3, color[0], color[1], color[2], 0)
1343 elif len(color) == 4:
1344 ll_pdf_set_annot_color2(annot, 4, color[0], color[1], color[2], color[3])
1345 else:
1346 raise Exception( f'Unexpected color should be float or list of 1-4 floats: {{color}}')
1347 def pdf_set_annot_color(self, color):
1348 return ll_pdf_set_annot_color(self.m_internal, color)
1349 {set_class_method('pdf_annot', 'pdf_set_annot_color')}
1350
1351 def ll_pdf_set_annot_interior_color(annot, color):
1352 """
1353 Low-level Python version of pdf_set_annot_color() using
1354 pdf_set_annot_color2().
1355 """
1356 if isinstance(color, float):
1357 ll_pdf_set_annot_interior_color2(annot, 1, color, 0, 0, 0)
1358 elif len(color) == 1:
1359 ll_pdf_set_annot_interior_color2(annot, 1, color[0], 0, 0, 0)
1360 elif len(color) == 2:
1361 ll_pdf_set_annot_interior_color2(annot, 2, color[0], color[1], 0, 0)
1362 elif len(color) == 3:
1363 ll_pdf_set_annot_interior_color2(annot, 3, color[0], color[1], color[2], 0)
1364 elif len(color) == 4:
1365 ll_pdf_set_annot_interior_color2(annot, 4, color[0], color[1], color[2], color[3])
1366 else:
1367 raise Exception( f'Unexpected color should be float or list of 1-4 floats: {{color}}')
1368 def pdf_set_annot_interior_color(self, color):
1369 """
1370 Python version of pdf_set_annot_color() using
1371 pdf_set_annot_color2().
1372 """
1373 return ll_pdf_set_annot_interior_color(self.m_internal, color)
1374 {set_class_method('pdf_annot', 'pdf_set_annot_interior_color')}
1375
1376 def ll_fz_fill_text( dev, text, ctm, colorspace, color, alpha, color_params):
1377 """
1378 Low-level Python version of fz_fill_text() taking list/tuple for `color`.
1379 """
1380 color = tuple(color) + (0,) * (4-len(color))
1381 assert len(color) == 4, f'color not len 4: len={{len(color)}}: {{color}}'
1382 return ll_fz_fill_text2(dev, text, ctm, colorspace, *color, alpha, color_params)
1383 def fz_fill_text(dev, text, ctm, colorspace, color, alpha, color_params):
1384 """
1385 Python version of fz_fill_text() taking list/tuple for `color`.
1386 """
1387 return ll_fz_fill_text(
1388 dev.m_internal,
1389 text.m_internal,
1390 ctm.internal(),
1391 colorspace.m_internal,
1392 color,
1393 alpha,
1394 color_params.internal(),
1395 )
1396 {set_class_method('fz_device', 'fz_fill_text')}
1397
1398 # Override mupdf_convert_color() to return (rgb0, rgb1, rgb2, rgb3).
1399 def ll_fz_convert_color( ss, sv, ds, is_, params):
1400 """
1401 Low-level Python version of fz_convert_color().
1402
1403 `sv` should be a float or list of 1-4 floats or a SWIG
1404 representation of a float*.
1405
1406 Returns (dv0, dv1, dv2, dv3).
1407 """
1408 dv = fz_convert_color2_v()
1409 if isinstance( sv, float):
1410 ll_fz_convert_color2( ss, sv, 0.0, 0.0, 0.0, ds, dv, is_, params)
1411 elif isinstance( sv, (tuple, list)):
1412 sv2 = tuple(sv) + (0,) * (4-len(sv))
1413 ll_fz_convert_color2( ss, *sv2, ds, dv, is_, params)
1414 else:
1415 # Assume `sv` is SWIG representation of a `float*`.
1416 ll_fz_convert_color2( ss, sv, ds, dv, is_, params)
1417 return dv.v0, dv.v1, dv.v2, dv.v3
1418 def fz_convert_color( ss, sv, ds, is_, params):
1419 """
1420 Python version of fz_convert_color().
1421
1422 `sv` should be a float or list of 1-4 floats or a SWIG
1423 representation of a float*.
1424
1425 Returns (dv0, dv1, dv2, dv3).
1426 """
1427 return ll_fz_convert_color( ss.m_internal, sv, ds.m_internal, is_.m_internal, params.internal())
1428 {set_class_method('fz_colorspace', 'fz_convert_color')}
1429
1430 # Override fz_set_warning_callback() and
1431 # fz_set_error_callback() to use Python classes derived from
1432 # our SWIG Director class DiagnosticCallback (defined in C), so
1433 # that fnptrs can call Python code.
1434 #
1435
1436 # We store DiagnosticCallbackPython instances in these
1437 # globals to ensure they continue to exist after
1438 # set_diagnostic_callback() returns.
1439 #
1440 set_warning_callback_s = None
1441 set_error_callback_s = None
1442
1443 # Override set_error_callback().
1444 class DiagnosticCallbackPython( DiagnosticCallback):
1445 """
1446 Overrides Director class DiagnosticCallback's virtual
1447 `_print()` method in Python.
1448 """
1449 def __init__( self, description, printfn):
1450 super().__init__( description)
1451 self.printfn = printfn
1452 if g_mupdf_trace_director:
1453 log( f'DiagnosticCallbackPython[{{self.m_description}}].__init__() self={{self!r}} printfn={{printfn!r}}')
1454 def __del__( self):
1455 if g_mupdf_trace_director:
1456 log( f'DiagnosticCallbackPython[{{self.m_description}}].__del__() destructor called.')
1457 def _print( self, message):
1458 if g_mupdf_trace_director:
1459 log( f'DiagnosticCallbackPython[{{self.m_description}}]._print(): Calling self.printfn={{self.printfn!r}} with message={{message!r}}')
1460 try:
1461 self.printfn( message)
1462 except Exception as e:
1463 # This shouldn't happen, so always output a diagnostic.
1464 log( f'DiagnosticCallbackPython[{{self.m_description}}]._print(): Warning: exception from self.printfn={{self.printfn!r}}: e={{e!r}}')
1465 # Calling `raise` here serves to test
1466 # `DiagnosticCallback()`'s swallowing of what will
1467 # be a C++ exception. But we could swallow the
1468 # exception here instead.
1469 raise
1470
1471 def set_diagnostic_callback( description, printfn):
1472 if g_mupdf_trace_director:
1473 log( f'set_diagnostic_callback() description={{description!r}} printfn={{printfn!r}}')
1474 if printfn:
1475 ret = DiagnosticCallbackPython( description, printfn)
1476 return ret
1477 else:
1478 if g_mupdf_trace_director:
1479 log( f'Calling ll_fz_set_{{description}}_callback() with (None, None)')
1480 if description == 'error':
1481 ll_fz_set_error_callback( None, None)
1482 elif description == 'warning':
1483 ll_fz_set_warning_callback( None, None)
1484 else:
1485 assert 0, f'Unrecognised description={{description!r}}'
1486 return None
1487
1488 def fz_set_error_callback( printfn):
1489 global set_error_callback_s
1490 set_error_callback_s = set_diagnostic_callback( 'error', printfn)
1491
1492 def fz_set_warning_callback( printfn):
1493 global set_warning_callback_s
1494 set_warning_callback_s = set_diagnostic_callback( 'warning', printfn)
1495
1496 # Direct access to fz_pixmap samples.
1497 def ll_fz_pixmap_samples_memoryview( pixmap):
1498 """
1499 Returns a writable Python `memoryview` for a `fz_pixmap`.
1500 """
1501 assert isinstance( pixmap, fz_pixmap)
1502 ret = python_memoryview_from_memory(
1503 ll_fz_pixmap_samples( pixmap),
1504 ll_fz_pixmap_stride( pixmap) * ll_fz_pixmap_height( pixmap),
1505 1, # writable
1506 )
1507 return ret
1508 def fz_pixmap_samples_memoryview( pixmap):
1509 """
1510 Returns a writable Python `memoryview` for a `FzPixmap`.
1511 """
1512 return ll_fz_pixmap_samples_memoryview( pixmap.m_internal)
1513 {set_class_method('fz_pixmap', 'fz_pixmap_samples_memoryview')}
1514
1515 # Avoid potential unsafe use of variadic args by forcing a
1516 # single arg and escaping all '%' characters. (Passing ('%s',
1517 # text) does not work - results in "(null)" being output.)
1518 #
1519 ll_fz_warn_original = ll_fz_warn
1520 def ll_fz_warn( text):
1521 assert isinstance( text, str), f'text={{text!r}} str={{str!r}}'
1522 text = text.replace( '%', '%%')
1523 return ll_fz_warn_original( text)
1524 fz_warn = ll_fz_warn
1525
1526 # Force use of pdf_load_field_name2() instead of
1527 # pdf_load_field_name() because the latter returns a char*
1528 # buffer that must be freed by the caller.
1529 ll_pdf_load_field_name = ll_pdf_load_field_name2
1530 pdf_load_field_name = pdf_load_field_name2
1531 {set_class_method('pdf_obj', 'pdf_load_field_name')}
1532
1533 # It's important that when we create class derived
1534 # from StoryPositionsCallback, we ensure that
1535 # StoryPositionsCallback's constructor is called. Otherwise
1536 # the new instance doesn't seem to be an instance of
1537 # StoryPositionsCallback.
1538 #
1539 class StoryPositionsCallback_python( StoryPositionsCallback):
1540 def __init__( self, python_callback):
1541 super().__init__()
1542 self.python_callback = python_callback
1543 def call( self, position):
1544 self.python_callback( position)
1545
1546 ll_fz_story_positions_orig = ll_fz_story_positions
1547 def ll_fz_story_positions( story, python_callback):
1548 """
1549 Custom replacement for `ll_fz_story_positions()` that takes
1550 a Python callable `python_callback`.
1551 """
1552 #log( f'll_fz_story_positions() type(story)={{type(story)!r}} type(python_callback)={{type(python_callback)!r}}')
1553 python_callback_instance = StoryPositionsCallback_python( python_callback)
1554 ll_fz_story_positions_director( story, python_callback_instance)
1555 def fz_story_positions( story, python_callback):
1556 #log( f'fz_story_positions() type(story)={{type(story)!r}} type(python_callback)={{type(python_callback)!r}}')
1557 assert isinstance( story, FzStory)
1558 assert callable( python_callback)
1559 def python_callback2( position):
1560 position2 = FzStoryElementPosition( position)
1561 python_callback( position2)
1562 ll_fz_story_positions( story.m_internal, python_callback2)
1563 {set_class_method('fz_story', 'fz_story_positions')}
1564
1565 # Monkey-patch `FzDocumentWriter.__init__()` to set `self._out`
1566 # to any `FzOutput2` arg. This ensures that the Python part of
1567 # the derived `FzOutput2` instance is kept alive for use by the
1568 # `FzDocumentWriter`, otherwise Python can delete it, then get
1569 # a SEGV if C++ tries to call the derived Python methods.
1570 #
1571 # [We don't patch equivalent class-aware functions such
1572 # as `fz_new_pdf_writer_with_output()` because they are
1573 # not available to C++/Python, because FzDocumentWriter is
1574 # non-copyable.]
1575 #
1576 FzDocumentWriter__init__0 = FzDocumentWriter.__init__
1577 def FzDocumentWriter__init__1(self, *args):
1578 out = None
1579 for arg in args:
1580 if isinstance( arg, FzOutput2):
1581 assert not out, "More than one FzOutput2 passed to FzDocumentWriter.__init__()"
1582 out = arg
1583 if out is not None:
1584 self._out = out
1585 return FzDocumentWriter__init__0(self, *args)
1586 FzDocumentWriter.__init__ = FzDocumentWriter__init__1
1587
1588 # Create class derived from
1589 # fz_install_load_system_font_funcs_args class wrapper with
1590 # overrides of the virtual functions to allow calling of Python
1591 # callbacks.
1592 #
1593 class fz_install_load_system_font_funcs_args3({rename.class_('fz_install_load_system_font_funcs_args')}2):
1594 """
1595 Class derived from Swig Director class
1596 fz_install_load_system_font_funcs_args2, to allow
1597 implementation of fz_install_load_system_font_funcs with
1598 Python callbacks.
1599 """
1600 def __init__(self, f=None, f_cjk=None, f_fallback=None):
1601 super().__init__()
1602
1603 self.f3 = f
1604 self.f_cjk3 = f_cjk
1605 self.f_fallback3 = f_fallback
1606
1607 self.use_virtual_f(True if f else False)
1608 self.use_virtual_f_cjk(True if f_cjk else False)
1609 self.use_virtual_f_fallback(True if f_fallback else False)
1610
1611 def ret_font(self, font):
1612 if font is None:
1613 return None
1614 elif isinstance(font, {rename.class_('fz_font')}):
1615 return ll_fz_keep_font(font.m_internal)
1616 elif isinstance(font, fz_font):
1617 return font
1618 else:
1619 assert 0, f'Expected FzFont or fz_font, but fz_install_load_system_font_funcs() callback returned {{type(font)=}}'
1620
1621 def f(self, ctx, name, bold, italic, needs_exact_metrics):
1622 font = self.f3(name, bold, italic, needs_exact_metrics)
1623 return self.ret_font(font)
1624
1625 def f_cjk(self, ctx, name, ordering, serif):
1626 font = self.f_cjk3(name, ordering, serif)
1627 return self.ret_font(font)
1628
1629 def f_fallback(self, ctx, script, language, serif, bold, italic):
1630 font = self.f_fallback3(script, language, serif, bold, italic)
1631 return self.ret_font(font)
1632
1633 # We store the most recently created
1634 # fz_install_load_system_font_funcs_args in this global so that
1635 # it is not cleaned up by Python.
1636 g_fz_install_load_system_font_funcs_args = None
1637
1638 def fz_install_load_system_font_funcs(f=None, f_cjk=None, f_fallback=None):
1639 """
1640 Python override for MuPDF
1641 fz_install_load_system_font_funcs() using Swig Director
1642 support. Python callbacks are not passed a `ctx` arg, and
1643 can return None, a mupdf.fz_font or a mupdf.FzFont.
1644 """
1645 global g_fz_install_load_system_font_funcs_args
1646 g_fz_install_load_system_font_funcs_args = fz_install_load_system_font_funcs_args3(
1647 f,
1648 f_cjk,
1649 f_fallback,
1650 )
1651 fz_install_load_system_font_funcs2(g_fz_install_load_system_font_funcs_args)
1652
1653 Py_LIMITED_API = {repr(build_dirs.Py_LIMITED_API) if build_dirs.Py_LIMITED_API else 'None'}
1654 ''')
1655
1656 # Add __iter__() methods for all classes with begin() and end() methods.
1657 #
1658 for classname in generated.container_classnames:
1659 text += f'{classname}.__iter__ = lambda self: IteratorWrap( self)\n'
1660
1661 # For all wrapper classes with a to_string() method, add a __str__()
1662 # method to the underlying struct's Python class, which calls
1663 # to_string_<structname>().
1664 #
1665 # E.g. this allows Python code to print a mupdf.fz_rect instance.
1666 #
1667 # [We could instead call our generated to_string() and rely on overloading,
1668 # but this will end up switching on the type in the SWIG code.]
1669 #
1670 for struct_name in generated.to_string_structnames:
1671 text += f'{struct_name}.__str__ = lambda s: to_string_{struct_name}(s)\n'
1672 text += f'{struct_name}.__repr__ = lambda s: to_string_{struct_name}(s)\n'
1673
1674 # For all wrapper classes with a to_string() method, add a __str__() method
1675 # to the Python wrapper class, which calls the class's to_string() method.
1676 #
1677 # E.g. this allows Python code to print a mupdf.Rect instance.
1678 #
1679 for struct_name in generated.to_string_structnames:
1680 text += f'{rename.class_(struct_name)}.__str__ = lambda self: self.to_string()\n'
1681 text += f'{rename.class_(struct_name)}.__repr__ = lambda self: self.to_string()\n'
1682
1683 text += '%}\n'
1684
1685 if 1: # lgtm [py/constant-conditional-expression]
1686 # This is a horrible hack to avoid swig failing because
1687 # include/mupdf/pdf/object.h defines an enum which contains a #include.
1688 #
1689 # Would like to pre-process files in advance so that swig doesn't see
1690 # the #include, but this breaks swig in a different way - swig cannot
1691 # cope with some code in system headers.
1692 #
1693 # So instead we copy include/mupdf/pdf/object.h into
1694 # {build_dirs.dir_mupdf}/platform/python/include/mupdf/pdf/object.h,
1695 # manually expanding the #include using a Python .replace() call. Then
1696 # we specify {build_dirs.dir_mupdf}/platform/python/include as the
1697 # first include path so that our modified mupdf/pdf/object.h will get
1698 # included in preference to the original.
1699 #
1700 os.makedirs(f'{build_dirs.dir_mupdf}/platform/python/include/mupdf/pdf', exist_ok=True)
1701 with open( f'{build_dirs.dir_mupdf}/include/mupdf/pdf/object.h') as f:
1702 o = f.read()
1703 with open( f'{build_dirs.dir_mupdf}/include/mupdf/pdf/name-table.h') as f:
1704 name_table_h = f.read()
1705 oo = o.replace( '#include "mupdf/pdf/name-table.h"\n', name_table_h)
1706 assert oo != o
1707 jlib.fs_update( oo, f'{build_dirs.dir_mupdf}/platform/python/include/mupdf/pdf/object.h')
1708
1709 swig_i = build_dirs.mupdfcpp_swig_i(language)
1710 swig_cpp = build_dirs.mupdfcpp_swig_cpp(language)
1711 include1 = f'{build_dirs.dir_mupdf}/include/'
1712 include2 = f'{build_dirs.dir_mupdf}/platform/c++/include'
1713 swig_py = f'{build_dirs.dir_so}/mupdf.py'
1714
1715 swig2_i = f'{build_dirs.dir_mupdf}/platform/{language}/mupdfcpp2_swig.i'
1716 swig2_cpp = f'{build_dirs.dir_mupdf}/platform/{language}/mupdfcpp2_swig.cpp'
1717 swig2_py = f'{build_dirs.dir_so}/mupdf2.py'
1718
1719 os.makedirs( f'{build_dirs.dir_mupdf}/platform/{language}', exist_ok=True)
1720 os.makedirs( f'{build_dirs.dir_so}', exist_ok=True)
1721 util.update_file_regress( text, swig_i, check_regress)
1722 jlib.fs_update( '', swig2_i)
1723
1724 # Disable some unhelpful SWIG warnings. Must not use -Wall as it overrides
1725 # all warning disables.
1726 disable_swig_warnings = [
1727 201, # Warning 201: Unable to find 'stddef.h'
1728 314, # Warning 314: 'print' is a python keyword, renaming to '_print'
1729 302, # Warning 302: Identifier 'pdf_annot_type' redefined (ignored),
1730 312, # Warning 312: Nested union not currently supported (ignored).
1731 321, # Warning 321: 'max' conflicts with a built-in name in python
1732 322, # Warning 322: Redundant redeclaration of 'pdf_annot',
1733 362, # Warning 362: operator= ignored
1734 451, # Warning 451: Setting a const char * variable may leak memory.
1735 503, # Warning 503: Can't wrap 'operator <<' unless renamed to a valid identifier.
1736 512, # Warning 512: Overloaded method mupdf::DrawOptions::internal() const ignored, using non-const method mupdf::DrawOptions::internal() instead.
1737 509, # Warning 509: Overloaded method mupdf::FzAaContext::FzAaContext(::fz_aa_context const) effectively ignored,
1738 560, # Warning 560: Unknown Doxygen command: d.
1739 ]
1740
1741 disable_swig_warnings = [ '-' + str( x) for x in disable_swig_warnings]
1742 disable_swig_warnings = '-w' + ','.join( disable_swig_warnings)
1743
1744 # Preserve any existing file `swig_cpp`, so that we can restore the
1745 # mtime if SWIG produces an unchanged file. This then avoids unnecessary
1746 # recompilation.
1747 #
1748 # 2022-11-16: Disabled this, because it can result in continuous
1749 # unnecessary rebuilds, e.g. if .cpp is older than a mupdf header.
1750 #
1751 swig_cpp_old = None
1752 if 0 and os.path.exists( swig_cpp):
1753 swig_cpp_old = f'{swig_cpp}-old'
1754 jlib.fs_copy( swig_cpp, swig_cpp_old)
1755
1756 if language == 'python':
1757 # Maybe use '^' on windows as equivalent to unix '\\' for multiline
1758 # ending?
1759 def make_command( module, cpp, swig_i):
1760 cpp = os.path.relpath( cpp)
1761 swig_i = os.path.relpath( swig_i)
1762 # We need to predefine MUPDF_FITZ_HEAP_H to disable parsing of
1763 # include/mupdf/fitz/heap.h. Otherwise swig's preprocessor seems to
1764 # ignore #undef's in include/mupdf/fitz/heap-imp.h then complains
1765 # about redefinition of macros in include/mupdf/fitz/heap.h.
1766 command = f'''
1767 "{swig_command}"
1768 {"-D_WIN32" if state_.windows else ""}
1769 -c++
1770 {"-doxygen" if swig_major >= 4 else ""}
1771 -python
1772 -Wextra
1773 {disable_swig_warnings}
1774 -module {module}
1775 -outdir {os.path.relpath(build_dirs.dir_mupdf)}/platform/python
1776 -o {cpp}
1777 -includeall
1778 {os.environ.get('XCXXFLAGS', '')}
1779 -I{os.path.relpath(build_dirs.dir_mupdf)}/platform/python/include
1780 -I{os.path.relpath(include1)}
1781 -I{os.path.relpath(include2)}
1782 -ignoremissing
1783 -DMUPDF_FITZ_HEAP_H
1784 {swig_i}
1785 '''
1786 return command
1787
1788 def modify_py( path_in, path_out):
1789 with open( path_in) as f:
1790 text = f.read()
1791
1792 # Change all our PDF_ENUM_NAME_* enums so that they are actually
1793 # PdfObj instances so that they can be used like any other PdfObj.
1794 #
1795 #jlib.log('{len(generated.c_enums)=}')
1796 for enum_type, enum_names in generated.c_enums.items():
1797 for enum_name in enum_names:
1798 if enum_name.startswith( 'PDF_ENUM_NAME_'):
1799 text += f'{enum_name} = {rename.class_("pdf_obj")}( obj_enum_to_obj( {enum_name}))\n'
1800
1801 # 2024-09-28: important to not include PDF_LIMIT here, because
1802 # pdf_drop_obj() treats all pdf_obj*'s as real pointers if they are
1803 # >= PDF_LIMIT.
1804 for name in ('NULL', 'TRUE', 'FALSE'):
1805 text += f'PDF_{name} = {rename.class_("pdf_obj")}( obj_enum_to_obj( PDF_ENUM_{name}))\n'
1806
1807 jlib.fs_update(text, path_out)
1808
1809 jlib.fs_update( '', swig2_cpp)
1810 jlib.fs_remove( swig2_py)
1811
1812 # Make main mupdf .so.
1813 command = make_command( 'mupdf', swig_cpp, swig_i)
1814 swig_py_ = f'{build_dirs.dir_mupdf}/platform/python/mupdf.py'
1815 rebuilt = jlib.build(
1816 (swig_i, include1, include2),
1817 (swig_cpp, swig_py_),
1818 command,
1819 force_rebuild,
1820 )
1821 jlib.log(f'swig => {rebuilt=}.')
1822 updated = modify_py( swig_py_, swig_py)
1823 jlib.log(f'modify_py() => {updated=}.')
1824
1825
1826 elif language == 'csharp':
1827 outdir = os.path.relpath(f'{build_dirs.dir_mupdf}/platform/csharp')
1828 os.makedirs(outdir, exist_ok=True)
1829 # Looks like swig comes up with 'mupdfcpp_swig_wrap.cxx' leafname.
1830 #
1831 # We include platform/python/include in order to pick up the modified
1832 # include/mupdf/pdf/object.h that we generate elsewhere.
1833 dllimport = 'mupdfcsharp.so'
1834 if state_.windows:
1835 # Would like to specify relative path to .dll with:
1836 # dllimport = os.path.relpath( f'{build_dirs.dir_so}/mupdfcsharp.dll')
1837 # but Windows/.NET doesn't seem to support this, despite
1838 # https://stackoverflow.com/questions/31807289 "how can i add a
1839 # swig generated c dll reference to a c sharp project".
1840 #
1841 dllimport = 'mupdfcsharp.dll'
1842
1843 # See https://www.swig.org/Doc4.2/CSharp.html `23.3.1 Primitive types`
1844 # for description of SWIGWORDSIZE64. If we were to build on 32-bit Linux
1845 # we would have to remove the `-DSWIGWORDSIZE64` flag.
1846 command = (f'''
1847 "{swig_command}"
1848 {"-D_WIN32" if state_.windows else ""}
1849 {"-DSWIGWORDSIZE64" if state_.linux else ""}
1850 -c++
1851 -csharp
1852 -Wextra
1853 {disable_swig_warnings}
1854 -module mupdf
1855 -namespace mupdf
1856 -dllimport {dllimport}
1857 -outdir {outdir}
1858 -outfile mupdf.cs
1859 -o {os.path.relpath(swig_cpp)}
1860 -includeall
1861 -I{os.path.relpath(build_dirs.dir_mupdf)}/platform/python/include
1862 -I{os.path.relpath(include1)}
1863 -I{os.path.relpath(include2)}
1864 -ignoremissing
1865 -DMUPDF_FITZ_HEAP_H
1866 {os.path.relpath(swig_i)}
1867 ''')
1868
1869 rebuilt = jlib.build(
1870 (swig_i, include1, include2),
1871 (f'{outdir}/mupdf.cs', os.path.relpath(swig_cpp)),
1872 command,
1873 force_rebuild,
1874 )
1875 # fixme: use <rebuilt> line with language=='python' to avoid multiple
1876 # modifications to unchanged mupdf.cs?
1877 #
1878 # For classes that have our to_string() method, override C#'s
1879 # ToString() to call to_string().
1880 with open(f'{outdir}/mupdf.cs') as f:
1881 cs = f.read()
1882 cs2 = re.sub(
1883 '(( *)public string to_string[(][)])',
1884 '\\2public override string ToString() { return to_string(); }\n\\1',
1885 cs,
1886 )
1887 jlib.log1('{len(cs)=}')
1888 jlib.log1('{len(cs2)=}')
1889 assert cs2 != cs, f'Failed to add toString() methods.'
1890 jlib.log1('{len(generated.swig_csharp)=}')
1891 assert len(generated.swig_csharp)
1892 cs2 += generated.swig_csharp
1893 jlib.log1( 'Updating cs2 => {build_dirs.dir_so}/mupdf.cs')
1894 jlib.fs_update(cs2, f'{build_dirs.dir_so}/mupdf.cs')
1895 #jlib.fs_copy(f'{outdir}/mupdf.cs', f'{build_dirs.dir_so}/mupdf.cs')
1896 jlib.log1('{rebuilt=}')
1897
1898 else:
1899 assert 0
1900
1901 # Disabled; see above for explanation.
1902 if 0 and swig_cpp_old:
1903 with open( swig_cpp_old) as f:
1904 swig_cpp_contents_old = f.read()
1905 with open(swig_cpp) as f:
1906 swig_cpp_contents_new = f.read()
1907 if swig_cpp_contents_new == swig_cpp_contents_old:
1908 # File <swig_cpp> unchanged, so restore the mtime to avoid
1909 # unnecessary recompilation.
1910 jlib.log( 'File contents unchanged, copying {swig_cpp_old=} => {swig_cpp=}')
1911 jlib.fs_rename( swig_cpp_old, swig_cpp)
1912
1913
1914 def test_swig():
1915 '''
1916 For testing different swig .i constructs.
1917 '''
1918 test_i = textwrap.dedent('''
1919 %include argcargv.i
1920
1921 %apply (int ARGC, char **ARGV) { (int retainlen, const char **retainlist) }
1922 %apply (int ARGC, char **ARGV) { (const char **retainlist, int retainlen) }
1923 %apply (int ARGC, char **ARGV) { (const char *retainlist[], int retainlen) }
1924
1925 %clear double a, int ARGC, char **ARGV;
1926 %clear double a, int argc, char *argv[];
1927 %clear int ARGC, char **ARGV;
1928 %clear (double a, int ARGC, char **ARGV);
1929 %clear (double a, int argc, char *argv[]);
1930 %clear (int ARGC, char **ARGV);
1931 %clear int retainlen, const char **retainlist;
1932
1933 int bar( int argc, char* argv[]);
1934 int foo( double a, int argc, char* argv[]);
1935
1936 int qwe( double a, int argc, const char** argv);
1937
1938 void ppdf_clean_file( char *infile, char *outfile, char *password, pdf_write_options *opts, int retainlen, const char **retainlist);
1939 void ppdf_clean_file2(char *infile, char *outfile, char *password, pdf_write_options *opts, const char **retainlist, int retainlen);
1940 void ppdf_clean_file3(char *infile, char *outfile, char *password, pdf_write_options *opts, const char *retainlist[], int retainlen);
1941
1942 ''')
1943 jlib.fs_update( test_i, 'test.i')
1944
1945 jlib.system( textwrap.dedent(
1946 '''
1947 swig
1948 -Wall
1949 -c++
1950 -python
1951 -module test
1952 -outdir .
1953 -o test.cpp
1954 test.i
1955 ''').replace( '\n', ' \\\n')
1956 )
1957
1958
1959 def test_swig_csharp():
1960 '''
1961 Checks behaviour with and without our custom string marshalling code from
1962 _csharp_unicode_prefix().
1963 '''
1964 test_swig_csharp_internal(fix=0)
1965 test_swig_csharp_internal(fix=1)
1966
1967
1968 def test_swig_csharp_internal(fix):
1969 '''
1970 Test utf8 string handling, with/without use of _csharp_unicode_prefix().
1971 '''
1972 # We create C++/C# source directly from this function, and explicitly run
1973 # C++ and .NET/Mono build commands.
1974 #
1975
1976 build_dir = f'test_swig_{fix}'
1977 os.makedirs( build_dir, exist_ok=True)
1978
1979 print('')
1980 print(f'### test_swig_internal(): {fix=}', flush=1)
1981
1982 # Create SWIG input file `test.i`.
1983 #
1984 test_i = '%module test\n'
1985
1986 if fix:
1987 test_i += _csharp_unicode_prefix()
1988
1989 test_i += textwrap.dedent(f'''
1990 %include "std_string.i"
1991
1992 // Returns escaped representation of `text`.
1993 const char* foo1(const char* text);
1994
1995 // Returns escaped representation of `text`.
1996 std::string foo2(const std::string& text);
1997
1998 // Returns 4-byte string `0xf0 0x90 0x90 0xb7`, which decodes as
1999 // utf8 to a 4-byte utf16 character.
2000 const char* bar();
2001
2002 // Returns 4-byte string `0xf0 0x90 0x90 0xb7`, which decodes as
2003 // utf8 to a 4-byte utf16 character.
2004 std::string bar2();
2005
2006 %{{
2007 // Returns string containing escaped description of `text`.
2008 std::string foo2(const std::string& text)
2009 {{
2010 std::string ret;
2011 for (int i=0; i<text.size(); ++i)
2012 {{
2013 char buffer[8];
2014 snprintf(buffer, sizeof(buffer), " \\\\x%02x", (unsigned char) text[i]);
2015 ret += buffer;
2016 }}
2017 return ret;
2018 }}
2019
2020 // Returns pointer to static buffer containing escaped
2021 // description of `text`.
2022 const char* foo1(const char* text)
2023 {{
2024 std::string text2 = text;
2025 static std::string ret;
2026 ret = foo2(text2);
2027 return ret.c_str();
2028 }}
2029
2030 // Returns pointer to static buffer containing a utf8 string.
2031 const char* bar()
2032 {{
2033 static char ret[] =
2034 {{
2035 (char) 0xf0,
2036 (char) 0x90,
2037 (char) 0x90,
2038 (char) 0xb7,
2039 0,
2040 }};
2041 return ret;
2042 }}
2043
2044 // Returns a std::string containing a utf8 string.
2045 std::string bar2()
2046 {{
2047 const char* ret = bar();
2048 return std::string(ret);
2049 }}
2050 %}}
2051 ''')
2052 with open(f'{build_dir}/test.i', 'w') as f:
2053 f.write(test_i)
2054
2055 # Run swig on `test.i` to generate `test.cs` and `test.cpp`.
2056 #
2057 jlib.system(
2058 f'''
2059 cd {build_dir} && swig
2060 {'-DSWIG_CSHARP_NO_STRING_HELPER=1 -DSWIG_CSHARP_NO_EXCEPTION_HELPER=1' if 0 and fix else ''}
2061 -D_WIN32
2062 -c++
2063 -csharp
2064 -Wextra
2065 -Wall
2066 -dllimport test.dll
2067 -outdir .
2068 -outfile test.cs
2069 -o test.cpp
2070 test.i
2071 ''')
2072
2073 # Compile/link test.cpp to create test.dll.
2074 #
2075 if state.state_.windows:
2076 import wdev
2077 vs = wdev.WindowsVS()
2078 jlib.system(
2079 f'''
2080 cd {build_dir} && "{vs.vcvars}"&&"{vs.cl}"
2081 /nologo #
2082 /c # Compiles without linking.
2083 /EHsc # Enable "Standard C++ exception handling".
2084 /MD
2085 /Tptest.cpp # /Tp specifies C++ source file.
2086 /Fotest.cpp.obj # Output file.
2087 /permissive- # Set standard-conformance mode.
2088 /FC # Display full path of source code files passed to cl.exe in diagnostic text.
2089 /W3 # Sets which warning level to output. /W3 is IDE default.
2090 /diagnostics:caret # Controls the format of diagnostic messages.
2091 ''')
2092
2093 jlib.system(
2094 f'''
2095 cd {build_dir} && "{vs.vcvars}"&&"{vs.link}"
2096 /nologo #
2097 /DLL
2098 /IMPLIB:test.lib # Overrides the default import library name.
2099 /OUT:test.dll # Specifies the output file name.
2100 /nologo
2101 test.cpp.obj
2102 ''')
2103 else:
2104 jlib.system(
2105 f'''
2106 cd {build_dir} && c++
2107 -fPIC
2108 --shared
2109 -o test.dll
2110 test.cpp
2111 ''')
2112
2113 # Create C# test programme `testfoo.cs`.
2114 #
2115 cs = textwrap.dedent(f'''
2116 public class HelloWorld
2117 {{
2118 public static void Main(string[] args)
2119 {{
2120 bool expect_fix = ({fix if state.state_.windows else 1} != 0);
2121
2122 // Utf8 for our string with 4-byte utf16 character.
2123 //
2124 byte[] text_utf8 = {{ 0xf0, 0x90, 0x90, 0xb7, }};
2125 string text = System.Text.Encoding.UTF8.GetString(text_utf8);
2126
2127 // Escaped representation of text_utf8, as returned by
2128 // calls of test.foo1() and test.foo2() below.
2129 //
2130 string text_utf8_escaped = " \\\\xf0 \\\\x90 \\\\x90 \\\\xb7";
2131 string incorrect_utf8_escaped = " \\\\x3f \\\\x3f";
2132
2133 // test.foo1()/test.foo2() return a `const
2134 // char*`/`std::string` containing an escaped
2135 // representation of the string that they were given. If
2136 // things are working correctly, this will be an escaped
2137 // representation of `text_utf8`.
2138 //
2139
2140 string foo1 = test.foo1(text);
2141 System.Console.WriteLine("foo1: " + foo1);
2142 string foo_expected_escaped = (expect_fix) ? text_utf8_escaped : incorrect_utf8_escaped;
2143 if (foo1 != foo_expected_escaped)
2144 {{
2145 throw new System.Exception(
2146 "foo1 incorrect: '" + foo1 + "'"
2147 + " - foo_expected_escaped: '" + foo_expected_escaped + "'"
2148 );
2149 }}
2150
2151 string foo2 = test.foo2(text);
2152 System.Console.WriteLine("foo2: " + foo2);
2153 if (foo2 != foo_expected_escaped)
2154 {{
2155 throw new System.Exception(
2156 "foo2 incorrect: '" + foo2 + "'"
2157 + " - foo_expected_escaped: '" + foo_expected_escaped + "'"
2158 );
2159 }}
2160
2161 // test.bar1() and test.bar2() return a `const
2162 // char*`/`std::string` containing the bytes of
2163 // `text_utf8`. If things are working correctly we will see
2164 // exactly these bytes.
2165 //
2166 byte[] bar_expected_utf8_incorrect = {{ 0xc3, 0xb0, 0xc2, 0x90, 0xc2, 0x90, 0xc2, 0xb7, }};
2167 byte[] bar_expected_utf8 = (expect_fix) ? text_utf8 : bar_expected_utf8_incorrect;
2168
2169 string ret3 = test.bar();
2170 byte[] ret3_utf8 = System.Text.Encoding.UTF8.GetBytes(ret3);
2171 print_bytes_as_string("ret3_utf8:", ret3_utf8);
2172 if (!equal(ret3_utf8, bar_expected_utf8))
2173 {{
2174 throw new System.Exception("ret3 != bar_expected_utf8");
2175 }}
2176
2177 string ret4 = test.bar2();
2178 byte[] ret4_utf8 = System.Text.Encoding.UTF8.GetBytes(ret4);
2179 print_bytes_as_string("ret4_utf8:", ret4_utf8);
2180 if (!equal(ret4_utf8, bar_expected_utf8))
2181 {{
2182 throw new System.Exception("ret4_utf8 != bar_expected_utf8");
2183 }}
2184 }}
2185
2186 static bool equal(byte[] a, byte[] b)
2187 {{
2188 if (a.Length != b.Length) return false;
2189 for (int i=0; i<a.Length; ++i)
2190 {{
2191 if (a[i] != b[i]) return false;
2192 }}
2193 return true;
2194 }}
2195
2196 static void print_bytes_as_string(string prefix, byte[] a)
2197 {{
2198 System.Console.Write(prefix);
2199 System.Console.Write("[");
2200 foreach (var b in a)
2201 {{
2202 System.Console.Write(" {{0:x2}}", b);
2203 }}
2204 System.Console.WriteLine("]");
2205 }}
2206 }}
2207 ''')
2208 with open(f'{build_dir}/testfoo.cs', 'w') as f:
2209 f.write(cs)
2210
2211 # Use `csc` to compile `testfoo.cs` and create `testfoo.exe`.
2212 #
2213 csc, mono, _ = csharp.csharp_settings(None)
2214 jlib.system(f'cd {build_dir} && "{csc}" -out:testfoo.exe testfoo.cs test.cs')
2215
2216 # Run `testfoo.exe`.
2217 #
2218 jlib.system(f'cd {build_dir} && {mono} testfoo.exe')