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,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()
|