1"""Module containing a preprocessor that converts outputs in the notebook from
2one format to another.
3"""
4
5# Copyright (c) Jupyter Development Team.
6# Distributed under the terms of the Modified BSD License.
7
8import base64
9import os
10import subprocess
11import sys
12from shutil import which
13from tempfile import TemporaryDirectory
14
15from traitlets import List, Unicode, Union, default
16
17from nbconvert.utils.io import FormatSafeDict
18
19from .convertfigures import ConvertFiguresPreprocessor
20
21# inkscape path for darwin (macOS)
22INKSCAPE_APP = "/Applications/Inkscape.app/Contents/Resources/bin/inkscape"
23# Recent versions of Inkscape (v1.0) moved the executable from
24# Resources/bin/inkscape to MacOS/inkscape
25INKSCAPE_APP_v1 = "/Applications/Inkscape.app/Contents/MacOS/inkscape"
26
27if sys.platform == "win32":
28 try:
29 import winreg
30 except ImportError:
31 import _winreg as winreg
32
33
34class SVG2PDFPreprocessor(ConvertFiguresPreprocessor):
35 """
36 Converts all of the outputs in a notebook from SVG to PDF.
37 """
38
39 @default("from_format")
40 def _from_format_default(self):
41 return "image/svg+xml"
42
43 @default("to_format")
44 def _to_format_default(self):
45 return "application/pdf"
46
47 inkscape_version = Unicode(
48 help="""The version of inkscape being used.
49
50 This affects how the conversion command is run.
51 """
52 ).tag(config=True)
53
54 @default("inkscape_version")
55 def _inkscape_version_default(self):
56 p = subprocess.Popen(
57 [self.inkscape, "--version"], # noqa:S603
58 stdout=subprocess.PIPE,
59 stderr=subprocess.PIPE,
60 )
61 output, _ = p.communicate()
62 if p.returncode != 0:
63 msg = "Unable to find inkscape executable --version"
64 raise RuntimeError(msg)
65 return output.decode("utf-8").split(" ")[1]
66
67 # FIXME: Deprecate passing a string here
68 command = Union(
69 [Unicode(), List()],
70 help="""
71 The command to use for converting SVG to PDF
72
73 This traitlet is a template, which will be formatted with the keys
74 to_filename and from_filename.
75
76 The conversion call must read the SVG from {from_filename},
77 and write a PDF to {to_filename}.
78
79 It could be a List (recommended) or a String. If string, it will
80 be passed to a shell for execution.
81 """,
82 ).tag(config=True)
83
84 @default("command")
85 def _command_default(self):
86 major_version = self.inkscape_version.split(".")[0]
87 command = [self.inkscape]
88
89 if int(major_version) < 1:
90 # --without-gui is only needed for inkscape 0.x
91 command.append("--without-gui")
92 # --export-pdf is old name for --export-filename
93 command.append("--export-pdf={to_filename}")
94 else:
95 command.append("--export-filename={to_filename}")
96
97 command.append("{from_filename}")
98 return command
99
100 inkscape = Unicode(help="The path to Inkscape, if necessary").tag(config=True)
101
102 @default("inkscape")
103 def _inkscape_default(self):
104 inkscape_path = which("inkscape")
105 if inkscape_path is not None:
106 return inkscape_path
107 if sys.platform == "darwin":
108 if os.path.isfile(INKSCAPE_APP_v1):
109 return INKSCAPE_APP_v1
110 # Order is important. If INKSCAPE_APP exists, prefer it over
111 # the executable in the MacOS directory.
112 if os.path.isfile(INKSCAPE_APP):
113 return INKSCAPE_APP
114 if sys.platform == "win32":
115 wr_handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
116 try:
117 rkey = winreg.OpenKey(wr_handle, "SOFTWARE\\Classes\\inkscape.svg\\DefaultIcon")
118 inkscape = winreg.QueryValueEx(rkey, "")[0]
119 except FileNotFoundError:
120 msg = "Inkscape executable not found"
121 raise FileNotFoundError(msg) from None
122 return inkscape
123 return "inkscape"
124
125 def convert_figure(self, data_format, data):
126 """
127 Convert a single SVG figure to PDF. Returns converted data.
128 """
129
130 # Work in a temporary directory
131 with TemporaryDirectory() as tmpdir:
132 # Write fig to temp file
133 input_filename = os.path.join(tmpdir, "figure.svg")
134 # SVG data is unicode text
135 with open(input_filename, "w", encoding="utf8") as f:
136 f.write(data)
137
138 # Call conversion application
139 output_filename = os.path.join(tmpdir, "figure.pdf")
140
141 template_vars = {"from_filename": input_filename, "to_filename": output_filename}
142 if isinstance(self.command, list):
143 full_cmd = [s.format_map(FormatSafeDict(**template_vars)) for s in self.command]
144 else:
145 # For backwards compatibility with specifying strings
146 # Okay-ish, since the string is trusted
147 full_cmd = self.command.format(*template_vars)
148 subprocess.call(full_cmd, shell=isinstance(full_cmd, str)) # noqa: S603
149
150 # Read output from drive
151 # return value expects a filename
152 if os.path.isfile(output_filename):
153 with open(output_filename, "rb") as f:
154 # PDF is a nb supported binary, data type, so base64 encode.
155 return base64.encodebytes(f.read()).decode("utf-8")
156 else:
157 msg = "Inkscape svg to pdf conversion failed"
158 raise TypeError(msg)