digichem-core 6.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. digichem/__init__.py +75 -0
  2. digichem/basis.py +116 -0
  3. digichem/config/README +3 -0
  4. digichem/config/__init__.py +5 -0
  5. digichem/config/base.py +321 -0
  6. digichem/config/locations.py +14 -0
  7. digichem/config/parse.py +90 -0
  8. digichem/config/util.py +117 -0
  9. digichem/data/README +4 -0
  10. digichem/data/batoms/COPYING +18 -0
  11. digichem/data/batoms/LICENSE +674 -0
  12. digichem/data/batoms/README +2 -0
  13. digichem/data/batoms/__init__.py +0 -0
  14. digichem/data/batoms/batoms-renderer.py +351 -0
  15. digichem/data/config/digichem.yaml +714 -0
  16. digichem/data/functionals.csv +15 -0
  17. digichem/data/solvents.csv +185 -0
  18. digichem/data/tachyon/COPYING.md +5 -0
  19. digichem/data/tachyon/LICENSE +30 -0
  20. digichem/data/tachyon/tachyon_LINUXAMD64 +0 -0
  21. digichem/data/vmd/common.tcl +468 -0
  22. digichem/data/vmd/generate_combined_orbital_images.tcl +70 -0
  23. digichem/data/vmd/generate_density_images.tcl +45 -0
  24. digichem/data/vmd/generate_dipole_images.tcl +68 -0
  25. digichem/data/vmd/generate_orbital_images.tcl +57 -0
  26. digichem/data/vmd/generate_spin_images.tcl +66 -0
  27. digichem/data/vmd/generate_structure_images.tcl +40 -0
  28. digichem/datas.py +14 -0
  29. digichem/exception/__init__.py +7 -0
  30. digichem/exception/base.py +133 -0
  31. digichem/exception/uncatchable.py +63 -0
  32. digichem/file/__init__.py +1 -0
  33. digichem/file/base.py +364 -0
  34. digichem/file/cube.py +284 -0
  35. digichem/file/fchk.py +94 -0
  36. digichem/file/prattle.py +277 -0
  37. digichem/file/types.py +97 -0
  38. digichem/image/__init__.py +6 -0
  39. digichem/image/base.py +113 -0
  40. digichem/image/excited_states.py +335 -0
  41. digichem/image/graph.py +293 -0
  42. digichem/image/orbitals.py +239 -0
  43. digichem/image/render.py +617 -0
  44. digichem/image/spectroscopy.py +797 -0
  45. digichem/image/structure.py +115 -0
  46. digichem/image/vmd.py +826 -0
  47. digichem/input/__init__.py +3 -0
  48. digichem/input/base.py +78 -0
  49. digichem/input/digichem_input.py +500 -0
  50. digichem/input/gaussian.py +140 -0
  51. digichem/log.py +179 -0
  52. digichem/memory.py +166 -0
  53. digichem/misc/__init__.py +4 -0
  54. digichem/misc/argparse.py +44 -0
  55. digichem/misc/base.py +61 -0
  56. digichem/misc/io.py +239 -0
  57. digichem/misc/layered_dict.py +285 -0
  58. digichem/misc/text.py +139 -0
  59. digichem/misc/time.py +73 -0
  60. digichem/parse/__init__.py +13 -0
  61. digichem/parse/base.py +220 -0
  62. digichem/parse/cclib.py +138 -0
  63. digichem/parse/dump.py +253 -0
  64. digichem/parse/gaussian.py +130 -0
  65. digichem/parse/orca.py +96 -0
  66. digichem/parse/turbomole.py +201 -0
  67. digichem/parse/util.py +523 -0
  68. digichem/result/__init__.py +6 -0
  69. digichem/result/alignment/AA.py +114 -0
  70. digichem/result/alignment/AAA.py +61 -0
  71. digichem/result/alignment/FAP.py +148 -0
  72. digichem/result/alignment/__init__.py +3 -0
  73. digichem/result/alignment/base.py +310 -0
  74. digichem/result/angle.py +153 -0
  75. digichem/result/atom.py +742 -0
  76. digichem/result/base.py +258 -0
  77. digichem/result/dipole_moment.py +332 -0
  78. digichem/result/emission.py +402 -0
  79. digichem/result/energy.py +323 -0
  80. digichem/result/excited_state.py +821 -0
  81. digichem/result/ground_state.py +94 -0
  82. digichem/result/metadata.py +644 -0
  83. digichem/result/multi.py +98 -0
  84. digichem/result/nmr.py +1086 -0
  85. digichem/result/orbital.py +647 -0
  86. digichem/result/result.py +244 -0
  87. digichem/result/soc.py +272 -0
  88. digichem/result/spectroscopy.py +514 -0
  89. digichem/result/tdm.py +267 -0
  90. digichem/result/vibration.py +167 -0
  91. digichem/test/__init__.py +6 -0
  92. digichem/test/conftest.py +4 -0
  93. digichem/test/test_basis.py +71 -0
  94. digichem/test/test_calculate.py +30 -0
  95. digichem/test/test_config.py +78 -0
  96. digichem/test/test_cube.py +369 -0
  97. digichem/test/test_exception.py +16 -0
  98. digichem/test/test_file.py +104 -0
  99. digichem/test/test_image.py +337 -0
  100. digichem/test/test_input.py +64 -0
  101. digichem/test/test_parsing.py +79 -0
  102. digichem/test/test_prattle.py +36 -0
  103. digichem/test/test_result.py +489 -0
  104. digichem/test/test_translate.py +112 -0
  105. digichem/test/util.py +207 -0
  106. digichem/translate.py +591 -0
  107. digichem_core-6.0.0rc1.dist-info/METADATA +96 -0
  108. digichem_core-6.0.0rc1.dist-info/RECORD +111 -0
  109. digichem_core-6.0.0rc1.dist-info/WHEEL +4 -0
  110. digichem_core-6.0.0rc1.dist-info/licenses/COPYING.md +10 -0
  111. digichem_core-6.0.0rc1.dist-info/licenses/LICENSE +11 -0
digichem/file/fchk.py ADDED
@@ -0,0 +1,94 @@
1
+ # General imports.
2
+ import subprocess
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from digichem.exception.base import File_maker_exception
7
+
8
+ # Digichem imports.
9
+ from digichem.file import File_converter
10
+ import digichem.file.types as file_types
11
+ import digichem.log
12
+ from digichem.memory import Memory
13
+ from digichem.misc.io import expand_path
14
+
15
+ class Chk_to_fchk(File_converter):
16
+ """
17
+ Class for creating Gaussian fchk files from Gaussian chk files.
18
+ """
19
+
20
+ # Text description of our input file type, used for error messages etc.
21
+ #input_file_type = "chk"
22
+ input_file_type = file_types.gaussian_chk_file
23
+ # Text description of our output file type, used for error messages etc.
24
+ #output_file_type = "fchk"
25
+ output_file_type = file_types.gaussian_fchk_file
26
+
27
+ def __init__(self, *args, chk_file = None, fchk_file = None, memory = None, formchk_executable = "formchk", **kwargs):
28
+ """
29
+ Constructor for Chk_to_fchk objects.
30
+
31
+ See Image_maker for a full signature.
32
+
33
+ :param output: The filename/path to the fchk file (this path doesn't need to point to a real file yet; we will use this path to write to).
34
+ :param chk_file: Optional chk_file to use to generate this fchk file.
35
+ :param fchk_file: An optional file path to an existing fchk file to use. If this is given (and points to an actual file), then a new fchk will not be made and this file will be used instead.
36
+ :param memory: The amount of memory for formchk to use.
37
+ :param formchk_executable: 'Path' to the executable to use for formchk.
38
+ """
39
+ super().__init__(*args, input_file = chk_file, existing_file = fchk_file, **kwargs)
40
+ memory = memory if memory is not None else "3 GB"
41
+ self.memory = Memory(memory)
42
+ self.formchk_executable = expand_path(formchk_executable)
43
+
44
+ @classmethod
45
+ def from_options(self, output, *, chk_file = None, memory = None, options, **kwargs):
46
+ """
47
+ Constructor that takes a dictionary of config like options.
48
+ """
49
+ return self(
50
+ output,
51
+ chk_file = chk_file,
52
+ memory = memory,
53
+ formchk_executable = options['external']['formchk'],
54
+ **kwargs
55
+ )
56
+
57
+ def make_files(self):
58
+ """
59
+ Make the files referenced by this object.
60
+ """
61
+ input_file = Path(str(self.input_file))
62
+
63
+ # The signature we'll use to call formchk.
64
+ signature = [
65
+ "{}".format(self.formchk_executable),
66
+ input_file.name,
67
+ str(self.output.absolute())
68
+ ]
69
+
70
+ # Sadly (but not unexpectedly), the g09 version of formchk (and maybe other versions too) will crash if their are certain characters (at least '(' and ')') in the input directory name. The final part of the input name appears to be fine, as does everything in the output dir.
71
+ # This is easily fixed by making the output dir absolute and cd'ing into the input directory.
72
+
73
+ try:
74
+ formchk_proc = subprocess.run(
75
+ signature,
76
+ # Capture both stdout and stderr.
77
+ stdout = subprocess.PIPE,
78
+ stderr = subprocess.STDOUT,
79
+ universal_newlines = True,
80
+ cwd = str(input_file.parent),
81
+ env = dict(os.environ, GAUSS_MEMDEF = str(self.memory))
82
+ )
83
+ except FileNotFoundError:
84
+ raise File_maker_exception(self, "Could not locate formchk executable '{}'".format(self.formchk_executable))
85
+
86
+ # If something went wrong, dump output.
87
+ if formchk_proc.returncode != 0:
88
+ # An error occured.
89
+ raise File_maker_exception(self, "Formchk did not exit successfully:\n{}".format(formchk_proc.stdout))
90
+ else:
91
+ # Everything appeared to go ok.
92
+ # Dump formchk output if we're in debug.
93
+ digichem.log.get_logger().debug(formchk_proc.stdout)
94
+
@@ -0,0 +1,277 @@
1
+ # General imports.
2
+ import subprocess
3
+ from subprocess import CalledProcessError
4
+ import re
5
+ import os
6
+ import copy
7
+ from pathlib import Path
8
+ import warnings
9
+ import json
10
+
11
+ # Digichem imports.
12
+ from digichem.exception.base import Digichem_exception
13
+ import digichem.log
14
+
15
+
16
+ class Openprattle_converter():
17
+ """
18
+ Provides an interface to oprattle.
19
+
20
+ Openprattle provides a library interface, but because of the GPL we cannot use it.
21
+ Calling the oprattle command is fine, however.
22
+ """
23
+
24
+ def __init__(self, *, input_file = None, input_file_buffer = None, input_file_path = None, input_file_type = None, executable = "oprattle"):
25
+ """
26
+ Constructor for the OpenBabel converter.
27
+
28
+ Input files can be specified in one of three ways:
29
+ - As an open file descriptor (input_file and input_file_type)
30
+ - As an in-memory buffer, most probably a string or bytes-like object (input_file_buffer and input_file_type)
31
+ - As a file path (input_file_path and optionally input_file_type)
32
+
33
+ :param input_file: An open file descriptor in the format given by input_file_type that should be converted.
34
+ :param input_file_buffer: Alternatively, a buffer (unicode string or bytes) in the format given by input_file_type that should be converted.
35
+ :param input_file_path: Alternatively, a path to a file that should be converted.
36
+ :param input_file_type: A shortcode identifying the format of the input file. If not given but input_file_path is given, then this will be determined automatically.
37
+ :param executable: Path or command name to the oprattle executable.
38
+ """
39
+ self.input_file = input_file
40
+ self.input_file_buffer = input_file_buffer
41
+ self.input_file_path = input_file_path
42
+ self.input_file_type = input_file_type
43
+ self.executable = executable
44
+
45
+ @property
46
+ def input_name(self):
47
+ """
48
+ A descriptive name of the file we are converting. Works even if converting from memory.
49
+ """
50
+ if self.input_file_path is not None:
51
+ return self.input_file_path
52
+ else:
53
+ return "(file loaded from memory)"
54
+
55
+ @classmethod
56
+ def get_cls(self, input_file_type):
57
+ """
58
+ This function is deprecated, and does nothing.
59
+ """
60
+ warnings.warn("get_cls() is deprecated, use Openprattle_converter directly instead", DeprecationWarning)
61
+ return self
62
+
63
+ @classmethod
64
+ def from_file(self, input_file_path, input_file_type = None, **kwargs):
65
+ """
66
+ This function is deprecated, and does nothing.
67
+ """
68
+ warnings.warn("from_file() is deprecated, use the Openprattle_converter constructor directly instead", DeprecationWarning)
69
+ return self.get_cls(None)(input_file_path = input_file_path, input_file_type = input_file_type, **kwargs)
70
+
71
+ @classmethod
72
+ def type_from_file_name(self, input_file_name, allow_none = False):
73
+ """
74
+ Get the type of a file based on its file name.
75
+
76
+ This method largely uses the file extension (.com, .tmol etc), with a few other simple rules.
77
+
78
+ :param input_file_name: The name of the file to check.
79
+ :param allow_none: If the type of the file cannot be determined and allow_none is False (the default), an exception is raised. Otherwise, None is returned.
80
+ """
81
+ try:
82
+ input_file_name = Path(input_file_name)
83
+
84
+ except TypeError:
85
+ if allow_none:
86
+ return None
87
+
88
+ else:
89
+ # No file given.
90
+ raise ValueError("Could not automatically determine file format; no file name was given") from None
91
+
92
+ # Get file extension (removing the dot character).
93
+ extension = input_file_name.suffix[1:]
94
+
95
+ if extension != "":
96
+ # All done.
97
+ return extension.lower()
98
+
99
+ elif input_file_name.name == "coord":
100
+ # This is a turbomole file.
101
+ return "tmol"
102
+
103
+ else:
104
+ if allow_none:
105
+ return None
106
+
107
+ else:
108
+ # Don't recognise the file format.
109
+ raise ValueError("Could not determine file format of file '{}'; the file does not have an extension and is not recognised".format(input_file_name))
110
+
111
+ def convert(self, output_file_type = None, output_file = None, *, gen3D = None, charge = None, multiplicity = None):
112
+ """
113
+ Convert the input file wrapped by this class to the designated output_file_type.
114
+
115
+ :param output_file_type: The file type to convert to.
116
+ :param output_file: Optional file name to write to. If not given, the converted file will be returned as a string (or binary string depending on format).
117
+ :param gen3D: If True and the loaded molecule does not have 3D coordinates, these will be generated (this will scramble atom coordinates).
118
+ :param charge: Optional charge of the output format.
119
+ :param multiplicity: Optional multiplicity of the output format.
120
+ :return: The converted file, or None if output_file is not None.
121
+ """
122
+ output_file = str(output_file) if output_file is not None else None
123
+
124
+ # The signature we'll use to run oprattle.
125
+ sig = [
126
+ str(self.executable),
127
+ "--json"
128
+ ]
129
+
130
+ # Add the input path if we're reading from file.
131
+ if self.input_file is None:
132
+ sig.append(str(self.input_file_path))
133
+
134
+ # Now add the input and output switches.
135
+ if output_file_type:
136
+ sig.extend([
137
+ "-o", output_file_type
138
+ ])
139
+ if self.input_file_type:
140
+ sig.extend([
141
+ "-i", self.input_file_type
142
+ ])
143
+
144
+ # Add gen3D command if we've been asked to.
145
+ if gen3D is True:
146
+ sig.extend(["--gen3D", "True"])
147
+ elif gen3D is False:
148
+ sig.extend(["--gen3D", "False"])
149
+
150
+ # If a file to write to has been given, set it.
151
+ if output_file is not None:
152
+ sig.extend(['-O', output_file])
153
+
154
+ # Give our input_file as stdin if we're not reading from file.
155
+ inputs = self.input_file
156
+
157
+ # GO.
158
+ try:
159
+ done_process = subprocess.run(
160
+ sig,
161
+ input = inputs,
162
+ stdout = subprocess.PIPE,
163
+ stderr = subprocess.PIPE,
164
+ # TODO: Using universal newlines is probably not safe here; some formats are binary (.cdx etc...)
165
+ universal_newlines = True,
166
+ check = True,
167
+ )
168
+
169
+ except CalledProcessError as e:
170
+ self.handle_logging(e.stderr)
171
+ raise
172
+
173
+ self.handle_logging(done_process.stderr)
174
+
175
+ # Return our output.
176
+ return done_process.stdout if output_file is None else None
177
+
178
+ def handle_logging(self, raw_output):
179
+ """
180
+ """
181
+ for raw_message in raw_output.split("\n"):
182
+ if raw_message == "":
183
+ # Nothing returned, nothing to do.
184
+ return
185
+
186
+ # Each message should be in JSON, but check.
187
+ try:
188
+ message = json.loads(raw_message)
189
+ message_text = message['message']
190
+ if message['exception']:
191
+ message_text += "\n" + message['exception']
192
+ digichem.log.get_logger().log(
193
+ message['levelno'],
194
+ message_text
195
+ )
196
+
197
+ except Exception:
198
+ digichem.log.get_logger().error("Unexpected output from oprattle: '{}'".format(raw_message), exc_info=False)
199
+
200
+
201
+
202
+ class Oprattle_formats():
203
+ """
204
+ Class for retrieving the supported file formats from oprattle.
205
+ """
206
+
207
+ def __init__(self, executable = "oprattle"):
208
+ """
209
+ """
210
+ self.executable = executable
211
+
212
+ def run(self, readwrite):
213
+ """
214
+ """
215
+ if readwrite not in ["read", "write"]:
216
+ raise ValueError("readwrite must be one of either 'read' or 'write'")
217
+
218
+ # The signature we'll use to run oprattle.
219
+ sig = [
220
+ self.executable,
221
+ "--json",
222
+ "--readable" if readwrite == "read" else "--writable"
223
+ ]
224
+
225
+ # GO.
226
+ done_process = subprocess.run(
227
+ sig,
228
+ stdout = subprocess.PIPE,
229
+ stderr = subprocess.PIPE,
230
+ universal_newlines = True,
231
+ check = True,
232
+ )
233
+
234
+ formats = json.loads(done_process.stdout)
235
+
236
+ return formats
237
+
238
+
239
+ def read(self, refresh = False):
240
+ """
241
+ Retrieve supported input (read) formats.
242
+ """
243
+ if refresh:
244
+ try:
245
+ del self._read
246
+
247
+ except AttributeError:
248
+ pass
249
+
250
+ try:
251
+ return self._read
252
+
253
+ except AttributeError:
254
+ # Cache miss.
255
+ self._read = self.run("read")
256
+ return self._read
257
+
258
+ def write(self, refresh = False):
259
+ """
260
+ Retrieve supported input (read) formats.
261
+ """
262
+ if refresh:
263
+ try:
264
+ del self._write
265
+
266
+ except AttributeError:
267
+ pass
268
+
269
+ try:
270
+ return self._write
271
+
272
+ except AttributeError:
273
+ # Cache miss.
274
+ self._write = self.run("write")
275
+ return self._write
276
+
277
+
digichem/file/types.py ADDED
@@ -0,0 +1,97 @@
1
+ # Functions and classes for determining file types etc.
2
+ from pathlib import Path
3
+ from itertools import accumulate
4
+
5
+ from digichem.exception.base import Unknown_file_type_exception
6
+
7
+ class File_type():
8
+ """
9
+ Class for representing a known file type.
10
+ """
11
+
12
+ def __init__(self, name, family = None, extensions = None, short_code = None):
13
+ """
14
+ Constructor for File_type objects.
15
+
16
+ :param name: The name of the file (eg, "JPEG").
17
+ :param family: The name of the family/group that the file type belongs to (eg, "image").
18
+ :param extensions: An iterable of known file extensions (this is case-insensitive) (eg, [".jpg", ".jpeg"]).
19
+ """
20
+ self.name = name
21
+ self.family = family
22
+ self.extensions = [extension.lower() for extension in extensions] if extensions is not None else []
23
+
24
+ if short_code is None and len(self.extensions) > 0:
25
+ # Take one of our extensions (without the dot).
26
+ self.short_code = self.extensions[0][1:]
27
+
28
+ else:
29
+ self.short_code = short_code
30
+
31
+ def check(self, file_path):
32
+ """
33
+ Determine whether a given file is of this file type.
34
+
35
+ :param file_path: A string-like path to the file to check.
36
+ :return: True if the file is of this type, false otherwise.
37
+ """
38
+ # Get a Path object.
39
+ file_path = Path(file_path)
40
+
41
+ # Check to see if the file extension matches one of our file extensions.
42
+ # We recurse through all the given file's extensions in case one of our extensions contains multiple parts (".tar.gz" for example).
43
+ for extension in accumulate(reversed(file_path.suffixes), lambda a, b: b+a):
44
+ if extension.lower() in self.extensions:
45
+ return True
46
+
47
+ # No match found.
48
+ return False
49
+
50
+ @property
51
+ def file_type(self):
52
+ """
53
+ Get a string uniquely identifying this file type.
54
+ """
55
+ if self.family is not None:
56
+ return "{}/{}".format(self.family, self.name)
57
+ else:
58
+ return self.name
59
+
60
+ def __str__(self):
61
+ """
62
+ Stringify this file type.
63
+ """
64
+ return self.file_type
65
+
66
+
67
+ # Known file types.
68
+ log_file = File_type("log", "general", [".log"])
69
+ gaussian_chk_file = File_type("checkpoint", "gaussian", [".chk"])
70
+ gaussian_NTO_chk_file = File_type("NTO-checkpoint", "gaussian", [".chk"])
71
+ gaussian_fchk_file = File_type("formatted-checkpoint", "gaussian", [".fchk"])
72
+ gaussian_rwf_file = File_type("read-write", "gaussian", [".rwf"])
73
+ gaussian_cube_file = File_type("cube", "gaussian", [".cub", ".cube"])
74
+
75
+ orca_gbw_file = File_type("gbw", "orca", [".gbw"])
76
+ orca_density_file = File_type("density", "orca", [".densities"])
77
+
78
+ # A list of all our known types.
79
+ known_types = [log_file, gaussian_chk_file, gaussian_fchk_file, gaussian_rwf_file, gaussian_cube_file, orca_gbw_file]
80
+
81
+ # TODO: This seems to be unused?
82
+ def get_file_type(file_path):
83
+ """
84
+ Get the type of a file from a list of known file types.
85
+
86
+ :raises Unknown_file_type_exception: If the given file is of an unknown type.
87
+ :param file_path: String-like path to the file to check.
88
+ :return: A File_type object.
89
+ """
90
+ # Iterate through our list of known file types.
91
+ for file_type in known_types:
92
+ if file_type.check(file_path):
93
+ return file_type
94
+
95
+ # Unknown file type.
96
+ raise Unknown_file_type_exception(file_path)
97
+
@@ -0,0 +1,6 @@
1
+ import PIL.Image
2
+
3
+ from .base import Image_maker
4
+
5
+ # Remove PIL's built in max image size protection.
6
+ PIL.Image.MAX_IMAGE_PIXELS = None
digichem/image/base.py ADDED
@@ -0,0 +1,113 @@
1
+ from PIL import Image
2
+ import numpy as np
3
+
4
+ from digichem.file import File_maker
5
+ from digichem.exception import File_maker_exception
6
+
7
+ class Cropable_mixin():
8
+ """Mixin class for those that can crop their images."""
9
+
10
+ @classmethod
11
+ def auto_crop_image(self, im):
12
+ """
13
+ Automatically remove excess white pixels around the outside of our image.
14
+
15
+ :param im: A PIL Image object.
16
+ :returns: A new PIL Image object with white-space removed.
17
+ """
18
+ # (try to) load our image from file.
19
+ #im = Image.open(file_path, "r")
20
+ im.load()
21
+
22
+ # This solution was inspired by https://stackoverflow.com/questions/14211340/automatically-cropping-an-image-with-python-pil
23
+ image_data = np.asarray(im)
24
+ if im.mode == "RGBA":
25
+ image_data_bw = image_data[:,:,3]
26
+
27
+ # Now we just check which rows and columns are not pure white.
28
+ non_empty_columns = np.where(image_data_bw.max(axis = 0) != 0)[0]
29
+ non_empty_rows = np.where(image_data_bw.max(axis = 1) != 0)[0]
30
+
31
+ else:
32
+ # Axis 2 is the tuple/array of RGB values for each pixel. We want to know which ones are fully white (255,255,255), so we'll take the min of the 3 values and see if this is equal to 255. If it is, then all the pixels are 255
33
+ image_data_bw = image_data.min(axis = 2)
34
+
35
+ # Now we just check which rows and columns are not pure white.
36
+ non_empty_columns = np.where(image_data_bw.min(axis = 0) != 255)[0]
37
+ non_empty_rows = np.where(image_data_bw.min(axis = 1) != 255)[0]
38
+
39
+ if len(non_empty_rows) == 0 or len(non_empty_columns) == 0:
40
+ raise ValueError("The image is empty!")
41
+
42
+ # Get the bounding box of non-white stuff.
43
+ cropBox = (min(non_empty_rows), max(non_empty_rows), min(non_empty_columns), max(non_empty_columns))
44
+
45
+ # Copy the image data we want.
46
+ image_data_new = image_data[cropBox[0]:cropBox[1]+1, cropBox[2]:cropBox[3]+1 , :]
47
+
48
+ # And save over our old file.
49
+ new_image = Image.fromarray(image_data_new)
50
+ return new_image
51
+
52
+
53
+ class Image_maker(File_maker, Cropable_mixin):
54
+ """
55
+ Top level class for image maker objects.
56
+ """
57
+
58
+ # Text description of our output file type, used for error messages etc. This can be changed by inheriting classes.
59
+ output_file_type = "image"
60
+
61
+ def __init__(self, *args, enable_rendering = True, **kwargs):
62
+ """
63
+ General constructor for Image_maker objects.
64
+
65
+ These object are used to represent and make images. Note that a single Image_maker object can represent several image_files
66
+
67
+ :param output: A path to an output file to write to. How exactly this operates depends on the inheriting class, it is often only used as a base file name. See the class you are using.
68
+ :param dont_modify: Flag that modifies how image creation works. If True, no new images will be written to file.
69
+ :param use_existing: Flag that modifies how image creation works. If True, existing files will be preferentially used if available (set to False to force overwriting existing files).
70
+ """
71
+ super().__init__(*args, existing_file = None, dont_modify = not enable_rendering, **kwargs)
72
+
73
+ def get_image(self, name = 'file'):
74
+ """
75
+ Get the path to one of the images that this class represents, rendering the image to file first if necessary.
76
+
77
+ The functioning of this method is controlled by the dont_modify & use_existing flags.
78
+
79
+ You can also use the normal python attribute mechanism (either through getattr() or dot notation) to get these paths.
80
+
81
+ :raises KeyError: If name is not the name of one of the images this class represents.
82
+ :param name: The name of an image to get. Depends on the images in self.file_path.
83
+ :return: A pathlib Path object pointing to the image represented by name, or None if no image could be created.
84
+ """
85
+ return self.safe_get_file(name)
86
+
87
+ @property
88
+ def creation_message(self):
89
+ """
90
+ A short message that may (depending on log-level) be printed to the user before make_files() is called.
91
+ """
92
+ return "Rendering '{}' to file(s)".format(self.output if self.full_path_names else self.output.name)
93
+
94
+
95
+ def get_constrained_size(self, max_width, max_height):
96
+ """
97
+ Get the maximum possible dimensions of this image that retain the same aspect ratio that don't exceed a set of dimensions.
98
+
99
+ :param max_width: The maximum width to resize to; set to math.inf for no max.
100
+ :param max_heigh: The maximum height to resize to; set to math.inf for no max.
101
+ """
102
+ # Weasyprint hack for broken max-width.
103
+
104
+ # Now we open our diagram with pillow.
105
+ im = Image.open(self.get_image())
106
+
107
+ width, height = im.size
108
+
109
+ scale_factor = min(max_width/width, max_height/height)
110
+
111
+ return (width *scale_factor, height * scale_factor)
112
+
113
+