Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
1b96c8590d | |||
62e23de949 | |||
e2173bb999 | |||
7b18fb10f1 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.pdf
|
35
README.md
35
README.md
@ -1,12 +1,15 @@
|
|||||||
# Calendar Generator Flask App
|
# Calendar Generator PDF App
|
||||||
|
This python application generates a pdf-file based calendar with images and associated texts for each day. It's designed to read images and texts from specified directories and display them in a calendar format. Per DIN A4 page 4 days get displayed.
|
||||||
This Flask application generates a web-based calendar view with images and associated texts for each day. It's designed to read images and texts from specified directories and display them in a calendar format. The web-based pages can be printed to a pdf-file. This is easy, because every browser application has the option to print a page to a pdf-file. Per DIN A4 page 4 days get displayed.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Python 3.x
|
- Python 3.x
|
||||||
- Flask
|
|
||||||
- Basic understanding of Python and Flask
|
- Basic understanding of Python and Flask
|
||||||
|
- reportlab (python modul)
|
||||||
|
- os (python modul)
|
||||||
|
- re (python modul)
|
||||||
|
- textwrap (python modul)
|
||||||
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@ -16,9 +19,9 @@ This Flask application generates a web-based calendar view with images and assoc
|
|||||||
|
|
||||||
2. **Install Required Packages**
|
2. **Install Required Packages**
|
||||||
|
|
||||||
Flask is required and can be installed via pip:
|
reportlab is required and can be installed via pip:
|
||||||
```bash
|
```bash
|
||||||
pip install Flask
|
pip install reportlab
|
||||||
``````
|
``````
|
||||||
|
|
||||||
### Folder Structure
|
### Folder Structure
|
||||||
@ -30,19 +33,18 @@ project_folder/
|
|||||||
│ app.py
|
│ app.py
|
||||||
│ README.md
|
│ README.md
|
||||||
│
|
│
|
||||||
├───static/
|
├───images/
|
||||||
│ └───images/
|
│ │ 2024-01-15.jpg
|
||||||
│ │ 2024-01-15.jpg
|
│ │ 2024-01-16.png
|
||||||
│ │ 2024-01-16.png
|
│ ...
|
||||||
│ ...
|
|
||||||
│
|
│
|
||||||
└───texts/
|
└───texts/
|
||||||
│ 2024-01-15.txt
|
│ 2024-01-15.txt
|
||||||
│ 2024-01-16.txt
|
│ 2024-01-16.txt
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
- `app.py`: Main Flask application file.
|
- `app.py`: Main python application file.
|
||||||
- `static/images/`: Directory for storing calendar images. Images should be named `YYYY-MM-DD.extension`.
|
- `images/`: Directory for storing calendar images. Images should be named `YYYY-MM-DD.extension`.
|
||||||
- `texts/`: Directory for storing text files corresponding to each day, named `YYYY-MM-DD.txt`.
|
- `texts/`: Directory for storing text files corresponding to each day, named `YYYY-MM-DD.txt`.
|
||||||
|
|
||||||
### Running the Application
|
### Running the Application
|
||||||
@ -52,13 +54,11 @@ project_folder/
|
|||||||
```bash
|
```bash
|
||||||
python app.py
|
python app.py
|
||||||
``````
|
``````
|
||||||
3. Access the calendar in a web browser at `http://127.0.0.1:5000/`.
|
3. "PDF created successfully" shoud appear, then you can access the pdf file which is in the same directory as the script.
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
- The application serves a web page displaying a calendar.
|
|
||||||
- Each day on the calendar can have an image and a text associated with it.
|
- Each day on the calendar can have an image and a text associated with it.
|
||||||
- Images are read from `static/images/`, and text files are from `texts/`.
|
- Images are read from `images/`, and text files are from `texts/`.
|
||||||
- Files are matched based on their filenames, formatted as `YYYY-MM-DD`.
|
- Files are matched based on their filenames, formatted as `YYYY-MM-DD`.
|
||||||
- The calendar is dynamically generated based on available images and texts.
|
- The calendar is dynamically generated based on available images and texts.
|
||||||
- If an image or text is not available for a specific day, it will be displayed without that content.
|
- If an image or text is not available for a specific day, it will be displayed without that content.
|
||||||
@ -66,7 +66,6 @@ project_folder/
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Text for each day should be concise. Lengthy texts may be truncated in the display to maintain the layout of the calendar.
|
- Text for each day should be concise. Lengthy texts may be truncated in the display to maintain the layout of the calendar.
|
||||||
- You can customize the appearance and behavior of the calendar by modifying the HTML, CSS, and Flask application logic.
|
|
||||||
|
|
||||||
## Copyright and license
|
## Copyright and license
|
||||||
This code is for my very good friend Felix W.. Code copyright 2024 Lorenz B.
|
This code is for my very good friend Felix W.. Code copyright 2024 Lorenz B.
|
97
app.py
97
app.py
@ -1,9 +1,23 @@
|
|||||||
from flask import Flask, render_template
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
from reportlab.lib.units import mm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import textwrap
|
||||||
|
|
||||||
app = Flask(__name__)
|
# Konstanten für die Abmessungen
|
||||||
|
CIRCLE_RADIUS = 3 * mm
|
||||||
|
CIRCLE_DISTANCE = 80 * mm
|
||||||
|
TOP_MARGIN_TO_CENTER = 12 * mm
|
||||||
|
IMAGE_TOP_MARGIN = 28 * mm
|
||||||
|
IMAGE_HEIGHT = 60 * mm
|
||||||
|
TEXT_MARGIN = 5 * mm
|
||||||
|
LEFT_MARGIN_TO_CENTER = (A4[0] / 4) - (CIRCLE_DISTANCE / 2) # Zentriert zwischen den Tagen
|
||||||
|
IMAGE_SHIFT_UP = 1 * mm # Bild nach oben verschieben
|
||||||
|
DATE_SHIFT_DOWN = 8 * mm # Datum nach unten verschieben
|
||||||
|
TEXT_SHIFT_DOWN = 8 * mm # Text nach unten verschieben
|
||||||
|
|
||||||
def get_dates_from_images(image_directory):
|
def get_dates_from_images(image_directory):
|
||||||
# Regular expression to match files with a date format YYYY-MM-DD
|
# Regular expression to match files with a date format YYYY-MM-DD
|
||||||
@ -50,18 +64,81 @@ def get_image_and_text_for_date(date, image_directory, text_directory):
|
|||||||
|
|
||||||
return image_file, text_content
|
return image_file, text_content
|
||||||
|
|
||||||
|
def draw_circle(c, x, y):
|
||||||
|
c.circle(x, y, CIRCLE_RADIUS, stroke=1, fill=0)
|
||||||
|
|
||||||
@app.route('/')
|
def create_pdf(pages_data, output_filename):
|
||||||
def calendar():
|
c = canvas.Canvas(output_filename, pagesize=A4)
|
||||||
|
c.setCreator('Lorenz B.')
|
||||||
|
c.setTitle('Calendar')
|
||||||
|
c.setAuthor('Lorenz B.')
|
||||||
|
c.setSubject('calendar generator')
|
||||||
|
width, height = A4
|
||||||
|
|
||||||
|
max_text_width = (width / 2) - (2 * TEXT_MARGIN)
|
||||||
|
DATE_FONT_SIZE = 20
|
||||||
|
DATE_FONT = "Helvetica-Bold"
|
||||||
|
|
||||||
|
for page in pages_data:
|
||||||
|
for index, day in enumerate(page):
|
||||||
|
c.setDash(1, 0)
|
||||||
|
c.setStrokeColor(colors.black)
|
||||||
|
|
||||||
|
# Berechnung der Positionen für jeden Tag
|
||||||
|
x = (index % 2) * (width / 2)
|
||||||
|
y = height - (index // 2 + 1) * (height / 2)
|
||||||
|
|
||||||
|
# Höhere Position für das Bild
|
||||||
|
image_y_position = y + (height / 2 - IMAGE_TOP_MARGIN - IMAGE_HEIGHT) + IMAGE_SHIFT_UP
|
||||||
|
|
||||||
|
# Kreise zeichnen
|
||||||
|
draw_circle(c, x + LEFT_MARGIN_TO_CENTER, y + height / 2 - TOP_MARGIN_TO_CENTER)
|
||||||
|
draw_circle(c, x + width / 2 - LEFT_MARGIN_TO_CENTER, y + height / 2 - TOP_MARGIN_TO_CENTER)
|
||||||
|
|
||||||
|
# Bild einfügen
|
||||||
|
if day['image_file']:
|
||||||
|
c.drawImage(day['image_file'], x, image_y_position, width=width / 2, height=IMAGE_HEIGHT, preserveAspectRatio=True, anchor='n')
|
||||||
|
|
||||||
|
# Datum direkt unter dem Bild einfügen
|
||||||
|
date_x_position = x + (width / 4) # Zentrum des Tagesbereichs
|
||||||
|
date_y_position = image_y_position - DATE_SHIFT_DOWN
|
||||||
|
date_str = day['date'].strftime('%A, %d.%m.%Y')
|
||||||
|
c.setFont(DATE_FONT, DATE_FONT_SIZE) # Schriftart und Schriftgröße setzen
|
||||||
|
date_width = c.stringWidth(date_str, DATE_FONT, DATE_FONT_SIZE)
|
||||||
|
c.drawString(date_x_position - (date_width / 2), date_y_position, date_str)
|
||||||
|
|
||||||
|
# Text unter dem Datum einfügen
|
||||||
|
text_y_position = date_y_position - TEXT_SHIFT_DOWN - 5 * mm # Extra Abstand nach einem größeren Datum
|
||||||
|
if day['text_content']:
|
||||||
|
c.setFont("Helvetica", 12) # Schriftart zurücksetzen für den Text
|
||||||
|
wrapped_text = textwrap.fill(day['text_content'], width=50) # Anpassen für die passende Zeilenlänge
|
||||||
|
text = c.beginText(x + TEXT_MARGIN, text_y_position)
|
||||||
|
for line in wrapped_text.split('\n'):
|
||||||
|
text.textLine(line)
|
||||||
|
c.drawText(text)
|
||||||
|
|
||||||
|
# Gestrichelte Linien zeichnen, falls notwendig
|
||||||
|
c.setDash(1, 2)
|
||||||
|
c.setStrokeColor(colors.grey)
|
||||||
|
if index % 2 == 0: # Vertikale Linie rechts für den Tag
|
||||||
|
c.line(x + width / 2, y, x + width / 2, y + height / 2)
|
||||||
|
if index < 2: # Horizontale Linie unten für den Tag
|
||||||
|
c.line(x, y, x + width / 2, y)
|
||||||
|
|
||||||
|
c.showPage()
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
def main():
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
image_directory = os.path.join(BASE_DIR, 'static', 'images')
|
image_directory = os.path.join(BASE_DIR, 'images')
|
||||||
text_directory = os.path.join(BASE_DIR, 'texts')
|
text_directory = os.path.join(BASE_DIR, 'texts')
|
||||||
|
|
||||||
# Get sorted dates from image files
|
# Get sorted dates from image files
|
||||||
dates = get_dates_from_images(image_directory)
|
dates = get_dates_from_images(image_directory)
|
||||||
if not dates:
|
if not dates:
|
||||||
return "No images found with the correct date format in the name.", 404
|
print("No images found with the correct date format in the name.")
|
||||||
|
return
|
||||||
|
|
||||||
# Generate data for each date
|
# Generate data for each date
|
||||||
pages_data = []
|
pages_data = []
|
||||||
@ -83,7 +160,11 @@ def calendar():
|
|||||||
if days_data:
|
if days_data:
|
||||||
pages_data.append(days_data)
|
pages_data.append(days_data)
|
||||||
|
|
||||||
return render_template('index.html', pages=pages_data)
|
# Create the PDF
|
||||||
|
output_filename = os.path.join(BASE_DIR, 'calendar.pdf')
|
||||||
|
create_pdf(pages_data, output_filename)
|
||||||
|
|
||||||
|
print("PDF created successfully.")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
main()
|
||||||
|
3
images/.gitignore
vendored
Normal file
3
images/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.png
|
||||||
|
*.jpeg
|
||||||
|
*.jpg
|
Binary file not shown.
Before Width: | Height: | Size: 209 KiB |
@ -1,107 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
@media print,
|
|
||||||
screen {
|
|
||||||
body {
|
|
||||||
width: 210mm;
|
|
||||||
height: 297mm;
|
|
||||||
margin: 0mm 0mm 0mm 0mm;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
width: 210mm;
|
|
||||||
height: 297mm;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
page-break-after: always;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day {
|
|
||||||
width: 50%;
|
|
||||||
height: 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
width: 10mm;
|
|
||||||
height: 10mm;
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid black;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle.top-left {
|
|
||||||
top: 10mm;
|
|
||||||
left: 10mm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle.top-right {
|
|
||||||
top: 10mm;
|
|
||||||
right: 10mm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container {
|
|
||||||
margin-top: 28mm;
|
|
||||||
text-align: center;
|
|
||||||
height: 60mm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 60mm;
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-info {
|
|
||||||
text-align: center;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
padding-left: 5mm;
|
|
||||||
padding-right: 5mm;
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{% for page in pages %}
|
|
||||||
<div class="page">
|
|
||||||
{% for day in page %}
|
|
||||||
<div class="day" style="{{ 'border-right: 1px dashed grey; border-bottom: 1px dashed grey;' if loop.index0 == 0 else '' }}
|
|
||||||
{{ 'border-bottom: 1px dashed grey;' if loop.index0 == 1 else '' }}
|
|
||||||
{{ 'border-right: 1px dashed grey;' if loop.index0 == 2 else '' }}">
|
|
||||||
<div class="circle top-left"></div>
|
|
||||||
<div class="circle top-right"></div>
|
|
||||||
<div class="image-container">
|
|
||||||
{% if day.image_file %}
|
|
||||||
<img src="{{ url_for('static', filename=day.image_file) }}" alt="Bild für {{ day.date.strftime('%d. %B %Y') }}" />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="date-info">
|
|
||||||
<h1>{{ day.date.strftime('%A %d.%m.%Y') }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
{% if day.text_content %}
|
|
||||||
<p>{{ day.text_content }}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
1
texts/.gitignore
vendored
Normal file
1
texts/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.txt
|
@ -1 +0,0 @@
|
|||||||
Beispiel Text
|
|
Loading…
Reference in New Issue
Block a user