Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nbconvert/preprocessors/svg2pdf.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

95 statements  

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 

12import warnings 

13from pathlib import Path 

14from shutil import which 

15from tempfile import TemporaryDirectory 

16 

17from traitlets import List, Unicode, Union, default 

18 

19from nbconvert.utils.io import FormatSafeDict 

20 

21from .convertfigures import ConvertFiguresPreprocessor 

22 

23# inkscape path for darwin (macOS) 

24INKSCAPE_APP = "/Applications/Inkscape.app/Contents/Resources/bin/inkscape" 

25# Recent versions of Inkscape (v1.0) moved the executable from 

26# Resources/bin/inkscape to MacOS/inkscape 

27INKSCAPE_APP_v1 = "/Applications/Inkscape.app/Contents/MacOS/inkscape" 

28 

29if sys.platform == "win32": 

30 try: 

31 import winreg 

32 except ImportError: 

33 import _winreg as winreg 

34 

35 

36class SVG2PDFPreprocessor(ConvertFiguresPreprocessor): 

37 """ 

38 Converts all of the outputs in a notebook from SVG to PDF. 

39 """ 

40 

41 @default("from_format") 

42 def _from_format_default(self): 

43 return "image/svg+xml" 

44 

45 @default("to_format") 

46 def _to_format_default(self): 

47 return "application/pdf" 

48 

49 inkscape_version = Unicode( 

50 help="""The version of inkscape being used. 

51 

52 This affects how the conversion command is run. 

53 """ 

54 ).tag(config=True) 

55 

56 @default("inkscape_version") 

57 def _inkscape_version_default(self): 

58 p = subprocess.Popen( # noqa:S603 

59 [self.inkscape, "--version"], 

60 stdout=subprocess.PIPE, 

61 stderr=subprocess.PIPE, 

62 ) 

63 output, _ = p.communicate() 

64 if p.returncode != 0: 

65 msg = "Unable to find inkscape executable --version" 

66 raise RuntimeError(msg) 

67 return output.decode("utf-8").split(" ")[1] 

68 

69 # FIXME: Deprecate passing a string here 

70 command = Union( 

71 [Unicode(), List()], 

72 help=""" 

73 The command to use for converting SVG to PDF 

74 

75 This traitlet is a template, which will be formatted with the keys 

76 to_filename and from_filename. 

77 

78 The conversion call must read the SVG from {from_filename}, 

79 and write a PDF to {to_filename}. 

80 

81 It could be a List (recommended) or a String. If string, it will 

82 be passed to a shell for execution. 

83 """, 

84 ).tag(config=True) 

85 

86 @default("command") 

87 def _command_default(self): 

88 major_version = self.inkscape_version.split(".")[0] 

89 command = [self.inkscape] 

90 

91 if int(major_version) < 1: 

92 # --without-gui is only needed for inkscape 0.x 

93 command.append("--without-gui") 

94 # --export-pdf is old name for --export-filename 

95 command.append("--export-pdf={to_filename}") 

96 else: 

97 command.append("--export-filename={to_filename}") 

98 

99 command.append("{from_filename}") 

100 return command 

101 

102 inkscape = Unicode(help="The path to Inkscape, if necessary").tag(config=True) 

103 

104 @default("inkscape") 

105 def _inkscape_default(self): 

106 # Windows: Secure registry lookup FIRST (CVE-2025-53000 fix) 

107 if sys.platform == "win32": 

108 wr_handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) 

109 try: 

110 rkey = winreg.OpenKey(wr_handle, r"SOFTWARE\Classes\inkscape.svg\DefaultIcon") 

111 inkscape_full = winreg.QueryValueEx(rkey, "")[0].split(",")[0] # Fix: remove ",0" 

112 if os.path.isfile(inkscape_full): 

113 return inkscape_full 

114 except (FileNotFoundError, OSError, IndexError): 

115 pass # Safe fallback 

116 

117 # Block CWD in PATH search (CVE-2025-53000) 

118 os.environ["NODEFAULTCURRENTDIRECTORYINEXEPATH"] = "1" 

119 

120 inkscape_path = which("inkscape") 

121 

122 # Extra safety for Python < 3.12 on Windows: 

123 # If which() resolved to a path in CWD even though CWD is not on PATH, 

124 # warn and treat as "not found". 

125 if sys.platform == "win32" and inkscape_path and sys.version_info < (3, 12): 

126 try: 

127 cwd = Path.cwd().resolve() 

128 in_cwd = Path(inkscape_path).resolve().parent == cwd 

129 cwd_on_path = cwd in { 

130 Path(p).resolve() for p in os.environ.get("PATH", os.defpath).split(os.pathsep) 

131 } 

132 

133 if in_cwd and not cwd_on_path: 

134 warnings.warn( 

135 "shutil.which('inkscape') resolved to an executable in the current " 

136 "working directory even though CWD is not on PATH. Ignoring this " 

137 "result for security reasons (CVE-2025-53000).", 

138 RuntimeWarning, 

139 stacklevel=2, 

140 ) 

141 inkscape_path = None 

142 except Exception: 

143 # If detection fails for any reason, prefer safety: ignore CWD result 

144 inkscape_path = None 

145 

146 if inkscape_path is not None: 

147 return inkscape_path 

148 

149 # macOS: EXACT original order preserved 

150 if sys.platform == "darwin": 

151 if os.path.isfile(INKSCAPE_APP_v1): 

152 return INKSCAPE_APP_v1 

153 # Order is important. If INKSCAPE_APP exists, prefer it over 

154 # the executable in the MacOS directory. 

155 if os.path.isfile(INKSCAPE_APP): 

156 return INKSCAPE_APP 

157 

158 msg = "Inkscape executable not found in safe paths" 

159 raise FileNotFoundError(msg) 

160 

161 def convert_figure(self, data_format, data): 

162 """ 

163 Convert a single SVG figure to PDF. Returns converted data. 

164 """ 

165 

166 # Work in a temporary directory 

167 with TemporaryDirectory() as tmpdir: 

168 # Write fig to temp file 

169 input_filename = os.path.join(tmpdir, "figure.svg") 

170 # SVG data is unicode text 

171 with open(input_filename, "w", encoding="utf8") as f: 

172 f.write(data) 

173 

174 # Call conversion application 

175 output_filename = os.path.join(tmpdir, "figure.pdf") 

176 

177 template_vars = {"from_filename": input_filename, "to_filename": output_filename} 

178 if isinstance(self.command, list): 

179 full_cmd = [s.format_map(FormatSafeDict(**template_vars)) for s in self.command] 

180 else: 

181 # For backwards compatibility with specifying strings 

182 # Okay-ish, since the string is trusted 

183 full_cmd = self.command.format(**template_vars) 

184 subprocess.call(full_cmd, shell=isinstance(full_cmd, str)) # noqa: S603 

185 

186 # Read output from drive 

187 # return value expects a filename 

188 if os.path.isfile(output_filename): 

189 with open(output_filename, "rb") as f: 

190 # PDF is a nb supported binary, data type, so base64 encode. 

191 return base64.encodebytes(f.read()).decode("utf-8") 

192 else: 

193 msg = "Inkscape svg to pdf conversion failed" 

194 raise TypeError(msg)