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.
- Photo_Composition_Designer/__init__.py +0 -0
- Photo_Composition_Designer/__main__.py +8 -0
- Photo_Composition_Designer/_version.py +34 -0
- Photo_Composition_Designer/cli/__init__.py +0 -0
- Photo_Composition_Designer/cli/__main__.py +8 -0
- Photo_Composition_Designer/cli/cli.py +106 -0
- Photo_Composition_Designer/common/Anniversaries.py +93 -0
- Photo_Composition_Designer/common/Locations.py +87 -0
- Photo_Composition_Designer/common/MoonPhase.py +85 -0
- Photo_Composition_Designer/common/Photo.py +113 -0
- Photo_Composition_Designer/common/__init__.py +0 -0
- Photo_Composition_Designer/common/logging.py +216 -0
- Photo_Composition_Designer/config/__init__.py +0 -0
- Photo_Composition_Designer/config/config.py +321 -0
- Photo_Composition_Designer/core/__init__.py +0 -0
- Photo_Composition_Designer/core/base.py +383 -0
- Photo_Composition_Designer/gui/GuiLogWriter.py +79 -0
- Photo_Composition_Designer/gui/__init__.py +0 -0
- Photo_Composition_Designer/gui/__main__.py +8 -0
- Photo_Composition_Designer/gui/gui.py +565 -0
- Photo_Composition_Designer/image/CalendarRenderer.py +319 -0
- Photo_Composition_Designer/image/CollageRenderer.py +433 -0
- Photo_Composition_Designer/image/DescriptionRenderer.py +74 -0
- Photo_Composition_Designer/image/MapRenderer.py +101 -0
- Photo_Composition_Designer/image/__init__.py +0 -0
- Photo_Composition_Designer/tools/DescriptionsFileGenerator.py +44 -0
- Photo_Composition_Designer/tools/GeoPlotter.py +211 -0
- Photo_Composition_Designer/tools/Helpers.py +18 -0
- Photo_Composition_Designer/tools/ImageDistributor.py +153 -0
- Photo_Composition_Designer/tools/__init__.py +0 -0
- __init__.py +0 -0
- firewall_handler.py +198 -0
- main.py +146 -0
- path_handler.py +10 -0
- photo_composition_designer-0.0.7.dist-info/METADATA +205 -0
- photo_composition_designer-0.0.7.dist-info/RECORD +40 -0
- photo_composition_designer-0.0.7.dist-info/WHEEL +5 -0
- photo_composition_designer-0.0.7.dist-info/entry_points.txt +3 -0
- photo_composition_designer-0.0.7.dist-info/licenses/LICENSE +24 -0
- 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()
|