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
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 10:59 +0000
1"""PlantUML to image file converter.
3 Author: Andreas Merkle (andreas.merkle@newtec.de)
4"""
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/>.
22# Imports **********************************************************************
23import os
24import subprocess
25import sys
26import zlib
27import base64
28import urllib
29import urllib.parse
30import requests
32from pyTRLCConverter.logger import log_verbose, log_error
34# Variables ********************************************************************
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-_"
41# Classes **********************************************************************
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())
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"]
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.
68 Args:
69 path (_type_): _description_
71 Returns:
72 _type_: _description_
73 """
74 absolute_path = path
76 if os.path.isabs(path) is False:
77 absolute_path = os.path.join(self._working_directory, path)
79 return absolute_path
81 def is_plantuml_file(self, diagram_path):
82 """Is the diagram a PlantUML file?
83 Only the file extension will be checked.
85 Args:
86 diagram_path (str): Diagram path.
88 Returns:
89 bool: If file is a PlantUML file, it will return True otherwise False.
90 """
91 is_valid = False
93 if diagram_path.endswith(".plantuml") or \
94 diagram_path.endswith(".puml") or \
95 diagram_path.endswith(".wsd"):
96 is_valid = True
98 return is_valid
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.
103 Args:
104 diagram_type (str): Diagram type, e.g. svg. See PlantUML -t options.
105 diagram_path (str): Path to the PlantUML diagram.
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')
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]
118 # Encode the compressed data using base64.
119 base64_encoded_data = base64.b64encode(compressed_data)
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')
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 )
135 return query_url
137 def generate(self, diagram_type: str, diagram_path: str, dst_path: str) -> None:
138 """Generate plantuml image.
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.
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)
156 def _generate_server(self, diagram_type: str, diagram_path: str, dst_path: str) -> None:
157 """Generate image using a plantuml server.
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.
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.
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)
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)
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)
188 log_verbose(f"Diagram saved as {output_file}.")
189 else:
190 raise requests.exceptions.RequestException(f"{response.status_code} - {response.text}")
192 def _generate_local(self, diagram_type, diagram_path, dst_path):
193 """Generate image local call to plantuml.jar.
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.
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:
206 if not os.path.exists(dst_path):
207 os.makedirs(dst_path)
209 plantuml_cmd = ["java" ]
211 if sys.platform.startswith("linux"):
212 plantuml_cmd.append("-Djava.awt.headless=true")
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 )
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.")
233# Functions ********************************************************************
235# Main *************************************************************************