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,319 @@
1
+ from __future__ import annotations
2
+
3
+ import calendar
4
+ import locale
5
+ import logging
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+
9
+ import holidays
10
+ import pytz
11
+ from astral import LocationInfo
12
+ from astral.sun import sun
13
+ from PIL import Image, ImageDraw
14
+
15
+ from Photo_Composition_Designer.common.Anniversaries import Anniversaries
16
+ from Photo_Composition_Designer.common.MoonPhase import MoonPhase
17
+ from Photo_Composition_Designer.config.config import ConfigParameterManager
18
+ from Photo_Composition_Designer.tools.Helpers import load_font
19
+
20
+
21
+ class CalendarRenderer:
22
+ """Responsible for rendering a weekly calendar strip with holidays,
23
+ sun times and anniversaries.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ backgroundColor: tuple[int],
29
+ textColor1: tuple[int],
30
+ textColor2: tuple[int],
31
+ holidayColor: tuple[int],
32
+ language: str,
33
+ startDate: datetime,
34
+ holidayCountries: list[str],
35
+ fontSizeSmall: float,
36
+ fontSizeLarge: float,
37
+ fontSizeAnniversaries: float,
38
+ useShortDayNames: bool,
39
+ useShortMonthNames: bool,
40
+ marginSides: float,
41
+ anniversaries: Anniversaries | None = None,
42
+ ) -> None:
43
+ self.anniversaries = anniversaries or Anniversaries()
44
+
45
+ # Convert colors to 0–255 int tuples used by Pillow
46
+ self.backgroundColor = backgroundColor
47
+ self.textColor1 = textColor1
48
+ self.textColor2 = textColor2
49
+ self.holidayColor = holidayColor
50
+
51
+ self.fontSizeSmall = fontSizeSmall
52
+ self.fontSizeLarge = fontSizeLarge
53
+ self.fontSizeAnniversaries = fontSizeAnniversaries
54
+
55
+ self.language = language
56
+ self.startDate = startDate
57
+ self.holidayCountries = holidayCountries
58
+ self.useShortDayNames = useShortDayNames
59
+ self.useShortMonthNames = useShortMonthNames
60
+ self.marginSides = marginSides
61
+
62
+ # Extract country code from locale ("de_DE" → "DE")
63
+ country_code = language.split("_")[1].upper()
64
+
65
+ self.localHolidays = self.get_combined_holidays(
66
+ startDate.year,
67
+ country_code,
68
+ holidayCountries,
69
+ )
70
+
71
+ # -------------------------------------------------------------------------
72
+ # Rendering
73
+ # -------------------------------------------------------------------------
74
+
75
+ def generate(self, d: datetime, width: int | float, height: int | float) -> Image.Image:
76
+ """Render full weekly calendar image."""
77
+ width = int(width)
78
+ height = int(height)
79
+ week_dates = [d + timedelta(days=i) for i in range(7)]
80
+
81
+ img = Image.new("RGB", (width, height), self.backgroundColor)
82
+ draw = ImageDraw.Draw(img)
83
+
84
+ font_large = load_font("DejaVuSans.ttf", int(self.fontSizeLarge))
85
+ font_small = load_font("DejaVuSans.ttf", int(self.fontSizeSmall))
86
+ font_ann = load_font("DejaVuSans.ttf", int(self.fontSizeAnniversaries))
87
+
88
+ # Header (month + year)
89
+ month_name = self.get_month_name(
90
+ week_dates[0].month,
91
+ locale_name=self.language,
92
+ abbreviation=self.useShortMonthNames,
93
+ )
94
+ header_text = f"{month_name} {str(d.year)[-2:]}"
95
+ draw.text(
96
+ (0, height - self.fontSizeAnniversaries),
97
+ header_text,
98
+ font=font_large,
99
+ fill=self.textColor2,
100
+ anchor="ld",
101
+ )
102
+
103
+ # Sun times for Europe/Berlin
104
+ location = LocationInfo("Dresden", "Germany", "Europe/Berlin", 51.0504, 13.7373)
105
+ tz = pytz.timezone("Europe/Berlin")
106
+ sun_times = sun(location.observer, date=d)
107
+
108
+ sunrise = sun_times["sunrise"].astimezone(tz).strftime("%H:%M")
109
+ sunset = sun_times["sunset"].astimezone(tz).strftime("%H:%M")
110
+ week_no = d.isocalendar().week
111
+
112
+ sun_string = f"KW {week_no} ● ↑ {sunrise} ○ ↓ {sunset}"
113
+ draw.text(
114
+ (0, height),
115
+ sun_string,
116
+ font=font_ann,
117
+ fill=self.textColor2,
118
+ anchor="ld",
119
+ )
120
+
121
+ # Day columns
122
+ month_cols, col_width = self.get_cols_property(width)
123
+
124
+ for idx, day_date in enumerate(week_dates):
125
+ x = self.marginSides + (idx + month_cols + 0.5) * col_width
126
+
127
+ date_key = (day_date.day, day_date.month)
128
+ holiday_name = self.localHolidays.get(day_date)
129
+
130
+ is_weekend = day_date.weekday() >= 5
131
+ is_holiday = day_date in self.localHolidays
132
+
133
+ color_day = self.holidayColor if (is_holiday or is_weekend) else self.textColor1
134
+
135
+ # Day name
136
+ day_name = self.get_day_name(day_date.weekday(), self.language)
137
+ if self.useShortDayNames:
138
+ day_name = day_name[:2]
139
+
140
+ moon_symbol = MoonPhase.get_moon_phase_symbol_dark(day_date)
141
+ if moon_symbol:
142
+ day_name = f"{day_name} {moon_symbol}"
143
+
144
+ draw.text(
145
+ (x, height - self.fontSizeAnniversaries - self.fontSizeLarge * 1.15),
146
+ day_name,
147
+ font=font_small,
148
+ fill=self.textColor2,
149
+ anchor="md",
150
+ )
151
+
152
+ draw.text(
153
+ (x, height - self.fontSizeAnniversaries),
154
+ str(day_date.day),
155
+ font=font_large,
156
+ fill=color_day,
157
+ anchor="md",
158
+ )
159
+
160
+ # Anniversaries + holidays
161
+ label = None
162
+ if date_key in self.anniversaries:
163
+ label = self.anniversaries[date_key]
164
+ if holiday_name:
165
+ label += f", {holiday_name}"
166
+ draw.fill = self.textColor1
167
+ elif holiday_name:
168
+ label = holiday_name
169
+
170
+ if label:
171
+ draw.text(
172
+ (x, height),
173
+ label,
174
+ font=font_ann,
175
+ fill=self.holidayColor,
176
+ anchor="md",
177
+ )
178
+
179
+ return img
180
+
181
+ def generateTitle(self, title: str, width: int | float, height: int | float) -> Image.Image:
182
+ width = int(width)
183
+ height = int(height)
184
+ img = Image.new("RGB", (width, height), self.backgroundColor)
185
+ draw = ImageDraw.Draw(img)
186
+ font_large = load_font("DejaVuSans.ttf", int(self.fontSizeLarge))
187
+
188
+ draw.text(
189
+ (width // 2, height - self.fontSizeAnniversaries),
190
+ title,
191
+ font=font_large,
192
+ fill=self.textColor1,
193
+ anchor="md",
194
+ )
195
+ return img
196
+
197
+ # -------------------------------------------------------------------------
198
+ # Helpers
199
+ # -------------------------------------------------------------------------
200
+
201
+ def get_cols_property(self, width: int) -> tuple[float, float]:
202
+ month_cols = 1.5 if self.useShortMonthNames else 4.0
203
+ col_width = (width - 3 * self.marginSides) / (7.0 + month_cols)
204
+ return month_cols, col_width
205
+
206
+ @staticmethod
207
+ def get_month_name(month: int, locale_name: str, abbreviation: bool = False) -> str:
208
+ try:
209
+ locale.setlocale(locale.LC_TIME, locale_name)
210
+ return calendar.month_abbr[month] if abbreviation else calendar.month_name[month]
211
+ except locale.Error:
212
+ return calendar.month_abbr[month] if abbreviation else calendar.month_name[month]
213
+ finally:
214
+ locale.setlocale(locale.LC_TIME, "")
215
+
216
+ @staticmethod
217
+ def get_day_name(day: int, locale_name: str) -> str:
218
+ try:
219
+ locale.setlocale(locale.LC_TIME, locale_name)
220
+ return calendar.day_name[day]
221
+ except locale.Error:
222
+ return calendar.day_name[day]
223
+ finally:
224
+ locale.setlocale(locale.LC_TIME, "")
225
+
226
+ @staticmethod
227
+ def get_combined_holidays(year: int, country: str, subdivs: list[str]) -> holidays.HolidayBase:
228
+ years = (year, year + 1)
229
+ combined = holidays.HolidayBase()
230
+ combined.update(holidays.country_holidays(country, years=years))
231
+ try:
232
+ for sub in subdivs:
233
+ combined.update(holidays.country_holidays(country, years=years, subdiv=sub))
234
+ except Exception as e:
235
+ logging.warning(f"Unable to load holiday subdivision {sub}: {e}")
236
+
237
+ return combined
238
+
239
+
240
+ # -----------------------------------------------------------------------------
241
+ # Main helpers for production use
242
+ # -----------------------------------------------------------------------------
243
+
244
+
245
+ def from_config(config: ConfigParameterManager) -> CalendarRenderer:
246
+ """Factory function to create CalendarGenerator using the config manager."""
247
+ return CalendarRenderer(
248
+ backgroundColor=config.colors.backgroundColor.value.to_pil(),
249
+ textColor1=config.colors.textColor1.value.to_pil(),
250
+ textColor2=config.colors.textColor2.value.to_pil(),
251
+ holidayColor=config.colors.holidayColor.value.to_pil(),
252
+ language=config.calendar.language.value,
253
+ startDate=config.calendar.startDate.value,
254
+ holidayCountries=[s.strip() for s in config.calendar.holidayCountries.value.split(",")],
255
+ fontSizeSmall=int(
256
+ config.layout.fontSizeSmall.value
257
+ * config.size.calendarHeight.value
258
+ * config.size.dpi.value
259
+ / 25.4
260
+ ),
261
+ fontSizeLarge=int(
262
+ config.layout.fontSizeLarge.value
263
+ * config.size.calendarHeight.value
264
+ * config.size.dpi.value
265
+ / 25.4
266
+ ),
267
+ fontSizeAnniversaries=int(
268
+ config.layout.fontSizeAnniversaries.value
269
+ * config.size.calendarHeight.value
270
+ * config.size.dpi.value
271
+ / 25.4
272
+ ),
273
+ useShortDayNames=config.layout.useShortDayNames.value,
274
+ useShortMonthNames=config.layout.useShortMonthNames.value,
275
+ marginSides=int(config.layout.marginSides.value * config.size.dpi.value / 25.4),
276
+ anniversaries=None, # use default
277
+ )
278
+
279
+
280
+ def main() -> None:
281
+ import os
282
+
283
+ from Photo_Composition_Designer.config.config import ConfigParameterManager
284
+
285
+ config = ConfigParameterManager()
286
+ cg = from_config(config)
287
+
288
+ # Create temp directory
289
+ project_root = Path(__file__).resolve().parents[3]
290
+ temp_dir = project_root / "temp"
291
+ temp_dir.mkdir(exist_ok=True)
292
+
293
+ title = config.general.compositionTitle.value + " " + str(config.calendar.startDate.value.year)
294
+ first = True
295
+
296
+ for w in range(config.calendar.collagesToGenerate.value):
297
+ dt = config.calendar.startDate.value + timedelta(weeks=w - 2)
298
+
299
+ if first:
300
+ img = cg.generateTitle(
301
+ title,
302
+ width=int(config.size.width.value * config.size.dpi.value / 25.4),
303
+ height=int(config.size.calendarHeight.value * config.size.dpi.value / 25.4),
304
+ )
305
+ first = False
306
+ else:
307
+ img = cg.generate(
308
+ dt,
309
+ width=int(config.size.width.value * config.size.dpi.value / 25.4),
310
+ height=config.size.calendarHeight.value * config.size.dpi.value / 25.4,
311
+ )
312
+
313
+ path = os.path.join(temp_dir, f"calendar_{dt.year}-{dt.month:02d}-{dt.day:02d}.jpg")
314
+ img.save(path)
315
+ print("Generated:", path)
316
+
317
+
318
+ if __name__ == "__main__":
319
+ main()