Photo-Composition-Designer 0.0.7__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 (40) hide show
  1. Photo_Composition_Designer/__init__.py +0 -0
  2. Photo_Composition_Designer/__main__.py +8 -0
  3. Photo_Composition_Designer/_version.py +34 -0
  4. Photo_Composition_Designer/cli/__init__.py +0 -0
  5. Photo_Composition_Designer/cli/__main__.py +8 -0
  6. Photo_Composition_Designer/cli/cli.py +106 -0
  7. Photo_Composition_Designer/common/Anniversaries.py +93 -0
  8. Photo_Composition_Designer/common/Locations.py +87 -0
  9. Photo_Composition_Designer/common/MoonPhase.py +85 -0
  10. Photo_Composition_Designer/common/Photo.py +113 -0
  11. Photo_Composition_Designer/common/__init__.py +0 -0
  12. Photo_Composition_Designer/common/logging.py +216 -0
  13. Photo_Composition_Designer/config/__init__.py +0 -0
  14. Photo_Composition_Designer/config/config.py +321 -0
  15. Photo_Composition_Designer/core/__init__.py +0 -0
  16. Photo_Composition_Designer/core/base.py +383 -0
  17. Photo_Composition_Designer/gui/GuiLogWriter.py +79 -0
  18. Photo_Composition_Designer/gui/__init__.py +0 -0
  19. Photo_Composition_Designer/gui/__main__.py +8 -0
  20. Photo_Composition_Designer/gui/gui.py +565 -0
  21. Photo_Composition_Designer/image/CalendarRenderer.py +319 -0
  22. Photo_Composition_Designer/image/CollageRenderer.py +433 -0
  23. Photo_Composition_Designer/image/DescriptionRenderer.py +74 -0
  24. Photo_Composition_Designer/image/MapRenderer.py +101 -0
  25. Photo_Composition_Designer/image/__init__.py +0 -0
  26. Photo_Composition_Designer/tools/DescriptionsFileGenerator.py +44 -0
  27. Photo_Composition_Designer/tools/GeoPlotter.py +211 -0
  28. Photo_Composition_Designer/tools/Helpers.py +18 -0
  29. Photo_Composition_Designer/tools/ImageDistributor.py +153 -0
  30. Photo_Composition_Designer/tools/__init__.py +0 -0
  31. __init__.py +0 -0
  32. firewall_handler.py +198 -0
  33. main.py +146 -0
  34. path_handler.py +10 -0
  35. photo_composition_designer-0.0.7.dist-info/METADATA +205 -0
  36. photo_composition_designer-0.0.7.dist-info/RECORD +40 -0
  37. photo_composition_designer-0.0.7.dist-info/WHEEL +5 -0
  38. photo_composition_designer-0.0.7.dist-info/entry_points.txt +3 -0
  39. photo_composition_designer-0.0.7.dist-info/licenses/LICENSE +24 -0
  40. photo_composition_designer-0.0.7.dist-info/top_level.txt +5 -0
@@ -0,0 +1,433 @@
1
+ import random
2
+
3
+ from PIL import Image, UnidentifiedImageError
4
+
5
+
6
+ class CollageRenderer:
7
+ def __init__(self, width=900, height=600, spacing=10, color=(0, 0, 0)):
8
+ self.color = color
9
+ self.width: int = width
10
+ self.height: int = height
11
+ self.spacing: int = spacing
12
+
13
+ def generate(self, images: list[Image.Image]) -> Image.Image:
14
+ """
15
+ Ordnet die Bilder in der Composition an. Bilder werden vorab auf Lesbarkeit geprüft.
16
+ """
17
+ # Bilder nach Seitenverhältnis sortieren
18
+ collage: Image.Image = Image.new(
19
+ mode="RGB", size=(self.width, self.height), color=self.color
20
+ )
21
+ images = self.sortByAspectRatio(images)
22
+ formats = self.analyzeImages(images)
23
+
24
+ try:
25
+ # Anordnungslogik basierend auf Bildanzahl
26
+ if len(images) == 1:
27
+ self.arrangeOneImage(collage, images[0], self.width, self.height)
28
+ elif len(images) == 2:
29
+ self.arrangeTwoImages(collage, images, formats, self.width, self.height)
30
+ elif len(images) == 3:
31
+ self.arrangeThreeImages(collage, images, formats, self.width, self.height)
32
+ elif len(images) == 4:
33
+ self.arrangeFourImages(collage, images, formats, self.width, self.height)
34
+ elif len(images) == 5:
35
+ self.arrangeFiveImages(collage, images, formats, self.width, self.height)
36
+ else:
37
+ self.arrangeMultipleImages(collage, images, self.width, self.height)
38
+ except (UnidentifiedImageError, OSError) as e:
39
+ print(f"Error in the arrangement of images: {e}")
40
+ # Entferne ungültige Bilder und versuche es erneut
41
+ photos = self.remove_invalid_images(images)
42
+ if photos:
43
+ print("Invalid images removed, try again...")
44
+ self.generate(photos)
45
+ else:
46
+ # Wenn keine gültigen Bilder mehr vorhanden sind, Fehler erneut werfen
47
+ print("No more valid images available.")
48
+ raise e
49
+ return collage
50
+
51
+ @staticmethod
52
+ def remove_invalid_images(photos: list[Image.Image]):
53
+ """
54
+ Überprüft eine Liste von Bildern und entfernt nicht lesbare oder kaputte Bilder.
55
+ """
56
+ valid_images = []
57
+ for img in photos:
58
+ try:
59
+ # Teste, ob das Bild ohne Fehler zugeschnitten werden kann
60
+ img.crop((0, 2, 3, 3))
61
+ valid_images.append(img)
62
+ except (UnidentifiedImageError, OSError) as e:
63
+ print(f"Invalid image skipped: {img.info} - {e}")
64
+
65
+ # Öffne die Bilder erneut, da der Dateizeiger möglicherweise geschlossen wurde
66
+ return valid_images
67
+
68
+ @staticmethod
69
+ def analyzeImages(images):
70
+ """
71
+ Analysiert, ob Bilder Hoch- oder Querformat haben.
72
+ """
73
+ analysis = []
74
+ for img in images:
75
+ width, height = img.size
76
+ if height > width:
77
+ analysis.append("portrait")
78
+ else:
79
+ analysis.append("landscape")
80
+ return analysis
81
+
82
+ @staticmethod
83
+ def sortByAspectRatio(images):
84
+ """
85
+ Sortiert Bilder basierend auf ihrem Seitenverhältnis (Breite / Höhe).
86
+ Schmalste ("portrait") zuerst, breiteste ("landscape") zuletzt.
87
+ """
88
+ return sorted(images, key=lambda img: img.size[0] / img.size[1], reverse=False)
89
+
90
+ @staticmethod
91
+ def cropAndResize(image, target_width, target_height):
92
+ """
93
+ Schneidet ein Bild proportional zu und skaliert es dann auf die gewünschte Größe.
94
+ """
95
+ img_width, img_height = image.size
96
+ aspect_ratio_img = img_width / img_height
97
+ aspect_ratio_target = target_width / target_height
98
+
99
+ if aspect_ratio_img > aspect_ratio_target:
100
+ # Bild ist breiter -> Seitlich beschneiden
101
+ new_width = int(aspect_ratio_target * img_height)
102
+ left = (img_width - new_width) // 2
103
+ right = left + new_width
104
+ cropped = image.crop((left, 0, right, img_height))
105
+ else:
106
+ # Bild ist höher -> Oben und unten beschneiden
107
+ new_height = int(img_width / aspect_ratio_target)
108
+ top = (img_height - new_height) // 2
109
+ bottom = top + new_height
110
+ cropped = image.crop((0, top, img_width, bottom))
111
+
112
+ return cropped.resize((target_width, target_height))
113
+
114
+ def arrangeOneImage(self, collage, image, width, height):
115
+ """
116
+ Layout für ein einzelnes Bild.
117
+ """
118
+ img = self.cropAndResize(image, width, height)
119
+ collage.paste(img, (0, 0))
120
+
121
+ def arrangeTwoImages(self, collage, images, formats, width, height):
122
+ """
123
+ Layout für zwei Bilder.
124
+ """
125
+ if "portrait" in formats:
126
+ portrait_idx = formats.index("portrait")
127
+ landscape_idx = 1 - portrait_idx
128
+ # Goldener Schnitt Layout
129
+ portrait_width = int(width * 0.4)
130
+ landscape_width = width - portrait_width - self.spacing
131
+ img1 = self.cropAndResize(images[portrait_idx], portrait_width, height)
132
+ img2 = self.cropAndResize(images[landscape_idx], landscape_width, height)
133
+ collage.paste(img1, (0, 0))
134
+ collage.paste(img2, (portrait_width + self.spacing, 0))
135
+ else:
136
+ # Beide Querformat -> nebeneinander
137
+ img_width = (width - self.spacing) // 2
138
+ img1 = self.cropAndResize(images[0], img_width, height)
139
+ img2 = self.cropAndResize(images[1], img_width, height)
140
+ collage.paste(img1, (0, 0))
141
+ collage.paste(img2, (img_width + self.spacing, 0))
142
+
143
+ def arrangeThreeImages(self, collage, images, formats, w, h):
144
+ """
145
+ Layouts für drei Bilder.
146
+ """
147
+ s = self.spacing
148
+ layouts = [
149
+ # Ein großes Bild quer oben, zwei kleinere unten nebeneinander LLL
150
+ lambda imgs: [
151
+ (self.cropAndResize(imgs[0], w, int(h * 0.6) - s), (0, 0)),
152
+ (self.cropAndResize(imgs[1], int(w * 0.5), int(h * 0.4)), (0, int(h * 0.6))),
153
+ (
154
+ self.cropAndResize(imgs[2], int(w * 0.5), int(h * 0.4)),
155
+ (int(w * 0.5) + s, int(h * 0.6)),
156
+ ),
157
+ ],
158
+ # Großes Querformat links, zwei Querformat rechts übereinander LLL
159
+ lambda imgs: [
160
+ (self.cropAndResize(imgs[0], int(w * 0.7), h), (0, 0)),
161
+ (self.cropAndResize(imgs[1], int(w * 0.3), int(h * 0.5)), (int(w * 0.7) + s, 0)),
162
+ (
163
+ self.cropAndResize(imgs[2], int(w * 0.3), int(h * 0.5) - s),
164
+ (int(w * 0.7) + s, int(h * 0.5) + s),
165
+ ),
166
+ ],
167
+ # Großes Hochformat links, zwei Querformat rechts übereinander PLL
168
+ lambda imgs: [
169
+ (self.cropAndResize(imgs[0], int(w * 0.4), h), (0, 0)),
170
+ (self.cropAndResize(imgs[1], int(w * 0.6), int(h * 0.5)), (int(w * 0.4) + s, 0)),
171
+ (
172
+ self.cropAndResize(imgs[2], int(w * 0.6), int(h * 0.5) - s),
173
+ (int(w * 0.4) + s, int(h * 0.5) + s),
174
+ ),
175
+ ],
176
+ # Großes Querformat links, zwei Hochformat rechts übereinander PPL
177
+ lambda imgs: [
178
+ (self.cropAndResize(imgs[0], int(w * 0.6), h), (0, 0)),
179
+ (self.cropAndResize(imgs[1], int(w * 0.4), int(h * 0.5)), (int(w * 0.6) + s, 0)),
180
+ (
181
+ self.cropAndResize(imgs[2], int(w * 0.4), int(h * 0.5) - s),
182
+ (int(w * 0.6) + s, int(h * 0.5) + s),
183
+ ),
184
+ ],
185
+ ]
186
+
187
+ if formats.count("portrait") == 0:
188
+ random.seed()
189
+ if random.random() > 0.8:
190
+ layout = layouts[0]
191
+ else:
192
+ layout = layouts[1]
193
+ elif formats.count("portrait") == 1:
194
+ layout = layouts[2]
195
+ elif formats.count("portrait") == 2:
196
+ layout = layouts[3]
197
+ else:
198
+ # Drei gleich große Bilder im Hochformat nebeneinander PPP
199
+ self.arrangeMultipleImages(collage, images, self.width, self.height)
200
+ return
201
+
202
+ for img, pos in layout(images):
203
+ collage.paste(img, pos)
204
+
205
+ def arrangeFourImages(self, collage, images, formats, w, h):
206
+ """
207
+ Layouts für vier Bilder.
208
+ """
209
+ s = self.spacing
210
+ layouts = [
211
+ # Zwei große Bilder oben, zwei etwas kleiner unten, leicht versetzt (LLLL)
212
+ lambda imgs: [
213
+ (self.cropAndResize(imgs[0], int(w * 0.45), int(h * 0.55) - s), (0, 0)),
214
+ (
215
+ self.cropAndResize(imgs[3], int(w * 0.55), int(h * 0.55) - s),
216
+ (int(w * 0.45) + s, 0),
217
+ ),
218
+ (self.cropAndResize(imgs[2], int(w * 0.55), int(h * 0.45)), (0, int(h * 0.55))),
219
+ (
220
+ self.cropAndResize(imgs[1], int(w * 0.45), int(h * 0.45)),
221
+ (int(w * 0.55) + s, int(h * 0.55)),
222
+ ),
223
+ ],
224
+ # Großes Quadrat, drei kleine landscape rechts Q-LLL
225
+ lambda imgs: [
226
+ (self.cropAndResize(imgs[0], int(w * 0.7), h), (0, 0)), # portrait, index 0
227
+ (self.cropAndResize(imgs[1], int(w * 0.3), int(h / 3)), (int(w * 0.7) + s, 0)),
228
+ (
229
+ self.cropAndResize(imgs[2], int(w * 0.3), int(h / 3) - s),
230
+ (int(w * 0.7) + s, int(h / 3) + s),
231
+ ),
232
+ (
233
+ self.cropAndResize(imgs[3], int(w * 0.3), int(h / 3) - 1 * s),
234
+ (int(w * 0.7) + s, int(h * 2 / 3) + s),
235
+ ),
236
+ ],
237
+ # Großes portrait-Bild links, rechts oben landscape,
238
+ # darunter zwei kleine landscape nebeneinander PLLL
239
+ lambda imgs: [
240
+ (self.cropAndResize(imgs[0], int(w * 0.4), h), (0, 0)), # portrait, index 0
241
+ (self.cropAndResize(imgs[1], int(w * 0.6), int(h * 3 / 5)), (int(w * 0.4) + s, 0)),
242
+ (
243
+ self.cropAndResize(imgs[2], int(w * 0.3 - s), int(h * 2 / 5) - s),
244
+ (int(w * 0.4) + s, int(h * 3 / 5) + s),
245
+ ),
246
+ (
247
+ self.cropAndResize(imgs[3], int(w * 0.3 - s), int(h * 2 / 5) - s),
248
+ (int(w * 0.7) + s, int(h * 3 / 5) + s),
249
+ ),
250
+ ],
251
+ # Großes portrait-Bild links, rechts oben landscape,
252
+ # darunter kleines portrait und landscape PPLL
253
+ lambda imgs: [
254
+ (self.cropAndResize(imgs[0], int(w * 0.4), h), (0, 0)), # portrait, index 0
255
+ (self.cropAndResize(imgs[2], int(w * 0.6), int(h * 3 / 5)), (int(w * 0.4) + s, 0)),
256
+ (
257
+ self.cropAndResize(imgs[1], int(w * 0.2), int(h * 2 / 5) - s),
258
+ (int(w * 0.4) + s, int(h * 3 / 5) + s),
259
+ ),
260
+ (
261
+ self.cropAndResize(imgs[3], int(w * 0.4 - 2 * s), int(h * 2 / 5) - s),
262
+ (int(w * 0.6) + 2 * s, int(h * 3 / 5) + s),
263
+ ),
264
+ ],
265
+ # Großes portrait-Bild links, rechts oben landscape,
266
+ # darunter zwei kleines portrait nebeneinander PPLL
267
+ lambda imgs: [
268
+ (self.cropAndResize(imgs[0], int(w * 0.4), h), (0, 0)), # portrait, index 0
269
+ (self.cropAndResize(imgs[3], int(w * 0.6), int(h * 2 / 5)), (int(w * 0.4) + s, 0)),
270
+ (
271
+ self.cropAndResize(imgs[1], int(w * 0.25), int(h * 3 / 5) - s),
272
+ (int(w * 0.4) + s, int(h * 2 / 5) + s),
273
+ ),
274
+ (
275
+ self.cropAndResize(imgs[2], int(w * 0.35 - 2 * s), int(h * 3 / 5) - s),
276
+ (int(w * 0.65) + 2 * s, int(h * 2 / 5) + s),
277
+ ),
278
+ ],
279
+ ]
280
+
281
+ if formats.count("portrait") == 0: # LLLL = 4x landscape
282
+ random.seed()
283
+ if random.random() > 0.5:
284
+ layout = layouts[0]
285
+ else:
286
+ layout = layouts[1]
287
+ elif formats.count("portrait") == 1: # PLLL
288
+ layout = layouts[2]
289
+ elif formats.count("portrait") == 2: # PPLL
290
+ layout = layouts[3]
291
+ else: # PPPL
292
+ layout = layouts[4]
293
+
294
+ for img, pos in layout(images):
295
+ collage.paste(img, pos)
296
+
297
+ def arrangeFiveImages(self, collage, images, formats, w, h):
298
+ """
299
+ Layouts für fünf Bilder.
300
+ """
301
+ s = self.spacing
302
+ layouts = [
303
+ # Zwei große Bilder oben, drei etwas kleinere unten (LLLLL)
304
+ lambda imgs: [
305
+ (
306
+ self.cropAndResize(imgs[0], int(w * 0.5), int(h * 0.6) - s),
307
+ (0, 0),
308
+ ), # portrait, index 0
309
+ (
310
+ self.cropAndResize(imgs[1], int(w * 0.5), int(h * 0.6) - s),
311
+ (int(w * 0.5) + s, 0),
312
+ ),
313
+ (
314
+ self.cropAndResize(imgs[2], int(w / 3), int(h * 0.4)),
315
+ (int(w * 0 / 3) + 0 * s, int(h * 0.6)),
316
+ ),
317
+ (
318
+ self.cropAndResize(imgs[3], int(w / 3), int(h * 0.4)),
319
+ (int(w * 1 / 3) + 1 * s, int(h * 0.6)),
320
+ ),
321
+ (
322
+ self.cropAndResize(imgs[4], int(w / 3), int(h * 0.4)),
323
+ (int(w * 2 / 3) + 2 * s, int(h * 0.6)),
324
+ ),
325
+ ],
326
+ # Links ein großes Portrait,
327
+ # rechts daneben im goldenen Schnitt vier kleinere Bilder (PLLLL)
328
+ lambda imgs: [
329
+ (
330
+ self.cropAndResize(imgs[0], int(w * 0.3 - s), int(h)),
331
+ (0, 0),
332
+ ), # portrait, index 0
333
+ (self.cropAndResize(imgs[1], int(w * 0.3), int(h * 0.55) - s), (int(w * 0.3), 0)),
334
+ (
335
+ self.cropAndResize(imgs[2], int(w * 0.40) - s, int(h * 0.55) - s),
336
+ (int(w * 0.6) + s, 0),
337
+ ),
338
+ (
339
+ self.cropAndResize(imgs[3], int(w * 0.40), int(h * 0.45)),
340
+ (int(w * 0.3), int(h * 0.55)),
341
+ ),
342
+ (
343
+ self.cropAndResize(imgs[4], int(w * 0.3) - s, int(h * 0.45)),
344
+ (int(w * 0.7) + s, int(h * 0.55)),
345
+ ),
346
+ ],
347
+ # zwei große aber dennoch leider recht breite Portrais oben,
348
+ # unten drei kleine landscape (PPLLL)
349
+ lambda imgs: [
350
+ (
351
+ self.cropAndResize(imgs[0], int(w * 0.5), int(h * 2 / 3) - s),
352
+ (0, 0),
353
+ ), # portrait, index 0
354
+ (
355
+ self.cropAndResize(imgs[1], int(w * 0.5), int(h * 2 / 3) - s),
356
+ (int(w * 0.5) + s, 0),
357
+ ),
358
+ (
359
+ self.cropAndResize(imgs[4], int(w / 3), int(h / 3)),
360
+ (int(w * 0 / 3) + 0 * s, int(h * 2 / 3)),
361
+ ),
362
+ (
363
+ self.cropAndResize(imgs[3], int(w / 3), int(h / 3)),
364
+ (int(w * 1 / 3) + 1 * s, int(h * 2 / 3)),
365
+ ),
366
+ (
367
+ self.cropAndResize(imgs[2], int(w / 3), int(h / 3)),
368
+ (int(w * 2 / 3) + 2 * s, int(h * 2 / 3)),
369
+ ),
370
+ ],
371
+ # Links ein großes Portrait, rechts daneben im goldenen Schnitt
372
+ # vier kleinere Bilder kleine unten (PPPLL)
373
+ lambda imgs: [
374
+ (
375
+ self.cropAndResize(imgs[0], int(w * 0.35 - s), int(h)),
376
+ (0, 0),
377
+ ), # portrait, index 0
378
+ (self.cropAndResize(imgs[1], int(w * 0.25), int(h * 0.55) - s), (int(w * 0.35), 0)),
379
+ (
380
+ self.cropAndResize(imgs[2], int(w * 0.40) - s, int(h * 0.55) - s),
381
+ (int(w * 0.6) + s, 0),
382
+ ),
383
+ (
384
+ self.cropAndResize(imgs[3], int(w * 0.40), int(h * 0.45)),
385
+ (int(w * 0.35), int(h * 0.55)),
386
+ ),
387
+ (
388
+ self.cropAndResize(imgs[4], int(w * 0.25) - s, int(h * 0.45)),
389
+ (int(w * 0.75) + s, int(h * 0.55)),
390
+ ),
391
+ ],
392
+ ]
393
+
394
+ if formats.count("portrait") == 0: # LLLLL = 5x landscape
395
+ layout = layouts[0]
396
+ elif formats.count("portrait") == 1: # PLLLL
397
+ layout = layouts[1]
398
+ elif formats.count("portrait") == 2: # PPLLL
399
+ layout = layouts[2]
400
+ else: # PPPLL
401
+ layout = layouts[3]
402
+
403
+ for img, pos in layout(images):
404
+ collage.paste(img, pos)
405
+
406
+ def arrangeMultipleImages(self, collage, images, width, height):
407
+ """
408
+ Raster-Layout für mehr als vier Bilder, mit gleichmäßiger Verteilung.
409
+ Passt automatisch die Anzahl der Zeilen und Spalten an.
410
+ """
411
+ # Bestimme die Anzahl der Spalten und Zeilen basierend auf der Anzahl der Bilder
412
+ rows = int(len(images) ** 0.5) # Quadratwurzel für möglichst gleichmäßige Aufteilung
413
+ cols = (len(images) + rows - 1) // rows # Rundung nach oben
414
+
415
+ # Berechnung der Zellgrößen basierend auf der Composition-Größe und Abstände
416
+ cell_width = (width - (cols - 1) * self.spacing) // cols
417
+ cell_height = (height - (rows - 1) * self.spacing) // rows
418
+
419
+ # Bilder in das Raster einfügen
420
+ for i, img in enumerate(images):
421
+ # Bestimme Zeile und Spalte des aktuellen Bildes
422
+ row = i // cols
423
+ col = i % cols
424
+
425
+ # Passe die Bildgröße an die Rasterzelle an
426
+ resized_img = self.cropAndResize(img, cell_width, cell_height)
427
+
428
+ # Berechne die Position des Bildes in der Composition
429
+ x_offset = col * (cell_width + self.spacing)
430
+ y_offset = row * (cell_height + self.spacing)
431
+
432
+ # Füge das Bild in die Composition ein
433
+ collage.paste(resized_img, (x_offset, y_offset))
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from PIL import Image, ImageDraw
4
+
5
+ from Photo_Composition_Designer.config.config import ConfigParameterManager
6
+ from Photo_Composition_Designer.tools.Helpers import load_font, mm_to_px
7
+
8
+
9
+ class DescriptionRenderer:
10
+ def __init__(
11
+ self,
12
+ width_px: int,
13
+ font_size: int,
14
+ spacing_px: int,
15
+ margin_side_px: int,
16
+ background_color,
17
+ text_color,
18
+ ):
19
+ self.width_px = int(width_px)
20
+ self.spacing_px = int(spacing_px)
21
+ self.font_size = int(font_size)
22
+ self.margin_side_px = int(margin_side_px)
23
+ self.background_color = background_color
24
+ self.text_color = text_color
25
+
26
+ # Height includes bottom spacing from your original code
27
+ self.height_px = self.font_size + self.spacing_px
28
+
29
+ # -------------------------------------------------------------------------
30
+
31
+ @classmethod
32
+ def from_config(cls, config: ConfigParameterManager) -> DescriptionRenderer:
33
+ width_px = mm_to_px(config.size.width.value, config.size.dpi.value)
34
+ spacing_px = mm_to_px(config.layout.spacing.value, config.size.dpi.value)
35
+ margin_side_px = mm_to_px(config.layout.marginSides.value, config.size.dpi.value)
36
+
37
+ font_size = int(
38
+ config.layout.fontSizeSmall.value
39
+ * config.size.calendarHeight.value
40
+ * config.size.dpi.value
41
+ / 25.4
42
+ )
43
+
44
+ bg = config.colors.backgroundColor.value.to_pil()
45
+ text_color = config.colors.textColor2.value.to_pil()
46
+
47
+ return cls(
48
+ width_px=width_px,
49
+ font_size=font_size,
50
+ spacing_px=spacing_px,
51
+ margin_side_px=margin_side_px,
52
+ background_color=bg,
53
+ text_color=text_color,
54
+ )
55
+
56
+ # -------------------------------------------------------------------------
57
+
58
+ def generate(self, text: str) -> Image.Image:
59
+ """Render simple text label."""
60
+
61
+ img = Image.new("RGB", (self.width_px, self.height_px), self.background_color)
62
+ draw = ImageDraw.Draw(img)
63
+
64
+ font = load_font(size=self.font_size)
65
+
66
+ draw.text(
67
+ (self.margin_side_px, self.height_px),
68
+ text,
69
+ fill=self.text_color,
70
+ font=font,
71
+ anchor="lb",
72
+ )
73
+
74
+ return img
@@ -0,0 +1,101 @@
1
+ from io import BytesIO
2
+ from pathlib import Path
3
+
4
+ import exifread
5
+ from PIL import Image
6
+
7
+ from Photo_Composition_Designer.common.Locations import Locations
8
+ from Photo_Composition_Designer.tools.GeoPlotter import GeoPlotter
9
+
10
+
11
+ class MapRenderer:
12
+ def __init__(
13
+ self,
14
+ mapHeight=100,
15
+ mapWidth=100,
16
+ minimalExtension=7,
17
+ backgroundColor=(30, 30, 30),
18
+ textColor1=(150, 250, 150),
19
+ locations: Locations = None,
20
+ ):
21
+ self.height = mapHeight
22
+ self.width = mapWidth
23
+ self.minimalExtension = minimalExtension
24
+ self.backgroundColor = backgroundColor
25
+ self.textColor1 = textColor1
26
+ self.locations = locations or Locations()
27
+
28
+ def generate(self, coordinates: list[tuple[float, float]]) -> Image.Image:
29
+ """
30
+ Generiert eine Karte als Bild mit den GPS-Koordinaten.
31
+ :param coordinates: Liste von (Breitengrad, Längengrad)-Tupeln.
32
+ :return: PIL.Image-Objekt mit der Karte.
33
+ """
34
+ # Plotter initialisieren
35
+ border = 15 # unwanted border to be eliminated
36
+ plotter = GeoPlotter(
37
+ minimalExtension=self.minimalExtension,
38
+ size=(self.width + 2 * border, self.height + 2 * border),
39
+ background_color=self.backgroundColor,
40
+ border_color=self.textColor1,
41
+ )
42
+
43
+ # GeoDataFrame aus Koordinaten erstellen
44
+ plt = plotter.renderMap(coordinates)
45
+
46
+ # In einen BytesIO-Puffer speichern
47
+ buf = BytesIO()
48
+ plt.savefig(buf, format="PNG", bbox_inches="tight") # Optional: Anpassung des DPI-Werts
49
+ plt.close() # Speicher freigeben
50
+ buf.seek(0)
51
+
52
+ map_image: Image.Image = Image.open(buf)
53
+ map_image = map_image.resize((self.width + 2 * border, self.height + 2 * border))
54
+ map_image = map_image.crop((border, border, self.width + border, self.height + border))
55
+
56
+ # Puffer als PIL.Image öffnen und zurückgeben
57
+ return map_image
58
+
59
+ def extract_gps_coordinates(self, img_path):
60
+ """
61
+ Liest die GPS-Koordinaten aus den EXIF-Daten eines Bildes.
62
+ """
63
+ with open(img_path, "rb") as img_file:
64
+ tags = exifread.process_file(img_file, details=False)
65
+ if "GPS GPSLatitude" in tags and "GPS GPSLongitude" in tags:
66
+ lat = self.convert_to_decimal(tags["GPS GPSLatitude"].values)
67
+ lon = self.convert_to_decimal(tags["GPS GPSLongitude"].values)
68
+ if tags.get("GPS GPSLatitudeRef") == "S":
69
+ lat = -lat
70
+ if tags.get("GPS GPSLongitudeRef") == "W":
71
+ lon = -lon
72
+ return lat, lon
73
+ return None
74
+
75
+ @staticmethod
76
+ def convert_to_decimal(dms: list[int]) -> float:
77
+ """
78
+ Konvertiert Grad, Minuten und Sekunden in Dezimalgrad.
79
+ """
80
+ return dms[0] + dms[1] / 60 + dms[2] / 3600
81
+
82
+
83
+ if __name__ == "__main__":
84
+ for size in range(100, 900, 200):
85
+ map_plt = Image.new(mode="RGB", size=(size, size))
86
+ output_dir = Path.cwd()
87
+ gps_coordinates = [
88
+ (51.0504, 13.7373), # Dresden
89
+ (51.3397, 12.3731), # Leipzig
90
+ (50.8278, 12.9214), # Chemnitz
91
+ (51.1079, 17.0441), # Breslau
92
+ (52.5200, 13.5156), # Berlin
93
+ ]
94
+ map_generator = MapRenderer()
95
+ map_generator.height = size
96
+ map_generator.width = size
97
+
98
+ img = map_generator.generate(gps_coordinates)
99
+
100
+ map_plt.paste(img)
101
+ map_plt.save(output_dir / f"map_{size}.jpg")
File without changes
@@ -0,0 +1,44 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ class DescriptionsFileGenerator:
6
+ """
7
+ Auxiliary class to generate a descriptions.txt file
8
+ """
9
+
10
+ def __init__(self, photo_dir: Path, output_dir: Path):
11
+ self.photo_dir: Path = photo_dir
12
+ self.output_dir: Path = output_dir
13
+
14
+ def generate_description_file(self, overwrite=False) -> str:
15
+ """
16
+ Generate a template description file for all collages
17
+ based on the generated photo directories
18
+ """
19
+ global_description_text: list[str] = []
20
+
21
+ for element in sorted(os.listdir(self.photo_dir)):
22
+ folder_path = os.path.join(self.photo_dir, element)
23
+ if not os.path.isdir(folder_path):
24
+ continue
25
+ global_description_text.append(f"{element}: Description text for week {element}")
26
+ out_path = self._descriptions_file_path()
27
+ if overwrite or not self.description_file_exists():
28
+ with open(out_path, "w", encoding="utf-8") as f: # type: ignore
29
+ f.writelines(text + "\n" for text in global_description_text)
30
+
31
+ return out_path
32
+
33
+ def description_file_exists(self) -> bool:
34
+ return os.path.exists(self._descriptions_file_path())
35
+
36
+ def _descriptions_file_path(self) -> str:
37
+ return os.path.join(self.output_dir, "descriptions.txt") # type: ignore
38
+
39
+
40
+ if __name__ == "__main__":
41
+ photodir = Path("../../../images")
42
+ outputdir = Path("../../../output")
43
+ _description_file_generator = DescriptionsFileGenerator(photodir, outputdir)
44
+ _description_file_generator.generate_description_file()