Coverage for src/pyTRLCConverter/plantuml.py: 45%

77 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 10:59 +0000

1"""PlantUML to image file converter. 

2 

3 Author: Andreas Merkle (andreas.merkle@newtec.de) 

4""" 

5 

6# pyTRLCConverter - A tool to convert TRLC files to specific formats. 

7# Copyright (c) 2024 - 2025 NewTec GmbH 

8# 

9# This file is part of pyTRLCConverter program. 

10# 

11# The pyTRLCConverter program is free software: you can redistribute it and/or modify it under 

12# the terms of the GNU General Public License as published by the Free Software Foundation, 

13# either version 3 of the License, or (at your option) any later version. 

14# 

15# The pyTRLCConverter program is distributed in the hope that it will be useful, but 

16# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

17# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License along with pyTRLCConverter. 

20# If not, see <https://www.gnu.org/licenses/>. 

21 

22# Imports ********************************************************************** 

23import os 

24import subprocess 

25import sys 

26import zlib 

27import base64 

28import urllib 

29import urllib.parse 

30import requests 

31 

32from pyTRLCConverter.logger import log_verbose, log_error 

33 

34# Variables ******************************************************************** 

35 

36# URL encoding char sets. 

37# See https://plantuml.com/de/text-encoding for differences to base64 in URL encode. 

38BASE64_ENCODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 

39PLANTUML_ENCODE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_" 

40 

41# Classes ********************************************************************** 

42 

43 

44class PlantUML(): 

45 # lobster-trace: SwRequirements.sw_req_plantuml 

46 """PlantUML image generator. 

47 """ 

48 def __init__(self) -> None: 

49 self._server_url = None 

50 self._plantuml_jar = None 

51 self._working_directory = os.path.abspath(os.getcwd()) 

52 

53 if "PLANTUML" in os.environ: 

54 plantuml_access = os.environ["PLANTUML"] 

55 try: 

56 # Use server method if PLANTUML is a URL. 

57 if urllib.parse.urlparse(plantuml_access).scheme in ['http', 'https']: 

58 self._server_url = plantuml_access 

59 else: 

60 self._plantuml_jar = os.environ["PLANTUML"] 

61 except ValueError: 

62 self._plantuml_jar = os.environ["PLANTUML"] 

63 

64 def _get_absolute_path(self, path): 

65 """Get absolute path to the diagram. 

66 This is required by PlantUML java program for the output path. 

67 

68 Args: 

69 path (_type_): _description_ 

70 

71 Returns: 

72 _type_: _description_ 

73 """ 

74 absolute_path = path 

75 

76 if os.path.isabs(path) is False: 

77 absolute_path = os.path.join(self._working_directory, path) 

78 

79 return absolute_path 

80 

81 def is_plantuml_file(self, diagram_path): 

82 """Is the diagram a PlantUML file? 

83 Only the file extension will be checked. 

84 

85 Args: 

86 diagram_path (str): Diagram path. 

87 

88 Returns: 

89 bool: If file is a PlantUML file, it will return True otherwise False. 

90 """ 

91 is_valid = False 

92 

93 if diagram_path.endswith(".plantuml") or \ 

94 diagram_path.endswith(".puml") or \ 

95 diagram_path.endswith(".wsd"): 

96 is_valid = True 

97 

98 return is_valid 

99 

100 def _make_server_url(self, diagram_type: str, diagram_path: str) -> str: 

101 """Generate a plantuml server GET URL from a diagram data file. 

102 

103 Args: 

104 diagram_type (str): Diagram type, e.g. svg. See PlantUML -t options. 

105 diagram_path (str): Path to the PlantUML diagram. 

106 

107 Raises: 

108 FileNotFoundError: PlantUML diagram file not found. 

109 """ 

110 # Read PlantUML diagram data from given file. 

111 with open(diagram_path, 'r', encoding='utf-8') as input_file: 

112 diagram_string = input_file.read().encode('utf-8') 

113 

114 # Compress the data using deflate. 

115 # Strib Zlib's 2 byte header and 4 byte checksum for raw deflate data. 

116 compressed_data = zlib.compress(diagram_string)[2:-4] 

117 

118 # Encode the compressed data using base64. 

119 base64_encoded_data = base64.b64encode(compressed_data) 

120 

121 # Translate from base64 to plantuml char encoding. 

122 base64_to_puml_trans = bytes.maketrans( 

123 BASE64_ENCODE_CHARS.encode('utf-8'), 

124 PLANTUML_ENCODE_CHARS.encode('utf-8') 

125 ) 

126 puml_encoded_data = base64_encoded_data.translate(base64_to_puml_trans).decode('utf-8') 

127 

128 # Create the URL for the PlantUML server. 

129 query_url = ( 

130 f"{self._server_url}/" 

131 f"{diagram_type}/" 

132 f"{urllib.parse.quote(puml_encoded_data)}" 

133 ) 

134 

135 return query_url 

136 

137 def generate(self, diagram_type: str, diagram_path: str, dst_path: str) -> None: 

138 """Generate plantuml image. 

139 

140 Args: 

141 diagram_type (str): Diagram type, e.g. svg. See PlantUML -t options. 

142 diagram_path (str): Path to the PlantUML diagram. 

143 dst_path (str): Path to the destination of the generated image. 

144 

145 Raises: 

146 FileNotFoundError: PlantUML java jar file not found in local mode. 

147 FileNotFoundError: PlantUML diagram file not found. 

148 requests.exceptions.RequestException: Error during GET request to PlantUML server. 

149 OSError: Destination path does not exist. 

150 """ 

151 if self._server_url is not None: 

152 self._generate_server(diagram_type, diagram_path, dst_path) 

153 else: 

154 self._generate_local(diagram_type, diagram_path, dst_path) 

155 

156 def _generate_server(self, diagram_type: str, diagram_path: str, dst_path: str) -> None: 

157 """Generate image using a plantuml server. 

158 

159 This is does not require java installed and is usually a lot faster 

160 as no java startup time needed when using the plantuml.jar file. 

161 

162 Args: 

163 diagram_type (str): Diagram type, e.g. svg. See PlantUML -t options. 

164 diagram_path (str): Path to the PlantUML diagram. 

165 dst_path (str): Path to the destination of the generated image. 

166 

167 Raises: 

168 FileNotFoundError: PlantUML diagram file not found. 

169 OSError: Destination path does not exist. 

170 requests.exceptions.RequestException: Error during GET request to PlantUML server. 

171 """ 

172 if not os.path.exists(dst_path): 

173 os.makedirs(dst_path) 

174 

175 # Send GET request to the PlantUML server. 

176 url = self._make_server_url(diagram_type, diagram_path) 

177 log_verbose(f"Sending GET request {url}") 

178 response = requests.get(url, timeout=10) 

179 

180 if response.status_code == 200: 

181 # Save the response content in image file. 

182 output_file = os.path.splitext(os.path.basename(diagram_path))[0] 

183 output_file += "." + diagram_type 

184 output_file = os.path.join(dst_path, output_file) 

185 with open(output_file, 'wb') as f: 

186 f.write(response.content) 

187 

188 log_verbose(f"Diagram saved as {output_file}.") 

189 else: 

190 raise requests.exceptions.RequestException(f"{response.status_code} - {response.text}") 

191 

192 def _generate_local(self, diagram_type, diagram_path, dst_path): 

193 """Generate image local call to plantuml.jar. 

194 

195 Args: 

196 diagram_type (str): Diagram type, e.g. svg. See PlantUML -t options. 

197 diagram_path (str): Path to the PlantUML diagram. 

198 dst_path (str): Path to the destination of the generated image. 

199 

200 Raises: 

201 FileNotFoundError: PlantUML java jar file not found. 

202 OSError: Destination path does not exist. 

203 """ 

204 if self._plantuml_jar is not None: 

205 

206 if not os.path.exists(dst_path): 

207 os.makedirs(dst_path) 

208 

209 plantuml_cmd = ["java" ] 

210 

211 if sys.platform.startswith("linux"): 

212 plantuml_cmd.append("-Djava.awt.headless=true") 

213 

214 plantuml_cmd.extend( 

215 [ 

216 "-jar", f"{self._plantuml_jar}", 

217 f"{diagram_path}", 

218 f"-t{diagram_type}", 

219 "-o", self._get_absolute_path(dst_path) 

220 ] 

221 ) 

222 

223 try: 

224 output = subprocess.run(plantuml_cmd, capture_output=True, text=True, check=False) 

225 if output.stderr: 

226 log_error(output.stderr, True) 

227 print(output.stdout) 

228 except FileNotFoundError as exc: 

229 raise FileNotFoundError(f"{self._plantuml_jar} not found.") from exc 

230 else: 

231 raise FileNotFoundError("plantuml.jar not found, set PLANTUML environment variable.") 

232 

233# Functions ******************************************************************** 

234 

235# Main *************************************************************************