flinventory 0.3.0__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.
- flinventory/__init__.py +8 -0
- flinventory/__main__.py +45 -0
- flinventory/box.py +289 -0
- flinventory/constant.py +285 -0
- flinventory/datacleanup.py +349 -0
- flinventory/defaulted_data.py +552 -0
- flinventory/generate_labels.py +214 -0
- flinventory/inventory_io.py +295 -0
- flinventory/location.py +455 -0
- flinventory/sign.py +160 -0
- flinventory/signprinter_latex.py +471 -0
- flinventory/thing.py +145 -0
- flinventory/thingtemplate_latex/.gitignore +6 -0
- flinventory/thingtemplate_latex/dummyImage.jpg +0 -0
- flinventory/thingtemplate_latex/sign.tex +26 -0
- flinventory/thingtemplate_latex/signlist-footer.tex +1 -0
- flinventory/thingtemplate_latex/signlist-header.tex +12 -0
- flinventory/thingtemplate_latex/signs-example.tex +95 -0
- flinventory-0.3.0.dist-info/METADATA +63 -0
- flinventory-0.3.0.dist-info/RECORD +23 -0
- flinventory-0.3.0.dist-info/WHEEL +4 -0
- flinventory-0.3.0.dist-info/entry_points.txt +4 -0
- flinventory-0.3.0.dist-info/licenses/LICENSE +626 -0
@@ -0,0 +1,471 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""Create a latex document with signs (For printing and glueing to boxes).
|
3
|
+
|
4
|
+
Todo:
|
5
|
+
rename font size to text scale. Because it is used to scale instead of setting
|
6
|
+
a font size. So the 'normal' is not 12 (pt) but 1.
|
7
|
+
"""
|
8
|
+
import importlib.resources
|
9
|
+
import os.path
|
10
|
+
import logging
|
11
|
+
import subprocess
|
12
|
+
from typing import Union, Iterable
|
13
|
+
|
14
|
+
from .box import BoxedThing
|
15
|
+
from .sign import Sign
|
16
|
+
|
17
|
+
from . import thingtemplate_latex
|
18
|
+
|
19
|
+
TEMPLATE_PACKAGE = thingtemplate_latex
|
20
|
+
TEMPLATE_PRE = "signlist-header.tex"
|
21
|
+
#importlib.resources.read_text("flinventory", "signlist-header.tex"))
|
22
|
+
TEMPLATE_POST = "signlist-footer.tex"
|
23
|
+
TEMPLATE_SIGN = "sign.tex"
|
24
|
+
DUMMY_IMAGE = "dummyImage.jpg"
|
25
|
+
AUX_DIR = "latexAux"
|
26
|
+
|
27
|
+
UNIT = "cm" # todo: replace by length_unit in options, includes scaling the constants
|
28
|
+
# estimated by one example word
|
29
|
+
# at standard fontsize german
|
30
|
+
STANDARD_LETTER_LENGTH = 0.2 # in cm at font size 12pt
|
31
|
+
STANDARD_LETTER_HEIGHT = (12 + 1) * 0.03515 # in cm at font size 12 + linesep
|
32
|
+
# weird number is size of pt in LaTeX in cm
|
33
|
+
STANDARD_FONT_SIZE_GERMAN = 1
|
34
|
+
STANDARD_FONTSIZE_ENGLISH = 0.8
|
35
|
+
# amount of space the location string is shifted down
|
36
|
+
# (usually negative amount to shift up)
|
37
|
+
STANDARD_LOCATION_SHIFT_DOWN = r"-.8\baselineskip"
|
38
|
+
# in symbols:
|
39
|
+
STANDARD_LINE_LENGTH = 15
|
40
|
+
# how much bigger german should roughly be than english
|
41
|
+
GERMAN_TO_ENGLISH_SHARE = 1.25
|
42
|
+
# how big the image should be in parts of entire sign (in portrait mode)
|
43
|
+
IMAGE_SHARE = 0.5
|
44
|
+
# how much of the sign height can actually be used for text (due to margins)
|
45
|
+
# (in landscape mode)
|
46
|
+
TEXT_SHARE = 0.7
|
47
|
+
# minimum ration width/height for using landscape mode
|
48
|
+
LANDSCAPE_MIN_RATIO = 2
|
49
|
+
# paper text width (a4 = 21 cm wide) in cm
|
50
|
+
PAPER_TEXT_WIDTH = 20
|
51
|
+
# minimal height of sign for including an image (in cm)
|
52
|
+
MINIMAL_HEIGHT_FOR_IMAGE = 3
|
53
|
+
STANDARD_WIDTH = 8
|
54
|
+
# width of signs without given width [cm]
|
55
|
+
STANDARD_HEIGHT = 8
|
56
|
+
# height of signs without given height [cm]
|
57
|
+
MAX_WIDTH = 18 # otherwise too wide for A4 page
|
58
|
+
# maximum width of a sign [cm]
|
59
|
+
MAX_HEIGHT = 28 # otherwise too long for A4 page
|
60
|
+
# maximum height for a sign [cm]
|
61
|
+
|
62
|
+
def sanitize_latex_code(latex: Union[str, int, float]):
|
63
|
+
"""Make a string insertable into LaTeX code.
|
64
|
+
|
65
|
+
Replace &, \\, ....
|
66
|
+
"""
|
67
|
+
try:
|
68
|
+
for orig, new in [
|
69
|
+
("\\", "\\textbackslash{}"),
|
70
|
+
("{", "\\{"),
|
71
|
+
("}", "\\}"),
|
72
|
+
("$", "\\$"),
|
73
|
+
("&", "\\&"),
|
74
|
+
("#", "\\#"),
|
75
|
+
("^", ""), # better: → \textasciicircum{} (requires the textcomp package)
|
76
|
+
("_", "\\_"),
|
77
|
+
("~", "\\textasciitilde{}"),
|
78
|
+
("%", "\\%"),
|
79
|
+
("<", "\\textless{}"),
|
80
|
+
(">", "\\textgreater{}"),
|
81
|
+
("|", "\\textbar{}"),
|
82
|
+
]:
|
83
|
+
latex = latex.replace(orig, new)
|
84
|
+
except AttributeError:
|
85
|
+
pass
|
86
|
+
return latex
|
87
|
+
|
88
|
+
|
89
|
+
class SignPrinterLaTeX:
|
90
|
+
# pylint: disable=too-many-instance-attributes
|
91
|
+
"""Class encapsulating algorithm for creating printable signs from thing list.
|
92
|
+
|
93
|
+
Bad design choice: you have to create an object but actually this object is never used,
|
94
|
+
could all just be module or class functions.
|
95
|
+
"""
|
96
|
+
|
97
|
+
def __init__(self, paths):
|
98
|
+
"""Create SignPrinter by reading in template files."""
|
99
|
+
# pylint: disable-next=unbalanced-tuple-unpacking
|
100
|
+
self.pre, self.sign, self.post = self._read_templates()
|
101
|
+
self.output_dir = paths.output_dir
|
102
|
+
self.signs_file = paths.output_signs_latex
|
103
|
+
self.logger = logging.getLogger(__name__)
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def _read_templates() -> list[str]:
|
107
|
+
"""Read the templates from the files.
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
(iterable) main template, css header,
|
111
|
+
single size sign html file, double size sign html file
|
112
|
+
"""
|
113
|
+
# todo remove function by using importlib, which gives directly the text
|
114
|
+
file_contents = []
|
115
|
+
for filename in [TEMPLATE_PRE, TEMPLATE_SIGN, TEMPLATE_POST]:
|
116
|
+
file_contents.append(importlib.resources.read_text(TEMPLATE_PACKAGE, filename))
|
117
|
+
return file_contents
|
118
|
+
|
119
|
+
def controlled_length(self, sign:Sign, key: str, backup: Union[int, float],
|
120
|
+
max: Union[int, float]):
|
121
|
+
"""Get the value for the key if it exists and is a number and < max, otherwise backup."""
|
122
|
+
value = sign.get(key, backup)
|
123
|
+
try:
|
124
|
+
value = float(value)
|
125
|
+
except ValueError:
|
126
|
+
self.logger(f"{key} {value} is not a number. Use {backup} {UNIT} instead.")
|
127
|
+
return backup
|
128
|
+
if value <= 0:
|
129
|
+
self.logger(f"{key} {value} is non-positive and therefore not a valid sign {key}. "
|
130
|
+
f"Use {backup} {UNIT} instead.")
|
131
|
+
return backup
|
132
|
+
if value == int(value):
|
133
|
+
return min(int(value), max)
|
134
|
+
return min(value, max)
|
135
|
+
|
136
|
+
def width(self, thing: BoxedThing) -> Union[int, float]:
|
137
|
+
"""Get the width of a sign, using backup and max size.
|
138
|
+
|
139
|
+
In UNIT.
|
140
|
+
"""
|
141
|
+
return self.controlled_length(thing.sign, key='width', backup=STANDARD_WIDTH, max=MAX_WIDTH)
|
142
|
+
|
143
|
+
def height(self, thing: BoxedThing) -> Union[int, float]:
|
144
|
+
"""Get the height of the sign of a boxed thing, using backup and max size."""
|
145
|
+
return self.controlled_length(thing.sign, key='height', backup=STANDARD_HEIGHT, max=MAX_HEIGHT)
|
146
|
+
|
147
|
+
def if_use_landscape_template(self, thing: BoxedThing):
|
148
|
+
"""Return if we want to use the landscape template for this thing.
|
149
|
+
|
150
|
+
The landscape template has the image to the right of the text
|
151
|
+
instead of below it. So a portrait sign is usually still wider
|
152
|
+
than high.
|
153
|
+
"""
|
154
|
+
try:
|
155
|
+
return thing.sign.get('landscape')
|
156
|
+
except KeyError:
|
157
|
+
try:
|
158
|
+
return self.width(thing) >= self.width(thing) * LANDSCAPE_MIN_RATIO
|
159
|
+
except KeyError:
|
160
|
+
return True
|
161
|
+
|
162
|
+
def guess_font_size(self, thing):
|
163
|
+
"""Guess good font size.
|
164
|
+
|
165
|
+
Based on the length of the name and the size of the sign.
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
guessed font size primary language, guessed font size secondary language in UNIT
|
169
|
+
"""
|
170
|
+
# at first we do not support landscape signs
|
171
|
+
# if self.if_use_landscape_template(thing):
|
172
|
+
# return self.guess_font_size_landscape(thing)
|
173
|
+
return self.guess_font_size_portrait(thing)
|
174
|
+
|
175
|
+
@staticmethod
|
176
|
+
def guess_font_size_landscape(thing):
|
177
|
+
"""Guess good font sizes for the landscape template."""
|
178
|
+
assert False, "landscape latex signs are not supperted"
|
179
|
+
# for simplicity assume only one line
|
180
|
+
width = self.width(thing)
|
181
|
+
height = self.height(thing)
|
182
|
+
used_width = width - height # that is approximately the part the image uses
|
183
|
+
german = thing.sign.get(('name', 0), '')
|
184
|
+
english = thing.sign.get(('name', 1), '')
|
185
|
+
german_expected_width_at_standard = len(german) * STANDARD_LETTER_LENGTH
|
186
|
+
german_max_by_width = (
|
187
|
+
STANDARD_FONT_SIZE_GERMAN * used_width / german_expected_width_at_standard
|
188
|
+
)
|
189
|
+
german_max_by_height = (
|
190
|
+
height
|
191
|
+
* TEXT_SHARE
|
192
|
+
/ (GERMAN_TO_ENGLISH_SHARE + 1)
|
193
|
+
* GERMAN_TO_ENGLISH_SHARE
|
194
|
+
)
|
195
|
+
english_expected_width_at_standard = (
|
196
|
+
len(english)
|
197
|
+
* STANDARD_LETTER_LENGTH
|
198
|
+
* STANDARD_FONTSIZE_ENGLISH
|
199
|
+
/ STANDARD_FONT_SIZE_GERMAN
|
200
|
+
)
|
201
|
+
english_max_by_width = (
|
202
|
+
STANDARD_FONTSIZE_ENGLISH * used_width / english_expected_width_at_standard
|
203
|
+
)
|
204
|
+
english_max_by_height = height * TEXT_SHARE / (GERMAN_TO_ENGLISH_SHARE + 1)
|
205
|
+
return (
|
206
|
+
min(german_max_by_width, german_max_by_height),
|
207
|
+
min(english_max_by_width, english_max_by_height),
|
208
|
+
)
|
209
|
+
|
210
|
+
def guess_font_size_portrait(self, thing: BoxedThing):
|
211
|
+
# pylint: disable=too-many-locals
|
212
|
+
"""Guess what a good font size is for this sign.
|
213
|
+
|
214
|
+
Based on the length of the name and the size of the sign.
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
guessed font size primary, guessed font size secondary in UNIT
|
218
|
+
"""
|
219
|
+
# use german and english as aliases for primary and secondary language
|
220
|
+
# because it's easier to read
|
221
|
+
german = thing.get(('name', 0), '')
|
222
|
+
english = thing.get(('name', 1), '')
|
223
|
+
german_words = german.replace("-", "- ").split() or [" "]
|
224
|
+
english_words = english.replace("-", " ").split() or [" "]
|
225
|
+
# ignore cases with more than 2 lines, should be considered by hand
|
226
|
+
max_font_sizes = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
|
227
|
+
# there are 4 line number cases, see below in for loop
|
228
|
+
GERMAN_INDEX = 0 # pylint: disable=invalid-name
|
229
|
+
ENGLISH_INDEX = 1 # pylint: disable=invalid-name
|
230
|
+
SUM_INDEX = 2 # pylint: disable=invalid-name
|
231
|
+
# self.logger.debug("g: {}; e: {}, width: {}, heigth: {}".format(
|
232
|
+
# german, english, *thing.sign.size # unpacking pair
|
233
|
+
# ))
|
234
|
+
width = self.width(thing)
|
235
|
+
height = self.width(thing)
|
236
|
+
for german_number_lines, english_number_lines, case in [
|
237
|
+
(1, 1, 0),
|
238
|
+
(1, 2, 1),
|
239
|
+
(2, 1, 2),
|
240
|
+
(2, 2, 3),
|
241
|
+
]:
|
242
|
+
german_length = max(
|
243
|
+
(
|
244
|
+
max(len(word) for word in german_words)
|
245
|
+
if german_number_lines == 2
|
246
|
+
else len(german)
|
247
|
+
),
|
248
|
+
1,
|
249
|
+
)
|
250
|
+
english_length = max(
|
251
|
+
(
|
252
|
+
max(len(word) for word in english_words)
|
253
|
+
if english_number_lines == 2
|
254
|
+
else len(english)
|
255
|
+
),
|
256
|
+
1,
|
257
|
+
)
|
258
|
+
german_expected_width_at_standard = german_length * STANDARD_LETTER_LENGTH
|
259
|
+
german_max_by_width = (
|
260
|
+
STANDARD_FONT_SIZE_GERMAN * width / german_expected_width_at_standard
|
261
|
+
)
|
262
|
+
text_height = height * (
|
263
|
+
1 if height < MINIMAL_HEIGHT_FOR_IMAGE else IMAGE_SHARE
|
264
|
+
)
|
265
|
+
german_max_by_height = (
|
266
|
+
STANDARD_FONT_SIZE_GERMAN
|
267
|
+
* text_height
|
268
|
+
/ STANDARD_LETTER_HEIGHT
|
269
|
+
/ (german_number_lines * GERMAN_TO_ENGLISH_SHARE + english_number_lines)
|
270
|
+
* GERMAN_TO_ENGLISH_SHARE
|
271
|
+
)
|
272
|
+
english_expected_width_at_standard = (
|
273
|
+
english_length
|
274
|
+
* STANDARD_LETTER_LENGTH
|
275
|
+
* STANDARD_FONTSIZE_ENGLISH
|
276
|
+
/ STANDARD_FONT_SIZE_GERMAN
|
277
|
+
)
|
278
|
+
# in factor size compared to normal size
|
279
|
+
english_max_by_width = (
|
280
|
+
STANDARD_FONTSIZE_ENGLISH * width / english_expected_width_at_standard
|
281
|
+
)
|
282
|
+
# in cm:
|
283
|
+
english_max_by_height = (
|
284
|
+
STANDARD_FONTSIZE_ENGLISH
|
285
|
+
* text_height
|
286
|
+
/ STANDARD_LETTER_HEIGHT
|
287
|
+
/ (german_number_lines * GERMAN_TO_ENGLISH_SHARE + english_number_lines)
|
288
|
+
)
|
289
|
+
logging.info(
|
290
|
+
f"german: {german}, english: {english}, case: {case} "
|
291
|
+
f"lines: ({german_number_lines}, {english_number_lines}, {case},"
|
292
|
+
f"german_max_by_height: {german_max_by_height}, "
|
293
|
+
f"german_max_by_width: {german_max_by_width}, "
|
294
|
+
f"english_max_by_height: {english_max_by_height}, "
|
295
|
+
f"english_max_by_width: {english_max_by_width}"
|
296
|
+
)
|
297
|
+
max_font_sizes[case][GERMAN_INDEX] = min(
|
298
|
+
[german_max_by_height, german_max_by_width]
|
299
|
+
)
|
300
|
+
max_font_sizes[case][ENGLISH_INDEX] = min(
|
301
|
+
[english_max_by_height, english_max_by_width]
|
302
|
+
)
|
303
|
+
max_font_sizes[case][SUM_INDEX] = (
|
304
|
+
max_font_sizes[case][GERMAN_INDEX] + max_font_sizes[case][ENGLISH_INDEX]
|
305
|
+
)
|
306
|
+
# self.logger.debug(
|
307
|
+
# "case: {}; gmaxH: {:.3f}; gmaxW: {:.3f}; emaxH: {:.3f}; emaxW: {:.3f}".format(
|
308
|
+
# case, german_max_by_height, german_max_by_width,
|
309
|
+
# english_max_by_height, english_max_by_width
|
310
|
+
# )
|
311
|
+
# )
|
312
|
+
german_max, english_max, _ = max(
|
313
|
+
max_font_sizes, key=lambda case: case[SUM_INDEX]
|
314
|
+
)
|
315
|
+
# self.logger.debug("used fs: g: {:.3f}, e: {:.3f}".format(german_max, english_max))
|
316
|
+
return german_max, english_max
|
317
|
+
|
318
|
+
def get_font_size(self, thing: BoxedThing):
|
319
|
+
"""Determine font size of sign for thing.
|
320
|
+
|
321
|
+
Take font size in the thing.
|
322
|
+
Guess font size if not specified.
|
323
|
+
|
324
|
+
Returns:
|
325
|
+
(german font size, english font size)
|
326
|
+
"""
|
327
|
+
default_german_font_size, default_english_font_size = self.guess_font_size(thing)
|
328
|
+
german_font_size = thing.sign.get(('fontsize', 0), default_german_font_size)
|
329
|
+
english_font_size = thing.sign.get(('fontsize', 1), default_english_font_size)
|
330
|
+
logging.info(
|
331
|
+
f"{thing.best('name')} font factor: (de) {german_font_size} (en) {english_font_size}"
|
332
|
+
)
|
333
|
+
return german_font_size, english_font_size
|
334
|
+
|
335
|
+
def create_latex(self, things: Iterable[BoxedThing]):
|
336
|
+
"""Create latex code (as str) that shows all signs.
|
337
|
+
|
338
|
+
Arguments:
|
339
|
+
things: list of things to be described
|
340
|
+
"""
|
341
|
+
content_latex = [self.pre]
|
342
|
+
current_line_filled_width = 0
|
343
|
+
for thing in [
|
344
|
+
tmp_thing for tmp_thing in things if tmp_thing.sign.should_be_printed()
|
345
|
+
]:
|
346
|
+
if current_line_filled_width + self.width(thing) > PAPER_TEXT_WIDTH:
|
347
|
+
content_latex.append(
|
348
|
+
""
|
349
|
+
) # empty line in code makes new line (= row) in pdf
|
350
|
+
current_line_filled_width = 0
|
351
|
+
content_latex.append(self.create_sign(thing))
|
352
|
+
current_line_filled_width += self.width(thing)
|
353
|
+
content_latex.append(self.post)
|
354
|
+
return "\n".join(content_latex)
|
355
|
+
|
356
|
+
def get_values_for_template(self, thing: BoxedThing):
|
357
|
+
"""Get values for the insertion into the templates.
|
358
|
+
|
359
|
+
Only the values that are common for portrait
|
360
|
+
and landscape template:
|
361
|
+
widthAdjusted (width - 0.14cm)
|
362
|
+
textscaleGerman
|
363
|
+
GermanName
|
364
|
+
textscaleEnglish
|
365
|
+
EnglishName
|
366
|
+
imagepath
|
367
|
+
imageheight
|
368
|
+
location
|
369
|
+
"""
|
370
|
+
german = thing.sign.get(('name', 0), '')
|
371
|
+
english = thing.sign.get(('name', 1), '')
|
372
|
+
|
373
|
+
width = self.width(thing)
|
374
|
+
height = self.height(thing)
|
375
|
+
width = width - 0.14 # todo: fix latex template or create constant
|
376
|
+
insertions = {
|
377
|
+
"PrimaryName": sanitize_latex_code(german),
|
378
|
+
"SecondaryName": sanitize_latex_code(english),
|
379
|
+
"location": sanitize_latex_code(str(thing.where)),
|
380
|
+
"height": height,
|
381
|
+
"widthAdjusted": width,
|
382
|
+
}
|
383
|
+
|
384
|
+
if not insertions["location"]: # empty string
|
385
|
+
logging.warning(
|
386
|
+
f"Print sign for {german} ({english}) without location."
|
387
|
+
)
|
388
|
+
|
389
|
+
# todo: make image height configurable
|
390
|
+
if height < MINIMAL_HEIGHT_FOR_IMAGE:
|
391
|
+
insertions["imageheight"] = 0
|
392
|
+
insertions["vspace"] = r"-1.5\baselineskip"
|
393
|
+
else:
|
394
|
+
insertions["imageheight"] = height * IMAGE_SHARE
|
395
|
+
insertions["vspace"] = "1pt"
|
396
|
+
|
397
|
+
# at first only portrait sign, landscape sign can be implemented later
|
398
|
+
# if self.if_use_landscape_template(thing): # would need different condition
|
399
|
+
if image_path := thing.thing.image_path(): # not None or ""
|
400
|
+
rel_path_to_image = os.path.relpath(
|
401
|
+
image_path,
|
402
|
+
self.output_dir
|
403
|
+
)
|
404
|
+
insertions["imagepath"] = rel_path_to_image
|
405
|
+
else:
|
406
|
+
if insertions["imageheight"] > 0:
|
407
|
+
# otherwise no image is printed and we do not need an unneccessary warning
|
408
|
+
logging.getLogger(__name__).warning(
|
409
|
+
f"Missing image for {thing.best('name')}."
|
410
|
+
)
|
411
|
+
insertions["imagepath"] = DUMMY_IMAGE
|
412
|
+
try:
|
413
|
+
image_file = open(os.path.join(self.output_dir, DUMMY_IMAGE), mode="bw")
|
414
|
+
except FileExistsError:
|
415
|
+
pass
|
416
|
+
else:
|
417
|
+
dummy_image = importlib.resources.read_binary(TEMPLATE_PACKAGE, DUMMY_IMAGE)
|
418
|
+
with image_file:
|
419
|
+
image_file.write(dummy_image)
|
420
|
+
font_sizes = self.get_font_size(thing)
|
421
|
+
insertions["textscalePrimary"] = font_sizes[0] # /12
|
422
|
+
insertions["textscaleSecondary"] = font_sizes[1] # /12
|
423
|
+
|
424
|
+
insertions["locationShiftDown"] = thing.sign.get('location_shift_down',
|
425
|
+
STANDARD_LOCATION_SHIFT_DOWN)
|
426
|
+
return insertions
|
427
|
+
|
428
|
+
def create_sign(self, thing):
|
429
|
+
"""Create a sign based on the template sign.tex."""
|
430
|
+
# text that is to be inserted into the template
|
431
|
+
insertions = self.get_values_for_template(thing)
|
432
|
+
# no landscape yet
|
433
|
+
# if self.if_use_landscape_template(thing):
|
434
|
+
# return self.signhtml_landscape.format(
|
435
|
+
# **insertions # unpacking dictionary
|
436
|
+
# )
|
437
|
+
return self.sign.format(**insertions)
|
438
|
+
|
439
|
+
def save_signs_latex(self, things: list[BoxedThing]) -> str:
|
440
|
+
"""Save signs as tex-file to file path.
|
441
|
+
|
442
|
+
Ignore things that should not be printed as saved in things.sign['printed'].
|
443
|
+
|
444
|
+
Arguments:
|
445
|
+
things: list of things to visualize
|
446
|
+
Returns:
|
447
|
+
path to created file
|
448
|
+
"""
|
449
|
+
things.sort(key=lambda t: self.height(t))
|
450
|
+
file_signs = os.path.join(self.output_dir, self.signs_file)
|
451
|
+
print(f"Print LaTeX file to {file_signs}.")
|
452
|
+
with open(file_signs, mode="w", encoding="UTF-8") as latex_file:
|
453
|
+
latex_file.write(self.create_latex(things))
|
454
|
+
return file_signs
|
455
|
+
|
456
|
+
def create_signs_pdf(self, things: list[BoxedThing]):
|
457
|
+
"""Create a pdf and latex files with signs."""
|
458
|
+
self.save_signs_latex(things)
|
459
|
+
latex = subprocess.run(['latexmk', self.signs_file,
|
460
|
+
f'-aux-directory={AUX_DIR}',
|
461
|
+
"-pdf"],
|
462
|
+
cwd=self.output_dir,
|
463
|
+
capture_output=True,
|
464
|
+
text=True) # text=True makes output a str instead of bytes
|
465
|
+
with open(os.path.join(self.output_dir, AUX_DIR, 'latex_output.txt'), mode="w") as stdout_file:
|
466
|
+
stdout_file.write(latex.stdout)
|
467
|
+
with open(os.path.join(self.output_dir, AUX_DIR, 'latex_error.txt'), mode="w") as stderr_file:
|
468
|
+
stderr_file.write(latex.stderr)
|
469
|
+
if latex.returncode != 0:
|
470
|
+
print(f"Latex finished with returncode {latex.returncode}."
|
471
|
+
f"Check in {os.path.join(self.output_dir, AUX_DIR)} for details.")
|
flinventory/thing.py
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
"""A thing in an inventory."""
|
3
|
+
import logging
|
4
|
+
import os.path
|
5
|
+
import pathlib
|
6
|
+
from typing import override, Optional
|
7
|
+
import yaml
|
8
|
+
|
9
|
+
from . import constant
|
10
|
+
from . import defaulted_data
|
11
|
+
|
12
|
+
|
13
|
+
class Thing(defaulted_data.DefaultedDict):
|
14
|
+
"""A thing in an inventory.
|
15
|
+
|
16
|
+
In addition to the functionality of DefaultedDict,
|
17
|
+
a thing has specific list and translated keys
|
18
|
+
and save and open from file functionality.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
data: defaulted_data.Data,
|
24
|
+
default: defaulted_data.Data,
|
25
|
+
options: constant.Options,
|
26
|
+
directory: str,
|
27
|
+
):
|
28
|
+
"""Create a new thing from some data.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
data: data specific to this thing
|
32
|
+
default: default values as in DefaultedDict
|
33
|
+
options: inventory-wide options,
|
34
|
+
directory: where to save data of this thing
|
35
|
+
"""
|
36
|
+
self._directory = None # None signals: no save
|
37
|
+
super().__init__(
|
38
|
+
data=data,
|
39
|
+
default=default,
|
40
|
+
non_defaulted=tuple(),
|
41
|
+
translated=("name", "name_alt", "description"),
|
42
|
+
lists=("name_alt", "part_of"),
|
43
|
+
default_order=("name", "name_alt", "part_of", "category", "url"),
|
44
|
+
options=options,
|
45
|
+
)
|
46
|
+
self._directory = directory
|
47
|
+
self._logger = logging.getLogger(__name__)
|
48
|
+
|
49
|
+
@property
|
50
|
+
def directory(self):
|
51
|
+
"""The directory in which the thing data file is saved."""
|
52
|
+
return self._directory
|
53
|
+
|
54
|
+
@directory.setter
|
55
|
+
def directory(self, new_directory: str):
|
56
|
+
"""Set the directory in which the data file is saved.
|
57
|
+
|
58
|
+
Save the data there.
|
59
|
+
The old data file is removed.
|
60
|
+
"""
|
61
|
+
if self._directory is not None:
|
62
|
+
pathlib.Path(os.path.join(self._directory, constant.THING_FILE)).unlink(
|
63
|
+
missing_ok=True
|
64
|
+
)
|
65
|
+
assert new_directory is not None
|
66
|
+
self._directory = new_directory
|
67
|
+
self.save()
|
68
|
+
|
69
|
+
@override
|
70
|
+
def __setitem__(self, key, value):
|
71
|
+
"""self[key] = value as in DefaultedDict but saved afterward."""
|
72
|
+
super().__setitem__(key, value)
|
73
|
+
if self._directory is not None:
|
74
|
+
self.save()
|
75
|
+
|
76
|
+
@override
|
77
|
+
def __delitem__(self, key):
|
78
|
+
"""del self[key] as in DefaultedDict but saved afterward."""
|
79
|
+
super().__delitem__(key)
|
80
|
+
if self._directory is not None:
|
81
|
+
self.save()
|
82
|
+
|
83
|
+
def image_path(self) -> Optional[str]:
|
84
|
+
"""Path to the image of the thing, if it exists.
|
85
|
+
|
86
|
+
If path is given and relative, it is assumed
|
87
|
+
that it is relative to my directory.
|
88
|
+
If it has no extension, try finding it with extension.
|
89
|
+
Returns:
|
90
|
+
If image is explicitly given, use this.
|
91
|
+
Otherwise, look in my directory for a suitably
|
92
|
+
named file with one of the image extensions.
|
93
|
+
Is relative path if my directory is relative.
|
94
|
+
None if no image was found.
|
95
|
+
"""
|
96
|
+
path_base = self.get('image', constant.IMAGE_FILE_BASE)
|
97
|
+
if not os.path.isabs(path_base):
|
98
|
+
path_base = os.path.join(self.directory, path_base)
|
99
|
+
for extension in ('', *(f".{ext}" for ext in constant.IMAGE_FILE_TYPES)):
|
100
|
+
if os.path.isfile(path := path_base + extension):
|
101
|
+
return path
|
102
|
+
return None
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def from_yaml_file(
|
106
|
+
cls, directory: str, default: defaulted_data.Data, options: constant.Options
|
107
|
+
):
|
108
|
+
"""Create a thing from data in a file.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
directory: directory with sign file
|
112
|
+
default: thing for which this is a sign. Used as default data.
|
113
|
+
options: inventory-wide options
|
114
|
+
"""
|
115
|
+
path = pathlib.Path(os.path.join(directory, constant.THING_FILE))
|
116
|
+
try:
|
117
|
+
thing_file = open(path, mode="r", encoding="utf-8")
|
118
|
+
except FileNotFoundError:
|
119
|
+
logging.getLogger(__name__).warning(f"{path} not found. That is unusual.")
|
120
|
+
return cls({}, default, options, directory)
|
121
|
+
with thing_file:
|
122
|
+
return cls(yaml.safe_load(thing_file), default, options, directory)
|
123
|
+
|
124
|
+
def save(self):
|
125
|
+
"""Save data to yaml file.
|
126
|
+
|
127
|
+
Do not check if something is overwritten.
|
128
|
+
|
129
|
+
Delete file if no data is there to be written.
|
130
|
+
Todo: include git add and commit.
|
131
|
+
Raises:
|
132
|
+
TypeError: if directory is None
|
133
|
+
"""
|
134
|
+
jsonable_data = self.to_jsonable_data()
|
135
|
+
path = pathlib.Path(os.path.join(self._directory, constant.THING_FILE))
|
136
|
+
if not jsonable_data:
|
137
|
+
if path.is_file():
|
138
|
+
self._logger.info(f"Delete {path}")
|
139
|
+
path.unlink(missing_ok=True)
|
140
|
+
else:
|
141
|
+
with open(path, mode="w", encoding="utf-8") as thing_file:
|
142
|
+
yaml.dump(jsonable_data, thing_file, **constant.YAML_DUMP_OPTIONS)
|
143
|
+
self._logger.info(
|
144
|
+
f"Saved {self.get(('name', 0), self._directory)} sign to {path}."
|
145
|
+
)
|
Binary file
|
@@ -0,0 +1,26 @@
|
|
1
|
+
\fbox{{%
|
2
|
+
% need sizes:
|
3
|
+
% width (reduce by 0.24 cm)
|
4
|
+
% height
|
5
|
+
% text primary language
|
6
|
+
% text secondary language
|
7
|
+
% image path
|
8
|
+
% location shortcut
|
9
|
+
% optional: text size (default: \relscale{{1}} or something guessed based on existing algorithm)
|
10
|
+
% optional: image height (otherwise half of entire size)
|
11
|
+
\begin{{minipage}}[t][{height}cm][t]{{{widthAdjusted}cm}}
|
12
|
+
\vspace{{1mm}}
|
13
|
+
\setlength{{\parskip}}{{0pt}}
|
14
|
+
\centering
|
15
|
+
{{\relscale{{{textscalePrimary}}}
|
16
|
+
{PrimaryName}
|
17
|
+
}}
|
18
|
+
|
19
|
+
{{\relscale{{{textscaleSecondary}}} {SecondaryName}}}
|
20
|
+
|
21
|
+
\vspace*{{{vspace}}}
|
22
|
+
\includegraphics[height={imageheight}cm,keepaspectratio,width={widthAdjusted}cm]{{{imagepath}}}
|
23
|
+
\vspace*{{{locationShiftDown}}}
|
24
|
+
\flushright {{\tiny {location}}}
|
25
|
+
\end{{minipage}}
|
26
|
+
}}
|
@@ -0,0 +1 @@
|
|
1
|
+
\end{document}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
\documentclass[12pt,a4paper]{article}
|
2
|
+
\usepackage[margin=0.5cm]{geometry}
|
3
|
+
\usepackage{xcolor}
|
4
|
+
\usepackage{graphicx}
|
5
|
+
\usepackage{adjustbox}
|
6
|
+
\usepackage[utf8]{inputenc}
|
7
|
+
\usepackage{relsize}
|
8
|
+
\usepackage{setspace}
|
9
|
+
\linespread{0.75}
|
10
|
+
\setlength\parindent{0pt}
|
11
|
+
\begin{document}
|
12
|
+
\setlength{\fboxsep}{0pt}
|